1
0
mirror of https://github.com/apachecn/eloquent-js-3e-zh.git synced 2025-05-29 16:22:22 +00:00
This commit is contained in:
wizardforcel 2018-05-31 18:30:24 +08:00
parent fbd9b88183
commit 65f4b6b655

96
16.md
View File

@ -4,23 +4,23 @@
>
> Iain Banks《The Player of Games》
就像许多小孩一样,我最初之所以对计算机感到着迷,都是始于计算机游戏。我沉迷在那个计算机所模拟出的小小世界中,我可以操纵这个世界,我同时也沉迷在那些尚未展开的故事之中。但我沉迷其中并不是因为游戏实际描述的故事,而是因为我可以充分发挥我的想象力,去构思故事的发展。
我最初对电脑的痴迷,就像许多小孩一样,与电脑游戏有关。我沉迷在那个计算机所模拟出的小小世界中,我可以操纵这个世界,我同时也沉迷在那些尚未展开的故事之中。但我沉迷其中并不是因为游戏实际描述的故事,而是因为我可以充分发挥我的想象力,去构思故事的发展。
我并不希望任何人把编写游戏作为自己的事业。就像音乐产业中,那些希望加入这个行业的热忱年轻人与实际的人才需求之间存在巨大的鸿沟,也因此产生了一个极不健康的就业环境。不过,把编写游戏作为乐趣还是相当不错的。
本章将会介绍如何实现一个简单的平台游戏。平台游戏(或者叫作“跳爬”游戏)要求玩家操纵一个角色在世界中移动,这种游戏往往是二维的,而且采用单一侧面作为观察视角,玩家需要频繁在世界的物体中来回跳跃。
本章将会介绍如何实现一个小型平台游戏。平台游戏(或者叫作“跳爬”游戏)要求玩家操纵一个角色在世界中移动,这种游戏往往是二维的,而且采用单一侧面作为观察视角,玩家可以来回跳跃。
### 15.1 游戏
我们游戏大致基于由Thomas Palef开发的Dark Blue[www.lessmilk.com/games/10](http://www.lessmilk.com/games/10))。我之所以选择了这个游戏,是因为这个游戏既有趣又简单,而且不需要编写大量代码。该游戏看起来如下页图所示。
黑色的方块表示玩家,玩家任务是收集黄色的方块(硬币),同时避免碰到红色素材(我们取名为“熔浆”)。当玩家收集完所有硬币后就可以过关。
![The game Dark Blue](img/darkblue.png)
黑色的方块表示玩家,玩家任务是收集黄色的方块(硬币),同时避免碰到红色素材(“岩浆”)。当玩家收集完所有硬币后就可以过关。
玩家可以使用左右方向键移动,并使用上方向键跳跃。跳跃正是这个游戏角色的特长。玩家可以跳跃到数倍于自己身高的地方,也可以在半空中改变方向。虽然这样不切实际,但这有助于玩家感觉自己在直接控制屏幕上那个自己的化身。
![](../Images/00437.jpeg)
该游戏包含一个固定的背景使用网格方式进行布局可移动的元素则覆盖在背景之上。网格中的元素可能是空气、固体或岩浆。可移动的元素是玩家、硬币或者某一块岩浆。不同于第7章中模拟的人造生命这些元素的位置不必局限在网格中。其坐标可以是小数这样元素可以在场景中平滑移动。
该游戏包含一个固定的背景,使用网格方式进行布局,可可移动元素则覆盖在背景之上。网格中的元素可能是空气、固体或岩浆。可可移动元素是玩家、硬币或者某一块岩浆。这些元素的位置不限于网格,它们的坐标可以是分数,允许平滑运动。
### 15.2 实现技术
@ -28,76 +28,64 @@
与屏幕和键盘相关的代码只是实现游戏代码中的很小一部分。由于所有元素都只是彩色方块因此绘制方法并不复杂。我们为每个元素创建对应的DOM元素并使用样式来为其指定背景颜色、尺寸和位置。
由于背景是由不会改变的方块组成的网格,因此我们可以使用表格来展示背景。自由移动元素可以使用绝对定位元素来覆盖在背景之上
由于背景是由不会改变的方块组成的网格,因此我们可以使用表格来展示背景。自由移动元素可以使用绝对定位元素来覆盖。
游戏和某些程序需要在不产生明显延迟的情况下绘制动画并响应用户输入性能是非常重要的。尽管DOM最初并非为高性能绘图而设计但实际上DOM的性能表现得比我们想象中要好得多。读者已经在第13章中看过一些动画在现代机器中即使我们不怎么考虑性能优化像这种简单的游戏也可以流畅运行。
游戏和某些程序应该在不产生明显延迟的情况下绘制动画并响应用户输入性能是非常重要的。尽管DOM最初并非为高性能绘图而设计但实际上DOM的性能表现得比我们想象中要好得多。读者已经在第13章中看过一些动画在现代机器中即使我们不怎么考虑性能优化像这种简单的游戏也可以流畅运行。
在下一章中,我们会研究另一种浏览器技术——<canvas>标签。该标签提供了一种更为传统的图像绘制方式直接处理形状和像素而非DOM元素。
在下一章中,我们会研究另一种浏览器技术——<canvas>标签。该标签提供了一种更为传统的图像绘制方式直接处理形状和像素而非DOM元素。
### 15.3 关卡
在第7章中我们使用数组和字符串来描述一张二维网格。我们这里可以采取同样的做法。这样我们不需要提前构建关卡编辑器就可以直接设计关卡
我们需要一种人类可读的、可编辑的方法来指定关卡。因为一切最开始都可以在网格,所以我们可以使用大型字符串,其中每个字符代表一个元素,要么是背景网格的一部分,要么是可移动元素
一个简单的关卡如下所示。
小型关卡的平面图可能是这样的:
```js
var simpleLevelPlan = [
" ",
" ",
" x = x ",
" x o o x ",
" x @ xxxxx x ",
" xxxxx x ",
" x!!!!!!!!!!!!x ",
" xxxxxxxxxxxxxx ",
" "
];
var simpleLevelPlan = `
......................
..#................#..
..#..............=.#..
..#.........o.o....#..
..#.@......#####...#..
..#####............#..
......#++++++++++++#..
......##############..
......................`;
```
这张平面图中包含了所有的固定网格和可移动元素。字符x表示墙壁空格表示空气而感叹号表示固定不会移动的熔岩
句号是空的位置,井号(`#`)字符是墙,加号是岩浆。玩家的起始位置是 AT 符号(`@`)。每个`O`字符都是一枚硬币,等号(`=`)是一块来回水平移动的熔岩块
字符@定义了玩家的起始位置。每个字符o是一枚硬币等号=)代表会在水平方向来回移动的熔岩块。你需要注意,我们将这些位置对应的网格设置为空气,并使用另一个数据结构来跟踪可移动元素的位置。
我们支持两种其他类型的可移动熔岩:管道符号(|表示在垂直方向移动的熔岩块而v表示下落的熔岩块——这种熔岩块也是垂直移动但不会来回弹跳只会向下移动直到遇到地面才会直接回到其起始位置。
我们支持两种额外的可移动熔岩:管道符号(`|`)表示垂直移动的熔岩块,而`v`表示下落的熔岩块——这种熔岩块也是垂直移动,但不会来回弹跳,只会向下移动,直到遇到地面才会直接回到其起始位置。
整个游戏包含了许多关卡,玩家必须完成所有关卡。每关的过关条件是玩家需要收集所有硬币。如果玩家碰到熔岩,当前关卡会恢复初始状态,而玩家可以再次尝试过关。
### 15.4 读取关卡
下面的构造函数用于构造Level对象。这个构造函数的参数是一个定义关卡的数组
下面的类存储了关卡对象。它的参数应该是定义关卡的字符串
```js
function Level(plan) {
this.width = plan[0].length;
this.height = plan.length;
this.grid = [];
this.actors = [];
for (var y = 0; y < this.height; y++) {
var line = plan[y], gridLine = [];
for (var x = 0; x < this.width; x++) {
var ch = line[x], fieldType = null;
var Actor = actorChars[ch];
if (Actor)
this.actors.push(new Actor(new Vector(x, y), ch));
else if (ch == "x")
fieldType = "wall";
else if (ch == "!")
fieldType = "lava";
gridLine.push(fieldType);
class Level {
constructor(plan) {
let rows = plan.trim().split("\n").map(l => [...l]);
this.height = rows.length;
this.width = rows[0].length;
this.startActors = [];
this.rows = rows.map((row, y) => {
return row.map((ch, x) => {
let type = levelChars[ch];
if (typeof type == "string") return type;
this.startActors.push(
type.create(new Vec(x, y), ch));
return "empty";
});
});
}
this.grid.push(gridLine);
}
this.player = this.actors.filter(function(actor) {
return actor.type == "player";
})[0];
this.status = this.finishDelay = null;
}
```
`trim`方法用于移除平面图字符串起始和终止处的空白。这允许我们的示例平面图以换行开始,以便所有行都在彼此的正下方。其余的字符串由换行符拆分,每一行扩展到一个数组中,生成了字符数组。
为了简洁明了,代码中并未检查有问题的输入。代码中假定调用者给出的关卡平面图正确无误,且包含玩家的起始位置和其他的关键元素
因此,`rows`包含字符数组、平面图的行。我们可以从中得出水平宽度和高度。但是我们仍然必须将可移动元素与背景网格分开。我们将其称为活动元素Actor。它们将存储在一个对象数组中。背景将是字符串的数组的数组持有字段类型`"empty"``"wall"`,或`"lava"`
关卡需要存储的数据有地图宽度、高度、两个数组其中一个数组表示网格另一个数组表示活动元素。我们使用一个数组的数组来表示网格使用内部数组存储水平方向的元素方块可以包含null、空方块或一个表示方块类型的字符串比如“wave”或“lava”
@ -259,7 +247,7 @@ DOMDisplay.prototype.drawBackground = function() {
};
```
前文提及过,我们使用&lt;table&gt;元素来绘制背景。这非常符合关卡中grid属性的结构。网格中的每一行对应表格中的一行&lt;tr&gt;元素)。网格中的每个字符串对应表格单元格(&lt;td&gt;元素的类型名。下面的CSS代码帮助我们利用生成的表格绘制出关卡背景。
前文提及过,我们使用<table>元素来绘制背景。这非常符合关卡中grid属性的结构。网格中的每一行对应表格中的一行<tr>元素)。网格中的每个字符串对应表格单元格(<td>元素的类型名。下面的CSS代码帮助我们利用生成的表格绘制出关卡背景。
```css
.background { background: rgb(52, 166, 251);