GameQuery入门教程


GameQuery⼊门教程 翻译:bjc5233 (Email:bjc5233@gmail.com) ⽬录 第1章 简介.… 1.1 开篇语.… 1.1.1 条件.… 1.1.2 警告.… 1.2 ⼯具.… 1.2.1 图⽚处理.… 1.2.2 开发环境.… 1.3 游戏描述.… 1.4 最终游戏展⽰.… 第2章 步骤1 – 精灵动画.… 2.1 游戏屏幕组织结构.… 2.1.1 对jQuery的超短介绍.… 2.1.2 开始快乐的编程吧!.… 2.2 游戏背景.… 2.3 游戏玩家.… 2.4 敌⼈和导弹.… 第3章 步骤2 – 对象模型.… 3.1 Javascript中的对象.… 3.2 Javascript中的继承.… 3.3 玩家对象.… 3.4 敌⼈对象.… 3.4.1 导弹.… 3.4.2 有头脑的敌⼈.… 3.4.3 boss型敌⼈.… 第4章 步骤3 – 游戏逻辑与控制.… 4.1 游戏状态.… 4.2 敌⼈的产⽣.… 4.3 敌⼈的⾏为.… 4.4 导弹.… 4.5 玩家操作.… 第5章 步骤4 – 杂七杂⼋.… – 36 - 5.1 游戏信息.… – 36 - 5.2 HTML⽂件.… – 37 - 5.3 开始按钮.… – 39 - 5.4 进度条.… – 39 - 5.5 结束语.… – 40 - 第1章 简介 1.1 开篇语 ⾃从这篇教程写了之后,API就发⽣过变化。但教程还没有更新过,所以请先看看变更向导吧 在这篇教程中,我会尽⼒指导你如何从头到尾做⼀个javascript的游戏。我会⼀⼀个⾮常基础的横轴滚动 射击游戏作为例⼦。为了将精⼒放在游戏开发的基本流程任务上,我会使这个游戏在拥有完整游戏开发 流程的基础上,尽量精简。所以导致的结果是,你会发现下⾯这个游戏其实并不怎么好玩,但是我们的 ⽬标是学习怎么使⽤Query和gameQuery来制作⼀个javascript游戏,⽽不是别的! ⽬前的版本(0.2.6, 翻译时最新为0.6.1)gameQuery⾮常适合⽤来做简单的基于精灵的2D游戏,也就 是我们现在将要做的游戏的类型。你可以⽤各种⽅式制作精灵,这完全取决于你。你可以,⼿画精灵, 扫描,然后⽤你最喜欢的图⽚编辑器编辑它们。当然,你也可以直接在电脑上画,就像我为了第⼀个演 ⽰程序⽽做的⼀样。 1.1.1 条件 我会尽⼒使这篇教程通俗易懂的,即使那些⼈没有编程基础、⽹页设计经验。当介绍这些内容的时候, 我会指出相关知识,如果你觉得有必要的话,你应该找些资料,更深⼊的去了解。或许这对那些有经验 的编程⼈员来说很⽆聊,那么你应该去看看程序源代码,它们都是⾃解释的。 代码⽚段都提供了到gameQueryAPI⽂档的链接。当你点击其中的关键字,⼀个⼩窗⼜会弹出并显⽰相 应的⽂档说明,再次点击这个弹出窗⼜,它就会关闭。这种⽅式会使我们明⽩哪些代码属于 gameQuery,⽽哪些不是。 1.1.2 警告 由于英语并不是我的母语,所以请容忍我的那些拼写错误,也请你能给我发邮件指出那些错误。我会尽 ⼒使得教程对于⼀个初学者来说易于理解的,⽽在这种情况下,我可能会有些你认为是不正确的话,也 希望你能在评论中说明,让其他读者了解。 1.2 ⼯具 下⾯是⼀系列你能马上⼊⼿的⼯具。它们可能是开源免费软件,也可能是些功能强⼤的商业软件。 1.2.1 图⽚处理 The Gimp – ⼀个强⼤的图形编辑器。虽然我并不是多窗⼜界⾯的爱好者,但我觉得在这软件⾥, 真的⾮常必要。即使你为你的精灵使⽤了3D建模,你也可以⽤Gimp来给精灵做抛光的渲染处理。 Blender – ⼀个令⼈惊讶的3D建模渲染软件,你甚⾄可以⽤它来做3D游戏。 Inkscape – 如果你需要制作⼀些⽮量图形的话,这就是你要找的软件。(我并没有⽤这个来做教 程中的游戏,但是我⽤它来做教程中的插图) 1.2.2 开发环境 Firefox – 很显然,⼤家都⽤过web浏览器吧!我使⽤Firefox,因为它有Firebug这个强⼤的⼯具。 Safari,,Chrome 和Opera都有类似的⼯具,但我还没有尝试过。如果你使⽤IE却不知道怎么调试 代码的话,看看这⾥。 Firebug – 这⼯具真的能改变你的⽣活!DOM浏览器,CSS,断点,⽹络分析,你还能要求更多 吗。 好的编辑器 – 这真是太多啦,你⾃⼰选⼀个吧。但是⽐起普通编辑器,就像微软的notepad,选择 个专业的编辑器会让你受益很多! 1.3 游戏描述 玩家操作⼀艘飞船往右边飞,玩家必须躲避或者消灭从右边出现朝飞船飞来的敌⼈。敌⼈分为三种,每 ⼀种都有不同的⾏动⽅式和武器。⼀旦玩家的飞船碰到敌⼈,或者敌⼈的导弹,飞船将减少⼀层护盾。 如果飞船在没有任何护盾的情况下再次碰撞,飞船会减少⼀条⽣命。如果玩家在飞船爆炸之后还 有“命”的话,飞船会再次出现,否则游戏结束。在飞船复活之后,它会有3秒钟的⽆敌时间。 第⼀种敌⼈会很频繁的出现,我们称之为“炮灰”。它们⾛直线。它们并不会发射导弹。 第⼆种敌⼈稍微有些难缠,它们⾏⾛路线有些不可预测,我们称之为“有头脑的”。它们以随机的速率发 射导弹。 最后⼀种敌⼈是某种“最终boss”。它们更⼤更强,但是⽐起其他两种敌⼈出现概率很少。它们并不怎么 移动,但发射导弹。但出现⼀个这样的敌⼈,就不会出现新的敌⼈直到你摧毁它为⽌。 这个游戏没有终点,没有分数,完全是随机的,记住,这仅仅是个向导!为游戏增加功能特⾊⾮常容 易,这对你来说也是个⾮常好的经历。 1.4 最终游戏展⽰ 这就是我们在教程结束得到的东西。使⽤a,d,w,s来移动飞船,k射击。 第2章 步骤1 – 精灵动画 2.1 游戏屏幕组织结构 在教程的步骤1当中,我们将会学习到游戏屏幕展⽰到底是由什么组成的。如果你了解Gimp或者 Photoshop的话,你可能会联想到游戏屏幕是由⼀系列的层叠加⽽成,就像图1. 为了在gameQuery中⽣成“屏幕”,你只需要操作两类对象:精灵和组(sprites、groups),为了渲染精 灵gameQuery使⽤了绝对定位元素。每⼀个精灵都有它⾃⼰的层。你添加精灵的顺序就是默认顺序,类 似“堆栈”结构。 在游戏的某些点上,如果想在⼀个已经存在的精灵之后再增加新的精灵的话,该怎么做呢?最简单的⽅ 法是使⽤组,就像图1展⽰的那样。如果你在⼀个已经定义过的组(例如 groupA)中增加新的精灵的 话,这个精灵就会位于groupA前元素之后(精灵3 精灵4)。 使⽤组还有其他好处:如果你移动组,那么其中所有的元素也会移动,如果你删除组,那么其中所有的 元素也会被删除。它们也可以作为某种遮罩(mask)来使⽤:如果你在组选项中指定overflow为hidden 的话,超过组边界的精灵就是不可见的。你可以在组中嵌套组。在某些情况下,正确使⽤组,通过减少 ⼿动指定移动、检测碰撞的元素数量,会有更好的性能。 你也许想知道图1中那些名字前⾯的“#”代表什么意思? 它们存在是因为组合精灵都是基本的html元素。 Html元素组成了⽹页,⽽为了更好的标识元素,我们使⽤两种标识符:给你的元素起个唯⼀的名字,这 就是“id”;把它归属为⼀类⽽起的名字,称为“class”。元素可以同时具有“class”名和“id”名。这种名称标 识常常被⽤来书写层叠样式表CSS。在CSS中,你描述⽹页中元素的展现⽅式。在CSS中,“class”类名前 加“.”号,⽽“id”名前加“#”号。所以在图1中 #groupA的意思是id名是groupA的html元素。这种标识⽅式 称为“CSS Selector”(CSS选择器)。我希望你能学习更多关于HTML和CSS的知识,但是要做好被混淆 的⼼理准备哦:) 现在让你看看接下来我们要⾯对的东西。图1的情形结果,如果⽤代码来描述的话,会是下⾯这个样⼦。 $(“#playground”).playground({height: PLAYGROUND_HEIGHT, width: PLAYGROUND_WIDTH}) .addSprite("sprite1",{animation: animation1}) .addGroup("groupA") .addSprite("sprite2",{animation: animation2}).end() .addSprite("sprite3",{animation: animation3}) .addGroup("groupB",{overflow: hidden}) .addSprite("sprite4",{animation: animation4}); 2.1.1 对jQuery的超短介绍 gameQuery是基于jQuery的,所以我会简单的介绍⼀下jQuery的⼯作⽅式。虽然介绍异常简单,但你可 以从这⾥开始理解jQuery。更多关于jQuery的信息,请看jQuery⽂档。 jQuery可以分为两部分内容:“选择”、“修改”。“选择”是通过CSS选择器(CSS selector)在页⾯中寻找 对应的元素。“修改”是指改变、增加、删除指定元素。你写代码的时候,其实就是这两种命令的联合。 例如,如果你想要删除页⾯中所有类(class)名叫“foo”的元素,你可以这么写: 在这⾥,$(“.foo”)的意思是选择:选择所有类名叫foo的元素;.remove()的意思是修改:删除它 们。当情况需要的时候,我会再深⼊讲解的。 2.1.2 开始快乐的编程吧! ⾸先为你的游戏画个草图,描述精灵和组的那种关系。在这篇教程中,草图将会是图2这个样⼦。就像 你所看到的那样,这个游戏是如此的简单以⾄于我们都不需要真正复杂的结构来描述。 让我们开始写游戏屏幕的代码吧:⾸先我们需要告诉gameQuery游戏该画在哪⾥。这可以通 过.playground()⽅法。gameQuery的每个⽅法都是基于jQuery的。这也是为什么下⾯代码是以$() 开头的原因。 playground()⽅法需要两个参数,第⼀个是容纳游戏的html元素的CSS选择器(CSS selector)。第⼆个 是⼀系列配置游戏屏幕(gamescreen)的值,这⾥我们制定了playgound的height和width。⼀个⾮常好的 做法是,⽤变量来代替你的常量,因为以这种⽅式,你可以在将来很容易的改变它们的值。这也使得代 码更具可读性。 var PLAYGROUND_HEIGHT = 250; var PLAYGROUND_WIDTH = 700; $("#playground").playground({height: PLAYGROUND_HEIGHT, width: PLAYGROUND_WIDTH}); 现在我们需要创建组来包含精灵们(参看图2)。背景(background)离远我们观察者最远,所以⾸先 创建它。我们可以这么写: $.playground().addGroup(“background”, {width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT}); 这⾥我们调⽤了playground()⽅法,却没有提供任何参数,这将返回之前定义的html元素。然后我们 通过调⽤addGroup()以及它的两个参数来添加新组。第⼀参数是我们想给组起的名字(这也将成为展 ⽰组的html元素的id)。第⼆个参数是⼀系列配置组的值(就像之前playground⽅法),这⾥我们定义 了组的⼤⼩,同playground⼀样。 如果上⾯两段代码最后⼀⾏是⼀个接着⼀个的话,使⽤链式语句将会更加⽅便。 $(“#playground”).playground({height: PLAYGROUND_HEIGHT, width: PLAYGROUND_WIDTH}) .addGroup("background", {width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT}); 这看起来更像图1的代码了。通过传输return返回值、分⾏对齐的⽅式使得代码更易阅读,它们并不改变 代码含义。 我们接着添加其他组: $(“#playground”).playground({height: PLAYGROUND_HEIGHT, width: PLAYGROUND_WIDTH}) .addGroup("background", {width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT}).end() .addGroup("actors", {width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT}).end() .addGroup("playerMissileLayer",{width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT}).end() .addGroup("enemiesMissileLayer",{width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT}); 注意.end()⽅法,这种⽅式返回上⼀个被选择的元素。⼀些“修改操作”会改变当前“被选择元素”:就像 addGroup()把⾃⼰这个组当做“被选择元素”返回,所以如果你在这样的命令之后再添加的话,之前创 建的组就会改变。你也可以使⽤这种⽅式⽅便的给组添加精灵。⽽如果你想在之前创建的组中嵌套组的 话,你可以再次调⽤addGroup()⽅法,当然,我们现在的代码中并不需要这样。我们需要的是为 playground再次添加组,所以我们调⽤end()⽅法来重新获取playground,继续链接代码。现在我们需 要在那些组中增加精灵了! 2.2 游戏背景 为了使背景看起来具有远近的感觉,我们将会使⽤到许多以不同速度移动的层。这被称为是平⾏效果 (parallax effect)。 这种效果是基于这样的事实:在观察者看来更远的物体,⽐更近的物体移动更少 距离,前提是它们在同样的时间内,以相同的速度。 背景需要⼀刻不停的移动着,为了产⽣parallax效果,我们需要⽤到在背景层中的两个精灵。当只有⼀ 个层显⽰的时候,我们可以让第⼆个层在运动到第⼀个层末尾的时候又回到第⼀个层的开始部位,就像 图3显⽰的。 由图⽚构成的精灵总是动画,即使精灵本⾝并不动!⼀个动画只需⼀张图⽚。这⾥,我⽤到6个精灵 (每层2个)来使层的滚动的重复性看起来更少。然后,我通过为“#background”层添加精灵,创建了6 个精灵。注意,当你创建精灵的时候就应该指定精灵的⼤⼩,⽽不是创建动画的时候! var background1 = new $.gameQuery.Animation({imageURL: “background1.png”}); var PLAYGROUND_WIDTH = 700; var PLAYGROUND_HEIGHT = 250; var background2 = new $.gameQuery.Animation({imageURL: "background2.png"}); var background3 = new $.gameQuery.Animation({imageURL: "background3.png"}); var background4 = new $.gameQuery.Animation({imageURL: "background4.png"}); var background5 = new $.gameQuery.Animation({imageURL: "background5.png"}); var background6 = new $.gameQuery.Animation({imageURL: "background6.png"}); $("#background"). .addSprite("background1", {animation: background1, width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT}) .addSprite("background2", {animation: background2, width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT, posx: PLAYGROUND_WIDTH}) .addSprite("background3", {animation: background3, width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT}) .addSprite("background4", {animation: background4, width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT, posx: PLAYGROUND_WIDTH}) .addSprite("background5", {animation: background5, width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT}) .addSprite("background6", {animation: background6, width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT, posx: PLAYGROUND_WIDTH}); 现在我们需要让这些精灵动起来。如果你想在动画的每个间隔中执⾏指定动作,那么可以使 ⽤registerCallback()⽅法。通过这样,你就可以每过n毫秒,周期性的调⽤指定函数。我们通过这 样就能够创造出图3中精灵的移动效果。为了使精灵移动,你不需要使⽤任何的gameQuery函数。 jQuery就能完全满⾜你的要求。最简单的⽅法当数操纵精灵的CSS属性,改变top属性就能使精灵垂直移 动,改变left属性就能使精灵⽔平移动。 对每个背景层的精灵,我们想要使它们产⽣从屏幕右边到屏幕左边的移动效果。由于是⽔平移动,所以 只需要从PLAYGROUND_WIDTH到-PLAYGROUND_WIDTH改变left值,⽽不⽤改变top属性。每当 你想要从时间间隔中获取指定值,并循环检测,⼀个⼗分有⽤的⽅法是使⽤求模表达式:%。这个操作 符会得到整数相除之后的剩余值。举个例⼦,如果15%6,那么整数相除的结果是2(15/6=2),因为 2*6=12,⽽15-12=3中的3就是余数,所以15%6=3。千万不要被混淆了。现在要是你想数字从0递增到 20,每次增量为1,那么可以这么写: while(condition){ myValue++; //equivalent to myValue = myValue + 1; if(myValue >= 20){ myValue = 0; } } 这⾥,你需要⼀个条件检测,以及为myValue赋值两次。使⽤求模表达式将会更简单: while(condition){ myValue = (myValue + 1) % 20; } 我们需要或得精灵的即时⽔平位置,并修改它。为了修改元素的css属性,我们使⽤到了jQuery中的css ⽅法,第⼀次我们只是⽤⼀个参数来获取当前值,第⼆次使⽤两个值来设定新值。现在你应该完全能理 解下⾯的代码。 $.playground().registerCallback(function(){ //Offset all the pane: var newPos = (parseInt($("#background1").css("left")) - smallStarSpeed - PLAYGROUND_WIDTH) % (-2 * PLAYGROUND_WIDTH) + PLAYGROUND_WIDTH; $("#background1").css("left", newPos); newPos = (parseInt($("#background2").css("left")) - smallStarSpeed - PLAYGROUND_WIDTH) % (-2 * PLAYGROUND_WIDTH) + PLAYGROUND_WIDTH; $("#background2").css("left", newPos); newPos = (parseInt($("#background3").css("left")) - mediumStarSpeed - PLAYGROUND_WIDTH) % (-2 * PLAYGROUND_WIDTH) + PLAYGROUND_WIDTH; $("#background3").css("left", newPos); newPos = (parseInt($("#background4").css("left")) - mediumStarSpeed - PLAYGROUND_WIDTH) % (-2 * PLAYGROUND_WIDTH) + PLAYGROUND_WIDTH; $("#background4").css("left", newPos); newPos = (parseInt($("#background5").css("left")) - bigStarSpeed - PLAYGROUND_WIDTH) % (-2 * PLAYGROUND_WIDTH) + PLAYGROUND_WIDTH; $("#background5").css("left", newPos); newPos = (parseInt($("#background6").css("left")) - bigStarSpeed - PLAYGROUND_WIDTH) % (-2 * PLAYGROUND_WIDTH) + PLAYGROUND_WIDTH; $("#background6").css("left", newPos); }, REFRESH_RATE); 这就是我们添加了6个精灵后的结果,是不是很炫? 2.3 游戏玩家 对于背景,我们并没有使⽤任何动画,仅仅是张静态图⽚。但对于“玩家的飞船”,我们得⽤上所谓真正 的动画了。做法是给⽤户提供飞船的可视化反馈,就像他们想的那样:向上、向下、向左、向右。为了 实现这样的效果,我们需要⽤来展现飞船的静态图⽚和展现喷射器效果的动画。在移动飞船的时候,为 了使精灵切换之间看起来具有⽆关性,我们把它们统统放到同⼀组⾥。 在gameQuery,动画的所有帧都包含在同⼀张图⽚中,有点类似电影的感觉。这些帧既可以⼀帧挨着⼀ 怔(⽔平⽅式)也可以⼀帧在另⼀帧之上(垂直⽅式)。如果精灵的每帧⼤⼩都相同,排列⽅式可以任 选,但如果精灵以某种排列⽅式⽐动画⼤,那么就只好选另⼀中排列⽅式了。当你在gameQuery中创建 多帧动画,你需要提供帧的数量,帧之间的距离(delta),帧之间的时间间隔(rate),以及动画的类 型(例如垂直动画还是⽔平动画)。 // Player spaceship animations: playerAnimation["idle"] = new $.gameQuery.Animation({imageURL: "player_spaceship.png"}); playerAnimation["explode"] = new $.gameQuery.Animation({imageURL: "explode.png"}); playerAnimation["up"] = new $.gameQuery.Animation({imageURL: "boosterup.png", numberOfFrame: 6, delta: 14, rate: 60, type: $.gameQuery.ANIMATION_HORIZONTAL}); playerAnimation["down"] = new $.gameQuery.Animation({imageURL: "boosterdown.png", numberOfFrame: 6, delta: 14, rate: 60, type: $.gameQuery.ANIMATION_HORIZONTAL}); playerAnimation["boost"] = new $.gameQuery.Animation({imageURL: "booster1.png" , numberOfFrame: 6, delta: 14, rate: 60, type: $.gameQuery.ANIMATION_VERTICAL}); playerAnimation["booster"] = new $.gameQuery.Animation({imageURL: "booster2.png"}); // Initialize the background $.playground().addGroup("actors", {width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT}) .addGroup("player", {posx: PLAYGROUND_WIDTH/2, posy: PLAYGROUND_HEIGHT/2, width: 100, height: 26}) .addSprite("playerBoostUp", {posx:37, posy: 15, width: 14, height: 18}) .addSprite("playerBody",{animation: playerAnimation["idle"], posx: 0, posy: 0, width: 100, height: 26}) .addSprite("playerBooster", {animation:playerAnimation["boost"], posx:-32, posy: 5, width: 36, height: 14}) .addSprite("playerBoostDown", {posx:37, posy: -7, width: 14, height: 18}); 表现飞船后推进器的精灵需要有3中状态:常态(中等⽕⼒)、关闭(飞船向后退)以及全开(飞船向 前进)。还需要另外两个精灵,分别是飞船上推进器、下推进器,它们各⾃具有开、关两个动画。随着 ⽤户键盘输⼊,这些动画被触发。jQuery通过事件监听器(event listener),能够很⽅便的实现对⽤户 键盘操作的回应。当特定的事件发⽣,你可以设定执⾏对应的函数,例如⿏标点击、按下按键。你可能 注意到,在创建某些精灵的时候,我们并没有为其设定动画,那是因为它们现在仅仅是占个位置,当需 要动画的时候,我们仍然可以设置。 现在让我们看看“键盘”是如何与“动画”绑定在⼀起的。这⾥我们发现了在浏览器中运⾏javascript游戏的 ⼩⼩不便之处:上/下/左/右键基本都是被⽤来滚动页⾯的。所以如果你的游戏超过了浏览器窗⼜⼤⼩, 那么按下这些键还会使页⾯发⽣滚动。虽然我们可以通过编程⼿段屏蔽页⾯滚动,但我仍然认为⼲涉浏 览器动作并不是个好主意,所以我使⽤了别的键来控制游戏。当然最好的⽅式应该是让⽤户⾃⼰定义按 键,这并⾮难事,不过我还是想让这篇教程尽量简单。 //this is where the keybinding occurs $(document).keydown(function(e){ switch(e.keyCode){ case 65: //this is left! (a) $("#playerBooster").setAnimation(); break; case 87: //this is up! (w) $("#playerBoostUp").setAnimation(playerAnimation["up"]); break; case 68: //this is right (d) $("#playerBooster").setAnimation(playerAnimation["booster"]); break; case 83: //this is down! (s) $("#playerBoostDown").setAnimation(playerAnimation["down"]); break; } }); //this is where the keybinding occurs $(document).keyup(function(e){ switch(e.keyCode){ case 65: //this is left! (a) $("#playerBooster").setAnimation(playerAnimation["boost"]); break; case 87: //this is up! (w) $("#playerBoostUp").setAnimation(); break; case 68: //this is right (d) $("#playerBooster").setAnimation(playerAnimation["boost"]); break; case 83: //this is down! (s) $("#playerBoostDown").setAnimation(); break; } }); 当键盘按下或者松开,相应的动画就发⽣。有时候我们需要移除某个动画,⽽不是⽤另⼀个动画来替 代,为了实现这种效果,我们使⽤不带参数的setAnimation⽅法。为了找出你想⽤来作为游戏按键的按 键代码(keyCode),你可以使⽤这个页⾯。飞船的爆炸效果将会在步骤3中讲解。 下⾯是结果,不过别忘了这会⼉我们只关注游戏精灵动画,所以飞船并不会动。 2.4 敌⼈和导弹 我们的每个敌⼈都需要带上两个动画,常态和爆炸态。敌⼈的⾏动和等级将在下⼀步骤中讨论。这⾥接 触到的新玩意⼉是ANIMATION_CALLBACK类型。回调(callback)是指将来会被调⽤的函数。在这⾥ 是指当前动画结束的时刻。我们使⽤这种类型来表现爆炸:当爆炸动画播放完成之后我们移除精灵(在 步骤3中会详细讨论具体操作⽅法)。 当你需要为动画指定两个或者更多的类型时,你可以使⽤“|”符号来分隔。例如,你的动画可以有这样的 类型:ANIMATION_CALLBACK | ANIMATION_ONCE | ANIMATION_VERTICAL,如此这般,动画 就会垂直⽅式播放⼀次,播放结束时执⾏回调函数!这对于想在两段循环播放动画之间增加过渡动画来 说是很⽅便的。在这种情况下,你可以通过回调函数改变循环播放的动画(但在这篇教程中我们并不需 要)。 var enemies = new Array(3); // There are three kind of enemies in the game //... /// List of enemies animations : // 1st kind of enemy: enemies[0] = new Array(); // enemies have two animations enemies[0]["idle"] = new $.gameQuery.Animation({imageURL: "minion_idle.png", numberOfFrame: 5, delta: 52, rate: 60, type: $.gameQuery.ANIMATION_VERTICAL}); enemies[0]["explode"] = new $.gameQuery.Animation({imageURL: "minion_explode.png", numberOfFrame: 11, delta: 52, rate: 30, type: $.gameQuery.ANIMATION_VERTICAL | $.gameQuery.ANIMATION_CALLBACK}); // 2nd kind of enemy: enemies[1] = new Array(); enemies[1]["idle"] = new $.gameQuery.Animation({imageURL: "brainy_idle.png", numberOfFrame: 8, delta: 42, rate: 60, type:$.gameQuery.ANIMATION_VERTICAL}); enemies[1]["explode"] = new $.gameQuery.Animation({imageURL: "brainy_explode.png", numberOfFrame: 8, delta: 42, rate: 60, type: $.gameQuery.ANIMATION_VERTICAL | $.gameQuery.ANIMATION_CALLBACK}); // 3rd kind of enemy: enemies[2] = new Array(); enemies[2]["idle"] = new $.gameQuery.Animation({imageURL: "bossy_idle.png", numberOfFrame: 5, delta: 100, rate: 60, type: $.gameQuery.ANIMATION_VERTICAL}); enemies[2]["explode"] = new $.gameQuery.Animation({imageURL: "bossy_explode.png", numberOfFrame: 9, delta: 100, rate: 60, type: $.gameQuery.ANIMATION_VERTICAL | $.gameQuery.ANIMATION_CALLBACK}); // Weapon missile: missile["player"] = new $.gameQuery.Animation({imageURL: "player_missile.png", numberOfFrame: 6, delta: 10, rate: 90, type: $.gameQuery.ANIMATION_VERTICAL}); missile["enemies"] = new $.gameQuery.Animation({imageURL: "enemy_missile.png", numberOfFrame: 6, delta: 15, rate: 90, type: $.gameQuery.ANIMATION_VERTICAL}); missile["playerexplode"] = new $.gameQuery.Animation({imageURL: "player_missile_explode.png", numberOfFrame: 8, delta: 23, rate: 90, type: $.gameQuery.ANIMATION_VERTICAL | $.gameQuery.ANIMATION_CALLBACK}); missile["enemiesexplode"] = new $.gameQuery.Animation({imageURL: "enemy_missile_explode.png", numberOfFrame: 6, delta: 15, rate: 90, type: $.gameQuery.ANIMATION_VERTICAL | $.gameQuery.ANIMATION_CALLBACK}); 这就是敌⼈以及导弹的动画!在下⼀步骤⾥,我们讲解这个游戏的对象模型(object model)。保持更 简单的javascript和更少的gameQuery。 第3章 步骤2 – 对象模型 3.1 Javascript中的对象 Javascript并不是⼀种典型的⾯向对象编程语⾔。如果你使⽤过其他⾯向对象语⾔的话,请记住,在 javascript中,⾯向对象稍微有些特殊。不过,如果你是个⾯向对象的新⼿的话,这应该不会困扰你吧。 我并不会深⼊讲解,要是你觉得有必要的话,可以看看javascript⾯向对象的教程与⽂档。 ⾯向对象编程是⼀种重新组织代码的⽅式,它把逻辑上相关的东西合为⼀体,你可以设定向⽤户公开的 或者关闭的代码部分(封装性)。类(class)是这样的东西:它描述了实体特性(属性attributes),实 体怎样对外开放(⽅法methods)。类的⼀个唯⼀实体被称为对象(object)。对象是类的实例化。当 你实例化⼀个类的时候,你其实调⽤了⼀个称为构造器(constuctor)的⽅法。为了创建类的实例,你 得⽤到关键字new,就像你在步骤1中创建动画⼀样。 var myObject = new myClass(some, important, arguments); 在javascript中,对象其实是函数(function)的实例,对的,你没有看错,就是函数!所以当你定义类 的时候,看起来就像是在写⼀个函数。事实上,当你在javascript中创建类的时候,你写的是它的构造 器。⼀个简单的类看起来应该是这个样⼦: var myClass() = function(var some, var important, var arguments){ this.someVisilbeAttribute = some + arguments; someLessVisiblAttribute = important + arguments; this.tickleMe = function(var name){ alert("hello "+name+" you should now that "+this.someVisibleAttribute); }; return true; } 这段代码定义了⼀个叫myClass的类,它有两个属性,分别是someVisibleAttribute、 someLessVisibleAttribute;⼀个⽅法,叫做tickleMe。与此同时,我们也定义了构造器的⼯作⽅式,⽤ 构造器参数定义对象的属性。/p> 就像你在例⼦中所看到的那样,存在着很多定义类属性的⽅式。当使⽤var关键字的时候,你在对象中创 建了⼀个本地变量,这意味着对象中任何执⾏性的代码都可以通过属性名引⽤到具体属性。通过使⽤ this关键字,对象属性也能被外界所以引⽤到。但这并不意味通过var关键字定义的属性不能被外界所引 ⽤,⽽是这稍嫌⿇烦,⽽且拐弯抹⾓。下⾯的代码展⽰了如何从外界引⽤对象的可见属性: myObject.someVisibleProperty = “a new value”; alert(myObject.someVisibleProperty) ⽽且你需要知道在构造器中的参数变量可以被对象中的可执⾏性代码所引⽤,就好像它们已经通过var声 明过⼀样。 你可以在对象实例化之后改变它,增加新属性或者⽅法。当这么做的时候,你需要考虑是要改变类的所 有对象还是仅仅这⼀个对象。下⾯的例⼦展现的是后者。 myObject.lastMinuteMethode = function(){/*Do something here*/}; myObject.aNewProperty = "aValue"; 有时你并不只是想改变这⼀个对象,还想改变它的兄弟姐妹对象“sibing”。为了实现这样的效果,可以 使⽤对象的原型(prototype)。原型就像在对象中对类进⾏定义,可以通过.prototype属性。任何相同 类的实例们都共享同⼀个原型。所以当你在⼀个对象的原型中做了什么,其他实例也受影响。 var firstObject = new myClass(1,2,3); var secondObject = new myClass(4,5,6); firstObject.prototype.aNewMethode = function(){alert("I'm new");} secondObject.aNewMethode(); 3.2 Javascript中的继承 当⼀个类继承另⼀个类,⼦类接受了⽗类的所有属性、⽅法。然后⼦类可以增加、重写某些⽅法来使其 不同于其他类。根据你所使⽤的语⾔的不同,继承会有不同的形式和约束。对于javascript来说,并没有 提供语⾔级别上的⽀持,不过很多爱钻⽜⾓尖的家伙还是让它出现了。所以选择哪种⽅式,取决于你想 ⽤到继承的哪些特性。 在这篇教程中我们选择⼀种⾮常简单且具约束性的javascript继承形式。我认为再增加更多可能会导致难 以理解。这种⽅式是在类中注⼊另⼀个类的实例。然后你课余==可以通过修改原型(prototype)来扩 展类,⽽不会影响⽗类。 function inheritingClass(){}; inheritingClass.prototype = new myClass(); inheritingClass.prototype.soneOnlyMethode = function(){/*Do something here*/}; inheritingClass.prototype.tickleMe = function(){/*Change the behavior of the parent methode*/}; 为了实现继承,这就是所以你需要了解的东西。但是如果你想要更复杂的编码⽅式,或者⽤别的⾯向对 象语⾔实现的话,你应该更加深⼊的去了解javascript继承。 3.3 玩家对象 玩家就是只有⼀个实例的简单类(因为这是个单⼈游戏嘛),所以在这⾥使⽤⾯向对象只会使得代码更 易阅读。 在玩家对象中,我们需要有属性来描述玩家飞船状态,例如飞船是否还有护盾、还有命?同时,飞船的 基本动作可以通过对象⽅法实现。因为在这个游戏⾥,我们不需要物理引擎来管理飞船移动,你仅仅只 需增加/减少位置,我选择不使⽤任何⽅法来管理飞船的移动。 第⼀个⽅法damage(),⽤来判断“损伤”事件。当导弹击中飞船时,我们调⽤这个⽅法,并根据返回值判 断飞船是否该“死”了。对外界来说,这个⽅法屏蔽了对护盾的管理。如果你想知道variabl–含义,它代 表variable = variable – 1,这也适⽤于其他操作符。 function Player(node){ this.node = node; //this.animations = animations; this.grace = false; this.replay = 3; this.shield = 3; this.respawnTime = -1; // This function damage the ship and return true if this cause the ship to die this.damage = function(){ if(!this.grace){ this.shield--; if (this.shield == 0){ return true; } return false; } return false; }; //... 第⼆个⽅法叫做respawn(), 它所关注的是当飞船死亡(之间的damage()⽅法返回true),⽽且玩家仍 然有剩余的“⽣命”时所发⽣的事。在这种情形下,飞船护盾必须重新⽣成,并且飞船会处于敌⼈⽆法攻 击到的⽆敌(grace)模式。⽆敌模式只能持续3秒钟,我们需要测定时间以便得知何时该启动⽆敌模 式,何时该关闭⽆敌模式,这就是代码this.respawnTime = (new Date()).getTime();所做的 事情。 为了让玩家知道飞船正处于⽆敌(grace)模式,我们使飞船变得稍微透明,jQuery的fadeTo()⽅法可以 实现。这个⽅法的第⼀个参数是变成指定透明度所花费的时间(这⾥因为是即使⽣效所以0ms),第⼆ 个参数是我们想要得到的透明度(1代表完全不透明,0代表不可见)。 //… // this try to respawn the ship after a death and return true if the game is over this.respawn = function(){ this.replay--; if(this.replay==0){ return true; } this.grace = true; this.shield = 3; this.respawnTime = (new Date()).getTime(); $(this.node).fadeTo(0, 0.5); return false; }; //... 玩家类的最后部分是检测⽆敌模式是否结束。这个⽅法必须在每个时隙都被调⽤。、 //… this.update = function(){ if((this.respawnTime > 0) && (((new Date()).getTime()-this.respawnTime) > 3000)){ this.grace = false; $(this.node).fadeTo(0, 1); this.respawnTime = -1; } } return true; } 3.4 敌⼈对象 存在3中类型的敌⼈,它们具有共同的祖先。我们在祖先类中定义敌⼈的默认共同⾏为,在继承类中定 义特殊⽅法。因为我想在之后调⽤⽅法的时候不必考虑对象到底是哪个类的实例,所以我们在⼦类中只 重写那些已经存在的⽅法。就像玩家类⼀样,敌⼈类也需要管理“损伤“的⽅法。你应该会注意到,其实 这个⽅法和玩家类的⼀模⼀样,除了检测⽆敌(grace)模式之外。 function Enemy(node){ this.shield = 2; this.speedx = -5; this.speedy = 0; this.node = $(node); // deals with damage endured by an enemy this.damage = function(){ this.shield--; if(this.shield == 0){ return true; } return false; }; //... 敌⼈具有某些符合基本逻辑的⾃动⾏为。我们需要⼀些飞来使敌⼈移动。这⾥我们有⼀个叫update()的 ⽅法,它会调⽤在每个⽅向轴上移动的函数。这使⼦类可以轻松重写其中任何⼀个。你可以发现敌⼈的 默认⾏为是在每个⽅向轴上以⼀定的速度移动。如果你仔细看updateX和updateY⽅法的话,你会发现其 实背景的移动也采⽤了同样的⽅式。 下图是敌⼈的家族树(family tree)。我们使⽤继承的⽬标是减少代码冗余。所以如果;两个对象拥有 同⼀⾏为,我们让它们从另⼀对象继承。 3.4.1 导弹 导弹与默认祖先敌⼈的差别并不⼤,导弹是想在屏幕中尽量停留⽽已。所以代码相当直观: function Minion(node){ this.node = $(node); } Minion.prototype = new Enemy(); Minion.prototype.updateY = function(playerNode){ var pos = parseInt(this.node.css("top")); if(pos > (PLAYGROUND_HEIGHT - 50)){ this.node.css("top",""+(pos - 2)+"px"); } } 3.4.2 有头脑的敌⼈ 对于有头脑的敌⼈,我们需要给它们配置更多护盾,我们只需在构造器中重新定义这个值即可。定义在 ⼦类中的同名值会重写⽗类,在构造器中定义的值则重写⼦类和⽗类。这种类型的敌⼈稍具智能,它们 尝试躲避玩家。为了实现这个我们重写updateY⽅法,使得垂直速度增加,或者远离玩家的相对位置。 这⾥也没有任何新的东西。注意“有头脑的敌⼈”类可不是继承⾄导弹(Minion)类⽽是敌⼈(Enemy) 类。 function Brainy(node){ this.node = $(node); this.shield = 5; this.speedy = 1; this.alignmentOffset = 5; } Brainy.prototype = new Enemy(); Brainy.prototype.updateY = function(playerNode){ if((this.node[0].gameQuery.posy+this.alignmentOffset) > $(playerNode) [0].gameQuery.posy){ var newpos = parseInt(this.node.css("top"))-this.speedy; this.node.css("top",""+newpos+"px"); } else if((this.node[0].gameQuery.posy+this.alignmentOffset) < $(playerNode)[0].gameQuery.posy){ var newpos = parseInt(this.node.css("top"))+this.speedy; this.node.css("top",""+newpos+"px"); } } 3.4.3 boss型敌⼈ boss型敌⼈的⾏为与同有头脑的敌⼈⼀样,它们躲避玩家。不同的是:它们不会朝玩家前进,⽽是垂直 移动,玩家得花些时间才能杀死它们(因为它们的护盾很厚)。 function Bossy(node){ this.node = $(node); this.shield = 20; this.speedx = -1; this.alignmentOffset = 35; } Bossy.prototype = new Brainy(); Bossy.prototype.updateX = function(){ var pos = parseInt(this.node.css("left")); if(pos > (PLAYGROUND_WIDTH - 200)){ this.node.css("left",""+(pos+this.speedx)+"px"); } } 敌⼈的⽣成、破坏和发射导弹并不需要在对象中设定,请看下⼀步骤。 第4章 步骤3 – 游戏逻辑与控制 4.1 游戏状态 每⼀个稍微复杂点的游戏都需要指定⼀系列的规则来决定哪些动作是允许或允许发⽣的。在这个游戏 ⾥,我们的游戏规则⼗分简单,但如果你想看更复杂的例⼦的话,看这个演⽰程序。我需要3种信息: ⾸先是游戏是否结束?⼀旦游戏结束了我们就不需要再做什么了。 第⼆个是屏幕中是否有个boos级敌⼈?当这种级别敌⼈出现的时候,我们停⽌产⽣更多的敌⼈。 第三个是飞船是否爆炸?当飞船护盾⽤完并爆炸,我们希望飞船能往屏幕底部掉。这段时间,玩 家不能操纵飞船。 另外还有⼀个我们曾经见过的隐藏的游戏规则,飞船对象的⽆敌(grace)变量。 为了存储上述3种信息,我们使⽤3个布尔变量(飞船的⽆敌也是如此)。其实我们可以把变量减少到2 个,因为boss模式并不和其他同事发⽣,但却使代码不易阅读(当然,如果你执意要这么做的话,⼀个 变量也是可以的,但那时我们的游戏规则会难以理解)。 4.2 敌⼈的产⽣ 为了产⽣敌⼈,我们注册⼀个回调函数。它每秒都会被调⽤,然后随机的决定是否创建新敌⼈。为了实 现随机效果,我们使⽤Math.random()⽅法产⽣⼀个伪随机数,伪随机数平均的分布于0到1之间 (javascript包含了⼀些预定义的内置函数)。我们给新创建的精灵定义⼀个名字,为了⽣成⼀个⼏乎唯 ⼀的名字,我们⽤到两个函数。Math.random()和Math.ceil()函数。Math.ceil返回的是所给浮点数的近 似整数值。联合上述两个函数,我们可以⽣成0到1000之间的随机整数。当然这并不是说,在同⼀时 刻,两个精灵的id永远不会⼀样,⽽是说这很少见。 ⼀旦通过addSprite()⽅法创建了新敌⼈,我们需要⽴即使⽤它,把它和之前定义的敌⼈类的实例相关 联。⼀个好的做法是保持对象集合的分离因为这使得代码看起来更清洁。当然每次选择对象节点是要花 点时间,但这却便于调试。为了通过jQuery选择器往指定节点存储信息,必须使⽤[0]符号。当只有⼀个 ⼀个对象的时候,这是很有意义的。【var s = $(“div”) jQuery对象默认都有个0索引,s为jQuery对象, s[0]为dom对象,可以使⽤dom对象的所有属性⽅法】 //This function manage the creation of the enemies $.playground().registerCallback(function(){ if(!bossMode && !gameOver){ if(Math.random() < 0.4){ var name = "enemy1_"+Math.ceil(Math.random()*1000); $("#actors").addSprite(name, {animation: enemies[0]["idle"], posx: PLAYGROUND_WIDTH, posy: Math.random()*PLAYGROUND_HEIGHT, width: 150, height: 52}); $("#"+name).addClass("enemy"); $("#"+name)[0].enemy = new Minion($("#"+name)); } else if (Math.random() > 0.5){ var name = "enemy1_"+Math.ceil(Math.random()*1000); $("#actors").addSprite(name, {animation: enemies[1]["idle"], posx: PLAYGROUND_WIDTH, posy: Math.random()*PLAYGROUND_HEIGHT, width: 100, height: 42}); $("#"+name).addClass("enemy"); $("#"+name)[0].enemy = new Brainy($("#"+name)); } else if(Math.random() > 0.8){ bossMode = true; bossName = "enemy1_"+Math.ceil(Math.random()*1000); $("#actors").addSprite(bossName, {animation: enemies[2]["idle"], posx: PLAYGROUND_WIDTH, posy: Math.random()*PLAYGROUND_HEIGHT, width: 100, height: 100}); $("#"+bossName).addClass("enemy"); $("#"+bossName)[0].enemy = new Bossy($("#"+bossName)); } } else { if($("#"+bossName).length == 0){ bossMode = false; } } }, 1000); //once per seconds is enough for this 我们让敌⼈先出现在屏幕右边的不可见区域。jQuery中的addClass()⽅法可以把参数值指定为精灵的类 名。为敌⼈精灵添加同⼀类名,可以⽅便修改位置,检测碰撞。当我们⽣成⼀个boss类型的敌⼈后,我 需要设置合适的游戏状态来描述;为了检测boss型敌⼈是否已经被消灭,我们也需要存储它的名字。请 注意,我是如何巧妙的在⾮boss模式下⽣成敌⼈,又是如何在boss模式下复位游戏到正常模式的。 4.3 敌⼈的⾏为 为了让敌⼈能够⾏动,我们已经完成了⼤部分的⼯作准备了,现在仅仅需要调⽤精灵节点对象上的call() ⽅法。为了实现这个我们注册了⼀个每30毫秒就执⾏⼀次的回调函数(callback)。在回调函数中,我 们通过jQuery选择了所以类名叫enemy的精灵。然后通过each()函数对每个选择到的精灵执⾏同⼀⽅ 法。当然游戏没结束之前我才需要这么做。 $.playground().registerCallback(function(){ if(!gameOver){ //Update the movement of the enemies $(".enemy").each(function(){ this.enemy.update($("#player")); var posx = parseInt($(this).css("left")); if((posx + 150) < 0){ $(this).remove(); return; } //Test for collisions var collided = $(this).collision("#playerBody,.group"); if(collided.length > 0){ if(this.enemy instanceof Bossy){ $(this).setAnimation(enemies[2]["explode"], function(node) {$(node).remove();}); $(this).css("width", 150); } else if(this.enemy instanceof Brainy) { $(this).setAnimation(enemies[1]["explode"], function(node) {$(node).remove();}); $(this).css("width", 150); } else { $(this).setAnimation(enemies[0]["explode"], function(node) {$(node).remove();}); $(this).css("width", 100); } $(this).removeClass("enemy"); //The player has been hit! if($("#player")[0].player.damage()){ explodePlayer($("#player")); } } }); } }, REFRESH_RATE); 我们还得好好想想敌⼈怎么从左边出去的问题。事实上,它们既回不来也不会再被玩家射中,所以让它 们继续留在游戏⾥还有什么意义呢?jQuery函数remove()会移除那些“没有⽤处的敌⼈”,关键字return 使得函数在此地停下。函数的第⼆部分关注的是敌⼈与玩家飞船的碰撞检测。 gameQuery函数collision返回指定对象之间发⽣碰撞的个体的列表。参数值是过滤器的作⽤。当你为其 指定参数,collision函数会检测指定元素的碰撞情况。由于碰撞检测是个开销相当⼤的函数,所以拜托 少⽤点。这⾥我们检测的是groups与player,gameQuery为那些通过addGroup()⽅法创建的节点添加了 共同的类名group。我们需要对每个组检测,因为player是被嵌套在组当中⽽返回结果却只是精灵的集 合。 在当前版本的gameQuery中,collision函数把精灵认定是标准的盒⼦形状(这意味着没有⼀像素⼀像素 的检测过去),所以会发⽣类似下⾯那样的情况。我希望更多的选项能在将来的版本中得到实现。 每当碰撞发⽣的时候,我们需要让敌⼈爆炸,让玩家掉护盾。为了实现敌⼈爆炸效果,我们就简单的将 敌⼈动画替换成爆炸动画(记住如果你需要的话,你可以改变精灵的⼤⼩)。但爆炸动画只需播放⼀ 次,然后就应该被移除。这也是Animation.CALLBACK被被使⽤的原因!当设置动画的时候,你可以将 函数作为第⼆个参数传⼊。这个函数将会在动画结束的时候被⾃动调⽤。在这个例⼦当中,我们只是简 单的移除节点。在这⾥有个⼩陷阱,我们必须把退场精灵从enemy类中移除,否则的话下次函数被调⽤ 的时候会把爆炸对象作为⼀个敌⼈的。 我们在步骤2中已经写过管理玩家“伤害”的函数了,当函数返回true代表飞船该爆炸了。我们已经将飞船 爆炸函数从中分离开来了,因为这段代码还会被使⽤到⼏次。 function explodePlayer(playerNode){ playerNode.children().hide(); playerNode.addSprite("explosion",{animation: playerAnimation["explode"], playerHit = true; } 由于玩家是个包含了⼀系列精灵的组,所以我们不能简单的为其更改动画。我们需要隐藏正常精灵,然 后将其替换为新的精灵。jQuery的hide函数会隐藏所有被选中的节点,获取节点是通过jQuery的 children⽅法。然后我们就可以设置正确的游戏状态,以便游戏知道飞船现在正处于爆炸。 4.4 导弹 如果想在按键之后飞船能够发射的话,我们需要⽤到经典的事件驱动模型。这意味着我们需要为步骤1中 的事件监听器增加⼀段⼩⼩的代码来改变飞船动画。 //… switch(e.keyCode){ case 75: //this is shoot (k) //shoot missile here var playerposx = parseInt($("#player").css("left")); var playerposy = parseInt($("#player").css("top")); var name = "playerMissle_"+Math.ceil(Math.random()*1000); $("#playerMissileLayer").addSprite(name,{animation: missile["player"], posx: playerposx + 90, posy: playerposy + 14, width: 20, height: 5}); $("#"+name).addClass("playerMissiles") break; //... ⾸先是读取飞船的位置信息。然后在导弹(missile)层的正确位置添加精灵。我们使⽤和敌⼈精灵同样 的⽅法⽣成导弹精灵id。还有最后⼀件事:把这个新创建的精灵添加统⼀的类名,这对之后检测碰撞来 说会很⽅便。 对于敌⼈发射导弹的处理,我们需要扩展之前写的update回调函数。只有“有头脑”和boss型敌⼈才能发 射导弹。为了了解当前敌⼈是否符合,我们使⽤instanceof操作符。如果操作符左边是右边的实例, 就返回true。由于boss型敌⼈继承了“有头脑”敌⼈,所以前者也是后者的实例。这⾥我们再次⽤到随机 数⽣成器来处理敌⼈随机发射导弹事件。 //Make the enemy fire if(this.enemy instanceof Brainy){ if(Math.random() < 0.05){ var enemyposx = parseInt($(this).css("left")); var enemyposy = parseInt($(this).css("top")); var name = "enemiesMissile_"+Math.ceil(Math.random()*1000); $("#enemiesMissileLayer").addSprite(name,{animation: missile["enemies"], posx: enemyposx, posy: enemyposy + 20, width: 15,height: 15}); $("#"+name).addClass("enemiesMissiles"); } } 如果你把这段代码与之前的代码⽚段相⽐较,你会发现它们⼏乎是⼀模⼀样的。 虽然发射导弹是件简单的事,但别忘了我们还得使导弹碰到玩家后,会对其造成伤害。我们⾸先看看敌 ⼈导弹:这段代码位于⼀个回调函数当中,并且和之前检测敌⼈与玩家碰撞代码很相似。对于敌⼈发射 的每个导弹,我们都需要检测它们是否与玩家碰撞。就像之前做的那样,我们调⽤玩家对象的对应⽅法 来减少护盾,并且检测飞船是否该爆炸了。如果飞船被证实爆炸,我们也想之前所做的为其设置爆炸动 画与回调函数。 不要忘记,导弹也和敌⼈⼀样,在跑出屏幕的时候需要被移除。 $(“.enemiesMissiles”).each(function(){ var posx = parseInt($(this).css("left")); if(posx < 0){ $(this).remove(); return; } $(this).css("left", ""+(posx-MISSILE_SPEED)+"px"); //Test for collisions var collided = $(this).collision(".group,#playerBody"); if(collided.length > 0){ //The player has been hit! collided.each(function(){ if($("#player")[0].player.damage()){ explodePlayer($("#player")); } }) $(this).setAnimation(missile["enemiesexplode"], function(node) {$(node).remove();}); $(this).removeClass("enemiesMissiles"); } }); 对于玩家导弹,其实也差不多,我们检测敌⼈类在碰到导弹之后是否设置了正确的动画。不过在最后, 你可能会发现⼀些不可思议的事情:我们不仅改变了精灵的长宽,也改变了它的位置。选取合适的动画 能解决精灵在屏幕跳动的异常,就像下图所⽰。 当然你也可以对所有精灵使⽤统⼀的⼤⼩。不过⽐起画图我更偏向使⽤代码来解决这个问题:) //Update the movement of the missiles $(".playerMissiles").each(function(){ var posx = parseInt($(this).css("left")); if(posx > PLAYGROUND_WIDTH){ $(this).remove(); return; } $(this).css("left", ""+(posx+MISSILE_SPEED)+"px"); //Test for collisions var collided = $(this).collision(".group,.enemy"); if(collided.length > 0){ //An enemy has been hit! collided.each(function(){ if($(this)[0].enemy.damage()){ if(this.enemy instanceof Bossy){ $(this).setAnimation(enemies[2]["explode"], function(node) {$(node).remove();}); $(this).css("width", 150); } else if(this.enemy instanceof Brainy) { $(this).setAnimation(enemies[1]["explode"], function(node) {$(node).remove();}); $(this).css("width", 150); } else { $(this).setAnimation(enemies[0]["explode"], function(node) {$(node).remove();}); $(this).css("width", 100); } $(this).removeClass("enemy"); } }) $(this).setAnimation(missile["playerexplode"], function(node) {$(node).remove();}); $(this).css("width", 38); $(this).css("height", 23); $(this).css("top", parseInt($(this).css("top"))-7); $(this).removeClass("playerMissiles"); } }); 4.5 玩家操作 如果游戏没有结束,飞船也没有坠落,我们就允许⽤户操作飞船的运动。我们需要每隔30毫秒就检测⼀ 下⽤户是否还按着按键,因为飞船只能在按键持续按下的情况下移动。注意这与我们之前在步骤1中看到 的keyup、keydown事件有何不同,前者是消极被动,后者是积极主动。Javascript并没有解决这种问题 的机制,不过我的gameQuery却提供了!为了激活这个功能,我们需要在指定playground节点的时候就 设置参数keyTracker: true。 $(“#playground”).playground({height: PLAYGROUND_HEIGHT, width: PLAYGROUND_WIDTH, keyTracker: true}); ⼀旦按键记录器激活了,你就可以把你想关注的按键代码作为索引值来读取 jQuery.gameQuery.keyTracker数组,这样就能了解指定按键情况。如果得到的值时true,那么指定按键 当前正被按下。这能⽅便你⽤简单的回调函数管理玩家控制(player controls)。为了控制精灵移动, 我们使⽤同样的技术⼿段。 // this is the function that control most of the game logic $.playground().registerCallback(function(){ if(!gameOver){ //Update the movement of the ship: if(!playerHit){ $("#player")[0].player.update(); if(jQuery.gameQuery.keyTracker[65]){ //this is left! (a) var nextpos = parseInt($("#player").css("left"))-5; if(nextpos > 0){ $("#player").css("left", ""+nextpos+"px"); } } if(jQuery.gameQuery.keyTracker[68]){ //this is right! (d) var nextpos = parseInt($("#player").css("left"))+5; if(nextpos < PLAYGROUND_WIDTH - 100){ $("#player").css("left", ""+nextpos+"px"); } } if(jQuery.gameQuery.keyTracker[87]){ //this is up! (w) var nextpos = parseInt($("#player").css("top"))-3; if(nextpos > 0){ $("#player").css("top", ""+nextpos+"px"); } } if(jQuery.gameQuery.keyTracker[83]){ //this is down! (s) var nextpos = parseInt($("#player").css("top"))+3; if(nextpos < PLAYGROUND_HEIGHT - 30){ $("#player").css("top", ""+nextpos+"px"); } } } else { //... 这个时候这些代码对你来说应该很眼熟了吧:),唯⼀的陷阱就是别忘记飞船不能跑出屏幕。当玩家飞 船爆炸,它应该能往下坠落。当飞船坠出屏幕之外并且还有剩余的⽣命的时候,飞重新⽣成船;否则游 戏结束。为了能重新⽣成飞船,我们需要移除包含爆炸动画的精灵,重新显⽰隐藏起来的飞船精灵。这 个简单的调⽤jQuery的show函数即可。不要忘了把飞船摆到正确的位置,重设游戏为正常模式。 //.. } else { var posy = parseInt($("#player").css("top"))+5; var posx = parseInt($("#player").css("left"))-5; if(posy > PLAYGROUND_HEIGHT){ //Does the player did get out of the screen? if($("#player")[0].player.respawn()){ gameOver = true; $("#playground").append(' 提供给玩家重启游戏的功能,看起来会更友好。这是个很意义的功能,不过gameQuery并没有提供简单 的⽅法来重设游戏状态。对于我的上个游戏以及这个游戏来说,我⽤了个⼩技巧:window.location。这 个javascript对象可以操纵浏览器的URL,重新载⼊页⾯当然不在话下。这对我们就⾜够了。 // Function to restart the game: function restartgame(){ window.location.reload(); }; 但“restart”按钮节点被添加,我们需要为其注册页⾯重载事件。就像我们之前⽤keydown、keyup那样。 这⾥⽤到的jQuery函数是click,使⽤⼀个函数作为参数,当事件触发的时候就执⾏这个函数。 厄,这就完了!祝贺你,如果你⼀直跟着进度看到这⾥!最后⼀部分是⼀系列为了使游戏更完善的⼩东 西。 第5章 步骤4 – 杂七杂⼋ 5.1 游戏信息 让玩家知道他的飞船到底还剩多少护盾、⽣命应该是很有必要的吧。如果我们有个分数系统的话,最好 也能显⽰给玩家。就像之前的#background,#actors以及其他组,我们创建⼀个新组(#overlay)来包 容这些信息。这⾥我们不再⽤任何的gameQuery函数,因为只需要显⽰指定的⽂字⽽已。我们需要在游 戏屏幕的右上⽅创建两个div节点来显⽰。通过append函数就能实现,因为组实质上(精灵也是)是个 简单的div。 $("#overlay").append("
"); 我们需要保持这新信息被更新。为了解决这个,厄……对了,还得⽤⼀个回调函数。在这个回调函数当 中,我们可以简单的选择这个div,然后⽤新内容来替代。这⾥我们没法再⽤append函数了,因为它是 增加内容;我们使⽤html⽅法(或者text⽅法),它⽤字符串参数来替代原来内容。 $("#shieldHUD").html("shield: "+$("#player")[0].player.shield); $("#lifeHUD").html("life: "+$("#player")[0].player.replay); 请记住,gameQuery写的是html节点,不过你仍然可以使⽤节点上的jQuery⽅法! 5.2 HTML⽂件 html⽂件做了两件事,第⼀件好是聚合了jQuery、gameQuery以及你⾃⼰写的javascript代码。然后它 向⼴⼤男⽣⼥⽣⽤户展现了⼀个不需要任何javascript参与的静态游戏窗⼜。这⾥我仅仅展⽰欢饮窗⼜以 及⼀个“click here”的⽂字。 gameQuery - Tutorial 1 - Final result 5.3 开始按钮 到这⼀步,你已经做了所⽤⼯作,除了“开始游戏”按钮。为了实现这个,我们只需要使⽤startGame⽅ 法。你可以在你代码的最后部分调⽤这个⽅法,这样游戏就能⾃动开始,但我觉得把何时开始游戏的决 定权交给⽤户更好吧。我们可以通过“cick here”⽂字调⽤这个⽅法,就像之前对“restart”所做的⼀样。 //initialize the start button $("#startbutton").click(function(){ $.playground().startGame(function(){ $("#welcomeScreen").fadeTo(1000,0,function() {$(this).remove();}); }); }) 当游戏初始化完毕,所以图⽚声⾳都载⼊的时候,startGame⽅法就会被调⽤。然后我们将欢迎屏幕慢 慢移除。 5.4 进度条 “但,⼤哥,html代码⾥头的loadingbar是什么东东啊?”你可能会这样问⾃⼰。当然这是个好问题,它 其实就是个进度条!gameQuery在游戏开始之前会载⼊所有资源,你可以通过进度条查看资源载⼊情 况。setLoadBar⽅法允许你在⼀个div上设置进度条,进度条随着资源载⼊⽽逐渐增长。你只需要提供 div的名称以及进度条的总长度。 $().setLoadBar("loadingBar", 400); 不过需要提醒你⼀下,进度指⽰器不不是⾮常有效。例如,每个精灵都被认为是同样⼤⼩,⼀个2kb的 精灵和300kb的精灵是⼀样的。对于声⾳素材,指⽰就更不如意了,因为没有任何插件可以探测声⾳载 ⼊情况,所以gamQuery只好尽量猜测了,它认为在最后⼀张图⽚载⼊完之后,声⾳也载⼊完了(当写 此⽂档的时候,对声⾳的⽀持仍然处于实验阶段)。 5.5 结束语 厄,那就这样了。谢谢你能花时间看这个,我希望这能让你确信⽤javascript写游戏并不是神话!我想强 调的是:你在教程中所看到学到的东西或许是开发游戏中最不重要的!当你制作游戏的时候应该关注界 ⾯友好以及可玩性!所以我推荐你能花时间在这两个领域上(但我还做到:D)。我⾮常期待你能⽤这 个js库(或者其他)做出不同寻常的东西。
还剩33页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

jwjiewei

贡献于2016-03-30

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf