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语言写成的。有许多工具都能设法解决此问题(比如采用“源码映射器”  该技术的详细内容请参见http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/。——译者注 (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  HTML 5风格的DOCTYPE与HTML 4.01风格的区别,请参阅:http://www.w3schools.com/tags/tag_doctype.asp。——译者注 。然后在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代码中,这两者也是在最顶层的作用域中定义的。由于其外围没有函数,所以它们处在全局作用域中。这样做似乎有些奇怪,按理说,变量应该在设置其值的时候再去定义,或是在即将为其赋值之前再定义才对,而这段代码为何一开始就要急着定义变量呢?原因在于,许多传统的编程语言中,代码块(也就是位于{}中的代码)本身也能起到区隔作用域的效果  也就是说,凡是定义在代码块内的变量,在代码块之外都不可见。——译者注 ,而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__属性,以及与该属性有关的其他信息都会因浏览器而异。如果你对这一点感到困惑,那么可以试着在对象上调用一些方法  可以在对象上调用的方法请参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object。——译者注 。虽然各浏览器存放对象时所用的内部结构以及描述这些结构的方式不尽相同,但一般情况下,它们都能正常执行这些方法。

[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/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object。——译者注