角色
角色对象表示,游戏中给定可移动元素的当前位置和状态。所有的角色对象都遵循相同的接口。它们的pos
属性保存元素的左上角坐标,它们的size
属性保存其大小。
然后,他们有update
方法,用于计算给定时间步长之后,他们的新状态和位置。它模拟了角色所做的事情:响应箭头键并且移动,因岩浆而来回弹跳,并返回新的更新后的角色对象。
type
属性包含一个字符串,该字符串指定了角色类型:"player"
,"coin"
或者"lava"
。这在绘制游戏时是有用的,为角色绘制的矩形的外观基于其类型。
角色类有一个静态的create
方法,它由Level
构造器使用,用于从关卡平面图中的字符中,创建一个角色。它接受字符本身及其坐标,这是必需的,因为Lava
类处理几个不同的字符。
这是我们将用于二维值的Vec
类,例如角色的位置和大小。
class Vec {
constructor(x, y) {
this.x = x; this.y = y;
}
plus(other) {
return new Vec(this.x + other.x, this.y + other.y);
}
times(factor) {
return new Vec(this.x * factor, this.y * factor);
}
}
times
方法用给定的数字来缩放向量。当我们需要将速度向量乘时间间隔,来获得那个时间的行走距离时,这就有用了。
不同类型的角色拥有他们自己的类,因为他们的行为非常不同。让我们定义这些类。稍后我们将看看他们的update
方法。
玩家类拥有speed
属性,存储了当前速度,来模拟动量和重力。
class Player {
constructor(pos, speed) {
this.pos = pos;
this.speed = speed;
}
get type() { return "player"; }
static create(pos) {
return new Player(pos.plus(new Vec(0, -0.5)),
new Vec(0, 0));
}
}
Player.prototype.size = new Vec(0.8, 1.5);
因为玩家高度是一个半格子,因此其初始位置相比于@
字符出现的位置要高出半个格子。这样一来,玩家角色的底部就可以和其出现的方格底部对齐。
size
属性对于Player
的所有实例都是相同的,因此我们将其存储在原型上,而不是实例本身。我们可以使用一个类似type
的读取器,但是每次读取属性时,都会创建并返回一个新的Vec
对象,这将是浪费的。(字符串是不可变的,不必在每次求值时重新创建。)
构造Lava
角色时,我们需要根据它所基于的字符来初始化对象。动态岩浆以其当前速度移动,直到它碰到障碍物。这个时候,如果它拥有reset
属性,它会跳回到它的起始位置(滴落)。如果没有,它会反转它的速度并以另一个方向继续(弹跳)。
create
方法查看Level
构造器传递的字符,并创建适当的岩浆角色。
class Lava {
constructor(pos, speed, reset) {
this.pos = pos;
this.speed = speed;
this.reset = reset;
}
get type() { return "lava"; }
static create(pos, ch) {
if (ch == "=") {
return new Lava(pos, new Vec(2, 0));
} else if (ch == "|") {
return new Lava(pos, new Vec(0, 2));
} else if (ch == "v") {
return new Lava(pos, new Vec(0, 3), pos);
}
}
}
Lava.prototype.size = new Vec(1, 1);
Coin
对象相对简单,大多时候只需要待在原地即可。但为了使游戏更加有趣,我们让硬币轻微摇晃,也就是会在垂直方向上小幅度来回移动。每个硬币对象都存储了其基本位置,同时使用wobble
属性跟踪图像跳动幅度。这两个属性同时决定了硬币的实际位置(存储在pos
属性中)。
class Coin {
constructor(pos, basePos, wobble) {
this.pos = pos;
this.basePos = basePos;
this.wobble = wobble;
}
get type() { return "coin"; }
static create(pos) {
let basePos = pos.plus(new Vec(0.2, 0.1));
return new Coin(basePos, basePos,
Math.random() * Math.PI * 2);
}
}
Coin.prototype.size = new Vec(0.6, 0.6);
第十四章中,我们知道了Math.sin
可以计算出圆的y
坐标。因为我们沿着圆移动,因此y
坐标会以平滑的波浪形式来回移动,正弦函数在实现波浪形移动中非常实用。
为了避免出现所有硬币同时上下移动,每个硬币的初始阶段都是随机的。由Math.sin
产生的波长是2π
。我们可以将Math.random
的返回值乘以2π
,计算出硬币波形轨迹的初始位置。
现在我们可以定义levelChars
对象,它将平面图字符映射为背景网格类型,或角色类。
const levelChars = {
".": "empty", "#": "wall", "+": "lava",
"@": Player, "o": Coin,
"=": Lava, "|": Lava, "v": Lava
};
这给了我们创建Level
实例所需的所有部件。
let simpleLevel = new Level(simpleLevelPlan);
console.log(`${simpleLevel.width} by ${simpleLevel.height}`);
// → 22 by 9
上面一段代码的任务是将特定关卡显示在屏幕上,并构建关卡中的时间与动作。