动作与冲突

现在我们是时候来添加一些动作了。这是游戏中最令人着迷的一部分。实现动作的最基本的方案(也是大多数游戏采用的)是将时间划分为一个个时间段,根据角色的每一步速度和时间长度,将元素移动一段距离。我们将以秒为单位测量时间,所以速度以单元每秒来表示。

移动东西非常简单。比较困难的一部分是处理元素之间的相互作用。当玩家撞到墙壁或者地板时,不可能简单地直接穿越过去。游戏必须注意特定的动作会导致两个对象产生碰撞,并需要采取相应措施。如果玩家遇到墙壁,则必须停下来,如果遇到硬币则必须将其收集起来。

想要解决通常情况下的碰撞问题是件艰巨任务。你可以找到一些我们称之为物理引擎的库,这些库会在二维或三维空间中模拟物理对象的相互作用。我们在本章中采用更合适的方案:只处理矩形物体之间的碰撞,并采用最简单的方案进行处理。

在移动角色或岩浆块时,我们需要测试元素是否会移动到墙里面。如果会的话,我们只要取消整个动作即可。而对动作的反应则取决于移动元素类型。如果是玩家则停下来,如果是岩浆块则反弹回去。

这种方法需要保证每一步之间的时间间隔足够短,确保能够在对象实际碰撞之前取消动作。如果时间间隔太大,玩家最后会悬浮在离地面很高的地方。另一种方法明显更好但更加复杂,即寻找到精确的碰撞点并将元素移动到那个位置。我们会采取最简单的方案,并确保减少动画之间的时间间隔,以掩盖其问题。

该方法用于判断某个矩形(通过位置与尺寸限定)是否会碰到给定类型的网格。

  1. Level.prototype.touches = function(pos, size, type) {
  2. var xStart = Math.floor(pos.x);
  3. var xEnd = Math.ceil(pos.x + size.x);
  4. var yStart = Math.floor(pos.y);
  5. var yEnd = Math.ceil(pos.y + size.y);
  6. for (var y = yStart; y < yEnd; y++) {
  7. for (var x = xStart; x < xEnd; x++) {
  8. let isOutside = x < 0 || x >= this.width ||
  9. y < 0 || y >= this.height;
  10. let here = isOutside ? "wall" : this.rows[y][x];
  11. if (here == type) return true;
  12. }
  13. }
  14. return false;
  15. };

该方法通过对坐标使用Math.floorMath.ceil,来计算与身体重叠的网格方块集合。记住网格方块的大小是1x1个单位。通过将盒子的边上下颠倒,我们得到盒子接触的背景方块的范围。

动作与冲突 - 图1

我们通过查找坐标遍历网格方块,并在找到匹配的方块时返回true。关卡之外的方块总是被当作"wall",来确保玩家不能离开这个世界,并且我们不会意外地尝试,在我们的“rows数组的边界之外读取。

状态的update方法使用touches来判断玩家是否接触岩浆。

  1. State.prototype.update = function(time, keys) {
  2. let actors = this.actors
  3. .map(actor => actor.update(time, this, keys));
  4. let newState = new State(this.level, actors, this.status);
  5. if (newState.status != "playing") return newState;
  6. let player = newState.player;
  7. if (this.level.touches(player.pos, player.size, "lava")) {
  8. return new State(this.level, actors, "lost");
  9. }
  10. for (let actor of actors) {
  11. if (actor != player && overlap(actor, player)) {
  12. newState = actor.collide(newState);
  13. }
  14. }
  15. return newState;
  16. };

它接受时间步长和一个数据结构,告诉它按下了哪些键。它所做的第一件事是调用所有角色的update方法,生成一组更新后的角色。角色也得到时间步长,按键,和状态,以便他们可以根据这些来更新。只有玩家才会读取按键,因为这是唯一由键盘控制的角色。

如果游戏已经结束,就不需要再做任何处理(游戏不能在输之后赢,反之亦然)。否则,该方法测试玩家是否接触背景岩浆。如果是这样的话,游戏就输了,我们就完了。最后,如果游戏实际上还在继续,它会查看其他玩家是否与玩家重叠。

overlap函数检测角色之间的重叠。它需要两个角色对象,当它们触碰时返回true,当它们沿X轴和Y轴重叠时,就是这种情况。

  1. function overlap(actor1, actor2) {
  2. return actor1.pos.x + actor1.size.x > actor2.pos.x &&
  3. actor1.pos.x < actor2.pos.x + actor2.size.x &&
  4. actor1.pos.y + actor1.size.y > actor2.pos.y &&
  5. actor1.pos.y < actor2.pos.y + actor2.size.y;
  6. }

如果任何角色重叠了,它的collide方法有机会更新状态。触碰岩浆角色将游戏状态设置为"lost",当你碰到硬币时,硬币就会消失,当这是最后一枚硬币时,状态就变成了"won"

  1. Lava.prototype.collide = function(state) {
  2. return new State(state.level, state.actors, "lost");
  3. };
  4. Coin.prototype.collide = function(state) {
  5. let filtered = state.actors.filter(a => a != this);
  6. let status = state.status;
  7. if (!filtered.some(a => a.type == "coin")) status = "won";
  8. return new State(state.level, filtered, status);
  9. };