diff --git a/16.md b/16.md
index 5832703..78240a1 100644
--- a/16.md
+++ b/16.md
@@ -85,169 +85,232 @@ class Level {
`trim`方法用于移除平面图字符串起始和终止处的空白。这允许我们的示例平面图以换行开始,以便所有行都在彼此的正下方。其余的字符串由换行符拆分,每一行扩展到一个数组中,生成了字符数组。
-因此,`rows`包含字符数组、平面图的行。我们可以从中得出水平宽度和高度。但是我们仍然必须将可移动元素与背景网格分开。我们将其称为活动元素(Actor)。它们将存储在一个对象数组中。背景将是字符串的数组的数组,持有字段类型,如`"empty"`,`"wall"`,或`"lava"`。
+因此,`rows`包含字符数组、平面图的行。我们可以从中得出水平宽度和高度。但是我们仍然必须将可移动元素与背景网格分开。我们将其称为角色(Actor)。它们将存储在一个对象数组中。背景将是字符串的数组的数组,持有字段类型,如`"empty"`,`"wall"`,或`"lava"`。
-关卡需要存储的数据有:地图宽度、高度、两个数组,其中一个数组表示网格,另一个数组表示活动元素。我们使用一个数组的数组来表示网格,使用内部数组存储水平方向的元素,方块可以包含null、空方块或一个表示方块类型的字符串(比如“wave”或“lava”)。
+为了创建这些数组,我们在行上映射,然后在它们的内容上进行映射。请记住,`map`将数组索引作为第二个参数传递给映射函数,它告诉我们给定字符的`x`和`y`坐标。游戏中的位置将存储为一对坐标,左上角为`0, 0`,并且每个背景正方形为 1 单位高和宽。
-活动元素数组负责保存对象,并跟踪关卡中动态元素的位置和状态。其中每个元素都包含以下属性:pos属性用于给定元素的当前位置(左上角坐标);size属性用于给定元素的尺寸;type属性用于保存表示元素类型的字符串(lava、coin或player)。
+为了解释平面图中的字符,`Level`构造函数使用`levelChars`对象,它将背景元素映射为字符串,角色字符映射为类。当`type`是一个角色类时,它的`create`静态方法用于创建一个对象,该对象被添加到`startActors`,映射函数为这个背景正方形返回`"empty"`。
-在构建完网格后,我们使用filter方法来查找类型为“player”的活动元素,我们将其保存为关卡的属性。status属性负责记录选手的胜负信息。当选手完成某个关卡时,我们调用finishDelay方法来确保关卡在一段时间内处于活动状态,因此我们可以显示简单的动画(如果直接重置或进入下一关会显得太简陋)。我们可以使用该方法判断某个关卡是否结束。
+角色的位置存储为一个`Vec`对象,它是二维向量,一个具有`x`和`y`属性的对象,像第六章一样。
+
+当游戏运行时,角色将停在不同的地方,甚至完全消失(就像硬币被收集时)。我们将使用一个`State`类来跟踪正在运行的游戏的状态。
```js
-Level.prototype.isFinished = function() {
- return this.status != null && this.finishDelay < 0;
-};
-```
+class State {
+ constructor(level, actors, status) {
+ this.level = level;
+ this.actors = actors;
+ this.status = status;
+ }
-### 15.5 活动元素
+ static start(level) {
+ return new State(level, level.startActors, "playing");
+ }
-为了存储活动元素的位置和尺寸,我们回顾一下之前介绍的Vector类型,该类型将x坐标和y坐标组合保存在同一个对象中。
-
-```js
-function Vector(x, y) {
- this.x = x; this.y = y;
+ get player() {
+ return this.actors.find(a => a.type == "player");
+ }
}
-Vector.prototype.plus = function(other) {
- return new Vector(this.x + other.x, this.y + other.y);
-};
-Vector.prototype.times = function(factor) {
- return new Vector(this.x * factor, this.y * factor);
-};
```
-我们可以使用times方法根据指定的数字来缩放向量。当我们需要计算特定一段时间内元素移动的距离时,可以使用该方法计算一个速度向量与时间间隔的乘积,此时该方法很实用。
+当游戏结束时,`status`属性将切换为`"lost"`或`"won"`。
-在上一节中,Level构造函数使用actorChars对象来将字符与其构造函数关联起来。该对象如下所示。
+这又是一个持久性数据结构,更新游戏状态会创建新状态,并使旧状态保持完整。
+
+### 15.5 角色
+
+角色对象表示,游戏中给定可移动元素的当前位置和状态。所有的角色对象都遵循相同的接口。它们的`pos`属性保存元素的左上角坐标,它们的`size`属性保存其大小。
+
+然后,他们有`update`方法,用于计算给定时间步长之后,他们的新状态和位置。它模拟了角色所做的事情:响应箭头键并且移动,因岩浆而来回弹跳,并返回新的更新后的角色对象。
+
+`type`属性包含一个字符串,该字符串指定了角色类型:`"player"`,`"coin"`或者`"lava"`。这在绘制游戏时是有用的,为角色绘制的矩形的外观基于其类型。
+
+角色类有一个静态的`create`方法,它由`Level`构造器使用,用于从关卡平面图中的字符中,创建一个角色。它接受字符本身及其坐标,这是必需的,因为`Lava`类处理几个不同的字符。
+
+这是我们将用于二维值的`Vec`类,例如角色的位置和大小。
```js
-var actorChars = {
- "@": Player,
- "o": Coin,
- "=": Lava, "|": Lava, "v": Lava
-};
-```
-
-我们将三个字符映射到Lava。Level构造函数将活动元素的原始字符作为第二个参数传递给构造函数,而且Lava构造函数使用该字符来调整其行为(水平移动、垂直移动或垂直下落)。
-
-我们可以使用下面的构造函数来生成表示玩家的类型。该类型包含一个名为speed的属性,用于存储当前速度,这对模拟动量和重力都非常有用。
-
-```js
-function Player(pos) {
- this.pos = pos.plus(new Vector(0, -0.5));
- this.size = new Vector(0.8, 1.5);
- this.speed = new Vector(0, 0);
+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);
+ }
}
-Player.prototype.type = "player";
+```
+
+`times`方法用给定的数字来缩放向量。当我们需要将速度向量乘时间间隔,来获得那个时间的行走距离时,这就有用了。
+
+不同类型的角色拥有他们自己的类,因为他们的行为非常不同。让我们定义这些类。稍后我们将看看他们的`update`方法。
+
+玩家类拥有`speed`属性,存储了当前速度,来模拟动量和重力。
+
+```js
+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);
```
因为玩家高度是一个半格子,因此其初始位置相比于@字符出现的位置要高出半个格子。这样一来,玩家角色的底部就可以和其出现的方格底部对齐。
-当构建动态Lava对象时,我们需要根据不同字符来初始化对象。动态的熔岩块会根据给定的速度移动,直到遇到障碍物为止。此时,如果Lava对象包含repeatPos属性,该对象会直接跳回起始位置(垂直下落)。如果不包含repeatPos属性,对象会改变速度方向,沿着原来的反方向继续移动(弹回)。构造函数仅仅设置必要属性,随后我们会编写完成实际移动的方法。
+`size`属性对于`Player`的所有实例都是相同的,因此我们将其存储在原型上,而不是实例本身。我们可以使用一个类似`type`的读取器,但是每次读取属性时,都会创建并返回一个新的`Vec`对象,这将是浪费的。(字符串是不可变的,不必在每次求值时重新创建。)
+
+构造`Lava`角色时,我们需要根据它所基于的字符来初始化对象。动态熔岩以其当前速度移动,直到它碰到障碍物。这个时候,如果它拥有`reset`属性,它会跳回到它的起始位置(滴落)。如果没有,它会反转它的速度并以另一个方向继续(弹跳)。
+
+`create`方法查看`Level`构造函数传递的字符,并创建适当的熔岩角色。
```js
-function Lava(pos, ch) {
- this.pos = pos;
- this.size = new Vector(1, 1);
- if (ch == "=") {
- this.speed = new Vector(2, 0);
- } else if (ch == "|") {
- this.speed = new Vector(0, 2);
- } else if (ch == "v") {
- this.speed = new Vector(0, 3);
- this.repeatPos = pos;
+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.type = "lava";
+
+Lava.prototype.size = new Vec(1, 1);
```
-Coin对象很简单,大多时候只需要待在原地即可。但为了使游戏更加有趣,我们让硬币轻微摇晃,也就是会在垂直方向上小幅度来回移动。每个硬币对象都存储了其基本位置,同时使用wobble属性跟踪图像跳动幅度。这两个属性同时决定了硬币的实际位置(存储在pos属性中)。
+`Coin`对象相对简单,大多时候只需要待在原地即可。但为了使游戏更加有趣,我们让硬币轻微摇晃,也就是会在垂直方向上小幅度来回移动。每个硬币对象都存储了其基本位置,同时使用wobble属性跟踪图像跳动幅度。这两个属性同时决定了硬币的实际位置(存储在pos属性中)。
```js
-function Coin(pos) {
- this.basePos = this.pos = pos.plus(new Vector(0.2, 0.1));
- this.size = new Vector(0.6, 0.6);
- this.wobble = Math.random() * Math.PI * 2;
+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.type = "coin";
+
+Coin.prototype.size = new Vec(0.6, 0.6);
```
-第13章中,我们知道了Math.sin可以计算出圆形的y坐标。因为我们沿着圆移动,因此y坐标会以平滑的波浪形式来回移动,正弦函数在实现波浪形移动中非常实用。
+第十四章中,我们知道了Math.sin可以计算出圆形的y坐标。因为我们沿着圆移动,因此y坐标会以平滑的波浪形式来回移动,正弦函数在实现波浪形移动中非常实用。
为了避免出现所有硬币同时上下移动,每个硬币的初始阶段都是随机的。由Math.sin产生的波长是2π。我们可以将Math.random的返回值乘以2π,计算出硬币波形轨迹的初始位置。
-我们现在已经编写了所有表示关卡信息的所需代码。
+现在我们可以定义`levelChars`对象,它将平面图字符映射为背景网格类型,或角色类。
```js
-var simpleLevel = new Level(simpleLevelPlan);
-console.log(simpleLevel.width, "by", simpleLevel.height);
+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
```
上面一段代码的任务是将特定关卡显示在屏幕上,并构建关卡中的时间与动作。
-### 15.6 当封装成为负担
+### 15.6 成为负担的封装
-本章中大多数代码并没有考虑进行封装。首先,封装需要耗费额外精力。封装使得程序变得更加庞大,而且会引入额外的概念和接口。我尽量将程序的体积控制在较小的范围之内,避免读者因为代码过于庞大而走神。
+本章中大多数代码并没有过多考虑封装。首先,封装需要耗费额外精力。封装使得程序变得更加庞大,而且会引入额外的概念和接口。我尽量将程序的体积控制在较小的范围之内,避免读者因为代码过于庞大而走神。
其次,游戏中的大量元素是紧密耦合在一起的,如果其中一个元素行为改变,其他的元素很有可能也会发生变化。我们需要根据游戏的工作细节来为元素之间设计大量接口。这使得接口的效果不是很好。每当你改变系统中的某一部分时,由于其他部分的接口可能没有考虑到新的情况,因此你需要关心这一修改是否会影响到其他部分的代码。
系统中的某些分割点可以通过严格的接口对系统进行合理的划分,但某些分割点则不是如此。尝试去封装某些本没有合理边界的代码必然会导致浪费大量精力。当你犯下这种大错之际,你就会注意到你的接口变得庞大臃肿,而且随着程序不断演化,你需要频繁修改这些接口。
-本章我们会封装的一部分代码是绘图子系统。其原因是我们会在下一章中使用另一种方式来展示相同的游戏。通过将绘图代码隐藏在接口之后,我们可以在下一章中使用相同的游戏程序,只需要插入新的显示模块即可。
+我们会封装的一部分代码是绘图子系统。其原因是我们会在下一章中使用另一种方式来展示相同的游戏。通过将绘图代码隐藏在接口之后,我们可以在下一章中使用相同的游戏程序,只需要插入新的显示模块即可。
### 15.7 绘图
-我们通过定义一个“显示器”对象来封装绘图代码,该对象负责显示指定的关卡。本章定义的显示器类型名为DOMDisplay,因为该类型使用简单的DOM元素来显示关卡。
+我们通过定义一个“显示器”对象来封装绘图代码,该对象显示指定关卡,以及状态。本章定义的显示器类型名为DOMDisplay,因为该类型使用简单的DOM元素来显示关卡。
我们会使用样式表来设定实际的颜色以及其他构建游戏中所需的固定的属性。创建这些属性时,我们可以直接对元素的style属性进行赋值,但这会使得游戏代码变得冗长。
-下面的工具函数提供了一种创建元素并赋予class属性的简单方式。
+下面的帮助函数提供了一种简洁的方法,来创建元素并赋予它一些属性和子节点:
```js
-function elt(name, className) {
- var elt = document.createElement(name);
- if (className) elt.className = className;
- return elt;
+function elt(name, attrs, ...children) {
+ let dom = document.createElement(name);
+ for (let attr of Object.keys(attrs)) {
+ dom.setAttribute(attr, attrs[attr]);
+ }
+ for (let child of children) {
+ dom.appendChild(child);
+ }
+ return dom;
}
```
我们创建显示器对象时需要指定其父元素,显示器将会创建在该父元素上,同时还需指定一个关卡对象。
```js
-function DOMDisplay(parent, level) {
- this.wrap = parent.appendChild(elt("div", "game"));
- this.level = level;
+class DOMDisplay {
+ constructor(parent, level) {
+ this.dom = elt("div", {class: "game"}, drawGrid(level));
+ this.actorLayer = null;
+ parent.appendChild(this.dom);
+ }
- this.wrap.appendChild(this.drawBackground());
- this.actorLayer = null;
- this.drawFrame();
+ clear() { this.dom.remove(); }
}
```
-由于appendChild会返回新添加的元素,因此我们使用其返回值创建整个游戏的包装器(wrapper)元素,并将其存储在wrap属性中,整个过程只需要一条语句。
+由于关卡的背景网格不会改变,因此只需要绘制一次即可。角色则需要在每次刷新显示时进行重绘。drawFame需要使用actorLayer属性来跟踪保存角色的动作,因此我们可以轻松移除或替换这些角色。
-由于关卡背景不会改变,因此只需要绘制一次即可。活动元素则需要在每次刷新显示时进行重绘。drawFame需要使用actorLayer属性来跟踪保存活动元素的动作,因此我们可以轻松移除或替换这些活动元素。
-
-地图坐标和尺寸以一个单元格尺寸为单位,也就是说尺寸或距离中的1单元表示一个单元格。在设置像素级尺寸时,我们需要将坐标按比例放大,如果游戏中的所有元素只占据一个方格中的一个像素,那将是多么可笑。而scale变量会给出一个单元格在屏幕上实际占据的像素数目。
+我们的坐标和尺寸以网格单元为单位跟踪,也就是说尺寸或距离中的1单元表示一个单元格。在设置像素级尺寸时,我们需要将坐标按比例放大,如果游戏中的所有元素只占据一个方格中的一个像素,那将是多么可笑。而scale变量会给出一个单元格在屏幕上实际占据的像素数目。
```js
-var scale = 20;
+const scale = 20;
-DOMDisplay.prototype.drawBackground = function() {
- var table = elt("table", "background");
- table.style.width = this.level.width * scale + "px";
- this.level.grid.forEach(function(row) {
- var rowElt = table.appendChild(elt("tr"));
- rowElt.style.height = scale + "px";
- row.forEach(function(type) {
- rowElt.appendChild(elt("td", type));
- });
- });
- return table;
-};
+function drawGrid(level) {
+ return elt("table", {
+ class: "background",
+ style: `width: ${level.width * scale}px`
+ }, ...level.rows.map(row =>
+ elt("tr", {style: `height: ${scale}px`},
+ ...row.map(type => elt("td", {class: type})))
+ ));
+}
```
-前文提及过,我们使用
元素来绘制背景。这非常符合关卡中grid属性的结构。网格中的每一行对应表格中的一行(元素)。网格中的每个字符串对应表格单元格()元素的类型名。下面的CSS代码帮助我们利用生成的表格绘制出关卡背景。
+前文提及过,我们使用元素来绘制背景。这非常符合关卡中grid属性的结构。网格中的每一行对应表格中的一行(元素)。网格中的每个字符串对应表格单元格()元素的类型名。扩展(三点)运算符用于将子节点数组作为单独的参数传给`elt`。
+
+下面的 CSS 使表格看起来像我们想要的背景:
```css
.background { background: rgb(52, 166, 251);
@@ -258,28 +321,26 @@ DOMDisplay.prototype.drawBackground = function() {
.wall { background: white; }
```
-其中某些属性(border-spacing和padding)只是用来取消一些我们不想保留的表格默认行为。我们不希望在单元格之间或单元格内部填充多余的空白。
+其中某些属性(border-spacing和padding)用于取消一些我们不想保留的表格默认行为。我们不希望在单元格之间或单元格内部填充多余的空白。
其中background规则用于设置背景颜色。CSS中可以使用两种方式来指定颜色,一种方法是使用单词(white),另一种方法是使用形如rgb(R,G,B)的格式,其中R表示颜色中的红色成分,G表示绿色成分,B表示蓝色成分,每个数字范围均为0到255。因此在rgb(52,166,251)中,红色成分为52,绿色为166,而蓝色是251。由于蓝色成分数值最大,因此最后的颜色会偏向蓝色。而你可以看到.lava规则中,第一个数字(红色)是最大的。
-我们绘制每个活动元素时需要创建其对应的DOM元素,并根据活动元素属性来设置元素坐标与尺寸。这些值都需要与scale相乘,以将游戏中的尺寸单位转换为像素。
+我们绘制每个角色时需要创建其对应的DOM元素,并根据角色属性来设置元素坐标与尺寸。这些值都需要与scale相乘,以将游戏中的尺寸单位转换为像素。
```js
-DOMDisplay.prototype.drawActors = function() {
- var wrap = elt("div");
- this.level.actors.forEach(function(actor) {
- var rect = wrap.appendChild(elt("div",
- "actor " + actor.type));
- rect.style.width = actor.size.x * scale + "px";
- rect.style.height = actor.size.y * scale + "px";
- rect.style.left = actor.pos.x * scale + "px";
- rect.style.top = actor.pos.y * scale + "px";
- });
- return wrap;
-};
+function drawActors(actors) {
+ return elt("div", {}, ...actors.map(actor => {
+ let rect = elt("div", {class: `actor ${actor.type}`});
+ rect.style.width = `${actor.size.x * scale}px`;
+ rect.style.height = `${actor.size.y * scale}px`;
+ rect.style.left = `${actor.pos.x * scale}px`;
+ rect.style.top = `${actor.pos.y * scale}px`;
+ return rect;
+ }));
+}
```
-为了赋予一个元素多个类别,我们使用空格来分隔类名。在下面展示的CSS代码中,actor类型会赋予活动元素一个绝对坐标。我们将活动元素的类型名称作为额外的CSS类来设置这些元素的颜色。我们并没有再次定义lava类,因为我们可以直接复用前文为熔岩单元格定义的规则。
+为了赋予一个元素多个类别,我们使用空格来分隔类名。在下面展示的CSS代码中,actor类型会赋予角色一个绝对坐标。我们将角色的类型名称作为额外的CSS类来设置这些元素的颜色。我们并没有再次定义lava类,因为我们可以直接复用前文为熔岩单元格定义的规则。
```css
.actor { position: absolute; }
@@ -287,15 +348,15 @@ DOMDisplay.prototype.drawActors = function() {
.player { background: rgb(64, 64, 64); }
```
-每次刷新显示时,drawFrame方法首先移除旧有的活动元素,如果需要则将其重绘在新位置。你可能想直接复用活动元素的DOM元素,但我们如果要这么做,需要在显示代码和模拟代码中加入大量的额外信息流。我们需要将活动元素与DOM元素关联起来,而绘图代码需要在活动元素消失时移除对应的DOM元素。由于游戏中活动元素数量不多,因此直接重绘所有元素的代码并不高昂。
+`setState`方法用于使显示器显示给定的状态。它首先删除旧角色的图形,如果有的话,然后在他们的新位置上重新绘制角色。试图将 DOM 元素重用于角色,可能很吸引人,但是为了使它有效,我们需要大量的附加记录,来关联角色和 DOM 元素,并确保在角色消失时删除元素。因为游戏中通常只有少数角色,重新绘制它们开销并不大。
```js
-DOMDisplay.prototype.drawFrame = function() {
- if (this.actorLayer)
- this.wrap.removeChild(this.actorLayer);
- this.actorLayer = this.wrap.appendChild(this.drawActors());
- this.wrap.className = "game " + (this.level.status || "");
- this.scrollPlayerIntoView();
+DOMDisplay.prototype.setState = function(state) {
+ if (this.actorLayer) this.actorLayer.remove();
+ this.actorLayer = drawActors(state.actors);
+ this.dom.appendChild(this.actorLayer);
+ this.dom.className = `game ${state.status}`;
+ this.scrollPlayerIntoView(state);
};
```
@@ -312,7 +373,7 @@ DOMDisplay.prototype.drawFrame = function() {
在遇到熔岩之后,玩家的颜色应该变成深红色,暗示着角色被烧焦了。当玩家收集完最后一枚硬币时,我们使用两个模糊的白色盒装阴影来创建白色的光环效果,其中一个在左上角,一个在右上角。
-我们无法假定关卡总是符合视口尺寸。所以我们需要调用scrollPlayerIntoVirw来确保如果关卡在视口范围之外,我们可以滚动视口,确保玩家靠近视口的中央位置。下面的CSS样式为包装器的DOM元素设置了一个最大尺寸,以确保任何超出视口的元素都是不可见的。我们可以将外部元素的position设置为relative,因此该元素中的活动元素总是相对于关卡的左上角进行定位。
+我们无法假定关卡总是符合视口尺寸。所以我们需要调用scrollPlayerIntoVirw来确保如果关卡在视口范围之外,我们可以滚动视口,确保玩家靠近视口的中央位置。下面的CSS样式为包装器的DOM元素设置了一个最大尺寸,以确保任何超出视口的元素都是不可见的。我们可以将外部元素的position设置为relative,因此该元素中的角色总是相对于关卡的左上角进行定位。
```css
.game {
@@ -379,7 +440,7 @@ DOMDisplay.prototype.clear = function() {
### 15.8 动作与冲突
-现在我们是时候来添加一些动作了。这是游戏中最令人着迷的一部分。实现动作的最基本的方案(也是大多数游戏采用的)是将时间划分为一个个时间段,根据活动元素的每一步速度(每秒的移动距离)和时间长度(以秒为单位)将元素移动一段距离。
+现在我们是时候来添加一些动作了。这是游戏中最令人着迷的一部分。实现动作的最基本的方案(也是大多数游戏采用的)是将时间划分为一个个时间段,根据角色的每一步速度(每秒的移动距离)和时间长度(以秒为单位)将元素移动一段距离。
这其实非常简单。比较困难的一部分是处理元素之间的相互作用。当玩家撞到墙壁或者地板时,不可能简单地直接穿越过去。游戏必须注意特定的动作会导致两个对象产生碰撞,并需要采取相应措施。如果玩家遇到墙壁,则必须停下来,如果遇到硬币则必须将其收集起来。
@@ -417,9 +478,9 @@ Level.prototype.obstacleAt = function(pos, size) {
如果元素处于关卡地图之外且触碰到左右边界或上边界我们返回“wall”,如果触碰到下底则返回“lava”。这可以确保玩家掉出世界时死亡。当元素完全在网格中时,我们循环扫描其覆盖的网格,并返回第一个找到的非空白方格。
-若玩家和其他动态元素(硬币或移动的熔岩块)产生碰撞,需要在玩家移动后进行处理。当玩家的动作导致玩家与其他活动元素相撞,游戏会触发相应的动作,即收集硬币或死亡。
+若玩家和其他动态元素(硬币或移动的熔岩块)产生碰撞,需要在玩家移动后进行处理。当玩家的动作导致玩家与其他角色相撞,游戏会触发相应的动作,即收集硬币或死亡。
-该方法的参数是一个活动元素,负责扫描活动元素数组并找出与参数中的活动元素重叠的活动元素。
+该方法的参数是一个角色,负责扫描角色数组并找出与参数中的角色重叠的角色。
```js
Level.prototype.actorAt = function(actor) {
@@ -435,9 +496,9 @@ Level.prototype.actorAt = function(actor) {
};
```
-### 15.9 活动元素与动作
+### 15.9 角色与动作
-Level方法中的animate方法让关卡中的每个活动元素都有一次移动的机会。其step参数为时间间隔,以秒为单位。keys对象包含了玩家按下的所有方向键的信息。
+Level方法中的animate方法让关卡中的每个角色都有一次移动的机会。其step参数为时间间隔,以秒为单位。keys对象包含了玩家按下的所有方向键的信息。
```js
var maxStep = 0.05;
@@ -460,7 +521,7 @@ Level.prototype.animate = function(step, keys) {
while循环将动画划分为合理的一段段动画。其确保每一步的时间间隔不大于maxStep。比如,如果step为0.12秒,我们会将其划分为3段动画,前两段都是0.05秒,第三段为0.02秒。
-活动对象有一个act方法,该方法有三个参数,分别为时间间隔、level对象和keys对象。这里给出Lava活动元素类型的act方法,该方法忽略了keys对象。
+活动对象有一个act方法,该方法有三个参数,分别为时间间隔、level对象和keys对象。这里给出Lava角色类型的act方法,该方法忽略了keys对象。
```js
Lava.prototype.act = function(step, level) {
@@ -558,7 +619,7 @@ Player.prototype.act = function(step, level, keys) {
};
```
-在移动之后,该方法检查玩家是否与其他活动元素碰撞,当找到产生碰撞的元素时再次调用playerTouched。此时,我们将活动对象作为第二个参数,因为如果另一个活动元素是硬币,playerTouched需要知道玩家应该收集那枚硬币。
+在移动之后,该方法检查玩家是否与其他角色碰撞,当找到产生碰撞的元素时再次调用playerTouched。此时,我们将活动对象作为第二个参数,因为如果另一个角色是硬币,playerTouched需要知道玩家应该收集那枚硬币。
最后,当玩家死亡(碰到熔岩),我们开始一段简短的动画,通过减少玩家对象的高度来实现玩家缩小下沉的动画效果。
@@ -581,7 +642,7 @@ Level.prototype.playerTouched = function(type, actor) {
};
```
-当玩家碰到熔岩,我们将游戏状态设置为“lost”。当遇到硬币时,我们将该硬币从活动元素数组中移除,如果移除的是最后一枚硬币,则将游戏状态设置为“won”。
+当玩家碰到熔岩,我们将游戏状态设置为“lost”。当遇到硬币时,我们将该硬币从角色数组中移除,如果移除的是最后一枚硬币,则将游戏状态设置为“won”。
这些代码实现了一个确实可以产生动画的关卡。现在唯一缺少的就是驱动这些动画的代码。
| |