JavaScript 游戏构建指南(六)
二十七、聪明的敌人
作为开发滴答滴答游戏的下一步,让我们通过添加危险的敌人给玩家带来一些危险。如果玩家接触到敌人,玩家死亡。敌人通常不受玩家控制(那会让事情变得太容易)。因此,你需要定义某种聪明(或者愚蠢)的行为。你不希望这些敌人太聪明:玩家应该能完成关卡。毕竟,这是玩游戏的目标:赢得游戏。好的是,你可以建立不同类型的敌人,表现出不同类型的行为。因此,玩家有不同的游戏选项,必须制定不同的策略来完成关卡。
定义敌人的行为会导致一些非常复杂的代码,有许多不同的状态、推理、路径规划等等。在这一章中,你会看到一些不同类型的敌人:一枚火箭,一只打喷嚏的乌龟(说真的),斯巴基,和几个不同的巡逻敌人。这一章并不涉及玩家应该如何与敌人互动——你只需要定义他们的基本行为。
火箭
最基本的敌人之一是火箭。一枚火箭从屏幕的一边飞到另一边,过了一段时间后再次出现。如果玩家接触到火箭,玩家就会死亡。在关卡描述中,你用 r 和 R 人物来表示一个火箭敌人应该被放置在一个关卡中。例如,考虑以下级别描述:
window.LEVELS.push({
hint : "Many, many, many, many, many rockets...",
locked : true,
solved : false,
tiles : ["....................",
"r..W...........X....",
"...--..W.......--...",
"....W.--........W..R",
"...--..........--...",
"r..W......W....W....",
"...--....--....--...",
"....W...........W...",
"...--........W.--...",
"r..W........--.W....",
"...--..........--...",
"....W...........W..R",
"...--..........--...",
".1..................",
"######..####..######"]
});
小写的 r 表示火箭应该从左往右飞,大写的 R 表示应该从右往左飞(参见表 24-1 )。
制造和重置火箭
让我们创建一个代表这种特殊敌人的Rocket类。您继承了AnimatedGameObject类,因为火箭是动画的。在构造函数中,初始化Rocket对象。您需要加载火箭动画并播放它,然后您需要检查动画是否应该镜像。因为动画中火箭向右移动,如果火箭向左移动,您需要镜像它。您还可以存储火箭的起始位置,这样当它移出屏幕时,您可以将它放回那个位置。最后,您需要一个变量spawnTime来跟踪火箭应该何时出现。这是完整的构造函数:
function Rocket(moveToLeft, startPosition, layer, id) {
powerupjs.AnimatedGameObject.call(this, layer, id);
this.spawnTime = 0;
this.startPosition = startPosition;
this.mirror = moveToLeft;
this.loadAnimation(sprites.rocket, "default", true, 0.5);
this.playAnimation("default");
this.origin = new powerupjs.Vector2(this.width / 2, this.height);
this.reset();
}
构造函数中的最后一条指令是对reset方法的调用。在这个方法中,您将火箭的当前位置设置为起始位置,将可见性设置为false(因此火箭最初是不可见的),并将速度设置为零。您还可以使用随机数生成器来计算一个随机时间(以秒为单位),在该时间之后火箭应该出现并开始移动。你把这个时间存储在成员变量spawnTime中。您将这些指令放在一个单独的reset方法中,因为您稍后也会调用这个方法,在火箭飞出屏幕之后。
编程火箭行为
火箭的行为(像往常一样)编码在update方法中。基本上,火箭表现出两种主要类型的行为:要么它是可见的并从屏幕的一端移动到另一端,要么它是不可见的并等待出现。通过查看spawnTime变量的值,可以确定火箭处于两种状态中的哪一种。如果这个变量包含一个大于零的值,火箭就在等待生成。如果该值小于或等于零,则火箭可见,并从屏幕的一端移动到另一端。
我们来看第一种情况。如果火箭正在等待产卵,您只需从产卵时间中减去自最后一次update调用以来已经过去的时间:
if (this.spawnTime > 0) {
this.spawnTime -= delta;
return;
}
第二种情况稍微复杂一些。火箭从屏幕的一端移动到另一端。因此,您将可见性状态设置为true,根据火箭移动的方向计算其速度,并更新其位置:
this.visible = true;
this.velocity.x = 600;
if (this.mirror)
this.velocity.x *= -1;
最后,你要检查火箭是否已经飞出屏幕。如果是这样的话,火箭应该重置。你可以使用边界框来检查火箭是否在屏幕之外。如果包围屏幕的边界框不与火箭的边界框相交,您知道火箭在屏幕之外,并重置它:
var screenBox = new powerupjs.Rectangle(0, 0, powerupjs.Game.size.x,
powerupjs.Game.size.y);
if (!screenBox.intersects(this.boundingBox))
this.reset();
这就完成了Rocket类,除了与玩家的交互,这是你在下一章会更详细看到的。有关完整的类,请参见属于本章的TickTick3示例代码。图 27-1 显示了本章第一节中定义的级别的屏幕截图。
图 27-1 。一个有许多火箭飞来飞去的关卡
巡逻的敌人
火箭是一种基本上没有智能行为的敌人。它从左向右飞,或者从右向左飞,直到飞出屏幕,然后它自己复位。你也可以添加稍微聪明一点的敌人,比如一个巡逻的敌人。让我们设置一些不同类型的巡逻敌人,你可以添加到游戏中。
基本的巡逻敌人类
PatrollingEnemy类类似于Rocket类。你希望巡逻的敌人被动画化,所以它从AnimatedGameObject类继承而来。你还需要在被覆盖的update方法中定义敌人的行为。巡逻的敌人的基本行为是从左到右再走回来。如果敌人到达一个缺口或一面墙砖,敌人停止行走,等待一段时间,然后转身。你可以在关卡中的任意位置放置敌人。对于玩家来说,你需要定义一些基本的物理概念,比如下落和跳跃。你不会为PatrollingEnemy类这么做,因为你为这个游戏定义的敌人只会从左到右来回走动。
在PatrollingEnemy类的构造函数中,你为巡逻的敌人角色加载主动画(一个愤怒的火焰,如图图 27-2 所示)。最初,你设置一个正的速度,这样敌人开始向右走。你还初始化了另一个名为_waitTime的成员变量,它记录了敌人在它行走的平台的一边等待了多久:
this._waitTime = 0;
this.velocity.x = 120;
this.loadAnimation(sprites.flame, "default", true);
this.playAnimation("default");
图 27-2 。几个巡逻的敌人
在update方法中,你要区分两种情况:敌人是在走还是在等。您可以通过查看_waitTime变量来区分这些状态。如果这个变量包含正值,说明敌人在等待。如果变量包含零或更小的值,则敌人正在行走。当敌人在等待时,你不必做太多。就像你在Rocket类中做的一样,你从_waitTime变量中减去游戏时间。如果等待时间已经到了零,你需要把角色转过来。下面是实现这一点的代码:
if (this._waitTime > 0) {
this._waitTime -= delta;
if (this._waitTime <= 0)
this.turnAround();
}
turnAround方法简单地反转速度并镜像动画:
PatrollingEnemy.prototype.turnAround = function () {
this.mirror = !this.mirror;
this.velocity.x = 120;
if (this.mirror)
this.velocity.x = -this.velocity.x;
};
如果敌人现在正在行走,而不是等待,你需要发现它是否已经到达它行走的平台的边缘。它在两种情况下达到了一个边缘:要么是有一个缺口,所以敌人无法进一步移动,要么是一个墙砖挡住了去路。你使用敌人的包围盒来找到这些信息。如果敌人向左走,你检查最左边的 x 值是否已经到达墙砖或平台的边界。如果敌人向右走,你检查最右边的 x 值。您可以如下计算这个 x 值:
var tiles = this.root.find(ID.tiles);
var posX = this.boundingBox.left;
if (!this.mirror)
posX = this.boundingBox.right;
现在,您计算这个 x 值所在的区块。你可以通过将 x 值除以图块的宽度来计算。为了确保您总是得到正确的(下限)瓦片索引,您使用了Math.floor方法:
var tileX = Math.floor(posX / tiles.cellWidth);
用类似的方法,你可以计算出敌人当前所站的那张牌的 y 指数:。
var tileY = Math.floor(this.position.y / tiles.cellHeight);
注意,因为你用雪碧的底来代表敌人的位置,所以你得到的 y 指数就是敌人下面的那个平铺的指数。
接下来你必须检查敌人是否已经到达墙砖或平台的边界。如果计算出的索引处的牌是背景牌,则敌人已经到达平台的边界,必须停止行走。如果索引(tileX, tileY - 1)处的瓷砖(换句话说,紧挨着敌人的瓷砖)是墙砖,敌人也必须停止行走。为了停止行走,您为等待时间指定一个正值,并将 x 速度设置为零:
if (tiles.getTileType(tileX, tileY - 1) === TileType.normal ||
tiles.getTileType(tileX, tileY) === TileType.background) {
this._waitTime = 0.5;
this.velocity.x = 0;
}
不同类型的敌人
可以通过引入几个品种让巡逻的敌人稍微有趣一点。这里你可以用继承的力量写几个PatrollingEnemy类的子类来定义不同的敌人行为。
例如,你可以通过让敌人偶尔改变方向来创造一个更难以预测的敌人。在这一点上,你也可以改变敌人的行走速度为一个随机值。您可以通过定义一个继承自PatrollingEnemy类的类UnpredictableEnemy来实现这一点。因此,默认情况下,它表现出与普通敌人相同的行为。您覆盖了update方法,添加了几行代码,随机改变敌人行走的方向和速度。因为您重用了大部分的PatrollingEnemy类代码,所以UnpredictableEnemy类相当短。下面是完整的类定义:
"use strict";
function UnpredictableEnemy(layer, id) {
PatrollingEnemy.call(this, layer, id);
}
UnpredictableEnemy.prototype = Object.create(PatrollingEnemy.prototype);
UnpredictableEnemy.prototype.update = function (delta) {
PatrollingEnemy.prototype.update.call(this, delta);
if (this._waitTime <= 0 && Math.random() < 0.01) {
this.turnAround();
this.velocity.x = Math.sign(this.velocity.x) * Math.random() * 300;
}
};
如您所见,您使用了一个if指令来检查随机生成的数字是否低于某个值。因此,在少数情况下,条件会产生true。在if指令的主体中,你首先让敌人掉头,然后你计算一个新的 x 速度。请注意,您将随机生成的速度乘以旧速度值的符号。这是为了确保新的速度设置在正确的方向上。您还首先调用基类的update方法,以便选择正确的动画,处理与玩家的冲突,等等。
我能想到的另一个变种是跟随玩家的敌人,而不是简单地从左到右再回来。同样,您继承了PatrollingEnemy类。这里有一个类叫做PlayerFollowingEnemy :
"use strict";
function PlayerFollowingEnemy(layer, id) {
PatrollingEnemy.call(this, layer, id);
}
PlayerFollowingEnemy.prototype = Object.create(PatrollingEnemy.prototype);
PlayerFollowingEnemy.prototype.update = function (delta) {
PatrollingEnemy.prototype.update.call(this, delta);
var player = this.root.find(ID.player);
var direction = player.position.x - this.position.x;
if (Math.sign(direction) !== Math.sign(this.velocity.x) &&
player.velocity.x !== 0 && this.velocity.x !== 0)
this.turnAround();
};
这个职业定义了一个在玩家移动时跟随玩家的敌人。这是通过检查敌人当前是否在玩家站立的方向行走来完成的(只考虑 x 方向)。否则,敌人会掉头。只有当玩家不在 x 方向移动时(换句话说,玩家的 x 速度为零),你才能限制敌人的智力。
你不应该让敌人太聪明。此外,不要让他们走得太快——如果敌人在跟踪他们时走得明显比玩家快,这将是一个短暂的游戏。玩家要打败敌人,这样玩家才能赢得游戏。玩一个敌人太聪明或者不可战胜的游戏并不好玩,除非你喜欢一次又一次地死去!
其他类型的敌人
你可以加入游戏的另一个敌人是打喷嚏的乌龟(见图 27-3 )。你会问,为什么是乌龟?为什么是打喷嚏的那个?这个问题我真的没有答案。但这个敌人背后的想法是,它既有消极的一面,也有积极的一面。不好的一面是,乌龟打喷嚏的时候会长尖刺,你不要碰它。但是如果乌龟不打喷嚏,你可以用它跳得更高。因为您现在还没有处理交互,所以您现在只添加了动画乌龟。可以用乌龟跳 5 秒,然后它打喷嚏长尖刺 5 秒,之后又回到之前的状态 5 秒,以此类推。
图 27-3 。不要跳到带刺的乌龟身上!
敌人由Turtle职业代表,它的设置方式与之前的敌人相似。一只海龟有两种状态:它是空闲的,或者它打了个喷嚏,因此有危险的刺。在这种情况下,您维护两个成员变量来跟踪海龟处于哪种状态以及在该状态下已经过了多长时间:waitTime变量跟踪当前状态下还剩多少时间,而sneezing变量跟踪海龟是否在打喷嚏。同样,在update方法中,你处理两个阶段之间的转换,就像你处理火箭和巡逻的敌人一样。我在这里不再赘述,因为代码和其他敌人职业非常相似。如果你想看完整的代码,可以查看本章解决方案中的TickTick3程序。
火花是你加入游戏的最后一种敌人。就像其他敌人一样,斯巴基有两种状态(见图 27-4 )。Sparky 是一个非常危险的,喜欢电的敌人。他静静地悬在空中,直到他收到一束能量。当那发生时,他摔倒了。当斯巴基悬在空中时,他并不危险;但他一倒下,就不要碰他!看看Sparky类就知道代码了。
图 27-4 。斯巴基通电时很危险
敌方软件架构
所有这些不同类型的敌人看起来不同,行为也不同,但他们通常有一个共同的职业设计。您也许可以设计一种更好的方法来定义这些敌人,使用几个泛型类来定义状态和它们之间的转换。每个过渡都可能有附加条件,例如必须经过一定的时间或者动画应该结束播放。这样的结构被称为有限状态机。这是人工智能系统中非常常见的技术。如果你准备好迎接挑战,试着写一个有限状态机库,并重新定义现有的敌人来使用它!
装载不同类型的敌人
现在你已经定义了不同种类的敌人,剩下唯一要做的就是在你读取等级数据变量时加载它们。不同敌人的精灵通过角色来识别。您将这些敌方角色存储在一个GameObjectList对象中,该对象是在Level类构造函数:中创建的
this._enemies = new powerupjs.GameObjectList(ID.layer_objects);
根据你在加载关卡时读取的角色,你调用不同的方法来加载敌人,通过在Level类中的switch指令中添加一些情况:
case 'R':
return this.loadRocketTile(x, y, true);
case 'r':
return this.loadRocketTile(x, y, false);
case 'S':
return this.loadSparkyTile(x, y);
case 'T':
return this.loadTurtleTile(x, y);
case 'A':
case 'B':
case 'C':
return this.loadFlameTile(x, y, tileType);
装载敌人很简单。你只需创建一个你想要添加的敌人的实例,设置它的位置,并将其添加到游戏对象的_enemies列表中。举个例子,下面是龟敌的装载方法:
Level.prototype.loadTurtleTile = function (x, y) {
var tiles = this.find(ID.tiles);
var enemy = new Turtle(ID.layer_objects);
enemy.position = new powerupjs.Vector2((x + 0.5) * tiles.cellWidth,
(y + 1) * tiles.cellHeight + 25);
this._enemies.add(enemy);
return new Tile();
};
你现在已经定义了一些不同种类的敌人,他们有着不同的智力和能力。根据你游戏的需要,由你来定义更聪明,更狡猾,甚至更愚蠢的敌人。你没有把任何物理学应用到敌人身上;然而,一旦你开始建造更聪明的敌人,例如,可以跳跃或跌倒,你将需要像你为玩家所做的那样实现物理。作为一个练习,试着去思考你如何能让这些敌人变得更有能力,而不必依赖物理。当玩家在附近时,你能让他们移动得更快吗?你能创造一个向玩家发射粒子的敌人吗?可能性是无穷无尽的,所以自己试试这些东西吧!
你学到了什么
在本章中,您学习了:
- 如何定义不同种类的敌人
- 如何使用继承来创造敌人行为的多样性
二十八、添加玩家互动
在这一章中,您将在玩家和关卡中的对象之间添加更多的交互。目前,玩家可以四处走动,一个基本的物理系统允许玩家跳跃,与墙砖碰撞,或从屏幕上摔下来。首先你看一种非常简单的互动:收集水滴。然后,您将看到如何创建允许玩家在冰上滑行的行为。最后,你要关注程序中处理游戏中各种玩家-敌人互动的部分。
收集水滴
首先要添加的是玩家收集水滴的可能性。如果炸弹人物与水滴碰撞,玩家收集水滴。在这种情况下,你使下降看不见。
一旦玩家收集了一个液滴,让它隐形并不是解决只画未收集的液滴问题的唯一方法,但这是最简单的方法之一。另一种方法是维护一个已经收集的水滴列表,然后只画那些玩家仍然需要找到的水滴,但是这种技术需要更多的代码。
检查玩家是否与水滴碰撞的地方在WaterDrop级。原因很清楚:和以前一样,每个游戏对象都要为自己的行为负责。如果在WaterDrop类中处理这些碰撞,每个水滴都会检查是否与玩家发生碰撞。你用update方法写这段代码。第一步是检索播放器:
var player = this.root.find(ID.player);
如果水滴当前可见,使用collidesWith方法检查它是否与玩家碰撞。如果是,您将拖放的可见性状态设置为false。您还可以播放声音,让玩家知道水滴已被收集:
if (this.collidesWith(player)) {
this.visible = false;
sounds.water_collected.play();
}
稍后,您可以通过检查每个水滴的可见性来确定关卡是否完成。如果所有的水滴都看不见,你知道玩家已经收集了所有的水滴。
冰块
你可以添加到游戏中的另一种互动是玩家在冰上行走时的特殊行为。当玩家在冰上移动时,您希望角色以恒定的速度继续滑动,并且在玩家释放箭头键时不停止移动。尽管继续滑行并不完全现实(在现实生活中,你会滑行并减速),但它确实会导致玩家容易理解的可预测行为,这在许多情况下比实现现实主义更重要。要实现这一点,你必须做两件事:
- 扩展
handleInput方法来处理在冰上移动。 - 计算玩家是否站在冰上。
你在Player类的成员变量walkingOnIce中跟踪玩家是否站在冰上。现在让我们假设这个变量在别的地方被更新了,让我们看看扩展handleInput方法。当角色在冰上行走时,你首先要做的是增加玩家的行走速度。你可以这样做:
var walkingSpeed = 400;
if (this.walkingOnIce) {
walkingSpeed *= 1.5;
}
速度乘以的值是一个影响游戏性的变量。选择正确的值很重要——太快,关卡就变得不可玩了;太慢了,而且冰面在任何有意义的方面都和普通的人行道没有什么不同。
如果玩家不是在冰上行走,而是站在地上,你需要将 x 速度设置为零,这样当玩家不再按下箭头键或某个触摸按钮时,角色就会停止移动。为了实现这一点,您将前面的if指令扩展如下:
var walkingSpeed = 400;
if (this.walkingOnIce) {
walkingSpeed *= 1.5;
this.velocity.x = Math.sign(this.velocity.x) * walkingSpeed;
} else if (this.onTheGround)
this.velocity.x = 0;
然后你处理玩家的输入。如果玩家按下左或右箭头键,您设置适当的 x 速度:
if (powerupjs.Keyboard.down(powerupjs.Keys.left))
this.velocity.x = -walkingSpeed;
else if (powerupjs.Keyboard.down(powerupjs.Keys.right))
this.velocity.x = walkingSpeed;
类似地,如果游戏是在触摸设备上进行的,您可以检查玩家是否正在触摸其中一个按钮,并相应地调整玩家角色的速度。
你唯一需要做的就是找出玩家是否在冰上行走,并相应地更新walkingOnIce成员变量。您已经在handleCollisions方法中查看了玩家周围的瓷砖,所以要扩展该方法来检查玩家是否在冰上行走,您只需要添加几行代码。在这个方法的开始,你假设玩家不是在冰上行走:
this.walkingOnIce = false;
玩家只有在地面上才能在冰上行走。你在下面的if指令中检查它们是否在地面上:
if (this._previousYPosition <= tileBounds.top && tileType !== TileType.background) {
this.onTheGround = true;
this.velocity.y = 0;
}
要检查玩家所站的瓷砖是否是冰瓷砖,您必须从瓷砖字段中检索瓷砖并检查其ice属性。这样做很简单:
var currentTile = tiles.at(x, y);
最后,您更新了walkingOnIce变量。你使用一个逻辑或操作符,这样如果玩家只是部分在冰砖上,变量也被设置为true:
if (currentTile !== null) {
this.walkingOnIce = this.walkingOnIce || currentTile.ice;
}
只有当currentTile变量没有指向null时,才执行这条指令。你使用逻辑或来计算玩家是否在冰上行走,以便考虑所有周围的牌。效果是角色继续移动,直到它不再站在冰砖上(甚至不是部分地)。
敌人与玩家相撞
最后一种要添加的互动是与敌人的碰撞。在很多情况下,当玩家与敌人发生碰撞时,会导致玩家死亡。在某些情况下,你必须做一些特殊的事情(比如跳到海龟身上时跳得特别高)。在玩家方面,你必须加载一个额外的显示玩家死亡的动画。因为您不想在玩家死亡后处理玩家输入,所以您需要更新玩家当前的存活状态。您可以使用在Player类的构造函数中设置为true的成员变量alive来做到这一点。在handleInput方法中,你检查玩家是否还活着。如果不是,你从方法返回,所以你不处理任何输入:
if (!this.alive)
return;
你还加了一个叫die的方法让玩家死掉。玩家有两种死法:掉进游戏屏幕外的洞里和与敌人相撞。因此,您向die方法传递一个布尔参数,以指示玩家是因摔倒还是因与敌人相撞而死亡。
在die方法中,您要做几件事情。首先你要检查玩家是否已经死亡。如果是这样,你什么都不做就从方法返回(毕竟一个玩家只能死一次)。你将变量alive设置为false。然后你将 x 方向的速度设置为零,以阻止玩家向左或向右移动。你没有重置 y 的速度,所以玩家继续下落:当你死亡时,重力并没有消失。接下来,你决定玩家死亡时播放哪种声音。如果玩家摔死,产生的声音和死于敌人之手截然不同(不要真实尝试这个;相信我的话)。如果玩家因为与敌人碰撞而死亡,你也给玩家一个向上的速度。这种向上的速度不太现实,但它确实提供了一个很好的视觉效果(见图 28-1 )。最后,你播放die动画。完整的方法如下:
Player.prototype.die = function (falling) {
if (!this.alive)
return;
this.alive = false;
this.velocity.x = 0;
if (falling) {
sounds.player_fall.play();
}
else {
this.velocity.y = -900;
sounds.player_die.play();
}
this.playAnimation("die");
};
图 28-1 。玩家在与敌人相撞后死亡
您可以在update方法中通过计算玩家的 y 位置是否落在屏幕之外来检查玩家是否会摔死。如果是这种情况,你调用die方法:
var tiles = this.root.find(ID.tiles);
if (this.boundingBox.top >=tiles.rows * tiles.cellHeight)
this.die(true);
在update方法的开始,您调用超类的update方法来确保动画被更新:
powerupjs.AnimatedGameObject.prototype.update.call(this, delta);
接下来你做物理和碰撞(即使玩家死了,仍然需要做)。然后你检查玩家是否还活着。如果没有,就完成了,从方法返回。
现在玩家可以以各种可怕的方式死去,你必须扩展敌人的职业来处理碰撞。在Rocket类中,您添加了一个名为checkPlayerCollision的方法,您在 rocket 的update方法中调用该方法。在checkPlayerCollision方法中,你只是简单的检查玩家是否与火箭相撞。如果是这种情况,您可以在Player对象上调用die方法。完整的方法如下:
Rocket.prototype.checkPlayerCollision = function () {
var player = this.root.find(ID.player);
if (this.collidesWith(player))
player.die(false);
};
在巡逻的敌人的情况下,你做完全相同的事情。您向该类添加相同的方法,并从update方法中调用它。Sparky类中的版本略有不同:只有当 Sparky 正在通电时,玩家才会死亡。因此,你改变方法如下:
Sparky.prototype.checkPlayerCollision = function () {
var player = this.root.find(ID.player);
if (this.idleTime <= 0 && this.collidesWith(player))
player.die(false);
};
最后,敌人增加了更多的行为。你从检查乌龟是否与玩家发生碰撞开始。如果不是这样,你只需从checkPlayerCollision方法返回,因为你已经完成了:
var player = this.root.find(ID.player);
if (!this.collidesWith(player))
return;
如果发生碰撞,有两种可能。首先是乌龟目前在打喷嚏。在这种情况下,玩家死亡:
if (this.sneezing)
player.die(false);
第二种情况是乌龟处于等待模式,玩家正跳到乌龟身上。在这种情况下,玩家应该做一个超高的跳跃。检查玩家是否跳到海龟身上的一个简单方法是看一下 y 速度。假设速度为正,玩家跳到海龟身上。所以,你调用jump方法让玩家跳得特别高:
else if (player.velocity.y > 0 && player.alive)
player.jump(1500);
当然,只有当玩家还活着的时候你才想这么做。
现在你有了主要的交互编程。在下一章中,你通过在背景中添加山脉和移动的云来完成这个游戏。您还可以添加管理级别之间转换的代码。
死还是不死?
我在这一节做了一个选择,玩家接触敌人时会立即死亡。另一个选择是给玩家几条命,或者给玩家增加一个健康指标,每次玩家碰到一个敌人,健康指标就会减少。
在游戏中加入多个生命或健康指标可以让游戏变得更有趣,但你也必须确保关卡仍然具有足够的挑战性。只有当游戏的等级比本章例子中的等级高得多时,健康条才有意义。您还需要添加侧滚动,以便级别可以比单个屏幕更大。
实现侧边滚动并不困难:你可以根据随玩家移动的相机偏移量来绘制游戏世界中的所有游戏对象。作为一个挑战,尝试用侧滚来扩展滴答滴答游戏,并为玩家添加一个健康栏。
你学到了什么
在本章中,您学习了:
- 如何设计各种玩家与水滴和敌人的互动
- 如何编程 ice tile 行为
- 如何在某些情况下导致玩家死亡
二十九、完成滴答滴答游戏
在这一章,你完成滴答滴答游戏。首先你添加一个计时器,这样玩家就有有限的时间来完成每一关。然后你在背景中添加一些山和云,使游戏在视觉上更有趣。最后,您通过添加两个额外的游戏状态来完成关卡:“游戏结束”状态和“关卡完成”状态。
添加计时器
我们先来看看给游戏添加一个定时器。您不希望计时器占用太多的屏幕空间,所以您使用它的文本版本。因此,TimerGameObject类继承了Label类。您希望能够暂停计时器(例如,当关卡完成时),所以您添加了一个布尔变量running来指示计时器是否正在运行。您还将剩余时间存储在一个名为_timeLeft的变量中。您重写了reset方法来初始化定时器对象。你需要给玩家 30 秒来完成每一关。结果,下面是完整的reset方法:
TimerGameObject.prototype.reset = function () {
powerupjs.Label.prototype.reset.call(this);
this._timeLeft = 30;
this.running = true;
};
为了方便起见,您还添加了一个属性gameOver,指示计时器是否已经到达零。稍后使用该属性来处理玩家没有及时完成关卡的事件:
Object.defineProperty(TimerGameObject.prototype, "gameOver",
{
get: function () {
return this._timeLeft <= 0;
}
});
现在您唯一需要做的就是实现update方法来编程定时器行为。作为第一步,您只需更新正在运行的计时器。因此,如果计时器没有运行,那么从方法:返回
if (!this.running)
return;
然后,像往常一样,从当前剩余时间中减去经过的游戏时间:
this._timeLeft -= delta;
接下来,创建要在屏幕上打印的文本。您可以简单地在屏幕上打印秒数,但是让我们使计时器更通用一些,这样也可以定义一个既能处理分钟又能处理秒钟的计时器。例如,如果您想定义一个从两分钟开始倒计时的计时器,您可以按如下方式初始化它:
this._timeLeft = 120;
你想在屏幕上显示“2:00”而不是“120”。为此,您需要在update方法中计算还剩多少分钟。你用Math.floor方法来做这个:
var minutes = Math.floor(this._timeLeft / 60);
使用这种方法,您可以确保分钟数不会超过允许值。例如,Math.floor(119)给出的结果是 1,这正是您所需要的,因为剩余 119 秒转化为 1 分钟,剩余 119–60 = 59 秒。
通过计算_timeLeft除以 60 后的余数,得到秒数。为了只有整数,您还需要对秒数进行舍入,但是您使用了Math.ceil方法。这个方法总是向上取整:例如,Math.ceil(1.2)的结果是 2。你总是想取整,因为你需要确保只有在真的没有剩余时间的时候才显示零秒。下面是你计算秒数的方法:
var seconds = Math.ceil(this._timeLeft % 60);
因为您不想显示负时间,所以您添加了下面的if指令:
if (this._timeLeft < 0)
minutes = seconds = 0;
注意,这里使用运算符链接?? 来设置分钟和秒钟。下面的if指令做的完全一样:
if (this._timeLeft < 0) {
minutes 0;
seconds = 0;
}
现在您已经计算了剩余的分钟数和秒数,您可以创建一个在屏幕上绘制的字符串:
this.text = minutes + ":" + seconds;
if (seconds < 10)
this.text = minutes + ":0" + seconds;
您将文本的颜色设置为黄色,以便更好地适应游戏的设计:
this.color = powerupjs.Color.yellow;
最后,如果玩家剩下的时间不多了,你要警告他们。当在屏幕上打印文本时,您可以通过在红色和黄色之间交替来做到这一点。您可以通过一条if指令和对模数运算符的巧妙使用来做到这一点:
if (this._timeLeft <= 10 && seconds % 2 === 0)
this.color = powerupjs.Color.red;
尽管以这种方式计算时间对于 Tick Tick 游戏来说已经足够了,但是您可能会发现自己想要进行更复杂的时间计算。JavaScript 有一个Date对象,它代表时间并允许更高级的时间处理,包括时区、转换为字符串等等。
使计时器走得更快或更慢
根据玩家所走的瓷砖种类,时间应该走得更快或更慢。在热瓷砖上行走会加快时间流逝的速度,而在冰瓷砖上行走会减慢时间流逝的速度。为了允许计时器以不同的速度运行,您在TimerGameObject类中引入了一个乘数值。这个值存储为一个成员变量,您最初将乘数设置为 1:
this.multiplier = 1;
在计时器运行时考虑这个乘数是相当容易的。您只需用update方法中的乘数乘以经过的时间,就可以了:
this._timeLeft -= delta * this.multiplier;
现在你可以改变时间流逝的速度,你可以根据玩家行走的瓷砖类型来改变时间流逝的速度。在Player类中,您已经维护了一个变量walkingOnIce,它指示玩家是否在冰砖上行走。为了处理热瓷砖,您定义了另一个变量walkingOnHot,其中您跟踪玩家是否在热瓷砖上行走。要确定这个变量的值,您可以使用与walkingOnIce变量相同的方法。在handleCollisions方法中,您最初将这个变量设置为false :
this.walkingOnHot = false;
然后,添加一行代码,根据玩家当前所处的区块更新变量的值:
this.walkingOnHot = this.walkingOnHot || currentTile.hot;
关于完整的代码,请参见属于TickTickFinal示例的Player类。
使用walkingOnIce和walkingOnHot变量,您现在可以更新计时器乘数。你在玩家的update方法:中这样做
var timer = this.root.find(ID.timer);
if (this.walkingOnHot)
timer.multiplier = 2;
else if (this.walkingOnIce)
timer.multiplier = 0.5;
else
timer.multiplier = 1;
从游戏设计的角度来看,明确地让玩家知道在热瓷砖上行走可以缩短完成关卡的时间,这可能是个好主意。您可以通过短暂显示一个警告叠层或更改计时器的显示颜色来实现这一点。您也可以播放警告声音。另一种可能是将背景音乐改为更疯狂的音乐,让玩家意识到有些事情已经改变了。
适应玩家的技能
改变计时器的速度可以使关卡更容易或更难。你可以延长游戏时间,这样在某些情况下,如果玩家拿起一个特殊的物品,计时器就会停止或者向后移动几秒钟。你甚至可以让等级进程自适应,这样如果玩家死得太频繁,每级 30 秒的最大时间就会增加。但是,这样做要小心。如果你以一种过于明显的方式帮助玩家,玩家会意识到这一点并调整他们的策略(换句话说,玩家会为了让关卡更容易而玩得更差)。此外,玩家可能觉得他们没有被认真对待。一个更好的处理适应每一关最大时间的方法是允许玩家(部分)将以前关卡剩余的时间转移到当前关卡。这样,困难的水平可以变得更容易,但玩家必须做一些事情来实现这一点。你也可以考虑增加难度等级,难度越高,计时越快,好处也越多,比如可以获得更多点数、额外物品或玩家的额外能力。休闲游戏玩家可以选择“我可以玩吗,爸爸?”难度级别,而熟练的玩家可以选择极具挑战性的“我是死亡化身”级别。
当计时器到达零时
当玩家没有按时完成关卡时,炸弹爆炸,游戏结束。Player类中的一个布尔成员变量表示播放器是否已经爆炸。然后,将名为explode的方法添加到启动爆炸的类中。这是完整的方法:
Player.prototype.explode = function () {
if (!this.alive || this.finished)
return;
this.alive = false;
this.exploded = true;
this.velocity = powerupjs.Vector2.zero;
this.playAnimation("explode");
sounds.player_explode.play();
};
首先,如果玩家角色一开始就不存在,或者玩家完成了关卡,那么玩家角色就不能爆炸。在这两种情况下,您只需从方法返回。然后,将活动状态设置为false,将分解状态设置为true。您将速度设置为零(爆炸不会移动)。然后,播放“爆炸”动画。该动画存储在一个 sprite 表中,由爆炸的 25 帧组成。最后,你播放一个合适的声音。
因为重力也不再影响爆炸的角色,所以只有当玩家没有爆炸时才进行重力物理:
if (!this.exploded)
this.velocity.y += 55;
在Level类的update方法中,您检查计时器是否已经到零,如果是,您调用explode方法:
if (timer.gameOver)
player.explode();
画山画云
为了让关卡背景更有趣一点,我们给它加上山和云。您可以在Level构造函数中这样做。先来看看怎么加几座山。为此,您可以使用一条for指令。在指令体中,创建一个精灵游戏对象,给它一个位置,并将其添加到backgrounds列表中。这是完整的for指令:
for (var i = 0; i < 5; i++) {
var sprid = "mountain_" + (Math.ceil(Math.random()*2));
var mountain = new powerupjs.SpriteGameObject(sprites[sprid], ID.layer_background_2);
mountain.position = new powerupjs.Vector2(Math.random() *
powerupjs.Game.size.x - mountain.width / 2,
powerupjs.Game.size.y - mountain.height);
backgrounds.add(mountain);
}
第一步是创建精灵游戏对象。你想在不同的山精灵中随机选择。因为有两个山精灵,所以创建一个随机数(1 或 2)在它们之间进行选择。您使用这个数字来创建对应于这个 sprite 的 ID。
然后你计算山的位置。 x 位置是随机选择的,你使用一个固定的 y 位置,这样山就在合适的高度(你不希望山悬在空中)。最后,山脉对象被添加到backgrounds列表中。
对于云,你做一些稍微复杂的事情。你希望云从左向右移动,反之亦然,如果云从屏幕上消失,你希望新的云出现。要做到这一点,您需要在游戏中添加一个Clouds类。在Level构造函数中创建这个类的一个实例,并赋予它一个比背景本身和山脉更高的层值。这确保了云被画在山的前面:
var clouds = new Clouds(ID.layer_background_3);
backgrounds.add(clouds);
因为Clouds类包含许多移动的云,所以它是GameObjectList类的子类。在构造函数中,您使用一个for指令来创建一些云并将它们添加到列表中。每个云都被赋予一个随机的位置和一个随机的速度。看看TickTickFinal例子中Clouds类的构造函数,看看这是如何实现的。
Clouds类也有一个update方法,在这个方法中,您可以检查云是否已经退出屏幕。因为您需要为每个云游戏对象做这件事,所以您使用一个for指令来遍历列表中的所有云对象。如果云已经退出屏幕,您可以创建一个具有随机位置和速度的新云对象。云可以出现在屏幕的左侧或右侧。如果一朵云位于屏幕外的左侧,并且它的 x 速度为负,你就知道它已经退出了屏幕。如果云位于屏幕外的右侧且其速度为正时也是如此。您可以在下面的if指令中为云c捕获这两种情况:**
if ((c.velocity.x < 0 && c.position.x + c.width < 0) ||
(c.velocity.x > 0 && c.position.x > powerupjs.Game.size.x)) {
// remove this cloud and add a new one
}
移除云很容易:
this.remove(c);
然后创建一个新的云游戏对象:
var cloud = new powerupjs.SpriteGameObject(sprites["cloud_" + Math.ceil(Math.random()*5)]);
你给这个云分配一个 x 的速度,它可以是正的也可以是负的。云的 y 速度总是为零,所以云只水平移动:
cloud.velocity = new powerupjs.Vector2(((Math.random() * 2) - 1) * 20, 0);
请注意,在本指令中,您计算一个介于-1 和 1 之间的随机数,然后将该数乘以 20。这允许你随机创建速度为正或负的云。通过将屏幕高度乘以 0 到 1 之间的一个随机数,你可以计算出一个随机的云 y 位置。从这个数字中减去云高度的一半,以确保不会生成完全绘制在屏幕下方的云:
var cloudHeight = Math.random() * powerupjs.Game.size.y - cloud.height / 2;
根据云移动的方向,您可以将云放置在屏幕的左边界或右边界:
if (cloud.velocity.x < 0)
cloud.position = new powerupjs.Vector2(powerupjs.Game.size.x, cloudHeight);
else
cloud.position = new powerupjs.Vector2(-cloud.width, cloudHeight);
现在,您将新的云添加到列表中:
this.add(cloud);
图 29-1 显示了一个背景中有山脉和移动的云的关卡的截图。
图 29-1 。背景中有山脉和移动的云的滴答滴答水平
在您完成本节之前,让我们再看一遍完整的代码:
for (var i = 0, l = this.length; i < l; ++i) {
var c = this.at(i);
if (/* c is outside of the screen */) {
this.remove(c);
var cloud = new powerupjs.SpriteGameObject(...);
// calculate cloud position and velocity
// ...
this.add(cloud);
}
}
仔细看看这个循环:在用一条for指令遍历列表时,您正在向列表中添加和删除对象。这可能很危险,因为您在for指令体中修改了列表的长度,而i的值取决于列表的长度。如果不小心的话,您可能会遇到这样的情况:您从正在遍历的列表中删除了一个项目,但是i仍然会递增,直到它达到列表的旧长度,当您试图访问超出其界限的列表时,会导致错误。在这种特殊的情况下,你不会遇到麻烦,因为每当你删除一个云,你添加了一个新的;但是在编写这类操作的程序时,你必须非常小心。确保程序在所有情况下都能正确运行的一个方法是使用break或return调用简单地跳出循环。这样,一旦以某种方式修改了列表,就停止了循环。
最终确定级别晋升
为了完成游戏,您仍然需要添加游戏状态来处理玩家输掉或赢得一个级别的事件。除了“关卡完成”游戏状态之外,这里还有一个明确的“游戏结束”游戏状态。这些状态以一种相当简单的方式编码,就像你在以前的游戏中那样。您可以在属于本章的TickTickFinal示例中的GameOverState和LevelFinished状态类中找到完整的代码。
为了确定玩家是否已经完成了一个关卡,您需要向Level类添加一个completed属性来检查两件事情:
- 玩家收集了所有的水滴了吗?
- 玩家到达出口标志了吗?
这两件事都很容易检查。要检查玩家是否到达结束符号,您可以查看他们的边界框是否相交。检查玩家是否收集了所有的水滴可以通过验证所有的水滴都是不可见的来完成。这是完整的属性:
Object.defineProperty(Level.prototype, "completed",
{
get: function () {
var player = this.find(ID.player);
var exit = this.find(ID.exit);
if (!exit.collidesWith(player))
return false;
for (var i = 0, l = this._waterdrops.length; i < l; ++i) {
if (this._waterdrops.at(i).visible)
return false;
}
return true;
}
});
在Level类的update方法中,您检查该级别是否完成。如果是这样,你调用Player类中的levelFinished方法,它播放“庆典”动画:
if (this.completed && timer.running) {
player.levelFinished();
timer.running = false;
window.LEVELS[this._levelIndex].solved = true;
}
你也停止了计时器,因为播放器完成了。再者,你把这一关的已解决状态设置为true,这样下次玩家开始游戏时,浏览器就会记住。在PlayingState类中,你根据关卡的状态处理切换到其他状态。下面是该类的update方法中相应的代码行:
PlayingState.prototype.update = function (delta) {
this.currentLevel.update(delta);
if (this.currentLevel.gameOver)
powerupjs.GameStateManager.switchTo(ID.game_state_gameover);
else if (this.currentLevel.completed)
powerupjs.GameStateManager.switchTo(ID.game_state_levelfinished);
};
处理等级转换的代码相当简单,几乎是企鹅配对游戏中使用的代码的翻版。看看TickTickFinal例子中的代码,看看这是如何做到的。
您现在已经看到了如何使用常见的元素来构建平台游戏,例如收集物品、躲避敌人、游戏物理、从一个级别进入另一个级别等等。到此为止了吗?那要看你了。要让 Tick Tick 成为商业上可行的游戏,还有很多工作要做。你可能想定义更多的东西:更多的关卡,更多的敌人,更多不同的物品,更多的挑战,更多的声音。你可能还想介绍一些我没有提到的东西:通过网络与其他玩家一起玩,侧边滚动,维护高分列表,在关卡之间播放游戏电影,以及你能想到的其他有趣的东西。使用 Tick Tick 游戏作为您自己游戏的起点。
这本书的最后一部分涵盖了在用 JavaScript 开发游戏和应用时需要了解的一些有用的东西。我将更详细地讨论文档,以及一些保护游戏代码和让玩家更快下载游戏的方法。
你学到了什么
在本章中,您学习了:
- 如何给关卡添加计时器
- 如何创建由山和云组成的动画背景*
三十、制作游戏
本章涵盖了几个与制作游戏相关的主题。我先谈设计 HTML5 游戏,然后谈开发。我还简要介绍了游戏素材的制作。最后,你会看到制作游戏的操作方面,比如如何在同一代码上与多人合作,以及如何在游戏制作团队中工作。马克·奥维马斯和彼得·维斯特巴卡在文中分享了关于这些话题的想法和技巧。
Peter vester backa:“html 5 和 JavaScript 在游戏开发方面被称为未来已经有很长时间了。他们还没有完全发挥他们的潜力,但我看到了很多希望。与此同时,原生开发工具、界面和易用性也有了很大改进。当然,在一个理想的世界中,能够到处使用 JavaScript 和 HTML5 代码就太好了。我认为本地应用和 HTML5/JavaScript 应用都有空间。”
设计游戏
这不是一本关于游戏设计的书。游戏设计是一个很大的研究领域,很多书都是关于这个主题的。欧内斯特·亚当斯的《游戏设计基础》这本书是一个很好的开始阅读游戏设计的地方。另一本有趣的书是《??:游戏设计的艺术:透镜之书》,作者杰西·谢尔(CRC 出版社,2008)。
马克·奥维马斯:“在设计我们的游戏时,我们总是牢记代码需要高效。例如,我们不会设计一款游戏,其中数万个角色在屏幕上移动很重要,或者许多视觉事情同时发生,或者非常流畅的运动至关重要。”
这一部分不包括设计过程本身;相反,它主要讨论用 JavaScript 编写基于 web 的应用如何影响游戏设计。前面的引用是这种方法的一个例子:因为你想让你的游戏在各种设备上玩得好,你需要设计允许有效实现的游戏。
马克:“许多设备没有键盘,所以你的游戏需要允许通过触摸输入来控制。对于一些游戏来说,键盘控制更加自然。一个挑战是确保使用键盘玩游戏不会比使用触摸输入更容易。如果你开发一款游戏,让人们在网上对战,或者开发一款使用在线高分列表的游戏,要注意不要因为一组用户使用特定的输入法,就让他们比其他用户拥有更大的优势。”
彼得:“愤怒的小鸟成功的秘密之一是它是第一款在设计时就考虑到触摸设备的游戏。当你为触摸设备设计游戏时,它通常会不同于为带控制器的游戏机设计的游戏。始终为相关平台开发最佳体验。在 PlayStation 4 上玩和在 iPad 上玩是非常不同的体验,因为情况和背景非常不同。在一种情况下,你可以在沙发上坐几个小时在游戏机上玩游戏,而在手机上玩游戏可能只需要几分钟。两者都可以是很棒的体验,但方式非常不同,设计应该考虑到这一点。”
在许多 JavaScript 游戏中,精灵会根据设备的不同而放大或缩小。您已经在本书开发的游戏中看到了如何做到这一点。这本书没有考虑到的一点是,设备之间的长宽比是完全不同的。例如,iPad 的屏幕相对来说是方形的,不像 iPhone 6 的屏幕更像矩形。
在当前的游戏实现中,不同的纵横比意味着游戏屏幕周围有白色(或黑色)空间。如果你在 iPhone 5 上显示一个为 iPad 长宽比设计的游戏,几乎三分之一的屏幕都是空白的!在设计游戏时,尝试调整用户界面、游戏场地、覆盖位置等的设计以适应设备的长宽比是有意义的。理想情况下,游戏应该自动调整其整体布局,以适应每个设备的大小和长宽比。
Mark:“除了长宽比,还有人像和风景模式的选择。在手机上,你通常想使用纵向模式,但在台式电脑或电视上,横向模式更有意义。你的模式选择也取决于游戏的类型,你是否想使用纵向或横向模式,或者你是否希望两者都允许。
在我们的游戏中,游戏元素的定位依赖于长宽比。例如,在屏幕的顶部放置一个用户界面,在其下方是游戏区。如果有可用的空间,游戏区域将下移,以便元素在屏幕上的布局看起来更好。按钮的位置根据屏幕的长宽比而变化。但是,请注意,您也要相应地调整交互(手指位置)。因此,您不能总是使用设备的全屏。此外,如果您想要放置广告横幅,必须从可用于播放的屏幕部分中减去广告横幅的空间。这也意味着你必须使用容易缩放或者可以部分展示而不会从设计中带走任何东西的艺术品。"
当你设计在各种设备上运行的游戏时,你不能总是使用设备上所有可用的功能。例如,如果你的游戏严重依赖玩家倾斜设备,那么这个游戏就不能在台式机上玩。台式机显然没有检测倾斜的传感器(虽然那会很好玩!).此外,您通常依赖于浏览器的版本和已经实现的内容。音频就是一个很好的例子。不同的浏览器以不同的格式播放音频(或者根本不播放)。因此,你不应该设计一个将音频作为设计关键部分的游戏。像吉他英雄这样的游戏很难移植到 JavaScript,因为它们依赖于对音频的精确控制以及测量音频和玩家正在做的事情之间的同步性。
如果你制作一个游戏,你需要在为最小公分母(换句话说,功能最少的设备)创建游戏从而不使用更多现代设备的许多功能与创建一个使用那些功能但不能在旧设备上玩的游戏之间进行权衡。如果你想把你的游戏卖给一个游戏门户(一个托管许多不同游戏的网站),这个门户会有一个你的游戏需要支持的设备列表。所以,在很多情况下,如果你想通过门户发布你的游戏,你根本没有选择。
Mark:“游戏设计最重要的一个方面就是关卡设计。我们花了很多时间来调整关卡,使得每一关的难度都有很好的进展。所有影响游戏的参数都存储在设置文件中。然后,设计师可以修改这些设置文件,将这些文件的新版本推送到服务器,并立即使用新设置玩游戏。”
开发游戏
如果你想开发游戏,你需要知道如何编程,但你也需要知道解决编程问题的常用解决方案或方法。最重要的是,其中一些解决方案可能比其他解决方案更通用,一些解决方案可能比其他解决方案更有效。从这个意义上说,编程通常是在快速解决特定问题和花时间一次性解决一类问题之间的权衡。尤其是在游戏行业,由于紧迫的截止日期,通常很少有时间来解决各类问题。所以作为游戏行业的开发者,你需要非常仔细地考虑你选择的解决问题的方法。另一方面,编写好的、可重用的代码并不总是比编写快速而不可靠的代码花费更多的时间。随着您获得更多的编程经验,您会注意到您开始形成一种思维模式,这种思维模式可以让您快速判断某个编程问题需要哪种解决方案。让我们考虑决定解决方案的几个方面。
第三方库
许多游戏和应用依赖于不同开发人员编写的代码。这就是为什么当你写代码时,你要以这样一种方式来写,即代码是有逻辑的,并且易于其他开发者理解。通常,开发人员将相关代码分组到库中。例如,开发人员可以创建处理游戏中的物理的类,并在库中发布这些代码,以便在任何需要物理的游戏中使用。开发人员已经用许多不同的编程语言创建了许多库,包括 JavaScript。例如,jQuery 是一个众所周知的 JavaScript 库,用于在网站上创建界面。还有一些工具将库与开发环境相结合来创建完整的游戏,比如 Unity ( http://unity3d.com)有一个脚本引擎,它使用了一种非常类似于 JavaScript 的东西,叫做 UnityScript。另一个值得一看的游戏引擎是 Cocos2D ( www.cocos2d-x.org)。当你想开发一个商业游戏时,考虑使用这样的库或游戏引擎是一个好主意,因为它们允许你将游戏作为原生应用导出到各种平台。
Peter:“在 Rovio,我们的大多数游戏都是 iOS、Android 等系统的原生代码。我们确实有一个使用 WebGL 的 HTML5 版本的愤怒的小鸟*,但我们暂时主要做原生移动应用开发。我们有自己多年来在内部开发的工具,所以我们可以非常容易地编写一次代码,然后将其部署到任何平台。对于一些项目,我们使用 Unity,这也使我们能够将代码部署到各种操作系统和设备上。”*
Mark:“在很大程度上,我们开发了自己引擎,编写了自己的库。我们这样做是因为从代码中挤出最后一滴效率对我们来说非常重要。我们发现有许多设计精美、非常通用的库,正因为如此,它们很慢,很难适应我们的框架和工作方式。在少数情况下,我们会使用库,例如游戏物理。我们确实经常使用第三方开发的工具,如代码编辑器或混淆/缩小工具,如 Closure 有关闭包、混淆和缩小的更多信息,请参见[第三十一章]
在本书中,您只使用了一个第三方库——Lab.js——您使用它来更容易地加载多个 JavaScript 文件。您可以选择使用更多的库,而不是从头开始编写所有代码。就本书而言,我的目标是教你 JavaScript 的重要编程习惯,以及它们如何应用于游戏编程。我选择最小化使用的库的数量,这样我可以保持代码简单明了,并且符合我在书中提出的游戏编程的一般方法。作为一名开发人员,您经常需要在使用他人编写的库和自己从头开始编程之间做出选择。如果这个库写得很好,并且做了一些你需要的事情,那么在你的游戏中使用它是很有意义的。你不必做所有的工作来编写别人已经写好的类。此外,如果一个库有很多用户,那么库代码中的主要错误可能已经被解决了。总而言之,如果你使用库,你的游戏代码可能会比你自己编程更健壮。最后,由于库通常是为通用目的而开发的,您可能会发现您合并的库解决了您刚刚在游戏中发现的问题,因此您可以简单地使用库中已经存在的额外功能。
在某些情况下,图书馆带来的麻烦比它们的价值更大。首先,库通常是在某种许可模式下发布的。如果您想在您的商业游戏中使用开源库,许可证可能不允许您出售包含该库的游戏代码。因此,使用库会限制您对代码的处理,因为并非所有代码都是您编写的。
Mark:“库的另一个问题是许可证并不总是明确定义的,特别是因为你是用 JavaScript 发布源代码。此外,最终我们喜欢将所有的 JavaScript 代码放在一个缩小且模糊的文件中,这一过程在使用第三方库时并不总是正确。”
如果你使用一个库,你可以避免写所有的代码,但是你依赖于库的限制。如果您事先没有适当地调查这个库是否真的能解决您的问题,您可能会花很大的力气将这个库集成到您的代码中,然后发现它实际上并没有做您需要它做的重要事情。还有,有时候从头开始写代码而不是使用库是个好主意,因为这样做会迫使你在深层次上理解问题;因此,您可能会找到对您的应用更有效的解决方案。最后,如果您从头开始编写所有代码,那么扩展或修改代码会更容易,因为是您编写的。
总的来说,作为一名开发者,你必须对游戏的哪些部分你想自己编程(这需要时间,但会让你更好地理解)以及哪些部分你想使用一个库(这能更快地给出结果,但可能并不完全符合你的需求)。
代码效率
JavaScript 程序可以在许多不同的设备上运行,从高端台式机到平板电脑和智能手机。这有时会限制可用的计算能力。因此,JavaScript 程序拥有高效的代码至关重要。这取决于程序员如何解决特定的编程问题。通常,有许多可能的解决方案。例如,考虑创建一个数组并用数字 0、1、2、3 等填充它的简单问题。有一种方法可以做到这一点:
var myArray = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19};
您也可以使用一个for循环:
var myArray = {};
for (var i = 0; i < 20; i++)
myArray.push(i);
这里还有另一个解决方案:
var myArray = {0};
while (myArray.length < 20)
myArray.push(myArray.length);
这些解决方案都提供了一个大小为 20 的数组,包含数字 0-19。但是您选择哪种解决方案可能取决于上下文。第一种解决方案(写出数组)非常简单,通过查看代码,可以立即清楚地看到代码执行后数组的内容。对于较小的数组定义,这种方法非常有效。然而,如果您需要一个大小为 300 的数组,这是行不通的。第二种解决方案使用了一个for循环,在这种情况下更合适,因为改变数组的期望长度只需要改变for指令头中的一个数字。第三个解决方案使用了一个while循环来解决这个问题。它避免了声明一个计数器变量(i)。但是,这种解决方案可能不如第二种解决方案有效,因为在每次迭代中,必须检索两次数组的长度(一次在头中,一次在正文中)。
当你写代码时,总是预先考虑解决特定问题的各种解决方案,并选择最适合的一个。这不一定总是最有效的解决方案。如果一个解决方案效率稍低,但能产生更清晰的代码,那么选择那个解决方案可能是个好主意。有工具可以测量代码中的瓶颈。对于 Firefox,Firebug 工具有一个分析器,可以分析你的代码,让你知道哪里花了最多的时间。类似地,Chrome 的开发工具包括一个基准套件,可以分析 JavaScript 的性能。
能够为一个问题选择最佳解决方案需要了解在解释和执行代码时会发生什么。有时候低效率可能很容易解决,但是你不知道它们的存在。考虑下面的基本for指令,它将数组中的每个元素递增 1:
for (var i = 0; i < myArray.length; i++)
myArray[i] += 1;
这条for指令很简单,但是效率不是很高。在循环的每次迭代中,检索数组的长度并与计数器进行比较。因为检索数组的长度需要时间,所以下面的for指令更有效:
for (var i = 0, l = myArray.length; i < l; i++)
myArray[i] += 1;
在这种情况下,您检索数组的长度并将其存储在一个变量中。每次迭代都执行循环的条件,但是因为它现在使用变量而不是直接检索长度,所以代码更有效。
您可以进一步改进代码。递增计数器是通过以下指令完成的:
i++
让我们更详细地看看这条指令。可以按如下方式使用它:
var i = 10;
var j = i++;
尽管第二条指令看起来有点奇怪,但它是完全有效的 JavaScript 代码。这是因为i++也是一个表达式,所以它有一个结果可以存储在一个变量中。i++表达式的结果是在增加之前i?? 的值。结果,在第二条指令被执行后,j将包含值 10,而i将包含值 11。因此,i++需要创建一个临时变量来存储该值并返回。然而,在前面的for循环中,您不使用那个临时值。还有另一种增加变量的方法,如下所示:
++i
这与i++做的完全一样,除了它返回i的新值:
var i = 10;
var j = ++i; // j and i both contain the value 11
因为它返回新值,所以您不必存储旧值,从而消除了对临时变量的需要。您可以在for循环中利用这一点,使其更加有效:
for (var i = 0, l = myArray.length; i < l; ++i)
myArray[i] += 1;
这些效率的提高看似微不足道,但如果对一个包含数千个粒子的阵列每秒执行 60 次for循环,那么效率的微小提高可能会决定一个流畅运行的游戏和一个不可玩的游戏,尤其是在计算能力有限的移动设备上。一些浏览器可能会在解释和运行 JavaScript 代码时进行优化,这种低效相对容易被自动检测到。但是,并非所有浏览器或版本都可以执行相同的优化。通过确保你的代码本身已经是高效的,你的游戏将在更多的平台上更流畅地运行。
Mark:“一般来说,图形是主要瓶颈。对于一些设备来说,把所有的东西都放在一个大的精灵中并画出来会更有效率。对于其他设备,这种方法没有帮助,因为瓶颈是绘制的像素数量。因此,我们并不总是知道瓶颈在哪里,因为瓶颈因设备、浏览器类型和版本、操作系统版本等而异。我们的观点是,我们需要尽可能高效地做每一件事。传统上,我们会使用一个分析器来找到瓶颈,然后尝试相应地优化代码。在 JavaScript 中,这是不可行的,因为有各种不同的设备和浏览器,更不用说对设备、浏览器和操作系统的某些组合使用分析器了。”
这本书里开发的游戏根本没有把重点放在效率上。还有很多可以改进的地方,尤其是在绘制图形的时候。目前,本书中的游戏在每次游戏循环迭代中都会重绘整个图像。在许多情况下,这是不必要的。屏幕的大部分不会改变,为什么要重画呢?HTML5 画布只允许你重画画布屏幕的一部分。如果你重写代码,使得屏幕的静态部分不被重绘,游戏会更有效率。例如,如果在数独游戏中玩家什么也没做,就没有必要重画任何东西。如果您使用动画效果,如闪光,只需重画显示该效果的屏幕部分。另一种提高游戏效率的方法是制作一个高分辨率版本和一个低分辨率版本。根据设备的功能,您可以自动选择应该使用哪个版本的游戏。
代码效率很重要,但不应该以代码清晰为代价。在许多情况下,效率没有编写清晰的代码重要。如果由于代码效率低下,按钮点击被延迟了百分之一秒,玩家不会注意到。另一方面,如果您决定将所有输入处理代码放在一个方法中以避免方法调用开销,您的代码将很难被其他人理解,包括未来的您。
马克:“在许多情况下,效率不是问题,但打嗝是问题。打嗝的一个重要原因是纹理交换。对于视频内存有限的设备,当游戏运行时,精灵将被交换进出内存,导致额外的计算。我们所做的就是将游戏中相同地方使用的精灵分组到一个精灵表中。例如,用于标题屏幕的子画面与用于级别选择屏幕的子画面放置在不同的子画面上。打嗝的另一个原因是垃圾收集(销毁不再使用的对象,释放内存)。不幸的是,没有办法控制垃圾收集何时发生。任何对象都属于垃圾收集的范畴。当你的游戏使用许多小物体如向量时,这就成了一个问题。在这种情况下,尽量减少创建新对象的数量,或者将 x 和 y 值传递给方法,而不是使用 vector 对象。
代码一致性
当你写代码时,另一件非常重要的事情是确保你的代码是连贯的。一致性可以在几个层面上实现。首先,一致性在代码的设计中很重要。例如,在本书的所有游戏中,我假设游戏循环做三件事:
- 处理玩家输入
- 更新游戏世界
- 绘制游戏世界
这是我做出的代码设计决定,但是其他开发人员可能会做出不同的选择。例如,一些游戏引擎不区分处理玩家输入和更新游戏世界。其他游戏引擎将绘制视为高度独立的过程,不属于游戏对象类。重要的是,这样的设计决策在整个游戏中得到连贯的应用。如果处理输入和更新游戏世界应该是两个独立的过程,这应该在所有的类中都很明显。在代码设计中可以看到一致性的另一个例子是您处理只需要一个实例的对象的方式,例如游戏状态管理器或负责在画布上绘图的对象(Canvas2D)。对于所有这些对象,本书的例子虔诚地使用了 Singleton 设计模式。当你开始为自己的游戏编程时,明确地思考你所做的设计选择,并在编程时连贯地应用它们。
一致性在代码的结构层次上也很重要。每个游戏对象类对于每个游戏循环元素都有一个单独的方法。这些方法在每个类中都有完全相同的头,因此它们需要相同的参数。例如,在所有的游戏对象类中,update方法只有一个参数delta。如果你在类的结构上是一致的,那些类的用户就知道会发生什么。另一个例子是将通用类如GameObjectList(可用于许多不同的游戏)与游戏专用类如WaterDrop分开,将通用类放在一个名为powerupjs的名称空间中。同样,这有助于其他开发人员理解如何使用这些类以及它们属于哪里。
最后,代码应该在词法层次上是一致的。确保所有的方法都有相似的命名约定。一些开发人员喜欢方法和属性名称总是以大写字符开头。本书遵循的惯例是,变量、方法和属性名称以小写字符开头,而类名以大写字符开头。此外,任何不应该在类外直接访问的变量前面都有一个下划线字符。在您的代码中有这样的约定是一件好事。这使得你的代码更容易理解。本书遵循的另一个惯例是,在由多个单词组成的名称中,后面的每个单词都以大写字符开头:
function GameObjectList() {
...
}
GameObjectList.prototype.handleInput = function() {
...
};
var thisIsAVeryLongVariableName;
这种命名变量的方式在编程中很常见。有些人试图定义命名方案的标准,比如匈牙利符号。在匈牙利符号中,变量名也包含关于它们类型的信息。看看下面的例子:
var bIsAlive = true;
b字符告诉你这个变量是一个布尔变量。这可能是在变量名中编码的有用信息,因为 JavaScript 不要求程序员在声明变量时提供变量的类型。您可能会在其他开发人员编写的代码中遇到匈牙利符号,尽管现在它的使用越来越少,因为编译器和开发环境可以自动提供关于变量的各种信息,例如变量的范围、它所代表的类型等等。
Mark:“在 JavaScript 中,你可以用一百种不同的方式编写代码。所以,在你开始开发之前,想想你最终真正需要的是什么。如果你从一个错误的方法开始,你会因为那个选择而遇到很多问题。然而,做出正确的选择并不总是容易的。很多情况下,当你开始开发的时候,游戏的设计还没有完成。有时候你最终意识到,你需要游戏中的某种视觉效果,但代码中没有地方放它。”
制作游戏内容
如果你想让你的游戏好看,你需要好的游戏资源。表现出一致性的好游戏素材会让你的游戏对玩家更有吸引力。这不仅包括视觉效果,还包括音效和背景音乐。一般来说,声音和音乐被低估了,但它们是营造氛围的重要因素。看一部没有声音的电影比当你听到在情感上支持正在发生的事情的音乐和给角色正在做的事情赋予形体的声音效果时看电影要有趣得多。游戏也需要音乐和音效,就像电影一样。
首先,你可以购买预制的精灵包。这里有几个网站的例子,在那里你可以免费得到精灵,购买精灵,或者雇佣艺术家为你创造精灵:
www.supergameasset.comwww.graphic-buffet.comwww.hireanillustrator.comhttp://opengameart.orgwww.3dfoin.comwww.content-pack.com
就像精灵一样,你也可以为你的游戏购买音乐和音效。看看这些网站:
www.soundrangers.comwww.indiegamemusic.comwww.stereobot.comhttp://audiojungle.netwww.arteriamusic.comhttps://soundcloud.com
如果你已经用这些股票素材创建了一些游戏,你就可以更容易地与其他独立开发者建立联系。你开发的游戏将形成一个作品集,展示你作为游戏开发者的能力。
在游戏制作团队工作
当你和其他人一起玩游戏时,你需要有不同技能的人。一般一个游戏制作团队都有一个领导整体制作流程的项目经理,游戏设计师,关卡设计师,美工,音效师,测试员,当然还有程序员。有时一个人扮演多个角色。比如,项目经理也可以是首席游戏设计师。测试人员和程序员通常是同一批人。
彼得:“我们有许多艺术家、设计师、程序员、专家和问答测试员。有很多合作。现在很多人都参与了游戏制作。
当一家游戏公司发展壮大时,一个挑战就是寻找人才。此外,忠于你的创业根基也很重要,这样你才不会变成一个又大又慢的公司。试着在你的公司里保持敏捷,并把公司的等级制度保持在最低限度。就像他们说的,文化早餐吃策略。保持创业文化的活力,确保你能把事情做好。这适用于许多成长中的公司。挑战在于保持心态,让事情保持运转和敏捷。"
Mark:“因为我们是一个相当小的团队,项目经理也是我们的游戏设计师。有时这会导致问题。项目经理希望游戏按时完成,而设计师希望不断改进或改变游戏。在这种情况下,项目经理应该告诉设计师应该做这个游戏,如果两者是同一个人,这是很难做到的。”
你可能已经掌握了制作一个游戏所需的各种元素,但并不认为你可以自己做所有的事情。如果你是一名优秀的程序员,并不意味着你也是一名优秀的艺术家。而且,对你来说不幸的是,最初往往是视觉效果决定了一个人是否会尝试你的游戏。和一个艺术家组成一个团队是个好主意,也许还可以和一个游戏设计师和一个音频专家组成一个团队。试着和其他人联系,这样你们可以一起创作游戏。活跃在社交网络中,开自己的博客,在论坛上发帖,等等。你将成为一名独立开发者,所以去看看像www.indiegames.com这样的网站,了解其他独立开发者在做什么。活跃在像全球游戏堵塞(www.globalgamejam.org)这样的游戏堵塞中,结识其他开发者。看看 HTML5 开发者论坛,比如 HTML5 游戏开发者(www.html5gamedevs.com)或者 Web 开发者论坛(www.webdeveloper.com/forum/forum.php)。
如果你在一个游戏上和多个开发者一起工作,你需要找到一种方法来共享代码并一起工作。你可以使用版本管理工具,比如 Subversion ( http://subversion.apache.org)来实现。Subversion 的替代品有 Git ( http://git-scm.com)和 Mercurial ( http://mercurial.selenic.com)。网上也有类似的工具,结合版本管理工具提供云存储。这允许您处理代码并将其提交给服务器,之后其他开发人员可以检索代码并使用它。这种在线代码和版本管理工具的例子有 GitHub ( https://github.com)和 Bitbucket ( https://bitbucket.org)。
与其他开发人员合作时,你需要考虑的另一件事是记录你的代码。如果您在源文件中以某种格式编写文档,有工具可以自动读取该文档并创建 HTML 文件,以漂亮的布局显示它。这类工具的例子有 YUIDoc ( http://yui.github.io/yuidoc)和 Doxygen ( www.stack.nl/~dimitri/doxygen)。文档是非常有用的,但是并不是所有的游戏开发者都花时间正确地记录他们正在编写的代码。
Mark:“我们用合流( www.atlassian.com/software/confluence )来管理一个游戏制作项目。在这个工具中,我们存储了很多关于我们的框架以及如何使用它的信息。我们已经为我们内部的游戏引擎编写了适当的文档,但并不是真正为游戏本身编写的。我们的游戏一般都是单人开发,然后完成。我们确实为每个游戏编写了一份文档,全面描述了游戏代码的结构。”
最后,想想你如何组织你的团队。许多人忘记了这一点,只是开始一起工作,但这可能会很快导致问题,因为团队中人们的角色和期望是不同的。如果有人认为他们是主要的游戏设计者,而团队中的另一个人认为这个人对事业并不重要,这将对团队的合作产生影响。当你在做一个项目时,想想你为什么要这么做,并确保和你一起工作的其他人也同意你的观点。你团队中的一个成员可能想做一个有创意的声明,但另一个成员可能只是想赚很多钱。
彼得:“扁平的组织通常比深层次的结构运作得更好。非常重要的一点是,你要创造一个环境,让人们知道他们应该做什么,这样就有了明确的方向和领导。给人们提供有目的的工作是很重要的,不管你是在游戏中编码、做艺术品还是从事营销工作。”
马克:“如果你想创建一家游戏公司,在选择你想合作的人时要非常谨慎。如果你一开始就选错了人,你会因为不得不在过程中改变团队而浪费时间。在开始阶段花很多时间来找到合适的人。”
三十一、发布游戏
彼得·维斯特巴卡:“如今,地球上几乎每个人都有一部智能手机,这使它成为有史以来最大的游戏平台。而我们才刚刚开始;它仍在大规模增长。移动是体量所在,重心所在。”
这本书的最后一章讲述了几个与让你的游戏走向世界相关的话题。首先,我介绍了测试和检查代码质量,以确保你的游戏能在许多不同的平台上运行良好。然后我说几个你想发布游戏需要考虑的事情,比如本地化和代码优化。本章最后讨论了游戏的销售和营销。如前一章所述,彼得·维斯特巴卡和马克·奥维马斯提供了许多建议和想法。
测试和检查代码质量
任何你想公开发布的软件都应该在发布前经过测试。特别是在 JavaScript/HTML5 游戏的情况下,玩家将拥有各种不同的设备,这些设备运行不同的操作系统,使用不同的浏览器和浏览器版本。你必须确保你的游戏能为尽可能多的玩家正常运行。
测试软件有许多不同的方法。您可以通过简单地检查代码、检查类和方法的结构来测试代码,而无需实际运行程序。您还可以通过运行程序并尝试各种不同的场景和参数值来测试程序,以确保代码完成预期的工作。确保代码不会做不希望它做的事情也很重要。如果玩家提供了无效的输入或者网络连接中断,游戏应该不会崩溃。
您可以手动进行软件测试,但也可以通过编写特殊的测试脚本来尝试各种不同的参数,从而自动进行测试。这个自动过程也被称为单元测试,因为你单独测试部分(单元)代码。
当你差不多写完代码的时候,测试还有另外两个主要阶段: alpha 测试和 beta 测试。Alpha 测试是由一组人在内部完成的,通常是开发人员自己。Alpha 测试很重要,因为它确保了开发人员构建的所有组件都能正常工作。在内部 alpha 测试之后,软件也可以进行外部测试。这被称为 beta 测试:软件由一组外部用户使用。通常,beta 测试不仅有助于消除 bug,还能发现一个程序是否能在各种设备上运行。在游戏的情况下,beta 测试也有助于验证游戏是否如预期的那样,教程等级是否清晰,以及等级进展是否顺利。在游戏行业,这一步被称为游戏测试。内部和外部都可以做 playtesting。无论如何,不要推迟游戏测试,直到你的游戏接近完成。有时候游戏测试的结果可能意味着游戏运行方式的重大改变。在这个过程中,你知道得越早越好。
马克·奥维马斯:“将不同的测试阶段分开很重要。开发人员试图在编写代码的同时测试他们的代码,但是他们不可能对所有设备和所有平台都这样做。我们的游戏设计师很快就会得到游戏的早期版本,然后就可以开始进行关卡设计和游戏性测试。这些测试也由公司中的其他人完成(但通常不是开发人员)。一旦我们认为游戏完成了,我们就把游戏放到网上,但是对公众是隐藏的。然后,我们在所有可能的设备和平台上测试游戏。我们首先测试游戏本身的所有方面是否都正常工作,这是我们不必在所有设备上检查的事情。第二,我们让许多不同的人在不同的设备和平台上试用游戏,我们让他们通过一个基本的检查清单来验证音频工作正常,屏幕工作正常,字体可见,等等。如果我们在那个时候发现了游戏中的主要问题,我们就让它下线,然后从头再来一遍整个过程。
如果一切顺利,游戏会被送到一个更注重游戏性的外部测试小组。我们希望在未来进一步扩展和改进这一流程。
过去,我们还雇佣了外部人员在各种不同的设备和平台上进行广泛的测试。这种情况下的主要目标是广泛测试我们的引擎。很可能我们很快会再做一次,因为从中得出了很多有用的东西。另一方面,这也让我们意识到我们的引擎实际上工作得很好,这是一件很好的事情。我们公司确实有很多不同的设备和浏览器,但不可能什么都有。令人沮丧的是,例如,如果你看看 Android,你的游戏可能在 Android 4.0 和 4.2 上运行,但在 Android 4.1 上则不行。不幸的是,一切运转得有多好是没有顺序的。"
与其他编程语言相比,在编写 JavaScript 代码时,代码写得好是非常重要的。调试和测试 JavaScript 代码比常规应用更难,因为涉及到太多的变量。因此,在让代码在各种设备上运行之前,确保代码运行良好是至关重要的。因为 JavaScript 允许松散的键入,所以许多代码编辑环境很难非常有效地自动完成代码,这是一个遗憾,因为代码编辑器中的代码完成功能可以为您节省大量时间——与其说是键入时间,不如说是您不必花费时间浏览在线帮助来查找您想要使用的方法是如何拼写的以及它需要什么参数。
JSLint ( www.jslint.com)是一个帮助你写出更好代码的工具。JSLint 是所谓的代码质量检查器。它会检查您的代码中通常被认为是不良编码实践的东西。例如,JavaScript 允许在声明变量之前使用它们(尽管在严格模式下这是不允许的)。JSLint 检查您的代码不包含任何未声明变量的使用。代码质量检查有用的另一个例子是 JSLint 报告以下类型的if指令:
if (a = 3) {
// do something
}
if指令的条件是赋值而不是比较。这是语法上有效的 JavaScript 代码,但程序员可能是这个意思:
if (a === 3) {
// do something
}
代码质量检查器有助于发现这类编程错误。此外,如果程序员实际上打算在if的条件中进行赋值,JSLint 报告它仍然是一件好事,因为在条件中给变量赋值是一种非常糟糕的编程实践!
部署
当你想发行你的游戏时,你需要考虑一些事情。首先你需要确保全世界的玩家都能理解你的游戏。虽然许多人可以阅读和书写英语,但如果你的游戏能够适应玩家的地区,那就很好了:例如,将游戏中使用的所有文本翻译成玩家的语言。这个过程叫做本地化。如果你想正确地做到这一点,你需要尽可能地将文本与实际的游戏代码分开。这包括按钮上显示的文本和当播放器悬停在用户界面元素上时显示的帮助文本。如果你想让你的游戏被翻译成世界上任何一种语言,本地化的成本会很高;但是,您可以通过确保游戏代码不包含任何文本元素,并且只使用引用在单个位置定义的文本的变量来降低成本。
如果你在游戏中使用语音,那么本地化可能会变得非常昂贵。不幸的是,自动文本到语音转换系统在现实中还不太现实,所以你需要让配音演员录制你想发布游戏的任何语言的语音音频。
你可以设计你的游戏来最小化本地化成本,例如通过主要依靠视觉来与玩家交流。“警告:你只剩下十秒钟了!”需要翻译,但闪烁的计时器不需要。大多数游戏都试图将显示给用户的文本最小化,但是教程关卡经常包含文本。当然,如果你有一个单词搜索游戏或任何其他游戏,其中的游戏性严重依赖于文本操作,你肯定需要仔细考虑本地化。
Mark:“我们有自己的本地化工具。每个游戏都有自己的字典文件,用唯一的 ID 存储游戏中所有语言的文本。我们制作了一个特殊的工具来编辑这些文本。很多文本不是游戏特有的,它们是我们框架的一部分,所以我们把它们和游戏特有的文本一起导出。我们的工具还可以将这些数据生成为电子表格,这样我们就可以将这些数据发送给翻译人员,他们只需填写一列即可。对于本地化,重要的是您的所有代码都使用 Unicode,因为您希望能够使用亚洲字体。”
彼得:“我们用《愤怒的小鸟》创造了一个在任何地方都能玩的游戏,因为它大部分是视觉化的。我们将游戏中的小文本本地化。虽然你可以为非常本地化的市场制作游戏,但我们设计游戏时使用的文字非常少,因此几乎不需要做任何本地化。”
除了本地化之外,你还需要确保在发布游戏时代码尽可能的紧凑。本书中的所有示例代码都分布在几个文件中。当你发布你的游戏时,理想的情况是你希望所有的代码都在一个 JavaScript 文件中。你也可能不希望玩家能够容易地理解你的代码。例如,如果你已经编写了一个创新的算法来非常有效地处理游戏中的物理问题,你可能希望避免其他人从你的游戏中复制代码,这样你就可以保持竞争优势。帮助你做所有这些事情的一个非常有用的工具是 Google Closure ( https://developers.google.com/closure)。Closure 允许你将你的 JavaScript 文件编译成一个优化大小的文件,这样玩家可以快速下载。您可以选择闭包编译器用来生成优化的 JavaScript 文件的优化级别。闭包可能的最高优化级别被称为高级优化级别。如果你打开并阅读这样一个高度优化的文件,你将无法理解其中的代码。因此,这种高级优化模式也被称为代码混淆模式。模糊处理非常有用,因为它可以保护您的代码不被他人复制。
Mark:“我们使用 Google 的 Closure 编译器进行缩小和混淆,它可以在三种不同的模式下运行。前两种模式基本上只是缩小版。第三种模式是一种重新编译,其中代码被重新安排,不使用的部分被删除,等等。我们的条件是,我们发布的任何游戏代码都需要能够经历那个过程,并且在之后仍然能够工作。”
当您从外部库调用代码时,代码混淆可能会带来问题。在这种情况下,模糊处理不应该改变你从那个库中调用的函数或变量的名字,因为那样名字就不再匹配了。您必须要么向混淆器提供指令,这样它就不会在这些情况下进行重命名操作,要么避免使用外部库。
出售你的游戏
如果你想以创作 HTML5 游戏为生,有相当多不同的可能性。用你的游戏赚钱的一个非常简单的方法是把它们放在你的网站上并添加广告横幅——例如,使用 Google AdWords。如果你有很多访问者,你将从广告中获得收入。但是,这个广告收入会很低,除非你吸引了很多玩家。也许更有利可图的方法是将你的游戏卖给门户网站。大多数门户网站免费提供他们的游戏,但他们也从广告中赚钱。也有一些经纪人网站,你可以付费购买你的游戏,比如游戏经纪(https://gamebrokerage.com)。
马克:“向门户网站出售游戏可以在收入共享的基础上进行,你可以分享广告收入。收入分成要么由门户支付给开发者,要么由开发者支付给门户。第一种情况,门户在你的游戏中投放广告,付给你一部分收入;第二种情况,开发者在游戏中投放广告,按收入的一定比例支付给门户网站。最后,你也可以通过固定费用的模式来销售你的游戏,在这种模式下,你向一个门户网站交付一个游戏就可以获得一次报酬。因为 HTML5 游戏市场相对年轻,门户网站仍然愿意与创作和销售游戏的个人打交道。但越来越多的 HTML5 游戏公司正在起步,因此这种情况可能不会持续很长时间。”
另一种尝试销售游戏的方式是通过 Android 或 iOS 上的应用商店。你不能直接把一个 HTML5 游戏发布到那些商店,需要先转换成原生格式。有一些包装器工具可以帮你做到这一点,比如 CocoonJS ( https://www.ludei.com/cocoonjs)和 PhoneGap ( http://phonegap.com /)。将你的游戏发布为原生应用的好处是,除了在应用商店销售游戏赚的钱之外,你还可以引入应用内购买等为你提供额外收入的东西。但是,请注意,使用 PhoneGap 之类的包装工具可能会导致性能滞后,需要进行彻底的测试。包装器生成的应用可能看起来像本机应用,但它实际上是运行在查看器中的 web 应用。
一旦你的游戏上市,你就可以开始考虑用它们赚钱的其他方法了。例如,如果你的游戏允许玩家互相对战,你可以引入一种订阅服务,让玩家在支付月费后可以访问你的游戏。玩家可以在你的服务器上存储他们的个人资料和他们在游戏中的成就。
试着想想用你的游戏赚钱的其他方法。有时,让已经拥有庞大关系网的另一方参与进来会有所帮助。为你选择的慈善机构创建一个游戏,并与它分享收益。你将帮助一个慈善机构,同时它将为你做市场营销!另一种试图用你的游戏赚钱的方法是依靠众筹。有专门为游戏众筹的网站,比如 Gambitious ( https://gambitious.com)。如果你打算在 Steam ( http://store.steampowered.com)上发布你的游戏,看看它为游戏开发者提供的出售作品的机制。例如,它有一个名为早期访问的机制,允许人们购买和玩尚未完成的游戏。这可能是一个有用的机制,你可以建立一个玩家网络,获得动力,获得反馈和错误报告,并提供定期的游戏更新。最后,看看 True Valhalla ( www.truevalhalla.com/blog)的博客,里面谈到了很多用 HTML5 游戏赚钱的不同方法。
营销
既然你正在编写自己的游戏,你可能已经开始考虑如何让它们在现实世界中运行。也许你不想仅仅为了成就而创造一个游戏,而是想用它赚点钱。幸运的是,现在发布游戏很容易。对于移动设备,在将游戏导出到特定的移动平台后,您可以将游戏提交到应用商店。如果你通过一个门户网站销售你的游戏,那么这个门户网站将(部分)为你负责营销。当然,你也可以让你的游戏出现在你自己的网站上。
彼得:“如果你看看我们在《愤怒的小鸟》和《??》之前发布的 51 款游戏,它们实际上都是非常好的游戏。主要的挑战是市场非常艰难。App Store 并不存在——你必须有良好的关系才能把你的游戏推出去。让我们成功的是应用商店。突然之间,任何人都可以使用数字发行,所以你可以创建一个像愤怒的小鸟这样的游戏,并立即分发给粉丝。
但是现在游戏的发行变得如此容易,这又带来了另一个挑战。任何人都可以通过 App Store 发布游戏。因此,有大量的游戏和应用。当然,你需要做出伟大的游戏。但是已经有很多很棒的游戏了。事情是这样的,每有一只愤怒的小鸟*,就有许多“不那么”的愤怒的小鸟。“那些游戏中有许多实际上是很棒的游戏,但是没有人知道它们。那么如何让你的游戏脱颖而出呢?以愤怒的小鸟为例,都是关于人物的。而另一方面,这也是一个品牌提出了一个有趣的问题:为什么鸟儿会生气?如果你认真做游戏,你就必须认真做营销和品牌。仅仅制作优秀的游戏是不够的。”*
显然,挑战在于让你的游戏可见。在 iOS 和 Android 上,每天都有超过 300 款新游戏出现。大部分都是少数人玩的。如果你为这个游戏创建自己的网站,你将如何吸引访问者?
首先,你需要制作一款优质的游戏。如果游戏不好,人们不会玩它。找其他有其他技能的人来帮助你。不要过于雄心勃勃:你不会创造下一个光环!设定合理的目标。从小而精的游戏开始。不要相信自己的判断:和别人谈论你的游戏,让他们玩原型,以确保玩家确实喜欢它。当你的游戏接近完成时,制定一个营销计划。你可以在任何地方发布关于游戏的消息,制作新闻包,制作视频,向博客和其他网站发送信息,等等。人们只会在听说你的游戏后才会玩。不要期望在你将游戏发布到应用商店后,这种情况会自动发生;你需要制定一个计划。在你的游戏发布之前,建立一个潜在玩家的网络——对你的工作感兴趣的人。为您的公司和/或您正在创建的游戏创建一个脸书群组。一定要和 Twitter 等社交网络上的关注者交流。鼓励其他人玩你的游戏,并写下来。
我在上一节提到了 Steam 上的早期访问机制。这样一个开放的开发机制,从营销的角度来看也是很有趣的。它允许你吸引玩家到你的游戏中,并让他们参与到游戏的开发中来。通过让玩家参与游戏开发的早期阶段,你可以在玩家和游戏之间建立一种非常牢固的纽带,因为他们感觉自己是开发过程的一部分。如果你聪明的话,这些玩家会成为你游戏的销售人员,为你做大量的营销工作。
彼得:“对我们来说,这一切都是为了我们的粉丝和品牌。品牌的力量愤怒的小鸟让我们在广告等传统营销手段上花更少的钱。如果你看看典型的游戏开发工作室,我认为人们没有意识到他们需要在营销上花多少钱。通常,游戏开发成本只是营销费用的一小部分。
我们用我们的游戏和我们在营销方面的行动来建立品牌。我们从非常强势的角色开始,并以此为中心打造品牌。要建立一个品牌,有多少家公司就有多少种不同的方式。如果你看看传统上品牌是如何建立的,你会发现这也开始在游戏行业发生,比如通过电视广告。就游戏而言,这与在任何行业建立品牌没有太大区别。"
最后的想法
这本书涵盖了 JavaScript 编程的许多方面。你现在已经在 JavaScript 编程,尤其是游戏编程方面打下了坚实的基础。通过阅读马克和彼得的思想,你可能已经意识到,世界变化很快。iOS 和 Android 等操作系统会定期更新新功能。移动设备的速度越来越快。几年前流行的设备现在已经过时了。在这中间是 HTML5 和 JavaScript。JavaScript 日益重要的一个显著例子是桌面/PC 游戏行星毁灭 ( www.uberent.com/pa),它的整个 GUI 都是用 JavaScript 和 HTML5 创建的!预测未来是不可能的,但有一点是肯定的:HTML5 和 JavaScript 将会继续存在。我希望这本书能帮助你掌握这门语言,并为你自己探索游戏编程提供一个良好的起点。我将用马克和彼得在采访中说的两件事来结束这本书:
马克:“想想你想要实现什么。例如,你可能只是有一个创造性的想法,你想实施。这和想靠制作游戏谋生是非常不同的目标。很多人玩你的游戏并不总是重要的。制作游戏本身就是一种奖励。”
彼得:“不要盲目复制,而是尝试做不同的事情,而不是做其他人都在做的事情。想想怎么才能从其他几十万游戏中脱颖而出。惊喜和喜悦。给人惊喜不用花什么钱。也就是说,尽可能多地从别人那里学习。然后做好自己的事。”
第一部分:入门指南
本书的第一部分涵盖了用 JavaScript 开发游戏应用的基础知识。您会看到许多结合 HTML 和 JavaScript 的简单例子。我将向您介绍 HTML5 标准以及随之而来的新的 HTML 元素,特别是 canvas。这一部分涵盖了核心 JavaScript 编程结构,如指令、表达式、对象和函数。此外,我还介绍了游戏循环以及如何加载和绘制精灵(图像)。
第二部分:创造丰富多彩的游戏
在这一部分中,你开发了一个名为画师的游戏(见图 II-1 )。在你开发这个游戏的同时,我也介绍了一些在游戏编程时非常有用的新技术,比如在类和方法中组织指令、条件指令、迭代等等。
图二-1 。创造丰富多彩的游戏
画家游戏的目标是收集三种不同颜色的颜料:红色、绿色和蓝色。颜料从空中落在由气球保持漂浮的罐子里,在颜料从屏幕底部落下之前,你必须确保每个罐子都有正确的颜色。您可以通过向下落的罐子发射所需颜色的颜料球来改变颜料的颜色。您可以使用键盘上的 R、G 和 B 键选择拍摄的颜色。你可以在游戏画面中左键点击射出一个彩球。通过点击远离油漆大炮,你给球一个更高的速度。你点击的地方也决定了大炮射击的方向。每有一个罐子落在正确的箱子里,你就得到 10 分。对于每个颜色错误的罐子,你失去一条生命(由屏幕左上角的黄色气球指示)。你可以通过下载属于第十二章的示例代码 zip 文件来运行这个游戏的最终版本。双击PainterFinal文件夹中的Painter.html文件开始玩游戏。
第三部分:宝石果酱
宝石果酱是一款益智游戏,在其中你试图找到宝石的组合(见图 III-1 )。不过要小心:宝石车正在慢慢移动。一旦购物车离开屏幕,你的时间就到了!棋盘由十行五列组成。游戏场上的宝石根据三种属性而不同:颜色(红色、蓝色或黄色)、形状(菱形、球形或椭圆形)和数量(一颗、两颗或三颗宝石)。
图三-1 。宝石果酱游戏
玩家可以使用鼠标或触摸屏(如果在手机或平板电脑上玩游戏)向左或向右移动行。目标是找到中间一列中三个相邻宝石的匹配组合。如果所有对象的每个属性都相同或不同,则三个宝石的组合是有效的。例如,黄色单菱形对象、蓝色单菱形对象和红色单菱形对象形成有效的组合,因为每个对象的颜色不同,而所有对象的形状和数量都相同。黄色的球体对象、黄色的双菱形对象和黄色的三椭圆对象也形成了一个有效的组合,因为这三个对象具有相同的颜色、不同的形状和不同的数字。黄色菱形、红色双球体和蓝色双椭圆的组合是无效的,因为尽管每个对象的颜色和形状都不同,但菱形对象的数量与其他两个不同。另一方面,黄色菱形、红色双球体和蓝色三椭圆的组合是有效的。
一旦玩家通过移动行找到了一个有效的组合,他们按下空格键,组成该组合的宝石就会消失,剩下的宝石会落下来填充空槽,三个新的宝石会从屏幕顶部落下。当玩家按下空格键时,如果中间栏中同时出现两个或三个组合,则会获得额外的分数,并且屏幕上会显示一个覆盖图,指示出现了两个或三个组合。
在接下来的章节中,你开发这个游戏。如果你想玩完整版来感受一下游戏是如何运作的,运行属于第十七章的例子!
第四部分:企鹅配对
在书的这一部分,你开发游戏企鹅配对(见图 IV-1 )。我介绍了一些游戏编程的新技术,比如精灵表、更好的游戏状态管理、在会话之间存储游戏数据等等。
图四-1 。企鹅配对游戏
企鹅配对是一个益智游戏,目标是让成对的企鹅颜色相同。玩家可以通过点击或轻拍企鹅来移动它们,并选择企鹅应该移动的方向。企鹅移动,直到它被游戏中的另一个角色(可以是企鹅、海豹、鲨鱼或冰山)阻止,或者它从游戏场地掉落,在这种情况下,它会掉进水里,被饥饿的鲨鱼吃掉。在游戏的不同关卡中,你引入新的游戏元素来保持游戏的刺激。例如,有一种特殊的企鹅可以与任何其他企鹅匹配,企鹅可以卡在一个洞里(意味着它们不能移动),吃企鹅的鲨鱼可以放在板上。你可以通过尝试属于第二十三章的示例程序来运行这个游戏的最终版本。在浏览器中打开PenguinPairs.html文件,即可立即开始播放。
第五部分:滴答滴答
前几章已经向你展示了如何构建几种不同类型的游戏。在这一部分,你用动画角色,物理,和不同的水平建立一个平台游戏。游戏的名字叫滴答滴答(见图 V-1 ),故事围绕一颗稍微有点压力的炸弹展开,这颗炸弹将在 30 秒内爆炸。这意味着游戏中的每一关都应该在 30 秒内完成。如果玩家收集到所有提神的水滴并及时到达终点面板,则一个关卡完成。
图 V-1 。滴答滴答的游戏
这款平台游戏包含许多其他游戏中常见的基本元素:
- 应该可以玩不同的关卡。
- 这些关卡应该从一个单独的文件中加载,这样就可以在不知道游戏代码如何工作的情况下改变它们。
- 游戏应该支持玩家和敌人的动画角色。
- 玩家应该控制可以跑或跳的玩家角色的动作。
- 游戏中应该有一些基本的物理学来管理坠落,与物体碰撞,在平台上跳跃等等。
这是一个很长的列表!幸运的是,您可以重用许多已经开发的类。接下来的章节将介绍清单上的所有项目。如果你想玩完整版的滴答滴答游戏,获取第二十九章的样本代码,并打开TickTickFinal文件夹中的TickTick.html文件。
第六部分:进入广阔的天地
现在你已经知道如何用 JavaScript 编写游戏了。但是接下来呢?你如何创造一个成熟的游戏,并为市场做好准备?为了发布你的游戏,你需要做些什么?你应该如何营销它?本书的最后一部分涵盖了这些主题。这一部分由两章组成。第一个涉及游戏制作,包括游戏设计、游戏开发和游戏制作的运营方面。第二章涉及游戏出版,包括从游戏中赚钱的模式,营销你的游戏,并使你的游戏可以在不同的语言和文化背景下玩。
因为听取游戏行业人士的建议非常有用,所以我采访了两位重要的人物。首先,我采访了马克·奥维马斯:他开发了 GameMaker 应用,这是一个快速创建游戏的伟大工具。GameMaker 已经发展成为一个成熟的应用,现在由 Mark 部分拥有的 Yoyo games 公司维护。他也是 Tingly games 的联合创始人兼首席技术官,该公司使用内部构建的游戏引擎开发所谓的 JavaScript 问候游戏。
第二,我采访了 Rovio Entertainment 公司的彼得·维斯特巴卡(Peter Vesterbacka ),该公司以其世界闻名的《愤怒的小鸟》( Angry Birds)系列而闻名。彼得与公司的创建有很大关系。2003 年,他在惠普工作期间组织了一次游戏制作比赛,目标是创作出最好的多人手机游戏。这是在 Android 和 iOS 出现之前的事情了。诺基亚刚刚推出了其首款智能手机。在芬兰阿尔托大学学习的三个人——尼克拉斯、亚诺和金——参加了一个名为“卷心菜世界之王”的游戏,并赢得了比赛。彼得建议他们开一家公司,他们照做了。51 个游戏之后,在 2009 年,他们创造了第 52 个游戏,叫做愤怒的小鸟*。彼得是公司所谓的雄鹰。他把自己的主要角色描述为“确保公司大事发生得足够快。”Peter 参与了公司的许多不同方面,包括营销和品牌,他希望帮助公司朝着新的创新方向发展。*
接下来的两章主要基于我对马克和彼得的采访。他们两个都是真正鼓舞人心的人。在整篇文章中,你会发现他们对游戏制作和出版的看法,他们分享了许多有用的技巧和诀窍。