角色

角色对象表示,游戏中给定可移动元素的当前位置和状态。所有的角色对象都遵循相同的接口。它们的pos属性保存元素的左上角坐标,它们的size属性保存其大小。

然后,他们有update方法,用于计算给定时间步长之后,他们的新状态和位置。它模拟了角色所做的事情:响应箭头键并且移动,因岩浆而来回弹跳,并返回新的更新后的角色对象。

type属性包含一个字符串,该字符串指定了角色类型:"player""coin"或者"lava"。这在绘制游戏时是有用的,为角色绘制的矩形的外观基于其类型。

角色类有一个静态的create方法,它由Level构造器使用,用于从关卡平面图中的字符中,创建一个角色。它接受字符本身及其坐标,这是必需的,因为Lava类处理几个不同的字符。

这是我们将用于二维值的Vec类,例如角色的位置和大小。

  1. class Vec {
  2. constructor(x, y) {
  3. this.x = x; this.y = y;
  4. }
  5. plus(other) {
  6. return new Vec(this.x + other.x, this.y + other.y);
  7. }
  8. times(factor) {
  9. return new Vec(this.x * factor, this.y * factor);
  10. }
  11. }

times方法用给定的数字来缩放向量。当我们需要将速度向量乘时间间隔,来获得那个时间的行走距离时,这就有用了。

不同类型的角色拥有他们自己的类,因为他们的行为非常不同。让我们定义这些类。稍后我们将看看他们的update方法。

玩家类拥有speed属性,存储了当前速度,来模拟动量和重力。

  1. class Player {
  2. constructor(pos, speed) {
  3. this.pos = pos;
  4. this.speed = speed;
  5. }
  6. get type() { return "player"; }
  7. static create(pos) {
  8. return new Player(pos.plus(new Vec(0, -0.5)),
  9. new Vec(0, 0));
  10. }
  11. }
  12. Player.prototype.size = new Vec(0.8, 1.5);

因为玩家高度是一个半格子,因此其初始位置相比于@字符出现的位置要高出半个格子。这样一来,玩家角色的底部就可以和其出现的方格底部对齐。

size属性对于Player的所有实例都是相同的,因此我们将其存储在原型上,而不是实例本身。我们可以使用一个类似type的读取器,但是每次读取属性时,都会创建并返回一个新的Vec对象,这将是浪费的。(字符串是不可变的,不必在每次求值时重新创建。)

构造Lava角色时,我们需要根据它所基于的字符来初始化对象。动态岩浆以其当前速度移动,直到它碰到障碍物。这个时候,如果它拥有reset属性,它会跳回到它的起始位置(滴落)。如果没有,它会反转它的速度并以另一个方向继续(弹跳)。

create方法查看Level构造器传递的字符,并创建适当的岩浆角色。

  1. class Lava {
  2. constructor(pos, speed, reset) {
  3. this.pos = pos;
  4. this.speed = speed;
  5. this.reset = reset;
  6. }
  7. get type() { return "lava"; }
  8. static create(pos, ch) {
  9. if (ch == "=") {
  10. return new Lava(pos, new Vec(2, 0));
  11. } else if (ch == "|") {
  12. return new Lava(pos, new Vec(0, 2));
  13. } else if (ch == "v") {
  14. return new Lava(pos, new Vec(0, 3), pos);
  15. }
  16. }
  17. }
  18. Lava.prototype.size = new Vec(1, 1);

Coin对象相对简单,大多时候只需要待在原地即可。但为了使游戏更加有趣,我们让硬币轻微摇晃,也就是会在垂直方向上小幅度来回移动。每个硬币对象都存储了其基本位置,同时使用wobble属性跟踪图像跳动幅度。这两个属性同时决定了硬币的实际位置(存储在pos属性中)。

  1. class Coin {
  2. constructor(pos, basePos, wobble) {
  3. this.pos = pos;
  4. this.basePos = basePos;
  5. this.wobble = wobble;
  6. }
  7. get type() { return "coin"; }
  8. static create(pos) {
  9. let basePos = pos.plus(new Vec(0.2, 0.1));
  10. return new Coin(basePos, basePos,
  11. Math.random() * Math.PI * 2);
  12. }
  13. }
  14. Coin.prototype.size = new Vec(0.6, 0.6);

第十四章中,我们知道了Math.sin可以计算出圆的y坐标。因为我们沿着圆移动,因此y坐标会以平滑的波浪形式来回移动,正弦函数在实现波浪形移动中非常实用。

为了避免出现所有硬币同时上下移动,每个硬币的初始阶段都是随机的。由Math.sin产生的波长是。我们可以将Math.random的返回值乘以,计算出硬币波形轨迹的初始位置。

现在我们可以定义levelChars对象,它将平面图字符映射为背景网格类型,或角色类。

  1. const levelChars = {
  2. ".": "empty", "#": "wall", "+": "lava",
  3. "@": Player, "o": Coin,
  4. "=": Lava, "|": Lava, "v": Lava
  5. };

这给了我们创建Level实例所需的所有部件。

  1. let simpleLevel = new Level(simpleLevelPlan);
  2. console.log(`${simpleLevel.width} by ${simpleLevel.height}`);
  3. // → 22 by 9

上面一段代码的任务是将特定关卡显示在屏幕上,并构建关卡中的时间与动作。