角色
角色对象表示,游戏中给定可移动元素的当前位置和状态。所有的角色对象都遵循相同的接口。它们的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
上面一段代码的任务是将特定关卡显示在屏幕上,并构建关卡中的时间与动作。
