Main 网页游戏开发秘笈

网页游戏开发秘笈

0 / 0
How much do you like this book?
What’s the quality of the file?
Download the book for quality assessment
What’s the quality of the downloaded files?
File:
PDF, 8.43 MB
Download (pdf, 8.43 MB)
0 comments
 

To post a review, please sign in or sign up
You can write a book review and share your experiences. Other readers will always be interested in your opinion of the books you've read. Whether you've loved the book or not, if you give your honest and detailed thoughts then people will find new books that are right for them.
2

网络攻防实战研究:漏洞利用与提权

Year:
2018
Language:
chinese
File:
EPUB, 25.35 MB
0 / 0
版权信息
书名:网页游戏开发秘笈
作者:(美)Evan Burchard
排版:zm
出版社:机械工业出版社
出版时间:2014-05-05
ISBN:9787111459927
— · 版权所有 侵权必究 · —

译者序
最近很多人开始关注网页游戏了。从游戏开发的角度来看,网页
游戏这一形式具有诸多优点。
首先,传统的游戏开发形式大多受制于移植问题。开发好一款游
戏之后,需要将其移植到多个操作系统中,而每个操作系统所适宜的
开发环境又各有区别。网页游戏则不然:开发者把大部分精力集中在
浏览器里即可。
此外,传统的开发形式一般需要大量的资金和人员支持,而网页
游戏则特别适合中小团队及独立游戏开发者。从创意,到实现,再到
测试并发布,这个周期可以缩得很短,而且过程也可以很灵活,发现
新想法之后,立刻就能实验并看出效果来。
但是,初学者在入门时会遇到几个困难,其一是JavaScript语言
与HTML5的特性太多,短期内很难将这些知识点全部掌握,而且有些特
性与游戏开发的关系并不是很大。其二在于,许多读者原来未必具备
丰富的开发经验,大家可能是从其他编程语言、其他开发平台,甚至
其他行业转入网页游戏开发领域的。我们必须找到一套实用的开发流
程,否则每次做游戏都要从头写起,这会耽误大量时间。
本书就相当顺畅地解决了这几个问题。作者Evan Burchard先生没
有讲述高深的理论,而是直接选了10种常见的游戏类型,告诉我们如
何通过适当的游戏引擎及工具,快速制作出这些游戏来。每章所选的
范例游戏,其制作过程都分为好几个步骤,读者可在看完每个步骤之
后及时总结当前制作进度。这些范例其实就是模板,只要根据每章最
后的建议稍加修改,就能做出一款颇具个人特色的网页游戏了。

通过引擎来做游戏,既能缩短学习时间,又能降低编写代码的难
度,而且只要学会一种引擎,就可以开发出许多款同类游戏了。由此
可以想见,在学完全部10款引擎之后,你的开发思路一定会大为开
阔。
这本既直观又实用的教程,不仅对初学者有用,中等水平的读者
也能从中收益。你可以对比书中所用的引擎与你所喜好的引擎之间有
何异同,也可以思考怎样用引擎来制作书中没有讲到的那些游戏类
型,还可以研究书中所提到的各种游戏算法及游戏创意。
要提醒大家注意的是,与具有深厚积淀的传统技术领域不同,网
页游戏是个变化很快的行业,各种HTML5新特性层出不穷,而且
JavaScript语言标准、程序库、游戏引擎等也都日新月异。一方面要
熟悉JavaScript语言、努力提高代码质量,另一方面也要紧跟潮流,
多看、多学、多练,不断提升开发熟练度及创意能力,有条件的朋友
还可适当参与开源项目。针对这些内容,作者在书后总结了三个很有
参考价值的附录,可供大家随时查阅。
本书翻译过程中,得到了机械工业出版社华章公司诸位编辑与工
作人员的帮助,在此深表感谢。
由于时间仓促,译者水平有限,错误与疏漏之处在所难免,敬请
读者批评指正。大家可发邮件至eastarstormlee@gmail.com与我联
系,也可访问网页http://agilemobidev.com/eastarlee/book/theweb-game-developers-cookbook留言。
爱飞翔
2014年3月

前言
笔者小时候的一件乐事就是玩日本出的游戏卡带(cartridge),
那上面都有“任天堂公司官方品质认证标贴”(Official Nintendo
Seal of Quality),而且带着一股塑料味儿。把这些游戏卡插在一个
“魔盒” [1]里,并按下“电源”键,然后,充满挑战、发现与征服
的娱乐之旅就开始了。后来我发现了一件令自己颇感吃惊的事:其实
我玩的这些游戏(以及其他类似游戏)普通人也可以做出来,而且有
的只需一个人或几个人就够了,但这些制作团队却变得越来越大了。
正如笔者所见,当初那些由几个游戏迷组建的小团队的行业,现今正
成长为价值500亿美元的电子游戏业。
时下,虽然大型游戏工作室已经占据市场主导地位,但是小型的
独立游戏开发团队依然有重新焕发生机的机会。这些团队在发布游戏
时有许多平台可供选择,然而那些平台中所发生的革新,都不如Web浏
览器领域这般显著,这个平台原本比较低调,而且容易为人忽视。但
现在,随着浏览器技术的发展,涌现出数以百计的免费游戏引擎,通
过这些引擎,游戏设计者只需独自一人,即能创建出具备个人风格的
游戏来,这些游戏可以做得非常有趣,给玩家留下深刻印象,甚至还
有潜在商机。要制作这种网页游戏,只需浏览器和文本编辑器,并掌
握本书所讲的一些知识即可。偶尔需要打开控制台,不过更多的时候
只用点几下按钮就行,想制作一款能给自己和他人带来乐趣的游戏,
这是最简单不过的办法了。
快按下电源键,开始跟我学做游戏吧!

致谢
首先,真诚地感谢每位读者。你肯阅读本书,笔者特; 别开心。非
常感谢。
还要感谢Pearson公司的工作团队,尤其是Laura、Olivia和
Songlin三位,感谢你们给我机会写作本书,并指导我写完。
感谢诸位友人及审阅者:Jon、Rich、Jason、Greg、BBsan、
Pascal、Tim和Tony。
感谢聪慧的母亲,感谢洞察秋毫的父亲,感谢耐心而卓识的Amy。
感谢Gretchen与Max,谢谢你们作为首轮测试者,来玩我所开发的游
戏,你们真是相当率直而喜乐之人。
在成长过程中,有很多好游戏伴随着我,所以要感谢这些游戏的
每位制作者。20世纪90年代,有很多ROM破解社区,我从这里初次了解
到如何剖析游戏,所以要感谢其中的每位成员。
感谢开源社区的贡献者。你们为这个世界贡献了许多精彩代码,
正因为受你们影响,我才会加入开源者的行列,并享受个中乐趣。笔
者在本书中用到了一些工具(参见附录C),尤其要感谢这些工具的作
者。若没有这些工具,本书绝对无法完成。在HTML5游戏制作的合成与
展示方面,Kesiev做了许多工作,我对此表示特别感谢。
感谢Morris先生边看我写书,边给我挑毛病;感谢Jamison博士教
我领会了“理解的广度与深度”是何等重要;感谢Hatasa博士给我提
供了一个全新的视角,令我重新审视这个世界。
感谢所有唱诗班与剧场里的孩子们,也感谢里面的诸位朋克、怪
咖、书呆子、极客、工程师、企业家、研究者、设计者、梦想家和百

事通,这些年来,是你们令我保持良好心态,你们给我带来了欢乐,
也带来了恰到好处的小小烦恼。尤其要感谢剧场里那一位能耐心忍受
我长时间纠缠的小朋友。
最后,感谢信任我并给我理由的每个人,也要感谢虽不信任我但
却能给我理由的每个人。
[1] 指 FC 游戏机之类的家用游戏主机。——译者注

导言
从前制作游戏时经常需要使用很多特殊工具。现在不同了,只需
浏览器和文本编辑器就行。不仅仅是HTML5游戏,采用其他技术来制作
游戏所需的时间与成本也比原来大大降低了,现在只需几天,甚至几
小时就能做好一款游戏。独立游戏开发者的参与平台正在渐渐扩大,
比如“游戏制作节”(game jam),这是一种以多人在线协作方式迅
速制作游戏的开发者集会。
game jam通常为期48小时,大型的在线开发者聚会活动(如
Global Game Jam与Ludum Dare等)都是这么长的时间。不过游戏设计
师们都喜欢自创规则,所以有些jam可以短至仅1小时。除了能增进沟
通及协作能力之外,游戏制作者还能迫使自己练就迅速制作游戏的本
领,以便下次能更快做好游戏,这项本领对长期与短期游戏项目都有
好处。
不是只有独立游戏开发者才需要“快速制作东西”这项技能。软
件公司也需要,他们把这称为“生产力”。有句口号叫做“变得更聪
明”,可惜这句话说得很不明确,其实,要想提高工作效率,“找对
工具并善用工具”才是更高明的办法。这句话所传达的信息更具说服
力,要是把数学等知识也包括在“工具”一词的定义中,那就更加能
令人信服了。
笔者起初列了份清单,其中有一百多个HTML5游戏引擎,成书时将
其缩减,只保留了最优秀的几个。通过这些引擎,以及本书中提供的
工具,就能用浏览器快速制作出游戏来了。使用这些引擎很简单,只
需把对应的JavaScript代码载入HTML文件即可,偶尔可能需要添加一
两行代码。总的来说,笔者选定的这些引擎都有良好的开发文档和活
跃的支持社区。某些引擎可能比其他引擎要大一些。它们都提供了开

发游戏所需要的特有功能,等学过几个引擎后,你就能体会出各引擎
之间的异同了。
本书每章讲解一个引擎,并选一种游戏类型与之相配。读者在阅
读过程中会发现,随着游戏类型越来越复杂,所选的游戏引擎也必须
功能越来越丰富才行。读完本书之后,再学其他游戏引擎就非常轻松
了,你甚至可以自己拼装一个引擎玩玩。
本书所讲的每个游戏都只需几个小时就能做好。那大家可能要问
了:每个类型里所举的那个范例游戏,会是我所喜欢的吗?笔者觉
得……很有可能不是。本书要做的是剖析这些游戏类型,将每个类型
都分解成几个基本部件。这就好比盖房子,本书只是打好地基,立好
框架,竖好白墙。某些情况下笔者可能会略加点缀。然而尚有不尽如
人意之处,比如屋顶可能会有个大洞,比如墙上可能挂的是笔者喜欢
的画作而非你自己喜欢的那幅。没关系,你可以自己动手,比方说,
设个庭院,铺上粗毛线毯,再种几株银杏。可以把我挂的那幅画换成
你自己喜欢的。我只是告诉你从哪里获取相关素材而已,至于具体如
何布置,那都由你决定。本书就是如此:读者按照自己的需要来行事
即可,只要把自己想要的东西添上去,这就是你自己的游戏啦。
读完本书之后,大家很容易就能构想出一款自己所中意的游戏,
将其拆解为若干功能,并使用本书所讲的这套工具,以相似的流程把
它做好。大家甚至都能预估出游戏的制作难度和所需时长。如能善用
工具,并且创意颇佳,那我想你应该能制作出一款好游戏,把大家立
刻迷住喔。

读者对象
若你打算阅读本书,我想可能有几个原因。如果你是对游戏感兴
趣,并想学着编程,那这本书很合适。如果你是一名网页开发者或设
计师,正在寻找制作游戏所需的工具、技术与模板,或者你是一位
JavaScript程序员,想从入门级提升为中级水准,那么这本书也很合
适。如果你是一名游戏设计者或开发者,原本在制作flash游戏、移
动/桌面端的原生游戏,或其他平台的游戏,而现在想来看看怎样在
HTML5/JavaScript环境下开发游戏,那么这本书还是很合适。但如果
你把HTML5盾形图标纹在身上,经常展示自己为开源社区贡献的游戏引

擎代码,而且只花一个周末就能用HTML5开发出“Mario 64”这种游
戏,并能将其移植到iPhone上面,那恐怕就不需要读这本书了吧。

本书代码风格及行文约定
新加入的或有所更改的代码行,用粗体标出。程序清单里省去的
一行或多行代码,以省略号(...)标识。如需刻意指明某行已经删掉
或改掉的代码,则以注释形式(以//开头)将原来的代码写在删改前
的位置上。若整段程序清单都是新代码,则不加粗。
接续符(

)表明当前这行代码是接着上一行代码来写的。

提示
某些需要稍加解释的内容,会写在这种“提示”框里面。

技巧
那些值得一提但却不适合放入正文的内容,作为“技巧”列于此
处。

警告
某些不太明显的地方,或是在不知情时容易出问题的地方,写在
“警告”框里面。

本书内容组织方式

本书分为11章,从第1章~第10章每章都讲一个游戏,最后有三个
附录(附录A讲解了JavaScript的基础知识,附录B讲解了管控代码质
量的方式,附录C列出了制作游戏时要用到的资源)。读者在阅读第1
章时,不需要任何HTML、CSS或JavaScript基础,也不要求会使用各种
工具。然而在阅读后续各章之前,最好能先把附录A与第1章掌握了。
从代码角度来讲,后续每一章都不需要用到前面各章的代码。不过,
各章所讲述的游戏类型渐趋复杂,所以最好是先把比较简单的几种游
戏类型学会,然后再学后面的章节,这样会更顺利些。第11章可以作
为一份指导材料,它告诉大家读完本书之后应该继续学习哪些内容。
最后的附录C里还有一份资源列表可作为补充材料,其中列出了本书第
1章~第10章创建游戏时所用的工具。
每个游戏的制作过程都分解为数个步骤,而每一步又细分为若干
段代码与文本,这样便于大家理解。这些步骤所对应的源文件均可在
jsarcade.com网站中找到。也就是说,每一步所对应的源文件都放在
一个文件夹中,而这些文件夹及其中的代码均可从本书英文版配套网
站里下载。如果在阅读过程中“迷了路”,或是想跳读,那么可以抛
开当前这一步的代码,直接从后续步骤的代码开始研究。有时也许想
看看这个游戏的最终成品是什么样,那就可以直接调到“final”目
录,这里列出了每章所做范例游戏的最终版本。
在掌握了第1章与附录A之后,如果对本书其他各章还是看不太懂
的话,那么可以参考附录B,其中讲了很多避免编程困境的办法,以及
遭遇编程难题时的解决方式。

本书阅读方式
为了充分理解书中内容,需要下载每章的源代码。这些源码包括
JavaScript、HTML、CSS、图片,以及每一步所需的全部附加文件。这
些都能在jsarcade.com上找到。代码首先按每章章名分成数个文件
夹。每章所对应的文件夹里,都包含运行本章游戏所需的全部代码,
这些代码会放在三种不同类型的目录中。“initial/”目录里含有运
行游戏所需的最基本代码。“after_recipe<x>/”目录中包含执行完
每一步之后的“成果” [1](基本上对应于每一章内的节标题),这
样的话,即便在阅读时乱了思路,也没有关系,因为顶多只有一两页
稍微看不明白而已,你马上就能在下个步骤开始时重新整理思路。
“final/”文件夹下的代码表示每章范例游戏的最终状态。在每一章

中,每个步骤所对应的文件夹里都有名为index.html的文件。可以双
击打开该文件,也可以通过其他方法将其放在浏览器中运行,此时看
到的效果就是执行完该步骤后游戏所应有的样子。每个范例游戏的最
终版本均可以在jsarcade.com网站上找到,可以访问该网站,预览一
下每个游戏,然后从中选定自己喜欢的游戏类型并实现它。

提示
所有游戏源码、游戏引擎及其他相关软件均可在jsarcade.com网站
或informit网站(informit.com/title/9780321898388)中下载。
本书可以跳读,然而要知道,每一章所讲的游戏类型都会比上一
章复杂。如果碰到无法理解的地方,可以查看每个步骤所对应的成果
代码(也就是after_recipe<x>文件夹中的代码),而且还需特别留意
第1章与附录A,看看自己是否已经掌握了其中的内容。如果在制作游
戏的过程中出了错,而又不明白其原因,那么请阅读附录B。
学完一章之后,你也许会觉得游戏中好像还是缺了点儿什么似
的。你可能还想实现一个爆炸效果,还想添加一出精彩的剧情,还想
设计一场“Boss战”(boss battle) [2]。每章结尾都给出了诸如此
类的建议,你可按照这些建议来改进游戏,如果你有不同的见解,也
可以按自己的想法行事。只要在电脑中运行了这些游戏代码,它就算
是你自己的作品了。这些代码本来就是模板,本来就是可以修改、扩
充,并按自己喜好来定制的。若你修改之后的游戏比笔者原先写的还
要好,那我绝不会嫉妒,而只会乐见其成的。
[1] 原文为“ checkpoint ”,指检查点、关键点、基准点。——译者注
[2] “ boss ”可理解为游戏中的妖怪头目或敌方首领。——译者注

第1章

问答游戏
此类游戏规则很简单:在数个选项中找出问题的正确答案。如果
不知道的话,随便猜一个也行。从酒馆中的竞猜到SAT [1],到处都能
见到这种游戏的身影,说它是“无孔不入的”游戏类型,其实都低估
了其流行程度。电视娱乐节目里也有互动形式的问答游戏。有时除了
简单的问题与答案之外,游戏里可能还有其他元素,不过这些游戏的
底层逻辑都一样。比方说,在某个游戏里,国王询问玩家想不想与恶
龙决斗,玩家回答“想”,那这就可以算作一个相当短小而简单的问
答游戏(quiz)。说得更宽泛一些,在RPG(Role-Playing Game,角色
扮演游戏)中,玩家从平台掉入洞穴里,或者失掉了全部生命值(hit
point),也可以看成是对答错问题的一种惩罚。不管问答机制出现在
哪一类游戏里,我们都得用相似的代码来实现这套规则,并设计好答
对或答错问题之后所要执行的逻辑。
[1] SAT 曾 经 是 “Scholastic Aptitude Test” ( 学 术 能 力 测 验 ) 或
“Scholastic Assessment Test”(学术评估测试)的简称,这种测验是
美国各大学决定是否录取申请者的重要参考指标之一。——译者注

1.1 第一步:出题
由于大家的网页开发水平各不相同,所以笔者把第1章尽可能写得
简单明了,以便入门级的读者能够看懂。后续各章会越来越复杂,不
过理解了本章之后,再去看那些章节,也就不会太难了。每个人对网
页开发的掌握程度不一样,有些读者或许真的要从这里开始学起。如
果觉得本章内容过于简单,那就可以略读或直接跳过。后面各章会比
本章复杂,也会更难一些。
本章通过制作范例游戏,想达成三个主要目标。第一,要使大家
理解HTML、CSS和JavaScript的基础知识。在这三者之中,JavaScript
最难。若是对某些JavaScript基础知识还不甚了解的话,请参阅附录
A。第二,要令读者学会如何引入程序库,因为本书将要用到很多外部
程序库。第三,要使大家掌握一套便捷的开发流程,用来创建、编
辑、保存和打开本书所用的各种文件,并反复运用此流程,以制作各
类游戏。
如果没有文本编辑器,那现在就得找一个。本书用到的这些
JavaScript、HTML和CSS文件,拿任何工具来创建或编辑都行。大家若
还没找到合适的文本编辑器,那就请参考附录C,根据其内容选择一
款。
现在开始做游戏。首先打开文本编辑器,将程序清单1.1中的代码
加入quiz/initial/index.html文件中。如果还没下载这份代码文件,
那么请先参阅导言部分,按照其中所给的网址将其下载下来。
程序清单1.1

在index.html文件中建立html结构

提示
HTML是HyperText Markup Language(超文本标记语言)的缩写。
从前,“链接”(link)曾经叫做“超链接”(hyperlink),那时还有
几个与“hyper”有关的东西,用于在文档之间跳转。“超文本”
(HyperText)可以理解为含有超链接的普通文本。而“标记”
(markup)则是包裹在超文本周围的辅助文本,用来赋予其更多含
义。所以说,HTML是一系列语法指南,用于将不同类型的文本制作成
可以互相链接的页面,并将其组合起来,而这些页面文件以.html为扩
展名。
<以尖括号形式出现的代码>叫做HTML标签(tag) [1]之间的代码
则称为HTML元素(element),元素也包括首尾两个标签在内。注意,
结束标签里要写上“/”字符。
首先声明DOCTYPE。浏览器看到此声明之后,就知道应该按照HTML
文件来解析并呈现其内容了。由于浏览器还可以打开其他类型的文
件,比如XML文档、音频文件和图片等,所以,为了使浏览器能按普通

的网页来处理此文件,我们需要为它加上这条声明。如果不加这一
行,会怎么样呢?这取决于你所使用的浏览器,在某些浏览器上可能
会产生难于察觉的小毛病,而在另外一些浏览器上则可能会出大问
题。实际上无法确定其后果,所以,最好别忘了在文档开头加上这条
声明。
接下来是<html>标签。通过此标签,可以把整个文档都包裹在全
局容器里,而其内容一般是一个<head>标签和一个<body>标签,此处
所举的范例代码也是如此。大家可能已经注意到,这三个标签都有对
应的结束标签与之配对(例如<body>标签有</body>标签与之配对),
而结束标签里带有斜线字符(/)。通过这种方式,我们可以把其他元
素包裹在一对开始标签与结束标签里,使其成为当前元素的内部元
素。
<head>标签里的东西通常对浏览器来说很重要,然而用户在浏览
器视窗里看到的大部分内容却和这个标签没有直接关系。<meta>标签
有许多用途。在本例中,它描述了文档所用的文本编码。如果不加这
个标签,那么浏览器在显示常用字符集(这个字符集相当小)之外的
字符时,说不定就会出问题。日常所用的大部分英文字符都能正确渲
染,但是如果要显示其他国家的字符,那就有可能出错了。此时,在
JavaScript控制台(Firefox浏览器的Firebug或Chrome浏览器的开发
者工具)中,还会看到警告信息,提示开发者应该加上这个标签。尽
管如此,本书在列举范例代码时,为了使读者能专注于相关内容,经
常会省去这个标签不写。
浏览器会把<title>标签所描述的文本显示在最上方(不同的浏览
器显示的位置也不一样,有些浏览器在标题栏(header bar)中显
示,有些在分页(tab)顶部显示,还有些在这两个地方都会显示),
另外,某些应用程序可以创建指向网页的快捷方式或书签,而此标签
内的文本则会成为快捷方式或书签的名称。
接下来是<link>标签,该标签的rel属性为stylesheet,type属性
为text/css,表明这是个样式表文件,而href属性则描述了样式表文
件在电脑中的存储路径。在本例中,通过路径可知,样式表文件所处
的文件夹与index.html相同(因为路径中只包含文件名,没有其他部
分)。在链接外部样式表文件(也就是CSS文件)时,经常会用到这个
标签,除了具体文件路径不同之外,其余部分大抵相同。还有一件事
要注意,<link>标签与<meta>标签一样,都不需要结束标签(比如

<link>标签不需要有</link>标签与之配对)。对于这种不用作容器的
标签来说,未必总要写上结束标签。
<body>标签里嵌套了两个元素。首先是标题标签(<h1>),此标
签采用默认样式显示文本,但是会将其变大。然后是<div>标签,它是
排布元素时所用的一种重要标签,能把各种标记信息按块组织起来。
在本例中,该标签在标签名后面写了一个id属性,通过这个属性,开
发者可以在CSS中选定某种样式来渲染此元素(比如改用一种与默认样
式不同的颜色与大小来渲染文本),也可以使此元素具备JavaScript
文件中所设定好的某种行为(比如点击鼠标之后,网页就会上下颠
倒)。在网页中实现上述功能时,有三种常见的方式可用于定位相关
元素,除了这里提到的通过id属性来指定之外,另两种方式是通过标
签名或class属性来进行。
现在<div>元素里什么都没有,然而在添加新东西之前,我们得先
确认一下当前网页是否能正常显示。把这份文件保存为index.html,
并打开浏览器。然后用浏览器运行此文件:可以直接在浏览器的URL地
址栏里输入文件路径,也可以将桌面上的文件图标拖到浏览器中,还
可以直接双击文件图标。
用浏览器打开文件之后,就会看到图1.1这样的画面。请注意,分
页顶部出现了“Quiz”一词,因为我们刚才在源文件的<title>标签里
就是这样写的。

图1.1 在Chrome浏览器中打开html文件

若是现在还没有安装Chrome和Firefox浏览器,那就将其下载下
来,并装到电脑里。因为开发HTML5游戏时这两种浏览器各有其用途,
所以需要把二者都装上。在学习本书内容时,这两种浏览器均会用
到,不能完全以其中一个浏览器取代另外一个。
现在修改<div>标签,将程序清单1.2中的粗体代码加入
index.html文件中,这样就可以把问答游戏中需要用到的问题添加到
网页里了。这段范例代码虽然比较长,但形式上很整齐。如果嫌敲键
盘输代码太麻烦的话,可以直接从quiz/after_recipe1/index.html文
件中复制过来用。
程序清单1.2

为问答游戏所设计的问题

以上问答游戏中每道题的源码结构都相同,只是题号、问题和选
项不同而已。现在我们单说第一道题。本例中,第一道题用<div>标签
来表示,此标签有id属性,其值为question1。id属性是个“唯一标识
符”(unique identifier),稍后可以通过该属性来引用此元素。这
个<div>标签把整个问题与其答案都包了起来。写好外围的<div>之
后,就该写嵌套在里面的那个<div>标签了,此标签用来书写问答游戏
将要给玩家出的问题。该标签有个class属性,其值为question。前面
讲过,要在源码中引用某元素,有三种方式:可通过标签名(例如
<div>)或id属性来引用,也可通过class属性来引用。class属性与id
属性的区别在于,id属性的值是唯一的,而class属性的值则可为多个
元素所共用。
接下来是<input>标签,它带有三个属性。type="radio"表示单选
按钮。若是没见过这种控件的样子,看看图1.2就明白了。第二个属性
是name,同一道题所对应的数个备选答案,其name值必须互不相同。
value属性的值通常在提交html表单时使用,它就好比用户在文本域
(text field)中输入的文本一样,提交表单时会把这个值提交上
去。本例不需要提交表单,但是稍后要通过JavaScript代码来检测玩
家在页面中选定的答案,那时将用到这些值。至此,大家已经看到,
有些元素的标签需要有与之配对的结束标签,有些则不需要。除了刚
才说的meta与link外,现在又碰到了input,它也属于那种需要用/>来
结尾的标签,这表示此标签既是开始标签,又是结束标签。

图1.2 为问答游戏所设计的问题与回答

在input元素外边所显示的文本可以置于<label>标签中。这种标
签的主要功能是:当用户点击标签文本时,浏览器的输入焦点会自动
切换到与该标签相对应的单选按钮上。本例不需要实现此功能,你如
果需要这项功能,那么可以给每个备选答案所对应的单选按钮都添上

属性值互不相同的id属性,比如,某个单选按钮的id属性值是
question-10-answer-b,那么与之对应的label元素,其for属性也必
须取同一个值,像是这样:<label for="question-10-answer-b">。
每两道题之间用换行标签(<br/>)隔开,此标签以斜线结束,这
个标签既是开始标签,也是结束标签。换行标签可以在垂直方向上把
两个物体隔开。间隔距离因浏览器而异,所以说,在网页布局很重要
的情况下(大多数网页都很重视布局,但是本例不讲究这个),应该
改用CSS样式来设定这个间隔距离。
保存这份网页源码文件,并用浏览器将其打开,如果一切顺利,
就会看到图1.2那样的画面。
[1] 为了与“markup”(标记)相区分,这里将“tag”一词译为“标
签”。——译者注】,而在<开始标签>(beginning tag)与<结束标签
>(closing tag)【也称“起始标签”与“闭合标签”。——译者注

1.2 第二步:隐藏题目
游戏中通常会有一些可解锁的物件。比如可解锁的角色、可解锁
的支线任务和可解锁的关卡等。对于本例来说,也许会有可解锁的题
目。这听起来是不是有些小题大做呢?其实不是。只因为刚才问答设
计得一目了然,所以不需要此功能而已。但有时就不同了,比方说,
像Mario这样的游戏,玩家不可能同时去玩所有关卡,所以开发者也许
会把某些关卡隐藏起来。同理,假设问答游戏有100道题,而不是10道
题的话,那么你也许就会觉得不太应该把所有题目全都显示出来了。
如何隐藏网页中的内容呢?有很多办法,比如,可以把问题放在
不同的页面中,但此处为了简单起见,我们采用的办法是:通过CSS文
件,使某些内容不在网页上出现。新建一个文件,将程序清单1.3中的
代码写进去,把它存为main.css,并放在与index.html相同的目录
下。
程序清单1.3

隐藏网页内容所用的main.css文件

#quiz中所列的样式,会应用在所有id属性为quiz的容器元素及其
内容之上,比如本例最外围的div元素及其中的全部内容都要受其影
响。display:none的意思是,把id属性为quiz的这个div所含的全部内
容都隐藏起来。如果想选定id属性为another-quiz的元素,那么可以
把样式选择器写成#another-quiz。若要选取class属性为quiz的元
素,则需要把选择器中的#号替换为圆点,也就是改成.quiz。
如果选择器是基于标签的,那么在标签名之前不用加字符,比
如,要选择body标签,就不需要在body之前再写#号了。margin-

left:50px;这行代码的意思是:给左边留出一定的空白,令网页内容
靠右一些。注意这两个样式块的格式。首先写选择器,其次写左花括
号,接下来写样式信息,最后写右花括号。在样式信息中,首先写css
属性名,后面跟一个冒号,然后写css属性值,这个值会影响到相应元
素的样式,最后,在行尾写一个分号。
对于刚接触网页开发的读者来说,这些语法可能有点复杂,尤其
是它们还要和html文档中的标签相搭配,而且又要使用id、class等属
性,同时掌握这么多内容,确实有些难。不过好在CSS与HTML的基础知
识也就是这么多了。刚开始学的时候,也许会经常出错,比如把本该
用圆点的地方误写为#号,比如忘了加分号或右花括号,比如其他各种
拼写错误等。其实,高手也经常犯这些错误。如果某段代码无法正确
运行,别着急,仔细检查,看看有没有写错的地方。
保存index.html文件并用浏览器将其打开之后,将会看到与图1.1
差不多的画面,只是网页的缩进变了,因为我们为body元素运用了样
式。

1.3 第三步:重现题目
所有题目都消失后,需要采用某种办法将其重新显示出来。这里
采用一种间接的方式:先把本章及后面9章所用的10个程序包引入网页
之中。然后,逐个检查这些程序包是否已经正确加载,每确认完一个
程序包,我们就把原来隐藏的一道题目显示出来。
在核查这些程序包是否正确载入之前,需要先确认浏览器是否能
正常载入JavaScript脚本。把程序清单1.4中加粗的那一行代码添加到
index.html文件靠近末尾处。
程序清单1.4

加载首个外部JavaScript文件

这样就可以把game.js这个JavaScript文件加载进来了。接下来,
需要创建该文件。在main.css与index.html所处的目录中新建一个文
件,将其命名为game.js,并把程序清单1.5中的代码加进去。
程序清单1.5

game.js文件

这段代码会在两个地方打印信息。用浏览器打开index.html文件
后,会弹出警示窗,这是第一个打印信息的地方,它比较明显。而代
码第二行的console.log则会向JavaScript控制台输出信息,
JavaScript控制台是开发者不可或缺的工具。关于如何启动并运行此
控制台,请参阅附录B。

解决好这个问题之后,接下来就该载入jQuery了。要想获取此程
序库,最快的办法是去jquery.com网站下载。至于如何通过网页把它
下载下来,由你决定。笔者所用的办法是,直接点击网页上那个最
大、最醒目的按钮,然后会跳转到另外一个页面,此页面专门用于显
示jQuery的全部代码。新建一个名为jquery.js的文件,把网页中的代
码复制下来,并粘贴到这个新文件里,然后将其保存。
网站上还有其他按钮,点击之后,可以按照传统方式下载源码文
件。虽然具体如何下载你可以自己决定,但是一定要把它放在文件系
统中正确的目录里面(也就是把它和index.html、main.css、game.js
放在同一个目录里)。
把jquery库的源文件放好之后,将程序清单1.6中的粗体代码加入
index.html文件末端。请注意,jquery源文件的名字要与index.html
文件里所写的名字一致。
程序清单1.6

将jQuery程序库引入index.html文件

假如你的jquery源文件不叫jquery.js,那么必须修改index.html
所引用的文件名,使二者相符,这样才能正确加载。
在继续往下写之前,先调整一下CSS文件。上一节中的CSS文件写
得太粗略了。这次我们不直接把整个quiz隐藏掉,而是像程序清单1.7
这样,写得具体一些,把要隐藏的每道题目逐个列出来。
程序清单1.7

不直接隐藏整个quiz,而是逐个隐藏每道题目

把原来写的#quiz这个id选择器删掉,改为一系列以question开头
的id选择器,并用逗号将其隔开。也可以不这么做,而是为这些问题
所在的每个<div>元素都添加相同的class属性,然后采用“点选择
器”(dot selector)来选取它们。不过,大家还是了解一下此处所
采用的这种选择器列表写法。
修改完代码之后,CSS这个“坏家伙”就会把所有题目都隐藏起来
了,然后,我们得用jQuery这个“好人”把隐藏掉的问题重新显示出
来。要实现此功能,需要编辑game.js文件,把程序清单1.8中的这段
代码加进去,并把文件里原来写的那些代码删掉。
程序清单1.8

若能正常加载jQuery,则将第一道题显示出来

第一行代码先判断jQuery库是否已经加载。如果加载好了,那么
就执行第二行代码。这行代码调用jQuery的$函数,在#question1这个
CSS选择器左右两端加上引号与括号,将其作为参数传给此函数。然
后,执行jQuery的show函数,将第一道题的样式由display:none改为
display:block。
保存game.js文件,并用浏览器打开index.html,你会看到,第一
道题又重新显示出来了。

1.4 第四步:引入各种程序库
在这一步里,我们还要引入9个程序库的源文件。读者可能会问,
像这样先引入程序包,判断其是否加载好,然后再把对应问题显示出
来,到底有什么意义呢?大家可能觉得像这样反复下载文件并将其引
入网页的练习很无聊,但这是为了学习如何获取并使用他人写好的代
码,这项技能很重要。很少有项目是从头开始写起的,所以,做游戏
时要学会“站在巨人的肩上”。如果还没有学会如何使用第三方代
码,那么现在一定要花些时间来学习这项技能。此外,本章还会简单
介绍一下后续章节所用到的各种程序库。
不过,若是你本来就已经学会如何将JavaScript程序库集成到项
目中,并且已经掌握了版本控制系统的用法,那么把下面这些内容当
作复习好了,可以略读或跳过这一步。

各程序库简介
此处列出这10个程序库,并介绍它们在本书中的用途。
1.jquery.js:我们已经引入了此文件。后面有好几章都会用到这个程
序库,它可以很便捷地选取并操作网页中的元素。
2.impress.js:第2章将把这个幻灯片演示工具(像PowerPoint一样,
只不过是使用JavaScript写的)当作游戏引擎来用,以便管理交互式小说
游戏中的“书页”。
3.atom.js:这是用未压缩的CoffeeScript代码写成的文件,仅有203
行,无疑是个小巧的游戏引擎。第3章将用它来制作派对游戏。
4.easel.js:开发者可以通过这套优雅的接口来调用canvas API。第4
章用它绘制方块游戏中的元素。
5.melon.js:第5章会用这个引擎来做“平台游戏”(platformer)。
6.yabble.js:第6章在制作格斗游戏时,会用这个程序库来载入
game.js游戏引擎(这是个游戏引擎的名字,不是指本章与其他各章所用
的game.js源文件)。
7.jquery.gamequery.js:这个jQuery插件也能当游戏引擎来用。第7章
用它制作横版射击游戏。

8.jaw.js:这是个可靠的全能游戏引擎,第8章将用它(并使用老式
的三角函数法)来制作第一人称射击游戏。
9.enchant.js:这款来自日本的游戏引擎功能很强大,而且能很好地
支持移动设备。第9章将用它制作角色扮演游戏。
10.crafty.js:这是个广受支持的全能游戏引擎,第10章将用它制作
即时战略游戏。(如果只允许带一款游戏引擎去荒岛上做游戏的话,
那我也许会选它。)
本章所用的主要程序库,也就是jQuery,现在已经加载好了,接
下来加载其他9个。要是想来点刺激的,可以根据附录C试着把它们分
别从各自项目的网页中下载下来。也可以在本章源文件的
after_recipe4文件夹下找到这些程序库。只要把它们放在index.html
文件所处的目录中就好。

提示
看看附录C你就会发现,这些文件其实也放在github上。想从github
获取文件,有三个办法。第一种就是把整个项目作为zip文件下载下
来,解压缩,然后找到你需要使用的那些文件。
第二种办法是在github网站上打开项目,找到相关文件,然后在自
己的电脑中新建一份空白文件,把github网站上的代码复制下来,粘贴
到自己的文件里。这么做似乎有点麻烦,不过执行起来其实很快。
第三种办法稍微复杂一些,但是对于现在和以后要做的游戏项目
来说,这种办法能使开发过程更加流畅。这个办法就是:安装git,用
它来下载(克隆)存放本书范例代码的那个项目 [1],然后进入与本章
同名的那个目录,把本章用到的这些文件都找出来。可以直接在这个
目录下编程,也可以把自己所需的文件从中复制到别处。
git是一种版本控制系统(version control system),该系统可以记录
文件变更历程。而github是个网站,上面有许多使用git的程序员,他们
会以各种开发语言来编程,并在网站里寻找其他项目,同时也管理自
己的项目。这些公共项目均可随意取用。所以,笔者强烈推荐采用这

种办法来管理本书范例代码。这里有份非常优秀的教程,会告诉大家
如何安装git:help.github.com/articles/set-up-git。
请读者在刚才提到的三种方法里选择一种,把所有程序库文件都
下载好,并使之就位,此时,文件夹结构应该像图1.3这样。

图1.3 包含所有相关JavaScript文件的文件夹

将文件放好之后,把程序清单1.9里的粗体代码加在index.html文
件的末端,以便在网页中引入这些JavaScript文件。
程序清单1.9

在index.html中引入JavaScript文件

务必保证文件名与index.html所引用的名称相符。在html中引入
JavaScript文件的方法很简单,只需使用<script>标签即可。这段代
码中值得一提的是atom.js之上的<canvas>元素,以及<canvas>之上的
<!-- -->这一行。如果没有<canvas>元素,那么atom.js就无法正常运
作了,所以必须加上它。在使用很多游戏引擎之前,开发者都必须先
调用初始化函数,或是引用引擎所需的<canvas>元素,以便启动引
擎。在网页中引入atom.js时,该脚本会立刻搜寻canvas标签。有两种
应对方法:要么编辑atom.js,将这个行为改掉,要么在网页中先提供
一个canvas给它用,我们选择后者。<!-- -->这一行是HTML注释。注
释是写给你自己或其他开发者看的,而浏览器则不看注释,它会将其
忽略。请注意,网页访问者也有可能会看到这些注释,因为他们通过
浏览器的查看源代码功能就能看到HTML页面的源码。如果不明白查看
源代码这项功能的用途,请参阅附录B。
接下来,修改game.js文件,把其余9个问题重新显示出来。将程
序清单1.10中的粗体代码加入game.js即可。

程序清单1.10

将其余9个问题重新显示出来

通过这段代码可以看出,在引入jQuery这种JavaScript程序库之
后,最常见、最明显的效果是,系统里出现了某些新对象。但是,显
示第7道题时所写的代码与其他地方不同,此处没有直接检测
playground。因为gameQuery只是jQuery的一个扩展插件,它是基于
jQuery而构建出来的,所以,它本身并不产生新的核心对象,于是,
我们只好先调用jQuery的$()函数,然后检查其返回值里是否定义了名
为playground的函数,以此来判断该插件是否正常载入。

警告:第三方程序库的代码未必归您所有
程序库的编写者经常会对“此程序库应该如何使用,以及必须如
何使用”表达其看法。他们会在源代码中包含一份软件许可协议,将
这些观点写成具备法律约束力的意见。当然,这并不是说不能或不应
该使用这些程序库,只是说使用时必须遵守其约束。比如,有些协议
禁止将程序库商用;有些协议要求使用者必须署上程序库原开发者的
名字;而还有些协议只是放在那里,令所有使用者都能看到它而已。
这些协议的种种细节不在本书讨论范围内,不过,大家可以读一读这
些项目的协议,或是看一看Creative Commons、GPL、BSD、MIT等协
议,这样就能更好地理解其开发者要求他人在使用其开源项目时,应
该遵守何种约定了。图片、声音文件以及其他各类文件均受软件协议
约束。

将game.js保存,并用浏览器打开index.html,你会发现,所有题
目又重新出现了,但是,点击鼠标之后却没反应,无法选择答案。这
是因为canvas元素会扩展至整个页面,从而无形中阻拦了鼠标,使之
无法点击答案旁边的单选按钮。解决办法是,将程序清单1.11中的粗
体代码加入main.css之中。
程序清单1.11

在main.css中加入CSS样式,以便隐藏canvas

[1] 项目地址是:http://github.com/evanburchard/jsarcade。——译者注

1.5 第五步:判断玩家所选答案是否正确
为了标识每道题的正确答案,我们可以给正确答案所对应的单选
按钮加上一个值为"correct"的class属性,不过这似乎太简单了:从
开发者的角度来看,实现起来太过容易,而从玩家的角度来看,又很
容易发现正确答案。网页源文件里的全部代码,包括注释在内,都能
为访问者所见,因此,即便其不知道正确答案,但只要查看源代码,
寻找标注为"correct"的那个单选按钮,也就能发现与之对应的正确答
案了。于是,笔者决定实现一个“弱哈希函数”(weak hashing
function) [1],用它来判断玩家所选答案是否正确,这样的话,那些
懂得编程的玩家就不那么容易偷看答案了,而对于那些不会编程的玩
家来说,则几乎不可能投机取巧。
所谓哈希函数,就是能根据某个输入值而算出另外一个输出值的
函数。其强弱程度取决于能否轻易根据“哈希之后的值”(hashed
value)反向推出哈希之前的原始值。如果很容易就能推出来,则是弱
哈希函数,否则就是强哈希函数。
在实现此函数之前,先创建一种样式,用以表示玩家答对全部问
题时的网页显示风格。将程序清单1.12中的粗体代码加入main.css。
程序清单1.12

向main.css中加入表示“获胜”状态的样式

此样式表明,如果元素的class属性值是correct,那么就将其背
景设为蓝色,而将其中的文本设为白色。也许在还未上幼儿园的时
候,大家就已经知道“白”这种颜色了,但是你很少会听人提到
#24399f这种颜色,即便读研究生课程时,也未必会碰到它。这是RGB
颜色(RGB是Red Green Blue的首字母缩写)。前两个数字表示红色
值,中间两个表示绿色值,最后两个表示蓝色值。
有个问题要先说一下。颜色值最后一位似乎是“f”。这好像不是
个数字吧?在十进制下,这确实不是个有效数位。如果红、绿、蓝三
个颜色分量都采用十进制,那么两位数字最多只能表示100种颜色(十
位可以取0~9,有10种不同的取值,个位也可以取0~9,也有10种不同
的取值,所以一共有10×10种取值)。有人觉得100种太少了,于是决
定采用十六进制,这样的话,RGB值的每个分量就有256(16×16)种
取值了。有些颜色可以直接用英文词来表示,不过,白色(white)也
可以写成#ffffff,而黑色(black)也可以写成#000000。顺便说一
下,有人觉得用6位数来表示颜色太麻烦了,于是采用仅含3个数位的
十六进制值来表示颜色(每个数位分别代表红、绿、蓝分量),如果
写成这种方式,那么黑色就是#000,白色就是#fff。

修改完CSS后,还要在index.html文件里写一点东西。你得用程序
清单1.13中的粗体代码,替换掉原来网页里的body起始标签。
程序清单1.13

在index.html中为body元素添加onclick事件处理程序

现在的<body>标签和原来不同了,它还带有名为onclick的属性,
而属性值是一个字符串,字符串左右两端有双引号,而引号里边是一
行JavaScript代码。如果不明白字符串是什么,请参阅附录A。该字符
串描述了一个onclick事件处理程序,也就是说,只要用户在页面中的
任何元素上点击鼠标,那么就执行checkAnswer函数。注意,调用函数
时要在函数名后面加上一对括号。如果不加括号,那么就表示只引用
此函数,而不调用它。
接下来是本章最后一段范例代码了,大家可以想一想:在执行完
程序清单1.14中的这个函数后,会发生什么。粗体代码需要添加至
game.js文件顶部,可以把它放在用于判断jQuery库是否正确加载的那
个if语句之后,并放在显示第一道题所用的那个语句之前。
程序清单1.14

判断玩家所选答案是否正确

这段代码中的粗体部分定义了两个函数。第一个函数是
checkAnswer,它先设置好一个空字符串,以便稍后向其中添加内容。
接下来,把玩家所选的每个单选按钮所对应的value值依次追加到这个
answerString字符串尾部。待循环执行完毕后,调用第二个函数,也
就是checkIfCorrect,判断刚才的字符串是否和那一长串数字相等,

以此来判定玩家是否已经答对全部问题。那么,为何要与这个数字相
比较呢?
刚才在讲CSS颜色的时候说过,十六进制值可以使用0~f这16个字
符作为其数位。而每个答案旁边的单选按钮,其value属性值在a~d之
间,这4个值也是有效的十六进制数位。(可以把a~d这四个十六进制
数位理解成十进制的10~13。)所以,只要把这些值拼成一串,并和表
示正确答案的那个十六进制字符串相比较就可以了,而代码中出现的
那一串数字,正是这个十六进制字符串的十进制形式。
若两者相符,则向body元素中添加值为“correct”的class属
性,这一操作会使网页背景变蓝,并使其中的文字变白。然后,修改
h1标签的文本,用“You Win!”替换掉原来的“Quiz”。最后,把原
来隐藏的那个canvas元素重新展示出来,以便阻隔鼠标操作。其实还
有另外一种做法,比这里所用的办法更为常见,那就是通过jQuery来
禁用单选按钮控件,不过笔者认为本例所采用的办法更有意思一些,
因为我们巧妙地利用了这个本来与问答游戏毫无关系的canvas元素。
有了这个canvas元素之后,可以通过atom.js来构建一款游戏,不过在
本章这个问答游戏里,我们只是用它来屏蔽用户的鼠标点击而已。
完成所有步骤之后,保存相关代码文件,并用浏览器打开
index.html,如果所有问题都答对了,那么就会出现图1.4这样的画
面。

图1.4 答对所有问题后,游戏进入“获胜”状态

[1] hashing function 亦称hash function,中文也叫“散列函数”、“杂凑
函数”。——译者注

1.6 小结
在本章中,我们用10道题构建了一个简单的问答游戏,而这10道
题分别与本书第1~10章所讲的知识点有关。此外,还在学习如何引入
JavaScript程序库的过程中,实现了一个附加功能,那就是:将每道
题隐藏起来,并于稍后将其解锁。为了判断玩家是否答对了全部问
题,我们实现了一个弱哈希函数,把用户选定的每个单选按钮所对应
的value值当成十六进制数位拼接起来,并将其转换为一个很长的十进
制数,与表示正确答案的那个十进制数相比较。
在制作游戏的过程中,笔者讲解了HTML、CSS和jQuery的基础知
识,也为大家介绍了git及软件许可协议。本章还概述了其他各章将要
用到的某些游戏引擎以及其他一些程序库。
假如想继续开发这个问答游戏的话,可以考虑再加一页问题,只
有玩家把第一页全部问题都答对之后,游戏才会切换到第二页。第2章
将会告诉大家一种动态显示信息的办法,学了那一章之后,你也许就
能想到如何实现此功能了。除此之外,还有另一种进化方案。由于整
个问答游戏都为canvas元素所覆盖,所以你还可以在canvas里再设计
一个游戏。既然我们已经把atom.js引进来了,那么有了这个程序库之
后,自然也就可以在canvas上画东西了,等学过第3章之后,可以再回
过头来试试。
如果觉得本章太难,那么请先花点时间看看附录A。若觉得本章太
容易了,别着急,难的在后面呢。第2章就开始有难度了,到了第6
章,则会变得非常复杂。

第2章

文字冒险游戏
文字冒险游戏的代表作有《魔域》(Zork)和《惊险岔路口》
(Choose Your Own Adventure Books)。
现在流行的游戏基本上是MMORPG [1]、实时的第一人称射击游
戏,以及逼真的体育类游戏,这种游戏几乎能模拟出真实的球场氛围
来。如果是玩着这些游戏长大的,那估计你可能不太熟悉早期的电子
游戏,那时候的游戏不像现在这样,它们没法利用今天这些功能强大
的硬件和软件。有种游戏叫做“文字冒险游戏” [2],它们和《Choose
Your Own Adventure》系列 [3]的“游戏书”(gamebook)很像,玩家
在玩这类游戏时,需要点击游戏中的相关元素,以控制情节走向。而
像《魔域》,则要算是文字冒险类游戏中的创新之作了,它向玩家呈
现了一片广阔的游戏场景,玩家可在命令行中输入许多交互式命令,
在完成游戏任务的过程中,还会发现大量道具,玩家可以捡起、探查
或吃掉这些道具。
在玩NES平台 [4]上的《暗影之门》(Shadowgate)与《疯狂大
楼》(Maniac Mansion)这类“点击型文字冒险游戏”(point-andclick adventure)时,玩家会历经许多恐怖或幽默的场景,并沉浸在
游戏所营造的这个充满互动的氛围里,考虑到彼时的技术限制,能做
出这样的游戏实属不易。顺便说一句,无论你是《疯狂大楼》这款游
戏的粉丝,还是JavaScript语言爱好者,也不管你是喜欢玩所有类型
的电子游戏,还是只对文字冒险类游戏情有独钟,你都应该读读
Douglas Crockford写的这篇文章
(www.crockford.com/wrrrld/maniac.html),文中回顾了他将该游
戏移植到NES时所遇到的种种轶事。没听过这个人吗?他就是

《JavaScript:The Good Parts》一书的作者,同时还是JSLint这款开
发工具的编写者,而且,Crockford先生也是JavaScript开发者社区中
的一位全才。
这类游戏虽然看起来似乎有些过时,但在有些情况下还是大有可
为的。比如,《猴岛小英雄》(Monkey Island)与《疯狂大楼》的设
计者,在2012年曾为其新作《Double Fine Adventure》筹集到将近
350万美元的开发资金。而日本的恋爱冒险游戏(dating sims)也曾
风靡一时,其他类型的游戏甚至都受其影响,比如,SNES平台 [5]上
的《牧场物语》(Harvest Moon)本来应该是个RPG游戏,结果,玩家
在实现管理农场这个主目标的同时,居然还可以去谈恋爱。笔者在这
里提醒大家,游戏中如果包含“追求女孩”或“英雄救美”之类的内
容,那么很多玩家都会觉得非常老套,甚至对其厌烦。策划游戏时要
考虑各类玩家的接受程度,尤其在制作这种一不小心就容易开罪玩家
的游戏时,更要提前做足功课。从技术角度与题材角度来看,当今开
发游戏时可选的方案要比从前广泛许多,所以最好花些时间来仔细研
究并考量一下各种方案。在设计这种类型的游戏时,有很多地方值得
研究,比如,你是打算叫玩家按其想法自由操控游戏,还是打算将其
限定在预设的情节里?又比如,你是想为游戏设定一种颇为阴郁的基
调,还是想设计得古灵精怪一些,抑或介于两者之间?
我们使用impress.js来制作本章范例游戏,此程序库本来是用于
创建幻灯片的。它所绘制出的界面很像一本普通的故事书,要一页一
页按着顺序来看。但是,通过标准的网页导航技术,我们很容易就能
实现“直接翻到第45页”这样的跳转功能,从而把它做得和一本游戏
书一样。制作游戏的过程中,我们会接触到切换(transition)、缩
放(scaling)和旋转(rotation)等CSS3特性,我们无须自己编写代
码来实现这些效果,只需通过CSS3来指定即可。为了做得和《疯狂大
楼》这种游戏更加接近,我们还会在游戏中加入道具及道具栏
(inventory),令玩家可以使用道具栏中的道具,而这套界面,我们
则打算采用原生的HTML5拖放接口来实现。一个真正富有表现力的文字
冒险类游戏引擎应该能够理解任意动词与一两个名词搭配出来的短语
含义,然而想构建这种引擎却是相当复杂的。甚至连引擎所使用的脚
本也不是那么轻易就能写出来的。于是,我们在这里采用一种简单的
办法,也就是把特定的物件直接绑定到一起,这样的话,每一种交互
操作的结果都是固定的。此特性使得游戏玩起来有点像《Minecraft》
[6],只不过我们是“以分镜头的形式”将其展示出来而已。

[1] Massive Multiplayer Online Role-Playing Game的缩写,意为“大型多
人在线角色扮演游戏”。——译者注
[2] interactive fiction,也称“交互式小说”、“互动小说”。——译者
注
[3] 中译《惊险岔路口》,是以第二人称写成的互动故事书,读者需要
在阅读过程中做出选择,这些选择会影响故事结局。这种游戏方式也
称“观众选角扮演”。——译者注
[4] Nintendo Entertainment System(任天堂娱乐系统)的简写,俗称
“FC游戏机”、“红白机”。——译者注
[5] Super Nintendo Entertainment System(超级任天堂娱乐系统)的简
写,俗称“超任”。——译者注
[6] 中译《我的世界》、《当个创世神》,是一款沙盒游戏,玩家可在
这个三维世界中使用各种方块构造建筑物。——译者注

2.1 第一步:设计页面样式
进入本书范例代码的interactive_fiction目录,并用编辑器打开
initial/index.html页面文件,你会发现,里面其实没什么内容。目
前的代码只是把impress程序库载入进来了,还没有创建代表故事页面
的div元素(本章把这种div元素称为幻灯片)。现在请把程序清单2.1
中的粗体代码加入网页文件中。
程序清单2.1

在index.html文件中加入显示故事情节所用的页面

首先请注意没有加粗的那部分样板代码(boilerplate code)。
charset="utf-8"这一行其实也可以不写,如果不写的话,即便在网页
中只显示简单的英语字符,也会使控制台出现错误信息,而加上这行

之后,控制台就不报错了。本书很多范例代码都没写这行,但如果你
不喜欢在控制台里看到那些错误的话,那还是写上为好。此外,对于
使用屏幕阅读器(screen reader) [1]的用户来说,加上这个属性,
可以使网页更具亲和力 [2](也就是说,屏幕阅读器能够正确判断出网
页内容是用何种语言写成的,从而将其播报给用户),比如,屏幕阅
读器可以询问用户是否需要翻译网页内容。还需注意的是,加载
JavaScript程序库所用的<script>标签,应该放在body元素的末尾,
而非开头。这样做是对的,因为加载JavaScript可能比较耗时,所以
应该使网页中的其他内容先显示出来,而像这样把加载语句放到最
后,就不会阻塞浏览器对网页的渲染过程了。还有一件事要注意,就
是这里没有canvas元素,而且载入impress.js程序库时也不会创建此
元素。我们在做这款游戏的时候,不仅会使用JavaScript与CSS3,而
且还会用到HTML5,然而,我们却用不到其中与canvas相关的那些功
能。
接下来看看创建网页所用的代码。这段程序很简单。外围的div元
素有个值为impress的id属性,而内部3个div元素的class属性均为
step及slide。这三张幻灯片中的文字会告诉玩家如何在本游戏的页面
之间切换,但是,我们现在还没有写好相关的CSS代码,所以各个幻灯
片之间还无法切换。网页代码中载入了一个名为main.css的文件,而
这个文件当前还没建立。需要创建该文件,并把程序清单2.2中的代码
加入其中。
程序清单2.2
代码

main.css中实现幻灯片切换功能及修改网页风格所用的

这段代码主要做了两件事。第一件事情就是通过CSS重置来重设网
页样式。网上能够找到很多执行CSS重置的范例代码,有些长达数百
行。由于各种浏览器在渲染元素时均会使用其各自的那套默认样式,
所以才需要执行CSS重置。我们这样做就可以把各种浏览器的样式尽可
能统一起来了,在这个基础上设计,可使网页风格较为一致,而不会
在浏览器之间出现太大差异。
从body这一行往下,才真正开始定义元素的样式。我们定义了一
种放射渐变色来渲染body标签:网页中心是蓝色,在向边缘发散的过
程中,逐渐变为深灰色。定义渐变色时采用了好几种不同的写法,这
是因为各种浏览器在实现较新的CSS功能时,使用了互不相同的方式。
接下来在定义与opacity特性相关的样式时,也是这么做的。通过这两
套样式,我们把当前活动的页面设为可见,把其他页面隐藏起来,并
且为页面跳转的过程添加1秒钟的切换效果。最后,我们为slide元素
[3]设定一些标准样式,令其可以在蓝黑背景中凸现出来。

提示
要是想再多学一些CSS3样式的话,可以花点时间逛逛这个网站:
http://css3please.com/。
用浏览器打开index.html,如果一切顺利,浏览器就会呈现如图
2.1这样的画面。

图2.1 设计好的幻灯片样式

然后,我们要增加一段简单的故事情节,并且实现页面跳转功
能,使这本“游戏书”看起来更像回事。
[1] 是一种安装在电脑中的应用程序,可供视觉或阅读有障碍的用户使
用。——译者注
[2] “亲和力”(accessibility)是网页设计领域的术语,用来描述访问者
(尤其是身心障碍人士)获取网页内容的便利程度。又称“可亲
性”、“可访问性”、“可达性”、“无障碍性”。——译者注
[3] slide元素是指其class属性为slide的元素。——译者注

2.2 第二步:实现页面跳转
我们需要实现的功能是:在页面上展示一段带分支的剧情供玩家
选择,选择之后就会跳转到显示游戏结局的相关页面了,可能是个好
结局,也可能是个坏结局。先来处理剧情选择页面。编辑index.html
文件,在id属性值为1的div元素中,将<q>标签删掉,把程序清单2.3
中的粗体代码加进去。
程序清单2.3

实现剧情选择页面所用的代码

这段代码里包含指向其他页面的链接,我们在外围的<div>元素里
新建一个<div>,并将其class属性设为slide-text,并在其中放入3个
<p>标签。稍后就会设定这个新<div>元素的样式。现在先用程序清单
2.4中的代码替换掉id属性值为2和3的那两个div元素。通过下面这段
代码可以看出,玩家在执行了选择操作之后,游戏会分别跳转到哪两
个页面。
程序清单2.4

实现游戏结局页面所用的代码

在这段代码中,我们略微调整了第二个与第三个页面。由于要用
impress.js来展示,所以需要为这两个div元素添加data-x属性。如果
不加此属性,那么所有幻灯片都会叠在一起,这就使得三张幻灯片在
展示时的优先级相同,而用户只要一点击网页,游戏就会直接调到最
后一个页面。在不加此属性的情况下,可以试着把.impressenabled.step样式中的opacity属性设为1,这样的话,一打开游戏,
还没等用户点击鼠标,直接就跳到第三张幻灯片了,而这张幻灯片并
不是游戏开始时应该显示的那张。设置好data-x属性后,第二张幻灯
片就会置于第一张右侧,而第三张幻灯片则会置于第一张左侧。
与实现第一个页面时所用的代码相似,我们把这两个页面所要展
示的主要文本也分别放在一个div元素中,并将其class设为slidetext。由于这两个页面都表示游戏结局,所以还要在页面下方再放一
个div元素,将其class设为menu,并在里面添加一个链接,用以跳回
到开始页面。
本游戏设计的这两个结局都不太有意思。你可以自己编写一些有
趣的故事结局,不过笔者在这里想说的是,这种游戏制作方式之所以
很热门,其原因之一就在于开发者很容易就能新增一种结局。只需再

增加一张幻灯片,用文字描述出另一种故事结局即可。不需要再为这
个结局重新制作一段带音乐的过场动画(cut scene) [1]。比如,
SNES平台上有一款RPG叫做《Chrono Trigger》,该游戏就有许多种结
局(而且在通关一次之后,还能以“NewGame+”模式 [2]重玩),此游
戏之所以成为经典,不仅因为其剧情、动画、音乐都设计得很精美,
而且还因为它给铁杆粉丝们留下了广阔的探索空间。
用这种方式制作游戏时,很容易就能加入新的结局,这样就能在
诸多“好结局”与“坏结局”之外,再提供一些平凡的结局留待玩家
发现了。稍后就会讲到如何添加新结局,现在先来为刚刚加入的元素
设定样式,把程序清单2.5中的代码加到main.css尾部。
程序清单2.5

为幻灯片内部的元素设定样式

这段代码使用的基本上都是标准的CSS。若是不确定某条规则的作
用,可以修改其值,看看网页有何变化。要是想快一些尝试各种效
果,可以通过Firefox浏览器的firebug或Chrome浏览器的开发者工具
来编辑这些值。这段代码除了调整一些简单的视觉样式之外,主要用
途是把菜单与页面中的故事区隔开。对于目前这一步来说,在显示故

事结局的幻灯片中,菜单里只有一个“START OVER”按钮,不过稍后
也可以将这里定义好的样式运用在其他菜单项中。
[1] 中译《时空之轮》、《超时空之钥》。——译者注
[2] 在该模式中,上一轮通关时的角色等级和技能都将保留下来。——
译者注

2.3 第三步:添加道具栏及道具拖放功能
现在已经实现了页面切换功能,可以在此基础上制作一款与游戏
书的阅读方式相似的作品了。然而,你也可以为其加入图片与道具
栏,使之更接近于《疯狂大楼》这类点击型文字冒险游戏,此外,还
可以向里面添加一些其他类型的文字冒险游戏所具备的特性。
在这一步里,需要添加三个文件:bat.png、game.js和
dragDrop.js。还需要修改index.html与main.css文件。首先将程序清
单2.6中的粗体代码加入index.html中。
程序清单2.6

将实现道具栏所用的代码加入index.html之中

较前一版本的index.html而言,此段代码主要修改了四个地方。
首先就是加入了一个id属性为player_inventory的div元素,并在其中
嵌套了另外一些元素。程序清单2.7将为这些元素设定样式。现在把它
看成一直显示在屏幕上的道具列表就行了,本小节稍后将会详细解释
如何在其中添加或移除道具。第二处改动是在各幻灯片元素的class属
性中添加了itemable属性值。稍后运用css样式及执行JavaScript脚本
时,该属性值将充当挂钩。第三处改动是:在首张幻灯片里加入一个

item-container [1],然后将id属性为bat的<img>标签放于其中,该标
签表示一张球棒图像,我们要想在本步骤中把这根球棒加入剧情里。
最后一处改动,是在body元素的结尾标签之前加载了两个新文件(分
别是game.js与dragDrop.js)。
现在为元素运用新样式,将程序清单2.7中的代码加入main.css末
尾。
程序清单2.7

实现道具栏及道具拖放功能所用的CSS代码

此段代码为道具栏、其中所含元素,以及每张幻灯片下方的菜单
按钮定义了样式。道具栏里不含图像的元素,其class属性会包含
empty这个值,而我们在这段样式代码中为此类元素定义了边框。由于
前面执行过CSS重置,所以h3标题标签所具备的默认样式会清空,此处
可以为其定义一些样式。
现在需要创建dragDrop.js文件,以便处理玩家与游戏界面之间的
交互操作。这有点复杂,我们一步一步来做。首先,新建名为
dragDrop.js的文件,并将程序清单2.8里的代码加入其中。
程序清单2.8

为道具箱中的元素添加事件处理程序

笔者向大家逐行讲解这段代码。第一行代码是要把所有class属性
中包含inventory-box值的元素都放在名为itemBoxes的变量中。接下
来用forEach循环为每个inventory-box元素添加与拖放功能有关的事
件处理程序。每个事件处理程序都通过addEventListener方法来绑
定。请注意,在旧版Internet Explorer浏览器中,需要改用
attachEvent方法。addEventListener函数的第一个参数表示拖放事件
的名称。除了各种与拖放相关的事件名称之外,还可以用'click'等值
作为参数,为元素绑定处理其他类型的事件所用的事件处理程序。第
二个参数表示要与该元素相绑定的函数,它会在相关事件发生时执
行。
大家可能觉得第二行的这种循环遍历写法有点奇怪。若改用传统
方式来实现,则可以写为程序清单2.9这样。请注意,下面这段代码并
不是本章所用的源文件,放在此处只是为了和forEach这种“函数式”
(functional style)循环遍历写法相对比而已。
程序清单2.9

较为传统的循环遍历方式,这段程序采用“过程式”写

法而非“函数式”写法

程序清单2.9这种写法是过程式的,与之相比,程序清单2.8则显
得更为函数式。两者之间的差别似乎不大,但是从实际效果来看,采
用函数式写法会好一些,对那种巨大而复杂的程序来说,更是如此。
原因在于,一般情况下,函数式代码使用的变量更少,且在必要时还
会创建一份新的数据,而不会改动原有数据。简言之:因为变量值与
函数行为在程序执行期间可以保持稳定,所以采用函数式写法更容易
把系统构建得简洁一些。

提示
虽然本书并不严格遵循函数式写法,但是大家要理解其优点,因
为这对掌握JavaScript语言来说很重要。在为较新版本的浏览器开发程序
时,ECMAScript5标准(也就是JavaScript语言所遵从的规格书)正在设
法鼓励开发者采用这种写法来编程,而对于不支持这一特性的旧版浏
览器来说,则可通过underscore.js等程序库来实现此功能。
程序清单2.8的第二行,其工作原理略微有些复杂。首先,是在
“数组字面量”(array literal)[]上面调用forEach函数,而这个
数组里不含任何元素。之所以要这么做,是因为我们想使用数组对象
所具备的forEach函数,而querySelectorAll所返回的却是NodeList而
非数组,故而没有forEach功能。于是,就要通过call来达到此目的,
使得NodeList看起来和数组一样,似乎也支持forEach函数。call的第
一个参数表示系统在执行它左侧的那个函数(在本例中是forEach)
时,this所指的对象。通过代码可知,在执行时this会指向
item_boxes而非原来的[]对象。其后的参数就是执行forEach函数时所
需的那些参数。由于在调用forEach时,要用其首个参数表示遍历过程
中应该执行的函数,所以在调用call时,它就成了第二个参数(也就
是从function关键字开始,一直到右花括号为止的这部分),用来表
示遍历item_boxes时,会在其中的每个元素(以参数item_box表示)
上所执行的函数。在作为forEach函数参数的这个函数中,我们可以通
过形式参数item_box来表示待遍历的每个元素。能这么做的原因在
于,尽管NodeList并未实现forEach函数,但它却提供了item方法,所
以系统在执行forEach函数时可以调用此方法,把待遍历的相关对象赋
给item_box参数。

这也许是本书中最为复杂的内容了(至少在第8章之前是如此)。
要是只看一遍就能懂,那真是太好了。如果暂时不理解,也没关系。
JavaScript语言中要学的内容很多,而且在大多数情况下,用for循环
来遍历也并无不妥。各种浏览器对JavaScript的实现方式不同,有时
for循环甚至比forEach还快。大家要知道,即便是最具天赋的开发
者,也要从某个知识点开始学起,不可能一开始就掌握全部内容,所
以说,明理之人是不会对仍在学习JavaScript语言的开发者太过苛责
的。尽管如此,也应该了解forEach这种函数式写法的概念,这样的
话,就更容易看懂别人所写的代码了,而且也有助于你去探索新的、
更好的编程方式。
说完这件事之后,我们继续回到游戏上面。接下来该定义与道具
栏相绑定的那些函数了。我们还是准备将其定义在dragDrop.js文件
里,从该文件顶部开始,一个一个往下写。首先把程序清单2.10列出
的这个handleDragStart函数写进去。
程序清单2.10

创建handleDragStart函数

这段代码把名为draggingObject的变量定义在全局作用域里,因
为还有很多函数也要使用该变量。现在无须担心全局作用域这个概
念,下一节就会详细讲解它。在函数代码中,我们把this变量赋给
draggingObject,用以表示玩家正在拖拽的这个inventory-box元素。
接下来,令系统在执行拖放操作时把该元素内的html代码当作信息传

递出去。最后四行代码将道具所对应的图像复制了一份,玩家在拖拽
道具时,此图像会显示在鼠标光标后面。在不加这几行代码的情况
下,拖放操作仍然能正常执行,但是光标后面所显示的图像却会因浏
览器而异。
接下来,把程序清单2.11中的handDragOver函数加入dragDrop.js
中。
程序清单2.11

创建handleDragOver函数

handleDragOver函数并没有太多事情可做。实际上,此函数的唯
一用途就是阻止浏览器在发生相关事件时按其默认方式来处理。
程序清单2.12里的这个handleDrop函数所执行的操作就远远多过
刚才那个函数。
程序清单2.12

创建handleDrop函数

为了能正常运作,这个事件处理程序也要调用
e.preventDefault()。其后的代码都包裹在一个条件判断语句里,只
有当拖动操作的起始对象(也就是draggingObject)与目标对象(也
就是this)不同时,才会执行这些代码。接下来四行代码把用来存放
道具元素的容器对象赋给相关的grandpa变量,并将道具元素的id赋给
draggingObjectId。其后两行代码则是通过inventoryObject对象,把
道具从起始道具栏移动到目标道具栏中。接下来就会解释这个
inventoryObject元素,不过此处我们先把这个函数写完。后面这两行
代码将起始对象与目标对象的innerHTML互换。最后两行代码把empty
属性值从目标对象的class属性里移除,并加到起始对象的class属性
里。
你也可以为dragenter及dragleave事件设置相关的事件处理程
序,然而本步骤并不需要处理这两个事件。
程序清单2.13用来实现inventoryObject。新建名为game.js的文
件,将下列代码加入其中。

程序清单2.13

创建一个inventoryObject对象以存储和检索道具

代码首行声明了名为inventoryObject的变量,并将另一个函数的
运行结果赋给它。由于整段代码最后有一对括号,所以我们可以判断
出:首行这个赋值语句的等号右端表示某个函数的运行结果。为了与
JavaScript语言的解析规则相协调,在最后一行代码中,函数体的右
花括号右侧还需要再包上一层括号,变成(function(){...})。否则就
会出语法错误。
接下来,用空对象字面量{}初始化inventory对象。然后,采用早
前讲过的函数式遍历法,将inventoryObject里每个下标所对应的对象
初始化为空数组,初始化的时候以幻灯片的id或player_inventory作
为键名。接下来又有一段函数式遍历代码,这段代码用html文件中出
现的全部道具来填充inventory对象。
然后声明了两个新函数:add与remove。这两个函数的代码都很简
单,基本上无须解释,不过需要注意其中的push与splice方法。两者
均是JavaScript数组API中很常用的方法。还有一件事要注意,就是这
两个函数都有返回值。在JavaScript语言中,如果不声明特定的返回
值,那么默认会返回undefined。开发者经常会在不同的情况下返回各
种不同的值,然而此处,根据这两个函数操作对象的方式,我们认
为,应该把函数所操作的那个对象(也就是inventory)当作返回值返
回给调用者。这么做不仅从道理上说得通,而且还使开发者可以把方
法调用串接起来,像是这样:
inventoryObject.remove(...).add(...)。

像本例这样,在定义好某个函数之后,立即执行,并把执行结果
赋给inventoryObject变量的做法其实也有其道理,只是不太明显。在
运行完这段配置代码之后,add与remove函数就无法在外层函数之外使
用了。那么如何使开发者能够在其他地方调用它们呢?最好的办法就
是令外层函数返回一个对象,也就是范例代码最后几行所定义的那个
对象。此对象里有三个方法:get、add和remove。因为add与remove方
法已经实现过了,所以不用在对象里再实现一次。对象里的方法与普
通方法一样,只不过这些方法现在是公用的,在函数外面可以通过
inventoryObject.add()与inventoryObject.remove()来调用。这种做
法的好处是,尽管这些方法现在处于公共作用域内,但是它们仍然能
访问函数早前生成的那个私有的inventory对象。此写法叫做闭包
(closure),这是个重要的概念,通过它,可以把程序各部分中的信
息适当区隔开。
在实现get函数时,我们没有像另外两个函数那样,先在外面写好
一份私有的实现代码,然后再于return代码块中引用,而是直接把实
现代码写在里面,并返回私有变量inventory的值。
这种把私有方法与公共方法分开的做法是对“模块模式”
(module pattern)的一种运用。这是最基本的模式之一,在别人所
写的代码里经常会看到它。如果对怎样复用、怎样组织JavaScript代
码感兴趣,可以深入研究模式这一领域,你会发现其中有许多令人兴
奋的知识。比如可以研究backbone这种MVC框架,也可以研究为了扩展
项目而用的AMD/conmmon.js框架,还可以看看逐渐流行起来的“发布订阅模式”,或是思考一下如何将经典的Gang of Four设计模式 [2]运
用到JavaScript语言中,总之,研究设计模式的收获很大,可以深化
对编程的理解。
本书是讲游戏的,不是讲设计模式的。而本节所描述的这一步骤
相当困难,其中用到了许多艰深的技巧,这比其他章节所用那些知识
更不易懂。所以现在来看看咱们辛苦编码的成果吧,用浏览器打开
index.html,应该就会出现和图2.2差不多的画面了。球棒可以在玩家
道具栏与幻灯片之间来回拖动。

图2.2 在游戏中加入道具栏之后,首张幻灯片的样子

[1] 实际上是指其class 属性中包含item-container属性值的那个<div>元
素,作者将其简称为itemcontainer。——译者注
[2] Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides所著的
《Design Patterns:Elements of Reusable Object-Oriented Software》(《设

计模式:可复用面向对象软件的基础》)一书中描述了23 种经典的设
计模 式。 业 界 将 这 四位 作 者称 为 “四 人组 ” (Gang of Four, 简称
GoF),将这些经典的设计模式称为GoF 模式。——译者注

2.4 第四步:添加复杂的交互功能
这一步将为游戏中的物件添加更为复杂的交互功能。最难理解的
那些概念已经在上一步中讲完了,本步骤要讲的内容理解起来并不会
太难,但是,为了实现这些新的交互操作,必须大幅修改代码。首先
按照程序清单2.14中的粗体代码修改index.html。
程序清单2.14

修改index.html文件

在这一步里,我们改变了引用元素的方式。不再通过itemable这
个class属性来引用幻灯片或玩家的道具栏了。另外,只用纯数字来充

当元素的id值的话,不便于引用或追踪元素,所以我们在这些id属性
值前面加上slide一词。id修改了之后,指向这些元素的链接也要随之
更改。还要给每张幻灯片里添加一个class属性为event-text的div元
素。第二张幻灯片中新加入了一只恐龙,幻灯片中的文字也要修改,
我们要告诉玩家这里有只恐龙。
尽管本游戏也引入了impress.js文件,但是为了实现游戏中的一
些功能,我们还需要对它略加修改才行。请按照程序清单2.15中的粗
体代码修改此文件。
程序清单2.15

修改impress.js

这段代码对原来的impress.js做了两处修改。第一处改动是创建
了game对象,这个位于全局作用域内的对象只在本章中使用。此对象
有两个属性:一个是数组,用于保存浏览过的幻灯片;另一个是函
数,用于将幻灯片加入数组中。这段代码可以放在文件顶部。第二处
改动是:每次播放新幻灯片时,都要调用刚才定义的那个函数。为了
执行这一改动,我们把程序清单2.15中的第二段粗体代码加入
impress.js文件中,这段代码应该添加在第421行附近。
接下来,按程序清单2.16修改dragDrop.js文件。
程序清单2.16

修改后的dragDrop.js文件

这份文件有两处大的改动。首先是整段代码都包裹在一个“自执
行函数”(self-executing function)里面。如果这段代码需要多次
运行(比方说需要重新绑定事件监听器),那么可以把第一行改为
game.dragdrop=(function)(){,这样就会把此函数作为属性放到全局
的game对象之中,不过,在此步骤里,这段代码只需执行一次即可,
所以不用这么写。第二处改动是,handleDrop函数与原来略有不同。
现在,只需要把正在拖放的对象,以及拖放操作中目标容器的id属性
传给dropItemInto函数即可,而这个函数则定义在全局game对象的
things属性里。
game.js文件需要改动的地方非常多,我们从头开始一点一点来分
析。请用程序清单2.17中的代码把当前game.js文件里的内容全部都替
换掉。
程序清单2.17

创建game.things属性

这段代码内容有点多。从结构上看,整个对象都包裹在一个自执
行函数中,并作为这个函数的返回值赋给game对象的things属性。看
看此函数最后所返回的那个对象,你也许会觉得这种写法看上去有些
眼熟。函数所返回的对象会把公共方法映射到私有方法上面,比如,
通过game.things.items即可引用函数内定义的那个items。在
game.things的公共接口里含有三个方法,其中items方法可以返回
items对象,get方法可以返回items中特定的道具对象(item),而
dropItemInto方法则会执行dragDrop.js里面的许多复杂代码。

现在回到函数开头,我们可以看到,这段代码是以数据来表示道
具对象的,在各种具体情况下需要执行的方法也是以数据形式放在道
具对象里面的。比如以bat对象为例,该对象内部含有名为bat的name
属性。而bat对象的effects属性里则含有一些附加的JSON数据,用以
表示玩家将这根球棒拖放到其他道具之上时所产生的效果,其中拖放
到player_inventory [1]的情况需要特殊处理。dino对象写在bat对象
后面,其所包含的映射关系要比bat简单,因为涉及此物件的交互操作
只有一种,那就是玩家也许会试着将其拖放至道具栏里。各种交互操
作都可能产生三个效果:subject表示当玩家拖放道具时,道具原来所
处的起始位置上会发生什么事情;object表示当玩家拖放道具时,将
要拖放到的目标区域中会发生什么;而message则描述了玩家在执行当
前这种交互操作时,幻灯片中所要显示的文本。
接下来是get方法,它会根据调用时所传入的name参数来返回与之
相应的道具。
最后一个函数叫做dropItemInto,它有些复杂。该函数接受两个
参数,分别是itemNode与target。sourceContext先与target相比较。
如果二者相同(也就是拖放操作的起始地点与目标地点相同),那么
函数就不用再执行下面的代码了。接下来的if,else if,else分支语句
用于判定当前这个道具所产生的效果。而下一个if分支语句则用来判
断effects里面是否定义了object属性,如果定义了,那么就找出此道
具将给拖放操作的目标地点所带来的效果,并执行此效果。以
if(!!effects.subject===true){开头的语句块也会通过类似的逻辑
对拖放操作的起始地点运用相关效果,只不过这次检测的是subject属
性,而非object属性。最后还有一个if语句块,它与前三个语句块均
处于相同的嵌套层级上,这个语句块会调用game.slide.setText方
法,将effects对象的message属性设置成当前这张幻灯片的eventtext。dropItemInto函数的最后一行调用game.screen.draw方法,以
便在道具更新之后重新绘制屏幕上的内容。
游戏中的大部分交互操作都由这个dropItemInto函数来驱动,讲
过此函数后,我们接下来详细看看这个函数在实现其功能时所依赖的
那些对象。game.js文件里的game.slide对象是用程序清单2.18中的代
码来实现的。这段代码可以放在程序清单2.17的那段代码之后。
程序清单2.18

在game.js中创建game.slide对象

在定义game对象的slide属性时所采用的这种写法大家现在应该已
经比较熟悉了。与前面几段代码相似,我们也可以先来看看return代
码块中的公共接口里都定义了哪些方法,以此来了解该对象的用途。
回到这段代码开头,我们看到inventory对象将每张幻灯片与其中所显
示道具的名称关联起来,对于没有道具的幻灯片来说,其值为null。
然后,定义了管理inventory所用的addItem与deleteItem函数,前者

用于将item道具对象的名字放入inventory对象中,而后者则可以根据
道具名称,将inventory对象中的相关值设为null。
接下来定义的这个findTextNode方法,可以在给定的幻灯片中找
到class属性中含有event-text属性值的div元素。由于return代码块
中并没有属性映射到这个方法上,所以它是个私有方法,仅能在
game.slide对象内部使用。只有setText函数才会调用这个方法。
getInventory方法会根据给定的幻灯片,返回inventory对象里与
之相应的道具。
setText方法会根据传入的message及slideId参数,将消息文本显
示在对应的幻灯片中。如果没有提供slideId,则默认将文本显示在
currentSlide上面。你若是用过支持默认参数值的编程语言,那么可
能会认为此函数应该写成
function(message,slideId=currentSlide()),这样的话,就可以在
调用者没提供slideId的时候以默认参数值来调用了。在JavaScript里
不能这么做。如果确实需要支持默认参数值的话,那么可以考虑将全
部参数封装在一个对象里,然后传给函数,令函数来解析其中的参
数,还有一种办法是,检测对应参数是否为null。
接下来就是刚才提到过好几次的那个currentSlide方法了。前面
之所以要修改impress.js文件,就是想为实现此方法做准备。该函数
会返回stepsTaken数组里的最后一个元素,而此元素正是impress.js
在每次显示新幻灯片时,添加到数组中的那个元素。
slide对象中的最后一个函数名为draw。此函数开头再次使用刚才
提到过的技巧,在调用者未提供slideId参数时,将其设为默认值
currentSlide。然后在对应的幻灯片中寻找inventory-box元素,把
inventory对象里的内容(可能是null,也可能是某个道具对象)加入
其中。此函数还会根据inventory-box中是否包含图像,在其class属
性列表里添加或移除empty属性值。
在本步骤中,还需要实现最后这个大对象,也就是
playerInventory。请把程序清单2.19中的代码加入game.js。
程序清单2.19

创建playerInventory对象

这次的写法和原来相同,也是定义一个立即执行的函数,并将其
返回的对象设为game对象的属性。代码中并无特别之处。与前几段范
例代码一样,return语句块也将某些方法设置成可供外界调用的公共
方法。现在回到代码开头,仔细分析一下这个对象的用途。
items是个用于存放道具的对象。你也许觉得使用数组比使用标准
对象更为合适,不过,若是数组很大,那么想要检测其中是否含有某
个元素就会相当耗时了,因为必须要搜寻整个数组才能判断元素在不
在其中,反之,对于我们这里使用的items对象来说,只需要查询相关
下标所对应的值是true还是false,即可判断出待查元素是否位于

items之中了。由于我们使用了这套方案,所以,凡是游戏中有可能出
现在玩家道具栏里的道具都得写在这里,并且要将其关联值设为
false。
接下来是名为clearInventory的私有函数,它会将玩家道具栏中
所有div元素里面的图像清除。在调用draw方法绘制道具栏之前,需要
先调用此方法把待绘制的区域清理干净。此方法会向每个
inventoryBox元素的class属性列表中添加empty属性值。
addItem与deleteItem方法很普通。它们只是把items对象中相关
道具所对应的值设为true或false,并返回this.items而已。
draw函数首先将道具栏清空。然后设置for循环所使用的counter
变量,这种for循环的写法本章前面未曾出现过。如果待遍历的不是数
组而是普通的对象,那么最好采用for...in这种写法来轮番处理对象
中的每个元素。循环内的代码需要引用特定的html元素,并从其class
属性列表移除empty属性值,而完成这两项操作时都需要用到刚才定义
的counter变量。
前面提到过的那个大对象已经写好了,现在还需要再加一个小对
象。请将程序清单2.20这段代码加入game.js文件中,以便实现screen
属性。
程序清单2.20

将screen属性加入game对象中

screen对象只是把playerInventory对象及slide对象里的draw函
数封装起来而已。
这一步到此结束。用浏览器打开index.html文件,然后用球棒打
恐龙,此时就会出现图2.3这样的画面了。

图2.3 用球棒打恐龙

[1] 是指幻灯片左侧纵向显示的那个道具栏。——译者注

2.5 第五步:添加历史记录导航功能
游戏现在看上去还不错,但是其导航功能很有限,因为玩家当前
只能按照预订好的流程往下玩。而“历史记录导航” [1]这一功能则是
想把玩家所看过的幻灯片记录下来,以便其可以返回原来那些页面。
为实现此功能,我们还得调整impress.js库的源代码,并加入一些
html/css代码。不过与上面两步相比,这一步简单多了。
首先需要对impress.js略加修改。这一版看上去和原来差不多,
但是updateAfterStep方法会多出来一些代码。添加程序清单2.21中的
粗体代码到impress.js文件中。
程序清单2.21

修改impress.js文件,以实现历史记录导航功能

原来我们是把当前这张幻灯片直接加到数组里,而修改后的方法
则是新建一个li元素,并于其中创建指向当前幻灯片的链接,然后把
这个li元素加到历史记录列表的顶端。
index.html文件中还有两个地方需要略微改动。首先,添加一个
名为stepsTaken的列表。然后修改链接,令其直接刷新页面。由于我
们已经把道具及道具栏处理好了,所以无须把UI和背景中的所有物件
都清理干净,直接刷新页面会更简单些。程序清单2.22中的粗体代码
就是要修改的地方。
程序清单2.22

修改index.html文件,以实现历史记录导航功能

本步骤最后要做的,就是把程序清单2.23这段CSS加入main.css之
中。
程序清单2.23

历史记录导航列表所用的CSS

修改完这些代码后,用浏览器打开index.html,在浏览过几张幻
灯片之后,页面就会像图2.4这个样子,其右侧是历史记录导航列表。

图2.4 已经实现好的历史记录导航功能

[1] 原文为Breadcrumb Trail,直译“面包屑踪迹”,也称“面包屑导
航”,是一种用户界面控件,使用户可以跳转到原来访问过的页面。
该词出自格林童话《糖果屋》,故事中的人物为防止迷路而沿途撒下
面包屑以标识路径。——译者注

2.6 第六步:添加精彩的结局
当前的游戏有两个结局:一个是平淡无奇的结局,另一个则是惹
怒了一只恐龙。那么如何表示恐龙发怒之后攻击玩家的页面呢?这可
以用一个有趣而古怪的jQuery插件来实现,此插件名为raptorize,可
以从http://www.zurb.com/playground/jquery-raptorize下载。
将raptor-sound.mp3与raptor-sound.ogg放入index.html所在的
目录。(这两个文件都要用到,因为不同的浏览器所支持的音频编码
器不同。)把jQuery库与jquery.raptorize插件的源文件,以及
raptor.png这张图像文件也放入上述目录。
把程序清单2.24中的粗体代码加入index.html文件里,用以载入
相关脚本。
程序清单2.24

修改index.html,以显示暴怒的恐龙

我们新添加了三个script标签。第一个用来从Google的“内容发
布网络”(Content Delivery Network,简称CDN)中加载jQuery,对
于正式发布的应用程序来说,这么做性能更好。若是无法从Google加
载jQuery(这通常说明用户的网络连接有问题,因为Google的CDN非常

稳定,一般不会出错),则通过第二个script标签来加载本地的
jQuery库文件。第三个script标签用于载入raptorize.js文件。
程序清单2.25列出了game.js中需要略加修改的地方。
程序清单2.25

修改game.js,以显示暴怒的恐龙

首先,在things对象中,调整玩家将球棒(bat)道具运用在恐龙
(dino)之上时,所产生的效果(effects)。之后使dino与bat均从
页面中消失,并将一个回调函数设为callback属性,在这个回调函数
中调用screen对象的callDino方法。接下来修改dropItemInfo函数,
增加一条判断语句,用以检查是否有callback属性,若有,则执行其
中的回调函数。最后,把callDino函数加入screen对象,并在其
return代码块中将它设为可供外界访问的公共方法。
若是一切正常,那么当玩家用球棒击打恐龙之后,恐龙就会暴
怒,呼啸着从屏幕前掠过,如图2.5所示。

图2.5 精彩的结局

2.7 小结
至此,大家已经闯入文字冒险游戏的世界,学会了如何将
impress.js妙用为游戏引擎来开发此类游戏。本章讲了很多内容,比
如JavaScript的基础知识、设计模式、函数式编程,以及显示暴怒恐
龙所用的raptorize插件等等。
大家也许觉得本章很难,没错,确实如此。这可能是本书最难的
一章。有时候直接使用纯JavaScript来编程会相当别扭,jQuery等程
序库之所以能流行起来,这也是原因之一,而且正是由于纯
JavaScript有这个缺点,所以本书很多章都是专门采用游戏引擎来开
发游戏的,那样做会比使用纯JavaScript来开发更为容易。使用程序
库开发出来的网页在各浏览器中的样貌会比较一致,而且程序库还有
规范的文档可供查阅,而采用纯JavaScript来开发时,则很容易陷入
各种琐碎的细节之中,你需要了解多份JavaScript语言规范书,需要
知道各浏览器如何实现这些规范,还需要知道它们在实现的时候有哪
些地方没遵循规范(这个问题是JavaScript语言的阴暗面,Douglas
Crockford也曾经提醒过大家)。
由本章出发,应该深入学习哪些内容呢?从JavaScript方面来
说,可以学习所有设计模式以及函数式编程的技巧,同时学习与这些
技术有关的程序库,这需要花费数月的时间。而从网页视觉设计方面
来说,可以深入研究HTML5与CSS3,这个学习量也是合理的。
那么此游戏如何继续往下做呢?如果不喜欢恐龙,可以换成别的
东西。也可以把游戏做得更真实一些,或把剧情做得更长一些,令其
带有更多分支。界面也可以换成纯文本式,或采用“东、南、西、
北”这样的方向键来切换页面,使玩家在探索游戏世界时获得另一种
体验。还可以用它来制作电子贺卡。也可以加入更多道具、更多道具
效果、更多结局,把游戏做得更惊悚、更有趣、更有意义。这些改进
方向都可以尝试。学完这章之后,你就有了一套游戏模板,可以按照
自己构想的剧情来改编,而这种改编游戏剧情的方式你原来也许并未
听说过吧。

第3章

派对游戏
派对游戏的代表作有《摇滚乐团》(Rock Band)和《马里奥派
对》(Mario Party)。
由于《Dance Dance Revolution》 [1]、《马里奥派对》 [2]和《摇滚
乐团》 [3]等游戏都设计得很简洁,新手很容易就能入门,所以,这类
游戏非常适合在休闲聚会的时候玩。在这类游戏中,有的游戏要求玩
家必须以很快的速度来按键,有的要求玩家在适当时机按键,还有的
则要求玩家根据提示按下对应的键,大家原来都没有意识到这种休闲
游戏居然能吸引这么多人来玩,而现在,游戏开发者与发行商意识到
了这一点,于是,越来越多的人都开始制作这类游戏。
本章我们使用atom.js游戏引擎,对游戏逻辑略加封装,并研究
canvas中与图形绘制相关的那些原生接口。
[1] 中译《劲爆热舞》,此游戏可搭配跳舞毯或跳舞机来玩。——译者
注
[2] 中译《马里奥聚会》或《马里奥派对》。——译者注
[3] 中译《摇滚乐队》或《摇滚乐团》。——译者注

3.1 第一步:采用atom.js创建范例游戏
本章采用atom.js这个极为精简、极为轻便的游戏引擎,把此类型
游戏所共有的一些基本逻辑抽象出来。此引擎有四项主要功能:首
先,它可以把各种浏览器实现requestAnimationFrame功能时所采用的
不同方式统一起来;其次,它可以把按键与鼠标事件抽象出来;再
次,它提供了一个事件处理器,用以在视窗大小改变时调整游戏屏幕
的尺寸;最后,也最重要的一项功能则是:定义了名为Game的基本对
象,并把与游戏循环有关的一些方法置于其中,而我们在制作本章范
例游戏时就要用到这个对象。
在party/initial文件夹里面,有这样两个文件,它们分别叫做
atom.js与atom.coffee,这两份代码中的变量与结构都很相似,可是
所用的语法却完全不同。如果不太熟悉这种JavaScript编程方式,那
笔者来讲讲第二个文件吧:它是用另外一种编程语言写成的,这种语
言名叫CoffeeScript。有人觉得用CoffeeScript来编程,要比
JavaScript简单(似乎atom.js库的作者也这么认为)。这种说法有几
分道理,CoffeeScript确实有其优点。JavaScript语言中的部分功能
若改用CoffeeScript来写,则更为简单,而且写出来的代码更容易读
懂。对于atom.js库来说,还有个好处,那就是:用CoffeeScript写成
的版本要比JavaScript版本少30行代码。
那么CoffeeScript有何缺点呢?缺点之一在于浏览器只能解释
(interpret)JavaScript代码,而无法解释CoffeeScript代码。这就
是说,每一份以CoffeeScript语言写成的源文件,必须先转换或“编
译”为浏览器可以读懂的JavaScript,然后才能执行。可以在电脑上
用某个程序来转换源码,不过对于CoffeeScript初学者来说,没必要
做得那么复杂,只需访问js2coffee.org/这类网站,即可转换源码。
另外一个缺点是该语言调试起来比JavaScript更难,因为浏览器在回
报运行过程中所发生的错误时,是以JavaScript版本为准的,而原始
代码却是以CoffeeScript语言写成的。有许多工具都能设法解决此问
题(比如采用“源码映射器” [1](source-mapper),或是在浏览器
中内置CoffeeScript语言解释器),不过在写作本书时,尚未出现标
准的解决方案。

除了这一章之外,其他各章都不会再出现CoffeeScript代码了,
但是,在使用backbone.js与Ruby on Rails技术的开发者群体中,仍
然有人会采用这种语言,所以有些概念还是要理解为好。
CoffeeScript语言可能会红极一时,也可能会就此沉寂,但不论怎么
说,它都只是对JavaScript的一种抽象方式而已。只要JavaScript语
言的基础扎实,自然就无须担心CoffeeScript的用法了。虽说如此,
但是学点新东西也无妨。http://coffeescript.org这个网站不错,若
是对这门语言感兴趣,可以去看看。
介绍完atom.js的背景知识后,我们开始创建index.html文件,并
将程序清单3.1中的代码加入其中。
程序清单3.1

为引入atom.js库而编写的简单HTML文件

第一行按照HTML5风格来设置doctype,也就是直接将其写为html
[2]。然后在head标签中设置网页标题(title)。(标题将出现在浏览

器的标题栏中,并作为分页名称出现在分页上方。)接下来,创建空
的canvas标签,并载入atom.js与game.js文件。

提示

各游戏引擎对canvas元素的使用方式有所差别。有些引擎不需要开
发者在html代码里直接创建canvas标签,因为引擎自己会创建,而另外
一些引擎则需要依靠canvas标签的id属性或canvas标签本身来运作。对于
本例所用的atom引擎来说,当系统将其载入时,它会寻找网页代码中
的首个canvas标签,并使用它来制作游戏。
接下来当然就该创建game.js文件了。在开始使用某个游戏引擎之
前,应该先做两件重要的事情:查看引擎的范例代码及开发文档。虽
说atom.js引擎的范例代码及文档都写得有些简略,但还是应该充分利
用它们。程序清单3.2列出了atom.js引擎的REAME.md文件中所含的范
例代码。
程序清单3.2

README文件中所含的CoffeeScript语言范例代码

这是一段CoffeeScript代码,所以在使用前必须转为
JavaScript,不过我们先来看看这段代码的功能。atom.js文件中曾经
定义了atom对象,而这段代码首先创建Game变量,令其扩展atom对象
的Game属性。在构造器中,super的意思是,如果在当前这个Game里找
不到某个方法,那么就在父对象中寻找相关的实现代码。游戏中需要
绑定的按键(本例中是左箭头)也在构造器里设置好。
引擎中的游戏循环将会反复调用update与draw方法。前者用于处
理玩家按下左箭头时应该执行的操作,而后者则将canvas范围内的背
景绘制为黑色(在本例中,会用黑色填充整个视窗)。然后,实例化
一个新的game对象,令其继承自上面定义好的Game(而Game又扩展了
atom.Game)。下面两行代码的意思是:当网页视窗失去焦点时,会执
行stop方法以停止游戏,而在获得焦点时则会执行run方法来启动游
戏。最后,执行run函数,启动游戏。
程序清单3.3列出了由代码转换器所生成的纯JavaScript代码。现
在无须保存此文件,因为接下来还要按照程序清单3.4来修改它。
程序清单3.3

转为纯JavaScript之后的范例代码

我们通过js2coffee.com网站等手段将刚才的CoffeeScript转换成
这一大段JavaScript代码。两者在语法上区别很大,不过那些写法很
奇怪的代码都出现在update函数之前。CoffeeScript中的类与扩展这
两个概念,改用常规的JavaScript语言实现之后,看上去就会比较复
杂。
代码一开始就定义了game与Game变量,而在原始的CoffeeScript
代码中,这两者也是在最顶层的作用域中定义的。由于其外围没有函
数,所以它们处在全局作用域中。这样做似乎有些奇怪,按理说,变
量应该在设置其值的时候再去定义,或是在即将为其赋值之前再定义

才对,而这段代码为何一开始就要急着定义变量呢?原因在于,许多
传统的编程语言中,代码块(也就是位于{}中的代码)本身也能起到
区隔作用域的效果 [3],而JavaScript则不同,在这门语言里,只有函
数才能充当作用域。如果在首次赋值之前才去定义变量的话,那么用
惯了其他编程语言的那些开发者就有可能误解变量的作用域。他们会
误以为定义在for循环或if语句块中的变量,出了这个语句块之后就不
可见了。而像本例这样,把变量直接定义在开头,则可以消除这一误
解。虽说本书不会严格遵循此风格,但大家还是应该知道采用这种写
法的原因。
在程序清单3.3所列的这42行代码中,至少有一半(基本上都出现
在前半部分代码中)是为了实现类与子类这两个概念而写的,这些代
码写得较为通用,但是读起来不够清晰。把CoffeeScript源码转换成
JavaScript代码时,由于代码转换器首先要保证生成的JavaScript代
码,其含义与原来的CoffeeScript代码相同,所以可能造成转换出来
的代码不太容易读懂。所幸JavaScript语言本身也提供了一个方法,
若用它创建对象,则会简单很多。程序清单3.4改用Object.create方
法来创建对象。请新建game.js文件,并把这段代码放在开头。
程序清单3.4

用Object.create来实现game.js所需的继承功能

修改之后,代码行数比原来少了很多。这段代码比JavaScript编
译后的更短小、更清晰。原来通过模板创建对象所用的那些复杂代码
都不见了,现在只需编写Object.create这一行代码就足够了。然而使
用此函数时,有个问题需要注意。atom.js中的Game变量是个构造器,
而我们想要继承的那个游戏对象,正是由此构造器构造出来的。虽然
有个别例外,但在一般情况下,prototype(原型)与
constructor(构造器)这两个属性的含义还是大有区别的。某对象的
prototype指的是其模板对象,而constructor则是一个函数,该函数
会根据prototype来制造对象。

运行游戏之后,如果按下键盘的左箭头,那么控制台中就会输出
消息(可通过Firefox浏览器的Firebug或Chrome浏览器的开发者工具
来打开控制台)。在新版浏览器中,Object.create方法是可以正常执
行的。game的constructor属性是Game函数,而这个constructor的责
任就是:根据Game的prototype来构造此类对象。

提示
如果你想深入了解原型和构造器,那么可以在控制台里执行
game.constructor。然后再执行game.constructor.prototype。实际上,这两
种写法都可以无限向后接续,比如前者可以重复地写成
game.constructor.prototype.constructor,后者可以重复地写成
game.constructor.prototype.constructor.prototype,依此类推。而不管重复
多少次,其各自的执行结果都是一样的:前一种写法总是会输出一个
函数,该函数描述了对象的创建过程(这就是“构造器”),而后一
种写法则总是会输出构造器创建对象时所用的模板(这就是“原
型”)。
这条规则并不只适用于game。普通的JavaScript对象也如此。可以
在控制台中执行"myString".constructor.prototype及var obj=
{};obj.constructor.prototype看看效果(也可以在数字等原生类型值或其
他自定义对象上面试试)。
如果不太理解这些与继承相关的内容,也没关系。只需要记住:
在制作本章游戏时,采用Object.create来创建对象更为合适,它比代
码转换器从CoffeeScript转过来的继承实现要好。不过还需注意,由
于此方法相对新一些,所以旧版本的浏览器可能不支持,于是,我们
就需要实现一个“polyfill函数”,以在旧版浏览器上模拟此功能。
你对“polyfill”这种技术好奇吗?请参阅附录C中的Modernizr工
具。

警告:各种浏览器描述对象的方式不同

在各种浏览器中调试代码时会发现,不同浏览器的控制台描述对
象所用的方式也不同,比如,__proto__属性,以及与该属性有关的其
他信息都会因浏览器而异。如果你对这一点感到困惑,那么可以试着
在对象上调用一些方法 [4]。虽然各浏览器存放对象时所用的内部结构
以及描述这些结构的方式不尽相同,但一般情况下,它们都能正常执
行这些方法。
[1]

该

技

术

的

详

细

内

容

请

参

见

http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/

。

——译者注
[2] HTML 5 风 格 的 DOCTYPE 与 HTML 4.01 风 格 的 区 别 , 请 参 阅 :
http://www.w3schools.com/tags/tag_doctype.asp。——译者注
[3] 也就是说,凡是定义在代码块内的变量,在代码块之外都不可见。
——译者注
[4] 可以在对象上调用的方法请参考:https://developer.mozilla.org/enUS/docs/Web/JavaScript/Reference/Global_Objects/Object。——译者注

3.2 第二步:用canvas元素绘图
引擎所需的update及draw函数已经写好了,而且游戏也已经可以
接受玩家的按键操作,并将相关信息打印到控制台了。然而本书却还
没有讲到canvas的用法,在开发HTML5游戏时,该元素可以说是最为重
要的HTML5元素。本步骤将通过canvas来绘制游戏背景。
对于初次接触canvas的开发者来说,采用atom.js来学习canvas是
非常合适的,因为此引擎几乎没有改动这个元素。canvas的核心部分
其实是一套相当简单的API,而其他游戏框架一般都会覆写其中的绘制
方法,并将某些功能抽象出来。程序清单3.5展示了atom.js引擎是如
何操作canvas元素的。由这段代码可知,经atom引擎封装过的canvas
元素与原生的canvas用起来没有任何差别。
程序清单3.5

atom.js引擎在封装canvas元素时所使用的代码

想要基于canvas来绘制图像,就必须执行上面这两行重要的代
码。第一行代码会在html文件中寻找首个canvas元素,并将其赋给
atom.canvas。第二行定义了一套绘制二维图形所用的接口。调用
getContext方法时,也可以传入另外一个参数值,那就是
experimental-webgl,若想获取三维绘图接口(3-D context),则使
用此选项。下面还有几行代码是绑定鼠标事件的,没有写在上述程序
清单中。

提示
三维绘图接口相当复杂,若是想讲清楚如何用这套接口来实现二
维绘图接口所具备的对应功能,那恐怕需要一两本专著才行。总之,

二维游戏依然很流行,而且开发者并不需要具备太多的编程及图形学
知识,就能制作出此种游戏。很多游戏都可以用二维方式呈现出来,
比如本书所讲的全部游戏都是二维的,不仅如此,当前许多主流游戏
大作也是二维的,如《Paper Mario》 [1]、《Super Smash Brothers》 [2]、
《Street Fighter》 [3]等,这说明二维绘图技术仍然非常有用。若是对三
维绘图感兴趣,可以查阅附录C,从中找出深入研究所需的参考资料。
既然atom引擎已经把canvas元素及其二维绘图接口给我们准备好
了,那么现在就开始绘制吧。用程序清单3.6中的代码替换掉game.js
文件里的draw方法。
程序清单3.6

绘制背景

首先,使用beginPath函数新建一条路径。在调用绘图函数之前,
最好先调用此函数。否则,上一次绘制时所用的形状会干扰到后续绘
制。接下来,将context的fillStyle属性设为蓝色。其后那行代码,
通过fillRect来填充canvas的上半部分。fillRect的四个参数分别表
示矩形左上角的x坐标、y坐标,以及矩形的宽度和高度。接下来,再
次调用fillStyle,将颜色设为黄色,为绘制太阳做准备。后面的arc
方法用来定义代表太阳的那个圆形,其参数分别为:圆心x坐标(距离
画面左边界140像素)、圆心y坐标(在画面中线稍微偏上的位置)、

半径(90像素)、起始角(以弧度为单位,360度化为弧度就是
2*pi)、终止角(0)。用arc将圆形确定好之后,调用fill方法来完
成真正的绘制操作。最后两行代码采用与绘制天空时相似的办法来绘
制地面。请记住,在绘制图形时,后画的会盖掉先画的。利用这一
点,我们就可以先画太阳,然后再画地面了,若不这么做,那就比较
麻烦了,需要绘制出地面之上的那个不规则形状。

提示
许多程序库与游戏引擎(比如第4章用到的easel.js)都自己实现了
一些便捷方法,使开发者调用起来更加方便,比方说,只需要给出半
径以及横纵坐标即可画出圆形。此外,它们通常不像本例这样,把描
述图形与渲染图形的代码放在两个步骤中分开执行,而是只需要一个
步骤即可。后续章节会介绍许多封装程度较高的绘制方式。
把绘制背景所用的draw方法修改好之后,用浏览器打开
index.html,就会看到如图3.1所示的画面了。

图3.1 绘制好的游戏背景

[1] 中译《纸片马里奥》。——译者注
[2] 中译《任天堂明星大乱斗》。——译者注
[3] 中译《街头霸王》或《快打旋风》。——译者注

3.3 第三步:绘制鼠洞
我们已经为这个“打地鼠”(whack-a-mole)类型的游戏绘制好
背景了。下一步就该画鼹鼠钻出来的地方了。我们把这叫“鼠洞”。
在绘制它之前,先把draw方法精简一下,请按照程序清单3.7来调整上
个步骤中所写的那些绘制代码。
程序清单3.7

精简draw方法

原来draw函数里的代码现在都放到game.drawBackground的方法里
了。draw函数调用这个方法来完成绘制。如果开始阅读本书前不太熟
悉JavaScript,那你可能仍然不太明白draw函数里的this是什么意
思。this的含义与其所处的运行环境相关,在game.draw函数中,this
指的就是game对象。在编写JavaScript代码时,如果不清楚某个变量
的含义,可以打开控制台,以此变量为参数调用console.log(),看看
打印出来的信息,也许就会明白了。附录B详细讲解了开发者在遇到编
程问题时可以采取的措施。

技巧
在不改变功能的前提下移动代码,就叫做重构。我们不可能一开
始就把程序规划得很完美。在构建过程中,应该花些时间重新组织一
下代码,不要使其变得一团糟。重构是个相当大的话题,有很多种重
构的办法。一般来说,这几种代码应该重构:太过冗长的函数、太过
庞大的文件、表意不清晰的函数名、设置了太多变量的函数,以及使
用条件控制逻辑(比如if语句)过于频繁的函数。上述写法都会使游戏
项目的代码难于维护,而且也不利于多人协作,所以,应该尽量通过
重构来解决这些问题。
接下来,将程序清单3.8中的代码加入game.js,用以绘制鼠洞。
这段代码定义了drawHoles方法,其中会画出四个圆形来表示鼠洞,同
时还会画出表示按键的文本,玩家稍后可以按下键盘上对应的键来打
鼹鼠。我们可以在draw函数里调用这个drawHoles方法。
程序清单3.8

绘制鼠洞

由于引擎能够保证在游戏循环的每一帧中,都去执行draw函数里
的代码(这正是atom.js引擎的一项主要功能),所以,只需把绘制代
码放在draw函数中,等着引擎来调用即可。为了绘制鼠洞,我们在
draw函数里调用drawHoles方法,它接受三个参数。第一个参数是个数
组,表示将要写在每个鼠洞下面的字母。第二个参数表示最左侧那个
鼠洞的横向偏移量,第三个参数表示全部鼠洞的纵向偏移量。请注
意,在一般的绘图系统中,左下角是原点,而在使用canvas来绘图
时,y轴却在顶端,所以原点(0,0)位于左上角。
drawHoles函数在执行时会遍历holeLabels数组,以确定每个鼠洞
的位置,并调用hole对象的draw函数来绘制它们,此函数将于稍后定
义。game.hole对象先定义了一些属性,有的是字符串,有的是数字,
然后再定义刚说的draw函数。想在这个draw函数里引用hole对象中的
那些字符串和数字是非常简单的,只需调用“this.属性名”即可。在
game.hole对象的draw函数里,大部分代码看上去应该都比较熟悉,因
为它们与前面绘制太阳时所用的代码类似,只是最后两行不一样。在
这两行代码中,第一行用context.font来设置文本大小及字型。第二
行用context.fillText来绘制文本,此方法接受三个参数,分别表示
待绘制的文本及其横、纵坐标。
编好上面这段代码后,用浏览器打开index.html文件,就会看到
图3.2中的画面了。

图3.2 画好的鼠洞

3.4 第四步:绘制鼹鼠
画好鼠洞之后,游戏环境就准备好了。现在轮到游戏中的敌人
(鼹鼠,mole)登场了。由于我们在本步骤中只是试着绘制一下,所
以无论把它画到何处都可以。按照程序清单3.9中的粗体代码修改draw
函数,令鼹鼠出现在画面左上方。
程序清单3.9

在game.draw函数中绘制鼹鼠

现在还没有game.mole对象。为了使这段代码能正常运行,我们需
要创建mole对象,并在其中编写它的draw方法。将程序清单3.10中的
代码加入game.js。这段代码放在哪里都可以,但是不要嵌套在已有的
那些函数或对象之中。
程序清单3.10

具备draw方法的mole对象

首先定义几个简单的整数及字符串属性,供其后的函数用。接下
来,在mole对象中定义draw函数,游戏引擎的主draw函数会调用它。

此函数的参数表示鼹鼠的坐标,它把实际绘制工作交给具体的小函数
来执行,那些函数负责绘制鼹鼠的各个部位(头、鼻子、眼睛、胡
须)。由于绘制圆形的步骤大家已经非常熟悉了,所以绘制头部、鼻
子、眼睛所用的这三个函数就不用多说了,但是绘制胡须所用的
drawWhiskers却调用了几个新函数,在这需要讲一下。
从概念上来说,moveTo函数可以理解为“把画笔移至此处”,而
lineTo函数则可以理解为“把画笔从其所在之处拖动至此”。但这两
个函数用的都是“隐形墨水”,所以还需调用stroke方法。该方法会
“把隐藏的墨迹显示出来”。
mole对象的属性列表看上去有点长。可以把它拆分成几小块,不
过在接下来的步骤中,我们还是会将其视为一个整体。这段程序有好
几个地方都出现了重复代码(比如drawWhiskers方法里面),所以大
家可能想把它们精简一下。如果你觉得自己已经无法驾驭某段代码
了,那就尽快重构,然而要注意:应该把代码写得通用一些,不要太
过局限,而且要保持明晰,不要模糊了代码的本意。清理代码,使其
更易于理解,这个想法是好的,不过,一般情况下,只有等到程序变
得稍微复杂一些时,我们才能看出来应该如何精简它。

提示
使用纯canvas方法来绘图时,画一只如此简单的鼹鼠,竟然需要这
么多行代码。许多程序库都提供了封装程度更高的绘制函数,可以减
少绘制图形时所需的代码量。如果不想写这么多代码,那还有个办
法,就是用图片来代替。这种方式也有其复杂之处,所幸大部分游戏
库(包括atom.js)都会把图片组织为“精灵”(sprite),而精灵可以
复用,绘制效率也比较高。本书其余章节将会频繁使用精灵,不过现
在我们还是先来学习如何用纯canvas来绘图吧。
实现好mole对象,令主绘制函数调用其draw方法,然后保存
game.js文件,用浏览器打开index.html,即可看到如图3.3所示的画
面了。

图3.3 绘于画面左上方的鼹鼠

3.5 第五步:将鼹鼠放入鼠洞
本步骤将要大幅修改与鼠洞相关的代码。现在为鼠洞所写的这部
分代码只能简单地把鼠洞画到屏幕上而已。如果要实现打地鼠的功
能,还要再增加一些逻辑代码才行。有好几种实现方式,其中一种是
为mole对象增加“bopped”属性,并记录鼹鼠当前位于哪个鼠洞里。
不过在真实的打地鼠游戏机上,鼹鼠只不过是藏在每个鼠洞里的塑料
娃娃而已。所以不要做得太复杂了,把鼹鼠当成鼠洞里的装饰物就行
了,在鼠洞处于激活状态时,将其显示出来,否则将其隐藏。
现在开始修改game.js,首先,需要将程序清单3.11中的这行粗体
代码添加到文件中,请把它放在run函数调用语句的上方。
程序清单3.11

调用makeHoles函数

这行粗体代码是刚加进来的。尽管我们仍然要在游戏每次循环时
于draw函数中绘制鼠洞,但绘制所需的那些对象却只需创建一次就够
了。所以,这段代码要把调用makeHoles的语句放在draw、update、
run等函数外面。现在看看程序清单3.12所列的实现代码,此函数可以
紧随game.draw函数而置于其后。
程序清单3.12

定义makeHoles函数

此函数先创建了名为game.holes的数组,用于存放新构建出来的
鼠洞对象。每构建完一个鼠洞,就为其设置几项属性,并把它加入
holes数组里。由于这次是逐个绘制鼠洞,因此现在可以把drawHoles
函数删掉了。
然后需要把程序清单3.13中的这一大段代码加入game.hole对象
中。该对象现在不仅要封装draw方法,而且还得包含重要的逻辑代
码,以便将标签、鼠洞、鼹鼠等物件绘制到屏幕上。
程序清单3.13

将更多的绘制任务交由hole对象完成

在这段代码中,第一处改动即是加入了名为moleOffset的变量,
因为稍后在绘制鼹鼠时,我们想把鼹鼠稍稍放在鼠洞上方一点,所以
要用这个变量来表示偏移量。早前在绘制鼹鼠时,我们是将鼹鼠拆分
成几个部位分别绘制的,而接下来,我们也要采用类似方式,将hole
对象的绘制任务拆分为绘制鼠洞、绘制标签、绘制鼹鼠等三个部分。
只有在hole对象的active属性为“真”时(也就是鼹鼠从洞中探出头
来的时候),才需要绘制鼹鼠。与绘制鼹鼠有关的那些细节都在
game.mole对象中写好了,所以到时只需将这些复杂的任务都交由mole
对象来完成即可。而mole对象的draw方法则无须修改。
在本步骤最后,我们按照程序清单3.14来修改game.draw函数。
程序清单3.14

新版draw方法

修改后的draw与原来一样,也是先绘制背景,但绘制完背景之
后,旧版函数是继续绘制各个洞穴,最后画一只鼹鼠,而新版函数则
是遍历holes里的各个hole对象,并调用其draw方法。
如果一切正常,那么所有鼠洞都处于激活状态,也就是说,每个
鼠洞中都会有鼹鼠探出头来。用浏览器打开index.html文件,即可看
到如图3.4所示画面。

图3.4 所有鼠洞中都有鼹鼠钻出来

技巧
本步骤中改动的代码很多。若运行程序时遇到了问题,请仔细检
查,尤其要检查调用每个函数时所传入的参数。如果实在无法解决,

请将所写的代码同本书范例代码中的party/after_recipe5/game.js文件相比
对。
与绘制相关的部分已经做好了,大家现在可以看到,夕阳下出现
了好几个可爱的鼹鼠,然而这还不能称为游戏。所以在下一步里,我
们还要加入一些动态代码,用以表现鼹鼠从洞中钻出来的过程。

3.6 第六步:令鼹鼠从洞中钻出来
当前所有鼠洞都处于激活状态。而在这一步里,我们改为每次只
激活一个鼠洞,并在两秒钟之后切换至另一个鼠洞。首先,根据程序
清单3.15来调整game.js中的update函数,并在函数前面先设定一些变
量,供函数中的代码使用。
程序清单3.15

在update函数中加入逻辑代码

currentMoleTime表示当前鼠洞已经激活了多长时间。下一行表示
游戏应该在当前这个鼠洞激活了多少秒之后,再去激活另外一个鼠
洞。若想使鼹鼠探出头来的时间长一些,则可以增加此数值。在
update函数中,我们把原来检测键盘左键所用的那段范例代码删掉,
改为程序清单3.15中的这四行粗体代码。现在我们就会用到函数中的
dt参数了,该参数表示这次执行update函数时,距离上次隔了多久
(以秒为单位,可能带有小数点,比如0.017)。这个带有小数点的时
间差会累计到currentMoleTime里面。如果发现鼹鼠在当前这个鼠洞里
的停留时间超过两秒,那么就随机挑选一个新的鼠洞,将其激活,并
把计时器清零。
游戏的主draw函数基本不变,但是需要设置每个鼠洞的active属
性,以反映其是否处于激活状态。请按照程序清单3.16来修改此函

数。
程序清单3.16

设置各个hole对象的active状态

只需修改粗体部分即可,它们就是设置active状态所用的代码。
由于我们决定把设置active属性的任务放在此处来做,所以就无须像
上一步那样,在创建完每个hole对象之后都要立刻设定其active属
性。也就是说,因为draw函数已经设置了active属性,所以需要把
game.makeHoles函数中的这一行删掉:
用浏览器打开index.html文件,即可看到鼹鼠在鼠洞之间钻来钻
去了。

3.7 第七步:使玩家可通过敲击键盘来打鼹鼠
结束上一步之后,玩家还不能打鼹鼠。而鼠洞旁边的那几个标签
(也就是A、S、D、F这几个字母)不能只是奇怪的摆设,必须使其发
挥作用才行。首先我们发现,['A','S','D','F']这个数组字面量不止
一次会用到,所以应该修改相关代码。程序清单3.17列出了game.js文
件开头及文件末尾需要的地方。
程序清单3.17

创建game.keys属性

实际上,删去的那行代码 [1]自范例游戏引入之后就没什么实际用
途了。它只是把键盘上的左箭头起了个别名叫做left而已。在新写的
循环里,我们把四个鼠洞所对应的按键都绑定好,这样稍后就可以侦
测到玩家按下键盘的事件了。在文件末尾,将调用game.makeHoles函
数时所传入的首个参数由原来的数组字面量改为现在的game.keys。

提示

大家可能会问,代码里为什么不写成game.keys.i,而要写作
game.keys[i]呢?在访问对象属性时,采用方括号[]和点·这两种写法通
常都可以,但有时却只能采用其中一种方式,而不能采用另外一种。
通过点来访问属性,更清晰、更简洁。所以只要有可能,就应该尽量
这么做。而在通过点来访问属性会引发错误时,则可改用方括号来访
问,比如["myProperty"+"1"]。另外,如果碰到本例这种情况,需要用变
量i作为下标来访问atom.keys数组中的元素,而不是访问keys中名为“i
值”的属性,那就得使用方括号了。
接下来编写bop对象,其中所含的逻辑代码用来处理玩家敲打鼹鼠
的操作。程序清单3.18中的代码可以放在update函数后面。
程序清单3.18

game.bop对象

这段代码可以紧跟在game.draw函数后面。稍后编写的update函数
在绘制新鼹鼠时,若发现bopped为false,则会扣掉玩家的分数,所
以,此处我们先得把bopped设为true才行,否则,刚开始绘制游戏
时,total的值就会减1,从而使得玩家一进入游戏就丢掉1分。接下
来,把玩家已经打到的鼹鼠个数设为0。然后定义draw函数,以便把总
分绘制到屏幕上。with_key函数是间接调用的,game.update方法如果
发现玩家按下了某个与鼠洞对应的按键,就会以该按键为参数来调用
此函数。若key参数与activeMole对象的label属性相符(也就是条件
判断语句的后半部分 [2]),那么就递增total值,然后把activeMole
设为-1、并将bopped设为true。若key参数与label属性不符,则递减
total值。程序清单3.19列出了修改后的game.update方法,其中含有
检测玩家是否打中地鼠所用的代码。

提示

在with_key函数中,条件判断语句的前半部分有些不太直观。当
game.activeMole为undefined时,后半部分的代码会出错,为了防止这个
问题发生,所以我们才加上了前半部分。这种技术叫做“guard” [3]。
也可以在某处为game.activeMole设定好默认值,不过本例所采用的这种
写法更为简单。笔者现在就将此写法解释一下。
JavaScript会把if(0)这种条件判断语句中的0判定为false。请注意,!!
(value)这种内联式写法可以判断出受测的value对象是否为“真”。但
game.activeMole的取值范围(可以取0、1、2、3)却会导致这种写法出
问题,因为在game.activeMole尚未赋值时,其值为undefined,而
JavaScript会把!!(undefine)与!!(0)都判定为false。我们所希望的是:当鼹鼠
在第一个鼠洞时(这个鼠洞的下标为0,所以此时activeMole也是0),
条件判断语句中的内容能够为true。而且,我们还要求:当玩家打中当
前钻出来的这只鼹鼠并使之消失时,条件判断语句依然能够正确处理
此后的情况。此时可以把activeMole设为undefined,但这么做会使后来
阅读代码的人感到非常费解。所以,为了判断当前是否有鼹鼠钻出
来,我们需要为原来的受测值加1。也就是说,原来的0~3,变成了现
在的1~4,这样一来,这四个值都能判定为true了。原来的-1(也就是
玩家打中了activeMole之后的情况)变成了现在的0,于是也可以正确判
定为false了。若activeMole是undefined,则JavaScript在判定时会将其视为
NaN。该值加1之后,JavaScript会将其判定为false,而此做法也能满足
我们在这种情况下的需求。
上面这段话简单介绍了如何在JavaScript中判断某个变量是否已赋
值、如何用guard提前拦住条件判断语句中的undefined,以及如何检测
对象是否为真。用JavaScript语言来编程时,有许多特殊情况 [4]需要考
虑,若想在条件判断语句中把它们全都处理得当,有时真要颇费一番
心思。
程序清单3.19

判断玩家是否打中鼹鼠

在刚加入的这部分粗体代码中,第一部分的意思是:如果绘制新
鼹鼠时发现玩家没打中上一只鼹鼠,那么就扣分。而else分支则负责
处理玩家打中鼹鼠时的情况,此时bopped会重置为false,用以记录玩
家是否打中下一只鼹鼠。for循环会遍历开发者原来向引擎所注册的那
些按键,若发现玩家按下了某键,则调用game.bop.with_key方法。
最后还要修改一处:请参照程序清单3.20把绘制玩家总得分的代
码放入主draw函数里。
程序清单3.20

修改后的主draw函数

太棒了!游戏终于做好了!如果一切顺利,那么打开游戏并玩过
一段时间之后,画面就会呈现图3.5这个样子。

图3.5 好游戏,好分数

游戏基本可以算完成了。不过在结束本章之前,请花点时间想一
想如何把这款“派对游戏”(party game)改编成“音乐节拍游戏”
(rhythm game)。
[1] 指的是“atom.input.bind(atom.key.LEFT_ARROW,'left');”。——译者
注

[2] 指的是“&&”右边的“key===game.holes[game.activeMole].label”。
——译者注
[3] 大意为“守护式条件判断”。——译者注
[4] edge case,也称“边界状况”。——译者注

3.8 HTML5的<audio>标签并不尽如人意
用HTML5与JavaScript来处理音频本该是件容易的事情。在理想状
况下,播放声音应该如同绘制图像一般简单,只需调用
audiocontext.play(noteOrFrequency)之类的API即可;无论浏览器版
本新旧,无论是在移动平台还是桌面操作系统,都要能正常播放声音
文件才对,即便是手机自带的浏览器也应如此;用户应该很容易就能
通过网页来谱曲、混编音乐(mix beat),或弹奏乐器,并且能在网
页这种新颖的作曲环境下展示自己的音乐天赋,享受其中乐趣。而对
于本章来说,理想状况应该是:可以加入一些已经获得使用授权的曲
目,再多绘制一些图像、多设计一些关卡,并使计分系统更加精确,
这样就能打造一款类似《Rock Band》的游戏了。
在写作本书时,上述这些想法还都没有实现。对于应该支持何种
音频编码/解码器(codec)这一问题,各浏览器阵营看法不同,分歧
主要集中在应该使用开源技术还是闭源技术上面,也就是说,如果想
在网页中播放音频文件,那么得准备两个版本(一个是.ogg格式,另
一个是.mp3格式)。除了如何播放声音之外,各浏览器对于如何在底
层创建声音这个问题,也未能达成一致看法。Mozilla近期曾致力于一
套名为Audio API Extension的方案,可是现在已经废弃了。而Chrome
则支持Web Audio标准,W3C正在开发这套API(可参考
http://www.w3.org/TR/webaudio/)。Firefox也宣称将来会遵循此标
准,不过在笔者写作本书时,还没有哪一套API能同时为Chrome及
Firefox浏览器所支持。
除了刚才谈到的这些,还有其他问题。比如,在移动设备上加载
多个声音文件,可能会使游戏崩溃。另外,如果想查询浏览器是否支
持某种声音格式,那结果也会令你失望。看看程序清单3.21这段代
码。
程序清单3.21

查询浏览器是否能播放某种格式的音频文件

上面这段特征检测代码有问题。我们在myAudio这种
HTMLMediaElement对象上面调用canPlayType函数,本来是想获得true
或false这样的明确回答,可浏览器所返回的值却比较模糊,有点像毕
业舞会受邀者的口气,而不太像个计算机程序。这三种返回值
是:"probably"(很可能)、"maybe"(也许)和" " [1]。
笔者最后想说,浏览器对音频文件的支持度应该会越来越好才
对。但愿如此吧,不过这也很难预测。过去几年里,各浏览器对音频
文件的支持方式变化很大,而且现在仍未稳定下来。

提示
别泄气,笔者在本节只是想解释浏览器播放音频文件时的一些底
层问题而已,虽然困难颇多,但是希望仍在。看看附录C吧,你会发现
有人在音频文件播放这一领域颇有成就。对HTML5技术来说,跨浏览
器绘制图形基本上已经没有障碍了,但是跨浏览器播放音频仍是个大
问题。
有一些封装程度较高的工具,可用来加载音乐及音效文件。但是
请记住,在采用这些工具来处理音频时,如果浏览器不支持相关格
式,那么就意味着需要使用flash来作为备援技术。而且还要知道,这并
不能解决动态生成音频文件的问题。
如果想根据本章的打地鼠游戏来制作《Rock Band》风格的作品,
那么在处理音频播放这个问题上,有两种办法。一种办法是采用
Firefox或Chrome浏览器各自所提供的底层API,动态地播放声音,还

有一种办法是采用预先准备好的音频文件。若采用后一种办法,则需
要设法创建音频文件(或是在取得授权的情况下使用他人所制作的音
频文件),并把这些短暂的声音片段与对应的按钮关联起来,以便在
适当的时机播放。笔者想把本书的范例代码写得更易扩展、适用面更
广一些,而这两套方案都无法满足此需要。
[1] 空字符串表示浏览器不支持此种音频文件格式。——译者注

3.9 小结
本章讲了很多内容。其中包括:怎样用atom.js这个极简游戏引擎
来制作游戏;怎样通过未经抽象的canvas API来绘制游戏内容(其他
游戏引擎都提供了抽象程度较高的绘制API,而atom.js没有提供);
CoffeeScript有哪些优点与缺点;如何理解JavaScript对象模型中的
prototype及constructor这两个难懂的概念,以及如何通过
Object.create函数来简化对象创建过程等。最后讲述了目前在浏览器
中播放声音时所遇到的麻烦。
除了鼠标事件绑定功能之外,atom.js引擎的其他主要内容都在本
章里用到了。若想继续研究atom.js引擎,可以深入学习CoffeeScript
编程或纯JavaScript编程,以便向引擎里增添更多新功能,或是据此
制作一套更为精简的引擎来。
打地鼠游戏也有很多种改进方式。可以提高游戏难度,比方说,
令屏幕上同时钻出来更多鼹鼠,或是调整鼹鼠钻出地面的时间间隔。
也可以为游戏加入高分榜。还可以引入游戏结束这一概念,并在结束
画面上放置“重新开始”(Play Again)按钮。游戏的输入机制也可
以由键盘操作改为鼠标操作。甚至可以通过CSS把鼠标光标换成锤头或
蚯蚓(鼹鼠的食物)图样,这样就能使玩家觉得游戏画面更为贴切
了。

第4章

解谜游戏
解谜游戏代表作有《宝石迷阵》(Bejeweled) [1]。
在玩《宝石迷阵》、《Snood》 [2]和《Tetris Attack》这种游戏时,
玩家要通过“增加”、“消除”、“移动”、“交换”等操作,尽快
把各色方块按相关规则匹配起来。对于休闲式的网页游戏来说,这种
形式很流行,不过其他游戏平台也会出现此类作品。
这类游戏有个特点,就是构建起来相对较为容易一些。除了可以
单独发行之外,我们也可以将之改编为解谜类游戏,或是把它做成
“迷你小游戏”(minigame),嵌套在其他大型游戏里面。SNES游戏
机(Super Nintendo console system)上面有款游戏叫做《Lufia
2》 [3],此游戏尤以频繁使用“地下城式的解谜机制”(dungeon
puzzle)而著称,比方说,游戏中需要通过移动色块来解谜。在大家
更为熟悉的《塞尔达传说》(Legend of Zelda)系列中,玩家也经常
需要通过此类解谜机制来打开门锁或开启宝箱,解谜要素贯穿于整个
游戏流程里面。
除了虚拟的电子游戏之外,真实世界里也有这种游戏。比如“十
六格数字推盘游戏” [4]正属于此类。我们可以为游戏中的“方块”设
定一些新的能力,令规则逐渐复杂起来,同时还可以增设一些判定胜
负及平局的机制,这样一来,就可以演变为井字棋 [5]、围棋、国际象
棋之类的游戏了(若想做成一款类似国际象棋的游戏,还需要向其中
加入某些中世纪风格的要素才行)。
本章将构建一款小朋友们经常玩的游戏,它叫做Memory,规则很
简单:点击内容相同的两张图,令其消失,直至消去全部图片。我们

采用名为easel.js的JavaScript游戏引擎来制作此游戏。
在编程中经常听到“高层抽象”(high-level abstraction)与
“底层抽象”(low-level abstraction)这两个说法。如果许多小细
节都要由开发者自己来处理,那就叫做“底层抽象” [6],反之,如果
这些细节都由程序库、编程语言,或本书所提到的JavaScript游戏引
擎来处理,那就叫做“高层抽象” [7]。书里所讲的各个游戏引擎,其
抽象程度会越来越高,不过,本章所用到的这个easel.js却显得相当
底层,也就是说,与其称之为游戏引擎,倒不如把它看成一套便捷而
通用的接口,这套接口背后封装着渲染canvas所用的API。由于本章要
制作的“图片记忆游戏”相当简单,所以采用easel.js这个底层引擎
正合适,在使用底层引擎的过程中,还会碰见一些其他章节所接触不
到的细节问题。
我们从程序清单4.1所列的这个index.html文件开始,它位于范例
代码的puzzle/initial目录下。这段代码设置了doctype、加载了
easel.js引擎,并创建了制作本章游戏所需的canvas元素。
程序清单4.1

刚开始制作游戏时所用的HTML文件

现在用Firefox或Chrome浏览器打开此文件会看到空白画面。下一
步我们就开始使用easel.js引擎,向屏幕上渲染一些内容。

[1] 中译《宝石迷阵》或《宝石方块》。——译者注
[2] 中译《泡泡怪》或《鬼脸泡泡龙》。——译者注
[3] 中译《四狂神战记2》。——译者注
[4] “15 Puzzle”,在4×4的16 格推盘上有15个滑块,上面分别写有数
字1至15,玩家需要通过移动滑块,使所有数字按顺序排列好。——译
者注
[5] “ tick tac toe”,两位玩家在3×3的棋盘上轮流落子,先使己方三子
连为一线者获胜。——译者注
[6] 也称“封装程度比较低”或“抽象程度比较低”。——译者注
[7] 也称“封装程度比较高”或“抽象程度比较高”。——译者注

4.1 第一步:用easel.js来渲染
按惯例,要想表明某个程序能正常运作,应该输出“Hello
World!”之类的文字。而easel.js是个图形渲染引擎,所以,我们打
算在屏幕上绘制颜色随机的方块,以此来检验引擎是否正常。
现在需要以某种方式启动JavaScript脚本。可选的办法有很多,
而且本书后续章节还会再介绍一些。我们可以把要运行的JavaScript
代码全部放在<script>标签里。不过这种办法可能有问题,因为这些
JavaScript代码也许会在所有元素尚未完全准备好的情况下执行,如
此一来,脚本便会在运行的时候出错。还有一个办法是像程序清单4.2
这样,在<script>标签里为window对象绑定JavaScript事件。尽管我
们不打算采用此办法,但是仍然可以了解一下这段范例代码。
程序清单4.2

将init函数绑定到window

本章打算采用一种更为简单,但不太优雅的办法,那就是:像程
序清单4.3这样,直接把JavaScript绑定到body标签的onload属性上。
请根据程序清单中的粗体代码修改index.html文件。
程序清单4.3

以稍显突兀的写法来加载JavaScript代码

严守网页开发规范的人看到这里可能会惊呼,因为业界把这种写
法贬称为“侵入式JavaScript”。此说法背后蕴含着的思考方式其实
完全合理。由于我们在维护网页开发项目时,总想使其变得易于扩展
一些,所以,最好是能把代码按其性质划分成内容层(HTML)、展示
层(CSS)和行为层(JavaScript),并尽量将其隔离开,以保持代码
的整洁与灵活。这种分层方式已经流行了一段时间,若有兴趣,可在
网上搜索“unobtrusive JavaScript”与“JavaScript Patterns”
(JavaScript模式)这两个词。不过我们还可以研究得更深入一些,
以便知晓何时应该坚持优雅的写法,而何时又应该采用权宜的应急方
案。有些时候,易扩展、灵活,且健壮的代码,是与“至简原则” [1]
相冲突的,因此,我们应该明白:想实现同一个功能,可以有好几种
不同的写法,而这些写法各有优势。onload等许多特性(其中也包括
能够触发JavaScript代码的其他手段,比如onclick()、
onMouseOver()等)经过这么多年还能留在HTML规范中,自然有其道
理,所以,还是应该知道这种写法。
讲完这一点之后,我们回到绘制色块的问题上。如果打算用这种
“侵入式”写法为body元素绑定onload()的话,那就得有init()函数
才行。可以像程序清单4.4这样,于加载easel.js库所用的<script>标
签下方再写一个<script>标签,使浏览器在加载此标签时,弹出含有
“hello world”字样的警示窗。
程序清单4.4

添加script标签,并在其中定义init函数

在显示色块之前,还有一些代码要写。请按照程序清单4.5修改
init()函数,使其内容变得更加丰富一些。刚才为了确认引擎是否能
正常工作而写的那行“hello world”代码现在可以删掉了。
程序清单4.5

包含更多内容的init函数

这段代码需要理解的地方很多。我们逐行来讲解。首先,声明变
量canvas与stage,前者用来表示HTML中的canvas元素,而后者则用来
表示easel.js引擎所定义的Stage对象实例。init函数首先通过
getElementById()这个原生的JavaScript方法(原生方法是指由浏览

器而非easel.js等引擎所定义的方法)找到id属性为myCanvas的
canvas元素,并将其赋给canvas变量。接下来,以canvas变量为参数
(该变量对应于网页中的canvas元素)新建Stage对象,并把此对象赋
给stage变量。然后,把drawSquare()方法的运行结果赋给square变
量。我们还没定义drawSquare()方法,不过假设现在已经定义好了,
那么其后这行代码会调用addChild()方法,把square变量加入stage对
象中。方块加入之后,并不会立即显示,所以最后还得再调用
stage.update()方法,把刚加入的square展示出来。
还算简单,对吧?接下来看看程序清单4.6中的drawSquare()方
法。请把这段代码放在<script>结束标签的上方。
程序清单4.6

drawSquare方法

easel.js引擎所提供的图形绘制方法,其封装程度依然很低,不
过用起来却比较容易。这段代码实际上是在操作shape对象,具体来
说,是操作shape对象里面的graphics。setStrokeStyle()方法用于定
义图形边线的宽度,beginStroke()方法的参数表示边线颜色,
beginFill()方法用于指定图形内部的填充色;而rect()方法的四个参
数则表示shape的横坐标、纵坐标、宽度及高度。最后,drawSquare函

数把shape对象返回,这样的话,init()函数就可以把此对象赋给
square变量并将其渲染出来了。
用浏览器打开index.html文件,会看到如图4.1所示的灰色方块。

图4.1 绘制灰色方块

灰色方块是很好,不过我们想令其变得更有色彩一些,并且想在
每次刷新页面的时候变换填充色。所以,需要略微修改drawSquare()
函数,并新增randomColor()函数(代码参见程序清单4.7)。
程序清单4.7

刷新页面时随机变换填充色

drawSquare()函数需要调用randomColor()函数来获取随机颜色。
randomColor()以0~255之间的随机分量来调配颜色,供
graphics.beginFill()函数所用。而Math.floor函数则可以把小数下
调为整数。现在每次刷新页面时,所看到的方块颜色都是随机的(参
见图4.2)。

图4.2 彩色方块

技巧
在看下个步骤之前,你或许还没分清哪些是JavaScript原生函数,哪
些是由easel.js的API所引入的函数。没关系,其实这并不难区分,因为
程序里出现的变量和函数名只可能有三种来源。第一种就是你自己定
义的,它们应该能在你自己所写的代码中找到。第二种则是JavaScript的
原生方法或原生对象,这种情况可以去https://developer.mozilla.org/网
站查阅其用法。第三种情况,则是由easel.js这种程序库所引入的。在这
种情况下,要想找到其文档或范例代码也很容易:我们只需在网上搜
索“程序库名称docs”、“程序库名称documentation”、“程序库名称
API”、“变量或函数名称程序库名称”等关键字即可。在编写
JavaScript程序时,这几种不同类型的API都会用到,附录A详细讲解了
此内容。
[1] “Ya Ain't Gonna Need It” , 简 称 YAGNI , 直 译 为 “ 你 并 不 需 要
它”。——译者注

4.2 第二步:渲染多个方块
只绘制一个方块肯定不行,这连最简单的游戏都算不上。与玩家
所要达成的游戏目标相关的那些代码将放在后续步骤中完成,而我们
现在仍处于基本的图形渲染环节,所以在当前这一步里还需要再绘制
一些内容才行。首先清理代码并创建几个变量,以供本步骤中的函数
使用。请把程序清单4.8中的粗体代码加入html文件。
程序清单4.8

声明创建方块所需的变量

这段代码没有太过特别之处。只是为一些变量赋值而已。
squareSide表示正方形的边长。max_rgb_color_value表示将要传递给
随机数生成器的参数,生成器会据此产生0~255之间的数值。gray对象
表示一种颜色,而此颜色是通过easel.js的Graphics对象所取得的。
我们将用这种颜色来绘制方块的边线。
接下来,把程序清单4.9中的粗体代码加入init()函数,这些代码
放在程序清单4.8那段代码后面。
程序清单4.9

声明init()函数所用到的变量

这段代码也声明了一些稍后要用到的变量,并为其赋值。rows与
columns表示将要绘制的方块共有多少行、多少列。根据现在所用的值
来看,将会画出36个方块来。squarePadding表示方块间隔。接下来把
上一步里绘制方块所用的代码删掉。由于本步骤绘制方块的过程相对
比较复杂,所以我们放在后面来讲。
如果你原来较少接触编程的话,那么可能会问,为何不把这些数
值直接内联在调用函数的代码里,而要把它们单独用变量来表示呢?
这么做是为了使代码便于管理。等到代码多起来之后,开发者就很难
记清每个数值的含义了,而像这样把数值声明成变量,则可使稍后阅
读代码的人很快就能理解其意图。这样复用变量还有个好处,那就是
修改起来更为方便,若想修改某数值,只需改动声明变量时所赋的那
个值即可。你可能还会问,为什么有些变量声明在函数外面,而有些
却声明在里面呢?这是因为,声明在函数外面的那些变量,脚本中的
所有函数都能使用,而声明在函数里面的那些变量,则仅能在本函数
内使用。

提示

编写JavaScript程序时,可能会为了提升执行速度而写出一些结构不
那么清晰的代码,在编写游戏这种对性能要求很高的程序时,更需要
权衡这一问题。有种非常流行的JavaScript优化技术:在开发阶段采用程
序员更易读懂的代码风格来编程(使用表意清晰的变量名与函数名、
加入适当空格以美化格式、撰写注释,等等),而在发布产品时则制
作一份“极简版”(minified version),其中的代码虽然看上去非常难
懂,但由于文件很小,所以浏览器下载起来比较快,而且执行效率也
比较高。
接下来该调整drawSquare函数了,请把程序清单4.10中的粗体代
码加入其中。
程序清单4.10

drawSquare函数

此函数改动不大,我们只是用早前声明的那些变量作为参数来调
用其他几个函数而已。
randomColor函数也有改动,新版代码使用了早前所定义的
max_rgb_color_value变量(参见程序清单4.11)。
程序清单4.11

修改后的randomColor函数

本步骤所做的最后一处修改,是为init()函数增加for循环。请将
程序清单4.12中的粗体代码加到init函数中。
程序清单4.12

完整的init()函数,其中加入了渲染方块所需的代码

如果你还不熟悉for循环的用法,那么此处有好几个地方要学。第
一行粗体代码首先创建了名为i的变量,用以充当计数器。然后是分
号,分号后面的i<rows*columns意思是:只要i比待显示的方块总数要
小,那就一直往下循环。该行代码的最后一部分(也就是第二个分号
后面的内容)意思是说,每轮循环执行完之后,就递增i的值。
计算方块x坐标的办法是:将单个方块的总宽度
(squareSide+squarePadding)乘以当前方块在该行中的列号。你如
果没有见过%操作符,那就将其理解为第一个操作数除以第二个操作数
之后的余数。计算方块y坐标的办法是:把循环计数器与总列数相除,
并对商取整,用单个方块的总高度(也是
squareSide+squarePadding)与刚才说的整数相乘。
若本节代码实现无误,则会在浏览器中看到如图4.3所示的画面。

图4.3 绘制好的多个方块

4.3 第三步:创建成对出现的同色方块
上一步所绘制的那些方块,其颜色都是随机的,几乎难以出现颜
色相同的一对方块。实际游戏中,玩家会通过点击鼠标来找出同色的
方块,而在编写处理此功能的逻辑代码之前,我们需要先保证屏幕上
能够成对地出现同色方块。
现在我们需要用随机颜色制作出若干对方块,并将其随机排列在
各个位置上。首先,创建名为placementArray的数组,该数组的大小
与待绘制的方块总数相同。请将程序清单4.13中的粗体代码加入
index.html文件。
程序清单4.13

构建placementArray

这段代码的意思是,声明一个空数组,然后调用另外一个函数,
以构建此数组。这段代码还新加了一个名叫numberOfTiles的变量,因
为在这一步所要编写的代码中,会多次用到rows*columns这个值。把

此值提取成变量有诸多好处,除了上一节所讲的那些优点之外,还有
个原因在于:如果不提取的话,那么每次用到这个值的时候,都要重
新计算,而重复执行这种无谓的计算将会拖慢程序运行速度。
接下来,按照程序清单4.14来实现刚才所调用的
setPlacementArray()函数。这段代码可放在</script>结束标记上
方。
程序清单4.14

setPlacementArray函数

此函数会把元素放入刚才创建好的那个placementArray数组中,
每个元素的值就是其下标,而放入的元素总数则与numberOfTiles参数
的值相同。push()函数的功能很简单,只是把下标值追加到数组末端
而已。执行完此函数后,数组就变成了这个样子:
[0,1,2,3,...,numberOfTiles]。
写好上述代码后,我们还需按照程序清单4.15来略微调整一下
init()函数。
程序清单4.15

调整init()函数中的循环代码

这段代码把for循环的终止条件由原来的row*column直接改成
numberOfTiles,除此之外,还有两处修改值得注意。其一是:原来我
们在循环中是根据计数器i来决定方块位置的,而现在则改为根据
placement变量来决定。其二是:现在每当循环计数器为偶数时,就调
用randomColor()来重新计算一个随机色,并在本轮与下一轮循环中,
将该颜色传给drawSquare()函数,以此制作出颜色相同的一对方块。
现在请按程序清单4.16所示,修改drawSquare()函数。为了调用
beginFill()方法,此函数原来需要通过randomColor()来决定方块颜
色,而现在则是直接把传进来的color参数当成填充色。
程序清单4.16

修改后的drawSquare()函数

用程序清单4.17中的代码来实现getRandomPlacement()函数,这
段代码可放在</script>结束标签的上方。
程序清单4.17

getRandomPlacement()函数

此函数做了下面几件事。首先,从placementArray数组里随机挑
选一个元素。然后,把该元素从数组中移除并返回此元素,使得
init()函数可以把返回值赋给其placement变量。每次执行循环时,
getRandomPlacement()都会从数组中随机选出一个数,而每轮可供选
择的数组元素都要比上轮少一个。
编写完本步骤所需的代码之后,每一对同色方块都会随机排布到
各个位置上,如图4.4所示。

图4.4 成对出现的同色方块

4.4 第四步:配对并消除同色方块
接下来该实现同色方块的配对与消除功能了。为此我们需要修改
制作方块时所用的代码,而且还要新增一个函数,用以处理鼠标点击
事件。首先来看程序清单4.18中的这几处简单调整。
程序清单4.18

为实现同色方块的点击、配对、消除功能而修改制作

方块时所用的代码

首先创建高亮颜色,以便给玩家当前所选中的这个方块绘制边
框,然后,创建名为tileClicked的变量,用以保存玩家当前这一轮所
点击的首个方块。最后那行粗体代码的意思是:为画面中的每个方块
都设置一个事件处理函数,用于处理鼠标点击事件。我们稍后再来定

义这个处理函数,现在先看看第三行粗体代码:
square.color=color;。
笔者自己非常喜欢JavaScript的这一特性。比方说有个名叫
square的JavaScript对象,那我们就可以通过这种非常简短的写法来
为其定义一项新属性,此属性可以是数字、对象,甚至函数。许多其
他编程语言都要求开发者必须定义一个方法,通过此方法才能获取并
设置该属性的值。而JavaScript不需要,这是其好的一面。而不好的
一面则是,如果square里面已经定义过名为color的属性,那么这条赋
值语句就会把原有的属性值覆盖掉。而且还要注意,尽管我们已经以
较为安全的方式将color属性放到square对象的名称空间里面了,但
是,color的作用域却依然与square相同。
现在来实现handleOnPress()函数,请将程序清单4.19中的代码置
于</script>结束标签之前。
程序清单4.19

handleOnPress函数

本函数主要操作了两个对象。首先是tile对象,也就是玩家现在
所点击的色块,然后是tileClicked对象,也就是玩家在点击tile之前
所点击过的色块。一开始,tileClicked的值是undefined,因为我们
并未向其赋过值。代码所用的条件检测语句看上去或许有点奇怪。本
来可以写成if(tileClicked===undefined),但如果这么写,那么等到
tileClicked设置成null之后,这个判断语句就失效了。你可能会说,
把tileClicked=null;改成tileClicked=undefined;不就行了吗?确实
如此,不过,直接把undefined赋给变量的做法显得十分怪异,会令阅
读代码的人感到困惑。所以,我们在此处采用
if(!!tileClicked===false)这种写法,来判断变量tileClicked到底

是已经赋值成对象了(此种情况视为“真”),还是尚处在undefined
或null状态(此种情况视为“假”)。!!这个操作符的意思是:首先
判断右侧!操作符的操作数是“真”还是“假”,并将估值结果反转,
然后通过左侧的!操作符把刚才反转的结果再反转回来。这么做实际上
就等于把!!右侧的操作数转换成了“真/假”值。如果你还是不明白,
那就请在控制台里试一试,或者花些时间读读附录A。为了便于理解,
可以把这个条件判断语句想象成“如果tileClicked是undefined或
null,那么……”接下来我们看看当条件判断语句为“真”时,会发
生什么事情。
首先,玩家当前点击的色块周围会出现高亮的框线。然后
tileClicked变量的值会变为tile。这样的话,玩家下次再点击方块
时,这个条件判断语句就会进入else分支了。else分支要检查前后两
次所点击的方块是不是同一个颜色的。我们通过color属性来判断这一
点。在这个判断语句里,&&后面的部分是为了检查玩家是不是把同一
个方块点了两次,若真是这样,则不算配对成功。如果玩家找到了一
对同色方块,那就把二者的visible属性都设为false,以便将其隐藏
起来。否则就把高亮线框从上一次点击的方块周围移走。不管是哪种
情况,我们都要把null赋给tileClicked,这样一来,下次执行此函数
时,if(!!tileClicked===false)语句就会判定为true了,于是玩家
也就可以开始下一轮配对操作了。
用浏览器打开index.html,并通过点击鼠标来消掉几对颜色相同
的方块,然后,游戏画面就会呈现图4.5这个样子了。

图4.5 消掉了几对同色方块之后的游戏画面

4.5 第五步:隐藏与翻转图片
上一步做完之后,效果还不错。现在看起来更像个游戏了,不过
这对于我们要制作的Memory游戏来说,还是显得有点太过简单了。
Memory游戏的一大乐趣就在于:起初各张图片都是面朝下放着的,玩
家必须通过点击鼠标使某一张图片翻转过来,然后反复执行此过程,
以记住各图片的位置,并将其配对。这种游戏过程对玩家来说,既带
有惊喜,又富于挑战,而我们现在的成果还达不到这一点,所以尚且
不能算作一款Memory游戏。本步骤将要实现图片的隐藏 [1]与翻转功
能。
首先,在调用drawSquare()方法时,我们不想令玩家一开始就看
到方块的真实填充色,而是想用灰色把方块绘制出来。原有的color属
性现在仍保留,但是调用drawSquare()所用的代码则需按照程序清单
4.20来修改。
程序清单4.20

将方块的真实填充色隐藏起来

接下来,参照程序清单4.21,把现在已经用不到的highlight变量
删掉。
程序清单4.21

移除highlight变量

本步骤所需的最后一处调整,就是参照程序清单4.22来修改
handleOnPress()函数。
程序清单4.22

修改handleOnPress函数

此函数增删之处较多,所以不用逐行修改了,直接把上述代码粘
贴过去,或是把这些代码用键盘敲到源文件里就好。这段代码首先会
把玩家所点击的这个方块用其真实颜色来填充。然后,判断此方块是
不是玩家在本轮配对中所点击的第一个方块,如果不是,那么它又是
不是和玩家上次所点击的方块同色。只要上述两条件中有一个成立,
那就更新tileClicked变量的值,并调用stage对象的update方法,以
便重新绘制游戏画面。

如果上述两项条件都不满足,那么还需要再处理两个小的分支情
况。第一种小情况(也就是嵌套在else里面的那个if)和上一小节的
处理方式相同,也是判断玩家所点的两个方块是否能配对,若能,则
将二者移除。第二种小情况则表示玩家所点的这两个方块无法配对,
此时我们会把玩家在本轮配对中所点击的第一个方块(也就是玩家上
一次用鼠标点击的那个方块)设为灰色。不论执行哪个小分支,最后
都要更新tileClicked变量并调用stage对象的update方法。
如果一切正常,那么在找到了几对同色方块之后,游戏画面就会
呈现图4.6这个样子。

图4.6 消除了几对同色方块之后的游戏画面,其中许多方块都没有显示其真实填充
色

[1] 本节所说的“隐藏”(hide),是指将图片或方块正面朝下盖起
来,令玩家看不到其内容。不是彻底将其从游戏画面中移除。——译

者注

4.6 第六步:胜负判定
游戏现在玩起来有些Memory的感觉了,需要实现的功能也差不多
都快做好了。本步骤需要给游戏加入胜负判定机制。首先声明一批新
的变量,分别用来表示玩家需要在多长时间内完成此游戏、玩家一共
需要找到多少对同色方块、目前已经找到了多少对,另外还要向画面
中输出一些文字,以便将目前的游戏状态告诉玩家,此外,还要把方
块保存到数组里,因为如果玩家在游戏时间耗尽之前没能完成全部配
对(也就是输掉了这盘游戏),那么需要把那些未配对的色块全部展
示出来。请按程序清单4.23来修改html文件。
程序清单4.23

在<script>标签中加入几个新的变量

接下来需要在init()函数里为这些变量及其中的属性赋值。代码
参见程序清单4.24。
程序清单4.24

init函数的其余代码

然后,需要创建记录游戏时间所用的Ticker对象。而且还要在程
序清单4.25的for循环中,把每个square对象加入squares数组里。代
码开头的三行可以紧跟着程序清单4.24来写,而其后的那行粗体代
码,则可放在square.y的赋值语句下面。
程序清单4.25

init函数的其余代码

接着实现tick()函数。把程序清单4.26中的代码放在</script>结
束标签上方。
程序清单4.26

tick()函数

此函数计算游戏当前留给玩家的剩余时间,并且将此信息更新到
游戏画面中,当时间耗尽时,调用gameOver()函数。有时候剩余时间
可能是负值(比如由于网页失去焦点而导致tick()函数计时不精
确),所以不能只检测secondsLeft===0,而要把小于0的情况也一并
考虑进去。现在按照程序清单4.27来实现gameOver()函数。这段代码
可置于tick函数之后。
程序清单4.27

gameOver()函数

看到这段代码后,你应该就会明白程序清单4.26在调用
gameOver()函数时,为什么要以false为参数了。这是因为gameOver()
会根据该参数来决定应该在屏幕上显示“You win!”还是“Game
Over”。不论是哪种情况,我们都要将每个方块绘制一遍,并把负责
处理鼠标点击事件的handleOnPress函数禁用 [1],如果玩家没能在限
定时间内完成游戏,那么尚未配对的那些方块将会显示出来。(这就
是早前要把方块保存到squares数组里的原因。)此函数首先暂停计时
器,然后处理squares数组,最后更新replay元素,使得玩家可通过点
击此链接来重玩。
为了把重玩功能做好,我们需要实现两样东西,一个是id属性为
replay的div元素,另一个是名为replay的函数,该函数就是<a>元素
的onClick属性值,玩家点击链接时,浏览器会执行此函数。请把程序
清单4.28中的代码加入html文件。

程序清单4.28

定义重新开始游戏所用的replay函数

本步骤中的最后一处改动,就是编写代码来判断玩家是否获胜。
这段代码可以写在handleOnPress函数里:每当玩家找到一对同色方块
时,matchesFound计数器的值就加1,如果玩家已经找到了画面中的最
后一对同色方块,那么就以true为参数,调用gameOver()函数(代码
参见程序清单4.29)。
程序清单4.29

判断玩家是否获胜

本步骤实现了判定胜负所用的逻辑代码。而在下一步中,我们来
看看如何优化程序的性能。
[1] 也就是把相关对象的onPress属性设为null。——译者注

4.7 第七步:缓存与性能优化
大家可能听过缓存这种技术。其核心主旨是尽力减少信息获取过
程中的计算量。可以把缓存机制比作冰箱,有它之后,我们就可以一
次储存许多食物,而不用每次想吃东西的时候都去商店买。easel.js
引擎也有自己的缓存机制,而且只需要研究两三行代码就能明白其用
法。不过我们还是先来谈谈缓存的优点与缺点吧。
缓存的优点在于:若使用得当,则可提升程序性能。在绘制游戏
中的图形时,如果使用了缓存技术,那么就无须每次循环都重绘全部
画面了,只需绘制屏幕中内容改变的那部分就好,此外,也可以用缓
存技术来降低绘制频率。假如屏幕里大部分内容都不动,而只有一个
元素在移动,那么,我们每次只需重新绘制该元素上一次与这一次所
占据的这两块地方就好。
然而缓存的缺点也很多。首先,开发者必须掌握创建缓存与更新
缓存的时机,而且还要能发现已经过期的缓存内容。如何判断缓存有
没有失效,这是个棘手的问题。使用了缓存的系统,调试起来更加困
难。因为其中的对象现在会表现出两种状态:有时候,其行为和没使
用缓存之前完全相同,而有时候,则会出现极其微妙的差别。除了这
个问题之外,还有个麻烦之处在于:一般情况下,缓存中的对象在一
定时间后会自动过期,但是有些开发者可能会通过代码来提前宣告某
块缓存已过期,或是绕开正常途径而直接获取缓存中的对象。笔者再
用刚才提到过的冰箱来打个比方:冰箱的好处在于,你可以存储大量
食物,而不必每次都去商店买。但是放在里面的食物会过期,所以为
了避免吃到变质食品,你得定好下次的购买计划才行。如果你不打算
亲自买,而是想请室友帮你买个苹果,那么除非你在买之前明确告知
其注意事项,或是在购买过程中仔细盯着,否则,就没办法控制苹果
的购买源头、采购时间,以及是否变质等问题了。
更为复杂的是,每种浏览器内部都有一套缓存机制,它们的运作
方式并不完全相同。这就好比你正在筹备餐点并等着朋友们来聚餐,
而他们却有可能自己已经先去商店把吃的东西买好了,然后再带过
来。这样的话,餐桌(也就是系统中的可用内存)就会弄得很乱,而
且大家还得花时间来讨论每个人要吃些什么。easel.js引擎中有个演
示缓存功能的范例程序,笔者写作本书时,该程序运行的状况是:在
Firefox浏览器上,开启缓存功能后,程序性能要比没开缓存时高;而

在Chrome浏览器上,则是开启缓存功能后,程序性能要比没开缓存时
低。如果用聚餐来做比喻,那就是:你需要记住所邀请的那些人,并
且想好当你把剩菜端上来时,他们会做何反应。
程序在各个平台上的运行效率会因各种原因而出现差异,所以最
好是在你要发布程序的那个平台上测一测性能。开发者通常可以借助
jsperf.com等工具来了解受测程序的性能,此外,本书附录B与附录C
也列出了一些建议供参考。如果仅仅想测试几个特定的函数,那么只
需在网上搜索“benchmarking JavaScript”,就能找到一些常用的测
试程序库了。对于游戏来说,性能问题主要体现在“帧速率”(frame
rate) [1]上面,而该指标通常采用“每秒帧数”(frames per
second)来衡量。(一般来说,游戏帧速率应该在每秒60帧或60帧以
上才算好。)
说过刚才这一大段话之后,我们来看看如何将easel.js引擎的缓
存功能运用到Memory游戏上面,此处不考虑缓存是提升了还是降低了
游戏的实际性能。首先,在init()函数的主for循环中把每个方块都加
入缓存。请按程序清单4.30中的粗体代码修改。
程序清单4.30

初始化缓存

接下来需要在handleOnPress()函数中更新缓存,这样的话,当方
块的颜色改变时,对应的缓存也就随之更新了。若是不加下面这几行
代码,那么玩家点击方块之后,它们依然会呈现灰色。sourceoverlay用以指示系统在渲染时如何处理canvas中新缓存的这块图像与
其下方图像的关系,该参数还可以取其他值(这段代码参见程序清单
4.31)。
程序清单4.31

更新缓存

最后还需要修改一个地方,就是在玩家输掉游戏的时候把方块从
缓存中移除,使玩家能够看到尚未配对的那些方块(参见程序清单
4.32)。
程序清单4.32

将方块从缓存中移走

除此之外,还需对原来的代码稍加清理,因为在重新开始游戏
时,要执行一些缓存操作,而uncache函数不能完全正确处理此问题。
无论是否调用uncache方法,cache id都会在下一盘游戏中复用。如果
玩家点击了某个方块,那么系统在更新缓存时,不仅会更新当前这个
方块,而且会更新上一盘游戏中此位置上的那个方块。当前游戏中的
新方块,其id本来应该与上一盘游戏中的那些旧方块互不相同,但是
引擎却将旧方块的id复用了,从而导致这种奇怪的现象,要想解决此
问题,可以修改引擎代码,不过我们这里选用一种更为简单的办法,
就是在玩家点击重玩按钮的时候直接刷新页面,如程序清单4.33所
示。
程序清单4.33

修改重玩按钮,不再调用replay()函数,而是重新加

载页面

修改后的代码利用JavaScript History API来使链接跳转到当前
页面。

[1] 亦称“帧频”。——译者注

4.8 第八步:将方块配对游戏改为字母认读游戏
怎样把Memory游戏做得更有趣一些呢?可以加入更为复杂的规
则,用以限定玩家操作方块的方式,这样就可以制作出类似《宝石迷
阵》那样的游戏了,不过也可以采用另外一种完全不同的思路。
我们未必要把所有方块都做成同一个样子,而是可以把它们两两
编为一组。比方说,可以把画有某种图样的方块与另一个写有对应单
词的方块编为一组。对于正在学习如何读单词的孩子来说,这种配对
方式很有帮助。我们也可以把某个单词与该词的定义、同义词,或是
其他语言中的相应词语配为一对。
本步骤将制作一套“认读卡片系统”(flashcard system) [1],
用以学习日文里最基础的字符,也就是平假名。脚本部分需要改动的
地方很多,我们从头开始,先按程序清单4.34来做。
程序清单4.34

新脚本的开头部分

首先定义了几个新变量,用以确定卡片位置,并且删掉了原来排
列方块时所用的那些变量,比如squareSide等。现在也用不到gray及
max_rgb_color_number变量了,因为我们将采用内联的方式来指定渲
染所用的颜色。此外,数组名称也由squares改为textTiles了。除去
程序清单4.35所定义的那个变量以外,在编写init函数之前所要定义
的全部变量都列在这里了。
为了修改程序中最关键的那部分代码,我们需要引入名为
flashcards的数组,该数组保存了所有日文平假名及其语音(用英文
字母表示,亦称“罗马字” [2])。这份对照表可以在
http://unicode.org/charts/PDF/U3040.pdf找到。与此类似的其他各
种对照表也可以在http://unicode.org/charts/找到。把程序清单

4.35中的flashcards数组放在程序清单4.34的代码后面,并且放在
init函数的前面。
程序清单4.35

flashcards数组

init()函数要比原来复杂,因为这次要绘制的是一对一对的卡
片,而不是外观完全相同的方块(代码参见程序清单4.36)。

程序清单4.36

init()函数的第一部分代码

之所以要如此修改代码,主要是为了调整卡片间隔,并确保原来
的游戏界面依然能适用于现在要绘制的这些卡片。由于日文认读比颜

色配对要难,所以我们把游戏时间定为500秒。与刚才一样,代码中的
squares也要改成textTiles。为了使游戏内容不超出画面所限,还得
把文本调得小一些才行。这段代码中,最显著的一处修改就是移除了
rows变量,这么做是必要的,因为游戏所用的卡片数量在游戏开始前
就已经知道了,而且在游戏中固定不变。现在只需确定屏幕上显示多
少列卡片就行了,不用再指明总行数。
接下来调整init()函数内的for循环。其中有好几个地方都要修改
(参见程序清单4.37)。
程序清单4.37

主for循环

此段代码改动很多。原来的代码是每两轮循环采用一种颜色来绘
制一对方块,而现在则不同,现在是把pairIndex变量(此变量表示卡
片中的日文平假名或其发音是放在哪一对数据里面的 [3])传给
drawTextTile()函数。稍后再来讲这个函数是如何使用pairIndex变量
的,现在先看看for循环遍历flashcards数组的方式:我们把循环计数
器与2相除,取商的整数部分,将其赋给pairIndex变量,并以此作为
第一维的下标,来获取flashcards这个二维数组中的相关一维数组;
针对这个一维数组,我们又通过%运算符求出循环计数器与2相除的余

数(余数不是0,就是1),以此作为下标来获取其中的元素。然后,
计算出绘制textTile所用的坐标,并将其加入stage对象。这段代码还
新建了名为background的shape对象,用来充当每个textTile的背景
色。我们通过easel.js引擎所提供的addChildAt()函数来添加
background对象,以便使文本出现在背景色前方 [4]。接下来,把
handleOnPress赋给onPress,以其作为事件处理程序,最后,调用
stage的update方法。
drawTextTile函数比原来的drawSquare函数稍微简单一些,不过
二者目标相同,都是为了绘制卡片或方块(参见程序清单4.38)。请
把原来的drawSquare函数删掉,并将drawTextTile函数的实现代码加
到程序中。
程序清单4.38

drawTextTile函数

这个函数大家应该比较熟悉,因为我们曾用类似代码来绘制文
本,以表示玩家找到了多少对同色方块,以及当前这盘游戏还剩下多
长时间。不过此函数与那些代码的区别在于,它为textTile对象新增
了一项名叫pairIndex的属性。原来是用color属性来检测两个方块是
否能够配成一对,而现在也差不多,只是改用pairIndex属性了。
程序清单4.39列出了setPlacementArray()与
getRandomPlacement()函数,这两个函数与原来完全一致。
程序清单4.39

setPlacementArray函数

handleOnPress()函数与没有开始隐藏色块时所执行的逻辑相似。
但为了使教程连贯,我们还是来看看程序清单4.40这段代码,其中与
上一步有区别的地方已经用粗体标出了。
程序清单4.40

handleOnPress函数

当玩家点击某张卡片之后,这段程序会填充其background(代码
中仍将其称为tile),用以表示开始配对,若玩家找到了一对卡片,
则将两者的background都移除。修改后的代码根据pairIndex属性,而
非color属性来判断卡片是否配对。此外,由于屏幕里的剩余空间比较
少,所以matchesFoundText也做得比原来短了一些。

gameOver()函数的for循环也简化了,现在不使用缓存了。完整的
函数代码列在程序清单4.41里。
程序清单4.41

gameOver函数

最后是tick()和replay()函数,二者的代码都与使用缓存前的那
一步相同(参见程序清单4.42)。
程序清单4.42

tick与replay函数

总算完成了!用浏览器打开index.html,如果一切正常,那么就
能看到如图4.7所示的游戏画面了。

图4.7 玩家选对了几张卡片之后的认读卡片系统

(文字翻译:【Recipe8:Making a Flashcard System】第八步〖制作认
读卡片系统〗)

[1] flashcard亦称“抽认卡”。——译者注
[2] romaji
[3] 每个平假名的发音及其字符构成一对数据,也就是一个小数组,而
若干个这样的小数组构成了flashcards这个大数组。pairIndex 就是此数组
第一维的下标。——译者注
[4] 如果调用普通的addChild()函数,那么背景色就会盖住文本。——译
者注

4.9 小结
本章所讲的这个游戏比较简单,然而在制作过程中,我们却学到
了很多东西。包括如何使用easel.js引擎、如何在HTML源代码中编写
“侵入式”事件处理程序、如何为JavaScript对象动态地添加属性、
如何提高游戏性能、如何使用缓存等。在学习这些内容的同时,我们
还实现了经典的卡片游戏《Memory》,并且制作了一款“认读卡片式
日文学习机”。
这个游戏如果想继续往下做,有很多种思路。可以在玩家胜利或
失败时,调整下一盘游戏的时间限制,这样玩家就会感到每盘游戏都
很值得挑战了。也可以设定高分榜,还可以记录下玩家最不擅长辨认
的颜色或日文字母。专门针对认读卡片系统来说,也会有很多种进化
方案:可以增加或减少卡片数量,在游戏开始时把卡片隐藏起来,在
玩家找到正确答案时给出提示,或是把其他内容放在这套卡片系统里
面,令玩家为其配对,比如国家和其首都、朋友与家人的名字及其生
日等,当然啦,如果你对日文颇感兴趣 [1],也可以向其中再添加一
些日语字母。
对于easel.js引擎来说,我们在本章中已经学到它的主要功能
了。如果还想继续研究,那么请访问网址
http://createjs.com/Docs/EaselJS/,其中的文档也许你会感兴趣。
引擎中还有一些高级话题本章没有讲到,你可以看看
Bitmap/BitmapAnimation类,并了解一下SpriteSheet的用法。
由于本章制作的游戏较为简单,所以采用easel.js这种底层游戏
引擎非常合适,开发者在绘制过程中可以控制更多的细节。若是喜欢
来点挑战,可以试着直接用canvas API来重制Memory游戏或平假名学
习机。第3章的游戏就是直接用canvas API来做的,可以参考一下。如
果真的这么做了,那你就会发现,即便是easel.js这种底层游戏引
擎,也能为开发者提供一些相当有用的工具及抽象机制。
[1] 原文为“ if that's your cup ofお茶”,作者把英文“ if that's your cup
of tea”中的“ tea”写为日文“お茶”。——译者注

第5章

平台游戏
平台游戏的代表作有《超级马里奥兄弟》(Super Mario Brothers)
和《刺猬索尼克》(Sonic the Hedgehog)。
谈到游戏,大家都会想起《超级马里奥兄弟》,这是一款经典的
“平台游戏” [1],它于1985年在NES游戏机上首次发行。以此为代表
的经典平台游戏流行了数十年,依然不褪色,而且时至今日,大型游
戏制作公司和独立游戏开发者仍在开发新产品。这种游戏之所以称为
平台游戏,其原因大家都知道,那就是:玩家经常要在各个平台
(platform)之间跳来跳去。平台游戏不仅可以在游戏机上玩,而且也
可以做成网页游戏,如果要做成网页游戏的话,那么用HTML5来开发
是最合适的。
[1] platformer,中国玩家通常把这类游戏称为“横版过关游戏”。——
译者注

5.1 初识melon.js
本章游戏将使用melonJS来做。该引擎提供了一套简单的API,用
它来制作游戏会非常直观,而且代码写起来也很容易。其中还自带了
一些函数,用于处理游戏角色在“侧视角型游戏环境”(side view
type environment)下的跳跃及移动。此引擎不仅大幅简化了编程工
作,而且还提供了许多功能,用以在游戏中实现更加复杂的行为。
对于游戏制作新手来说,melonJS的一项优势就在于,它可以同
Tiled这款瓦片地图编辑器相结合。瓦片地图编辑器非常有用,这种软
件不仅可以生成每一关的地图,而且还能使开发者预先看到将在游戏
里出现的图层与物件。本书其他游戏所用的地图,都是从数组这种简
单的数据结构中读取的。而本章游戏所用的地图,则是由Tiled编辑器
生成的.tmx格式文件(此格式是一种XML文档)。
打开本书范例代码的platformers/initial目录,会发现其中并没
有tmx文件。可以从后续步骤所对应的文件夹里拷贝一份过来,也可以
去mapeditor.org网站下载Tiled编辑器,然后按照下一节所讲的方式
自己做一个出来。

5.2 第一步:创建瓦片地图
打开Tiled编辑器,选择File菜单下的New菜单项。在对话框中可
以选择地图方向、地图大小和瓦片大小。请按照图5.1所示,将各选项
的值设定如下:Orientation:Orthogonal,Width:40 tiles,
Height:30 tiles,Width:16 px,Height:16 px。
在编辑器界面右方,有个名叫“Tile Layer 1”的瓦片图层。请
将其改名为foreground(前景),这样就能更好地表达出该图层的用
途了。
接下来该引入瓦片集(tileset)了,在其他场合及本书其他章节
中,我们也将其称为精灵表(spritesheet)。本游戏所用的全部精灵
都已经做好了,位于platformers/initial目录下。对游戏开发新手来
说,用现成的图片就行了,不过如果愿意,你也可以自己画一套(请
参阅附录C)。不论是采用现成的图片,还是自己来画,都必须注意:
精灵尺寸是16像素宽、16像素高,而且精灵与图片边界之间不要留
白。
现在开始引入。点击Tiled编辑器的Map菜单,选择New Tileset菜
单项,然后就会弹出新建瓦片集所用的对话框了。按照图5.2所示填好
其中各选项,并点击“OK”按钮。请注意,如果要使用的
levelSprites图片不在platformers/after_recipe1文件夹里,那么请
在对话框中指定该图所在的路径。

图5.1 用Tiled 编辑器新建地图

图5.2 用Tiled 编辑器新建瓦片地图

图5.3 用Tiled 新建关卡地图

现在开始编辑地图。这个过程很有趣。从Tiled界面的右侧选择精
灵,并将其放在界面中间的灰色区域里。大地、水流、岩浆、天空、
道具箱等,都可以往地图里面摆。最终拼好的地图应该是图5.3这个样
子。你若不想故意刁难玩家的话,可以把道具箱摆到其他地方,而不
要放在岩浆上方。
接下来,需要把地图保存成melonJS引擎所支持的格式。点击
Tiled的Edit菜单,选择Preferences菜单项,在Store Tile Layer
Data As这几个字旁边有个下拉列表框,其中有五个选项。melonJS可
以使用XML、Base64(uncompressed)(未压缩的Base64编码)、CSV这
三种格式。此引擎不支持压缩后的Base64格式。在其支持的格式中,
Base64(uncompressed)所占空间最小,因此最好是用这种格式来保
存。

把地图保存为CSV格式也很有用,因为在这种格式的文件中,开发
者更容易看出每个精灵所处的位置(此格式的地图文件可以直接用文
本编辑器打开)。Tiled编辑器除了可以把地图文件保存起来之外,还
可以将其导出(使用File菜单下的Export As菜单项)。一般情况下,
把地图文件存储为tmx格式就行了,不过有时候为了和别的游戏制作软
件或游戏引擎相配合,我们会把地图导出为其他格式(比如json)。
Tiled不仅有保存和导出这两项功能,而且还能打开多种格式的地图文
件。
请把地图文件保存为level1.tmx,并与index.html放在同一个目
录里。

5.3 第二步:启动游戏
创建好地图之后,就该在浏览器里运行游戏了。本步骤需要用到
早前创建好的那个.tmx文件,而且还需要使用melonJS引擎的源文件。
首先,按程序清单5.1所示,写好index.html文件的大体框架。
程序清单5.1

在HTML文档中载入JavaScript文件

由于笔者给游戏起名为“Guy's Adventure”(Guy的冒险之旅,
实际上这个名字是我侄女起的),所以我们把网页标题也设置成这个
名字。接下来,加入一些样式代码,稍稍美化一下游戏屏幕。然后就
是网页文件的重点部分了:我们创建id属性为jsapp的div元素,并把

melonJS库、resources.js文件、main.js文件和screens.js文件都引
入网页。这个div稍后就会讲到。
这些JavaScript文件其实可以合并成一份文件,本书其余章节的
游戏都是这么做的。不过,了解一下其他做法也无妨。在制作第4章的
游戏时,本来可以把JavaScript代码分置于js文件里,但最后却把它
们合起来放在html文件中了;而此处则相反,我们把本来可以合并的
文件都分隔开了。那么,这些文件是做什么用的呢?
melon.js文件就是游戏引擎的源代码。本章将通过其中几个很好
用的API来制作游戏,在构建过程中如果需查阅参考资料,可以去
http://www.melonjs.org/docs/index.html花些时间看看该引擎的开
发文档。本书所用到的全部引擎,其项目网址都列在附录C中。若只想
看看引擎的代码究竟是什么样子,那应该不会想着去修改它,然而
melon.js与本书用到的其他引擎一样,也是开源的。这就是说,如果
发现引擎少了某项功能,或是还有某个bug尚待修复,那么你可以去实
现或修复相关代码,使引擎变得更好,这样既能帮助自己,也能惠及
他人。
我们需要用resources.js文件来保存游戏里的全部图像、声音及
关卡文件(由Tiled编辑器所生成)。目前这份文件写起来很简单。程
序清单5.2描述了游戏精灵、游戏关卡,及各自所用到的资源。新建名
为resources.js的文件,并把这段代码置于其中。
程序清单5.2

新建resources.js文件

警告:注意逗号的用法
声明JavaScript的数组及对象时,要注意逗号的用法。在某些浏览器
中,如果最后一个数组元素后面还有逗号,那么可能会出bug。另外,
无论采用哪种浏览器,数组元素之间的逗号最好不要省略掉。
screens.js文件也很简单。可以把“屏幕”(screen)理解为游
戏在某个大的状态下(比如Play状态、Menu状态和GameOver状态)所
呈现的样貌 [1]。目前只需新建PlayScreen对象就够了,我们令其继承
自me.ScreenObject,并编写代码,使游戏在切换到该状态时,会载入
第一关地图(level1)。把程序清单5.3中的代码存为screens.js文
件。
程序清单5.3

在screens.js文件中新建PlayScreen对象

你可能会问,代码中的me是什么意思呢?me表示Melon Engine,
它是个名称空间,melonJS引擎中的所有对象都位于me里面。虽然很少
有其他人会写出名为levelDirector的对象,但是一般来说,采用名称
空间可以确保一个名字只对应于一个对象。之所以要用var关键字来声
明变量,这也是原因之一。在JavaScript中,变量声明时如果不加var
关键字,那么就会放在全局名称空间里,也就是说,所有代码都能随
意访问它。
现在回到游戏代码上面来。请把程序清单5.4中的代码放到
main.js文件中,这段代码用于实现游戏的高层逻辑,而且稍微有点复
杂。首先,创建了名为jsApp的变量。然后采用“对象式写法”
(object pattern)来创建两个函数。浏览器加载完window对象后,
会执行jsApp中的onload函数。而该函数则会把id属性为jsapp的那个
div元素声明成canvas对象,供游戏绘制图形所用。me.video.init函
数除了'jsapp'之外,还有四个参数,分别表示宽度、高度、双缓冲
(double buffering)和缩放倍数。由于我们的精灵尺寸是16×16,
所以要把游戏的缩放倍数设为2.0(也就是将游戏画面由melonJS的默
认尺寸放大为其2倍)。使用了缩放功能之后,双缓冲选项也要设为
true才行。
接下来,把loaded函数赋给me.loader.onload属性,这样的话,
等到onload函数执行完,引擎就会回调loaded函数了。写上
bind(this)是为了保证引擎在回调的时候,函数里的this能够指向
jsApp对象。preload函数用于从资源文件中预先加载图像及关卡地
图。
loaded回调函数会调用state.set函数,把screens.js里创建的
PlayScreen对象同引擎内置的PLAY状态关联起来,然后,再调用
state.change函数把游戏状态变为PLAY。最后,调用

window.onReady()函数,以便使浏览器在加载好window对象之后,会
执行jsApp.onload()中的代码。
程序清单5.4

初始化游戏程序并载入相关资源

用浏览器打开index.html文件,就可以看到如图5.4所示的画面了
[2]。其中只显示了一部分地图。这尚且不能算作游戏。那么还需要些

什么呢?我们下一步再讲。

图5.4 浏览器加载完关卡地图之后的画面

[1] 例如Play状态下所对应的screen就是PlayScreen。——译者注
[2] 本书某些范例游戏可能无法用Chrome 浏览器运行。如遇到此问题,
请关掉系统中的所有Chrome进程,然后以--allow-file-access-from-files选
项启动Chrome 浏览器,再运行游戏即可。——译者注

5.4 第三步:加入游戏角色
现在为游戏添加角色。我们给他起名叫做Guy,他就是要去冒险的
那个人。首先,打开Tiled编辑器,设定角色起始位置。想设定起始位
置,必须先添加对象图层。点击Layer菜单下的Add Object Layer菜单
项。然后通过界面右侧的Layers面板(若是找不到该面板,那就点击
View菜单下的Layers菜单项,令其重新显示出来)将这个对象图层改
名为player。与早前设计关卡地图时一样,也要点击Map菜单下的New
Tileset菜单项来新建瓦片集,请将player.png图片引入,并把瓦片集
的名称设为player。

图5.5 插入对象所用的按钮

图5.6 设置player对象的属性

然后可以把Guy放在离地面不远的某个安全位置上。要放置游戏角
色,可以点击insert object按钮(如图5.5所示),也可以按下键盘
上的O键,然后点击地图上的某个位置 [1]。请注意,与前景层上面的
精灵不同,我们并不想让用户看到这个对象。所以,请用鼠标右键点
击代表对象的那个灰色方框,于弹出式菜单中选择Object
Properties...菜单项,在对话框的Name:文本框里填写player。此
外,还需在对话框下方设置两个新属性。这两个属性分别叫做image与
spritewidth,前者的值是player,后者的值是16(如图5.6所示)。
如果你现在就急着用浏览器打开游戏的话,那么什么角色都看不
到。因为我们还需要再编一些代码才能把Guy集成到引擎里面。首先,
按程序清单5.5所示,把Guy的图像信息添加到resources.js文件的数
组里面。写程序的时候注意逗号的用法。

程序清单5.5

将player对象的相关信息加入resources.js

接下来,按照程序清单5.6所列的代码修改main.js文件的loaded
函数,把Guy加到melonJS引擎的实体池里。
程序清单5.6

修改main.js文件,把player对象加到实体池里

现在还需要创建本章范例游戏所用的最后一个程序文件,那就是
entities.js。在开发游戏时,经常需要把某些重要的对象视为实体。
比如,开发者可能会把敌人、玩家、抛射体(projectile)等做成实
体。与传统的面向对象编程不同,基于实体的系统(entity-based
system)所使用的对象体系一般都不那么严格。这种开发范式需要一
定的时间才能适应,我们等到第10章再来深入讨论它。刚开始接触
时,只需要知道实体是由若干个逻辑单元组合而成的就行了,这些单
元描述了各项属性,比如移动能力,比如相互碰撞时的行为等。此外
还要注意,别把实体仅仅理解成代码中的对象,而应该同时将其看作
游戏中的物件才对。当有人说player实体时,既要想到程序中表示该
对象的那几行代码,同时还要想到游戏环境中表示玩家角色的那个
“东西”(thing)。
请按照程序清单5.7所示,把引入entities.js所用的代码加入
index.html文件中,这行代码应该和加载其他JavaScript文件所用的
那些代码放在一起。

程序清单5.7

在index.html文件里加载entities.js

程序清单5.8中的代码相当直观。我们把entities.js文件创建好
之后,首先定义并初始化PlayerEntity变量,令其继承自
ObjectEntity,然后在初始化函数里设定引擎的viewport,令其跟随
玩家。接下来,设定update函数,以便在玩家移动游戏主角的时候更
新其动画。
程序清单5.8

将PlayerEntity对象加入entities.js文件

用浏览器运行一下游戏,就会发现主角已经能显示出来了。这当
然很令我们开心,不过对游戏里的英雄来说可就有点惨了。Guy虽然能
在Tiled所定义的初始位置上冒出来,但是出来之后立即就会从屏幕中

垂直跌下。这是怎么回事呀?这是因为还没创建可供他立足的坚实地
面呢。
[1] 由于Tiled 编辑器的版本和用户所使用的操作系统不甚相同,所以插
入对象所使用的界面及所需的步骤也会略有不同。请读者按照实际情
况操作。在0.9.0 版本的编辑器中,点击工具条上的Insert Rectangle按
钮,然后在地图中的某个位置上点击鼠标左键,即可于此插入对象。
——译者注

5.5 第四步:构建碰撞图层
现在新加一个瓦片层。与早先一样(参见图5.2),点击Layer菜
单下的Add Tile Layer菜单项。将这一层命名为collision。有些瓦片
层的名字可以随便取,但若是碰撞层(collision layer),则名称中
必须包含collision,这样才能为melonJS所识别。接下来点击Map菜单
下的New Tileset菜单项,引入collision.png文件,与原来一样,引
入的时候也要把margin设为0,并且要把瓦片尺寸设为16×16。然后,
在Tiled编辑器界面右侧的collision瓦片集里找到第一块瓦片,用鼠
标右键点击它,并在弹出式菜单中选择Tile Properties...菜单项。
(如果没看到瓦片集面板,请点击View菜单下的Tilesets菜单项,令
其显示出来。如果当前面板中显示的不是碰撞层,那就需要点击
collision标签,切换到碰撞层。)新增名为type的属性,并将其值设
为solid。
在碰撞层中,用名为solid的瓦片来绘制地面。效果参见图5.7,
其中的黑色瓦片就是刚刚绘制在碰撞层中的solid瓦片。由于现在的碰
撞层在其他图层最上方,所以我们会看到碰撞层中的黑色瓦片盖住了
前景层中的地面瓦片,可以在图层面板里重新排列各图层的前后顺
序,或隐藏掉某些图层,也可调整图层透明度,通过这些方式,就能
看到关卡地图的各种显示效果了。

图5.7 覆盖在前景层之上的碰撞层

保存index.html文件,再次用浏览器打开它,这回Guy总算能站在
地面上不掉下去了。能做到这一步真了不起。或许将来会流行这种
“只站在那儿什么也不做”的游戏吧,不过现在还看不到此趋势。所
以接下来我们依然遵循平台游戏的传统做法,使Guy具备一些冒险能
力。

5.6 第五步:行走与跳跃
要想使主角能够行走与跳跃,得修改两个地方才行。首先,把跳
跃、向左移动、向右移动这三个动作与相应的键盘按键绑定起来。请
按程序清单5.9修改screens.js文件,以实现按键绑定。
程序清单5.9

绑定移动主角所用的按键

然后打开entities.js文件,修改PlayerEntity的init与update函
数。在init函数中,我们要设定默认的行走与跳跃速度,而在update
函数里,则需要根据玩家的输入来更新主角的移动状态,同时还需检
测碰撞。请按程序清单5.10修改代码以实现这些功能。
程序清单5.10

处理主角的移动操作

通过melonJS引擎所提供的doJump()与doWalk()函数,我们很容易
就能实现行走及跳跃功能,这对游戏开发新手来说挺好。不过请记
住:若想令游戏更为出色,那还是得亲自来调整主角的移动操作,把
从静止到走动的这段加速过程体现出来才行,这样做虽然代码写起来
更困难,但效果却非常好。比如《刺猬索尼克》 [1](Sonic the
Hedgehog)就是个典型的例子,这款游戏之所以受欢迎,很大程度上
是因为游戏主角从慢速移动到高速腾跃的这个加速过程做得特别流
畅。实际上,还有人特意创建了一款HTML5游戏引擎,用于研究索尼克
在三维空间中的移动效果。
用浏览器打开index.html文件,你会发现已经能用右箭头和空格
键来控制Guy了。离游戏目标又近了一步。在移动Guy时,可以注意他
在行走过程中的抬腿动作。这部分功能是melonJS引擎帮你实现的。开

发者只需把player.png这张含有两个精灵图样的精灵表加载进来就好
了,剩下的事情会由引擎自动实现,真棒!
接下来该怎么做呢?尽管玩家会很小心地操控游戏,但Guy还是偶
尔会掉到洞里。然后玩家必须刷新当前页面才能重新开始玩。这样不
太好。下一步我们要实现游戏重置功能,当Guy走错路时,游戏会自动
重新开始。
[1] 也称“音速小子”。——译者注

5.7 第六步:标题画面
首先,需要创建TitleScreen对象,因为当玩家刚开始启动游戏或
Guy跌到洞里时,需要通过此对象来显示标题画面。把程序清单5.11中
的代码加入screens.js文件末端。
程序清单5.11

创建TitleScreen对象

我们来看看程序清单5.11中的代码都做了些什么。首先,创建
TitleScreen变量,令其继承自me.ScreenObject。然后,在init函数
里调用this.parent(true),以便将TitleScreen设为可见,并确保
update与draw函数能正常运作。接下来,把空格键与jump绑定到一
起。
在onResetEvent函数中,判断原来是否已经载入过标题图像,如
果没有,那就把titleScreen图片加载进来。update函数用于等待玩家
按键下空格键,并在按下空格键之后切换到主游戏循环。
draw函数会在指定的坐标位置上绘制图像,第一个参数表示要绘
制的图像,第二个及第三个参数表示绘图位置。你若是没用过其他章
节所讲的那种基于canvas的引擎,那么可能不太明白draw函数里
context参数是什么意思。这是个Canvas Rendering Context [1]。此
context对象已由melonJS引擎创建好了。在本例中,它表示二维桌布
的画笔,获取此对象所用的API是canvas.getContext('2d'),调用
getContext时也可以把参数值写成webgl,那样获取到的将是三维桌布
的画笔。
还需稍微清理一下代码: