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  实际上是指其class 属性中包含item-container属性值的那个<div>元素,作者将其简称为itemcontainer。——译者注 ,然后将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设计模式  Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides所著的《Design Patterns:Elements of Reusable Object-Oriented Software》(《设计模式:可复用面向对象软件的基础》)一书中描述了23 种经典的设计模式。业界将这四位作者称为“四人组”(Gang of Four,简称GoF),将这些经典的设计模式称为GoF 模式。——译者注 运用到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 模式。——译者注