精通JavaScript--11使用CanvasAPI创建游戏
11.1 在Canvas中的基本绘画操作
所有在canvas表面出现的绘图只通过JavaScript实现,所有的canvas都默认为空白。在绘画前,我们需要先获取对<canvas>元素二维绘图上下文(two-dimensional drawing context)的引用,也就是可以返回一个我们希望能在其上绘画的表面的引用。将来,根据canvas规范进行的开发,可能会按照不同的需求建立额外的上下文,如现在已经可以通过WebGL规范(http://webgl.org)来在canvas中建立三维图形了。我们很容易就能获取绘图上下文的引用,只需执行canvas DOM元素的getContext()方法即可。从该引用中可以得到并执行一系列的方法,以在canvas上绘出各种图形和添加文字,如代码清单11-1所示。
代码清单11-1 Canvas中的基本绘图操作
1 //创建一个新的<canvas>元素 2 var canvas=document.createElement("canvas"), 3 4 //获取对canvas绘图上下文的引用 5 context=canvas.getContext("2d"); 6 //设置canvas的尺寸 7 canvas.width=200; 8 canvas.height=200; 9 10 //默认状态下,canvas是空白的。如果我们需要在绘图之后情况canvas的内容,则可以执行此函数 11 function emptyCanvas(){ 12 13 //从canvas的左上角起计,到距离左上角200px*200px的位置,抹除区域内容 14 context.clearRect(0,0,200,200); 15 } 16 17 //当绘图上下文建立后,我们就可以执行任何绘图命令,在空白的canvas上进行绘图了。例如,如果我们想要 18 //在canvas的左上角画一个圆,我们可以执行以下函数 19 function drawCircle(){ 20 21 //首先,我们要告诉绘图上下文我们在建立一条路径。本质上来说,路径就是在一个点与另一个点之间的一条线, 22 //但在两点之间可以按任意路径方式连结 23 24 context.beginPath(); 25 26 //绘图上下文的arc()方法告诉路径采用一段弧形。方法的前两个参数指出弧的开始位置(圆心), 27 //分别沿x轴和y轴以像素计;第3个参数指出弧的尺寸(圆半径),以像素计;最后2个参数分别 28 //指出弧的起始位置和结束位置,以弧度计。如果要画一个圆,则可以令开始位置为0,结束位置为PI的2倍,即表示一段360°的弧. 29 context.arc(100,100,100,0,2*Math.PI); 30 31 //默认状态下,此线的路径是不可见的,然而,stroke()方法可以沿着路径画出可见的线条, 32 //使得形状的轮廓可见。我们还可以使用fill()方法将圆以实心颜色填充 33 context.stroke(); 34 } 35 36 //画直线与画圆的方法类似,我们必须先对线段进行定义,再调用stroke()方法来真正把可视图像化的“墨水”画到canvas上 37 function drawLine(){ 38 39 //移动绘图上下文位置到点位50px(从canvas的左边沿器计) * 40px (从canvas的上边沿起计) 40 context.moveTo(50,40); 41 42 //从绘图上下文的当前点位,向点位150px * 160px,拟出一条直线,但没有真正在canvas画出可视的直线 43 context.lineTo(150,160); 44 45 //把“墨水”应用到canvas上,来填充刚刚拟定的直线 46 context.stroke(); 47 } 48 49 //定义一个函数,使用绘图上下文的fillRect()方法,在canvas上画出一个红色的正方形。在实施填充动作前, 50 //先设定所使用的绘制颜色 51 function drawSquare(){ 52 53 //为下一个绘制动作设定填充样式,#FF0000是红色的十六进制色码值表示 54 context.fillStyle="#FF0000"; 55 56 //在点位200px * 200px绘制一个边长为100px的红色正方形 57 context.fillRect(20,20,100,100); 58 } 59 60 //我们甚至还可以在我们的canvas上添加文本。方法是:使用绘画上下文的fillText()和strokeText(), 61 //如以下函数所示 62 function writeText(){ 63 64 //先设定在canvas绘制文本所使用的字体样式 65 context.font="30px Arial"; 66 67 //在canvas上写一些文本,绘写起始位置为0px * 0px 68 context.fillStyle="#000"; 69 context.fillText("Filled Text",0,30); 70 71 //在已绘写的文本下方(起始位置为0px * 400px),写一些描边的文本 72 context.strokeText("Outlined Text",0,70); 73 } 74 75 //执行若干我们所定义的绘图函数,将它们的形状和添加到canvas上 76 emptyCanvas(); 77 drawCircle(); 78 drawLine(); 79 drawSquare(); 80 writeText(); 81 82 window.addEventListener("load",function(){ 83 document.body.appendChild(canvas); 84 },false);
11.2 高清Canvas元素
11.3 使用Canvas制作游戏
□ 设定出游戏面板(棋盘)或游戏中的世界,定义出游戏中各种行动的约束。
□ 对用户操控的游戏角色以及在游戏面板中出现的敌人或物体进行绘制,并使之产生动画效果,同时在游戏面板中对每一项物体的位置进行跟踪。
□ 使用各种输入机制(如键盘,点击,屏幕触摸,动作及其他相关的输入设备)来控制游戏角色在游戏面板中的行动。
□ 设置一个随着游戏进行而不断更新的分值,记录历史最高分,追踪游戏角色剩余的生命值,并且(或者)设置游戏角色完成关卡的剩余时间。
□ 在游戏面板上检测一个或多个游戏角色或物体相互之间的碰撞,并对游戏角色所损失的生命值,完成关卡或游戏等情况进行处理。
现在,让我们来详细了解一下,如何使用Canvas API来按一定的方式为上述每一项内容编写代码,并将其进行整合,以形成一个可运行的游戏。
11.3.1 在Canvas上绘制图像
大多数的游戏都会涉及屏幕上图像的移动。玩家的角色极少会用一个简单的形状来表示,如一个圆或正方形,这是在游戏中使用的图像当中最容易设计的游戏图案。需要将来自各个文件的图像绘制到canvas上。可以使用canvas绘图上下文的drawImage()方法,将一个<img>元素的引用以及需要在canvas上绘图的位置作为参数传给该方法,如代码清单11-2所示。
代码清单11-2 在Canvas上绘制图像
1 //创建一个新的<canvas>元素,以在其上绘制图像 2 var canvas=document.createElement("canvas"), 3 4 //获取<canvas>元素的绘图上下文 5 context=canvas.getContext("2d"), 6 7 //创建一个新的<img>元素,来引用所要绘制到<canvas>上的图像 8 img=document.createElement("img"); 9 10 //指派一个函数(里面那个匿名函数),在所引用要绘制<canvas>上的图像 11 //(在图像的src属性被设置之前,图像不会开始加载) 12 img.addEventListener("load",function(){ 13 14 //在<canvas>元素上绘制图像,位置为0px * 0px,即<canvas>元素的左上角 15 context.drawImage(img,0,0); 16 },false); 17 18 //设置<img>元素的src属性,指向我们所希望在<canvas>元素上显示图像的路径位置。这样,图像将会加载, 19 //并且,之前所指派的事件处理函数将会执行 20 img.src="worker.png"; 21 22 //一旦页面加载完成,便把这个新的<canvas>元素添加到当前页面末端 23 window.addEventListener("load",function(){ 24 25 document.body.appendChild(canvas); 26 },false);
使用图片拼合技术(Sprite Map Images)避免加载多个图片
在网页上,要避免加载多张需在页面中一起使用的图片,可以使用一项通用的技术。这项技术把多张图片拼合为一张大图,这张大图当中包含了所有独立的小图片。通过减少所产生的HTTP请求数量(HTTP请求在浏览器端及服务器端都要作出响应而耗用处理资源),有助于提高性能。在标准的网页页面,各个独立图片可以从一张更大的图片中抽取出来进行显示。方法是,利用CSS的background-position属性,结合width和height属性来实现。对于在canvas上显示各张图片的情况,可以在drawImage()方法中,通过各项参数的变化,来实现从一张较大的拼合图片中抽取出图片的较小部分进行显示,如代码清单11-3所示。
代码清单11-3 从一张拼合图片中绘制一个独立的图像到canvas上
1 var canvas=document.createElement("canvas"), 2 context=canvas.getContext("2d"), 3 img=document.createElement("img"); 4 5 img.addEventListener("load",function(){ 6 var individualImagePositionTop=200,//实为独立图片的左位置 7 individualImagePositionLeft=150,//实为独立图片的上位置 8 individualImageWidth=300, 9 individualImageHeight=40, 10 displayPositionTop=100,//实为显示点的左位置 11 displayPositionLeft=100,//实为显示点的上位置 12 displayWidth=150, 13 displayHeight=40;//如果渲染为原来尺寸的一半,此处应为20 14 15 //小图片来自大图片的200px * 150px点位,小图片的尺寸为300px * 40px 。把小图片绘制在<canvas>元素 16 //点位100px * 100px处,按原来尺寸的一半进行渲染,其实只是宽度缩到原来的一半,高度不变,仍为40px。 17 context.drawImage(img,individualImagePositionTop,individualImagePositionLeft,individualImageWidth,individualImageHeight,displayPositionTop,displayPositionLeft,displayWidth,displayHeight); 18 },false); 19 20 img.src="worker.png"; 21 22 window.addEventListener("load",function(){ 23 document.body.appendChild(canvas); 24 },false);
11.3.2 Canvas中的动画
对于所有游戏,动画都是一项最基本的元素。正是元素因素使得Canvas API成为了制作游戏的一个绝佳平台。它需要实现对其中所绘制的像素进行位置更新和外观改变的支持。因为canvas的内容是通过一些在某一固定空间内的像素来表示的,而不是其他的表现形式(如矢量图),我们没有办法对一个单独的图片、形状或canvas中其他部分进行定位,而且,我们也不能在不影响canvas上其余内容的前提下去更新上述内容。为了制作出动画效果,我们需要以足够快的频率对canvas的内容进行重新渲染,以使肉眼无法识别出任何生硬的变化而能看到流畅的动画。我们把canvas的各个组成部分绘制出来,然后在经过某一固定的时间之后,清空canvas,并重新对内容进行绘制,在有需要的情况下,将元素绘制于新位置上。通过每一秒的多次重新绘制,我们就可以实现动画效果了。
代码清单11-4演示了一个简单的动画,即一个圆在一个<canvas>元素上移动。每50ms对canvas进行重绘,并在每一次重绘时都根据新位置来更新圆。
1 var canvas=document.createElement("canvas"), 2 context=canvas.getContext("2d"), 3 4 //定义要在canvas上绘制的圆的位置,尺寸和属性 5 leftPosition=0, 6 topPosition=100, 7 radius=100, 8 startDegree=0, 9 endDegree=2 *Math.PI;//即弧度为360度 10 11 //定义一函数,按一定的时间周期执行,以更新圆的位置,并在新位置重绘该圆 12 function animate(){ 13 14 //根据该圆将会在屏幕中出现的点位更新位置数据 15 leftPosition++; 16 17 //清空canvas的内容 18 context.clearRect(0,0,canvas.width,canvas.height); 19 20 //在新位置把圆绘制到canvas上 21 context.beginPath(); 22 context.arc(leftPosition,topPosition,radius,startDegree,endDegree); 23 context.stroke(); 24 } 25 26 //每50ms执行一次animate(),每一次都在更新后的位置重绘该圆 27 setInterval(animate,50); 28 29 window.addEventListener("load",function(){ 30 document.body.appendChild(canvas); 31 },false);
11.3.3 游戏的控制
所有的游戏都会根据运行该游戏的设备的某种形式的数据作出响应,否则,该游戏将会相当乏味。大多数情况下,这当中会涉及到控制一个主要游戏角色,躲避沿途遇到的敌人和障碍物,尝试运用某种灵活技巧来确保游戏角色达到目标。在台式电脑上,通常可以通过按压键盘上特定的按键或移动,点击鼠标来控制游戏角色的位置。在移动设备上,对游戏角色的控制则可能通过对触屏的点触或以该设备某种方式的旋转,移动来实现。因为基于canvas制作的游戏会在这两种设备上运行,我们应该确保所制作的任何游戏都可以按照任何游戏都可以按照任何种类的设备配置的输入方式进行控制。
代码清单11-5演示了如何获取特定的按键或屏幕上的点触,以实现对一个基于canvas制作的游戏控制。
代码清单11-5 获取输入以实现对游戏角色的控制
1 var canvas = document.createElement("canvas"); 2 3 //定义一个函数,以供在<canvas>上移动玩家的角色时调用 4 function move(direction) { 5 //在这里插入代码,用于更新游戏角色在canvas上的位置 6 alert(direction); 7 } 8 9 //当玩家按压键盘上的各个方向键时,将玩家角色按相应的方法移动 10 window.addEventListener("keydown", function(event) { 11 12 //根据各方向键的按键编码定义按键变量 13 var LEFT_ARROW = 37, 14 UP_ARROW = 38, 15 RIGHT_ARROW = 39, 16 DOWN_ARROW = 40; 17 18 //基于所按压的方向键,把相应的方向作为参数传给move()函数,执行该函数。忽略任何其他按键的按压 19 switch(event.keyCode) { 20 case LEFT_ARROW: 21 move("left"); 22 break; 23 case RIGHT_ARROW: 24 move("right"); 25 break; 26 case UP_ARROW: 27 move("up"); 28 break; 29 case DOWN_ARROW: 30 move("down"); 31 break; 32 } 33 }, false); 34 35 //当玩家在触屏上点触<canvas>的某些位置时,根据屏幕所被点触的位置,按相应方向移动玩家角色 36 canvas.addEventListener("touchstart",function(event){ 37 38 //获取对屏幕上点触位置的引用,以<canvas>的左上角起计,按像素计量 39 var touchLeft=event.targetTouches[0].clientX, 40 touchTop=event.targetTouches[0].clientY; 41 42 //基于<canvas>元素的点击位置,把相应的方向作为参数传给move()函数,执行该函数 43 if(touchLeft<(canvas.width/8)){ 44 move("left"); 45 }else if(touchLeft>(3*canvas.width/8)){ 46 move("right"); 47 }else if(touchTop<(canvas.height /8)){ 48 move("up"); 49 }else if(touchTop>(3*canvas.height /8)){ 50 move("dowm"); 51 } 52 53 },false); 54 55 window.addEventListener("load",function(){ 56 document.body.appendChild(canvas); 57 },false);
11.3.4 碰撞检测
到目前为此,我们已经知道了如何对<canvas>元素上游戏中的各个图形元素进行绘制、动画效果制作以及控制。下一步要处理的是,当玩家角色和障碍物或敌人发生接触时所发生的事情。在游戏开发术语上,这叫做碰撞处理。在很多游戏中,当玩家角色与敌人角色发生碰撞时,玩家角色会遭受一些伤害或损失一条命。因为<canvas>元素只包含像素数据,我们没有办法将一个角色与另一个角色直接通过JavaScript来扫描元素的可视化内容并进行区分。在游戏中,我们所需要做的是持续处理我们的主要角色和所有的障碍我及敌人的位置。因此,我们必须要想办法计算出他们在各自的动画系列中的下一个动作。我们可以获取每个元素的位置数据,并使用一个函数对它们进行比较,以判断出包围着玩家角色的边界与包围着障碍物或敌人的边界是否发生相交。代码清单11-6中的代码演示了一个示例函数,它可以用来判断玩家角色是否<canvas>内的其他元素发生碰撞。
代码清单11-6 简单碰撞测试
1 //定义一个函数,来判断玩家角色的边界是否与一个障碍物或敌人的边界发生相交(引发一次碰撞) 2 function intersects(characterLeft,characterWidth,characterTop,characterHeight,obstacleLeft,obstacleWidth,obstacleTop,obstacleHeight){ 3 4 //定义2个布尔值变量,指出碰撞是否在y轴出现,碰撞是否在x轴出现 5 var doesIntersectVertically=false, 6 doesIntersectHorizontally=false, 7 8 //基于所提供的各个参数,确立角色与障碍物的边界 9 characterRight=characterLeft+characterWidth; 10 characterBottom=characterTop+characterHeight; 11 obstacleRight=obstacleLeft+obstacleWidth; 12 obstacleBottom=obstacleTop+obstacleHeight; 13 14 //如果角色的上部位置处于障碍物的上下位置之间,或角色的下部位置处于障碍物的上下位置之间 15 //则在y轴上发生碰撞 16 if((characterTop>obstacleTop && characterTop<obstacleBottom) ||(characterBottom>obstacleTop && characterTop <obstacleBottom)){ 17 doesIntersectVertically=true; 18 } 19 20 if((characterLeft>obstacleLeft && characterLeft <obstacleRight)||(characterRight>obstacleLeft && characterLeft <obstacleRight)){ 21 doesIntersectHorizontally=true; 22 } 23 24 //如果角色与障碍我在x轴和y轴均发生相交,则碰撞成立 25 return doesIntersectVertically && doesIntersectHorizontally; 26 }
11.3.5 游戏主循环
游戏主循环是一个根据固定的时间间隔不断地重复执行的函数。实质上,这就是游戏的核心。它负责更新游戏面板中各角色的位置,检测所发生的碰撞,将<canvas>元素中的各个角色在新位置进行渲染。虽然玩家的输入可能会随时出现,以尝试去更新角色在屏幕上的位置,但只有在下一轮的游戏主循环函数发生时,角色才会根据这个输入在新位置进行绘制。
有一项技术可以确保游戏主循环以特定的时间间隔运行,使动画按一个固定的帧频出现,以实现流畅的效果。它就是,利用浏览器的setInterval()函数,如代码清单11-7所示。
代码清单11-7 利用setInterval()函数,以一个固定的帧频运行游戏主循环
1 //定义一个函数,用作游戏主循环 2 function gameLoop(){ 3 //更新角色的位置数据,进行碰撞测试,并在新位置绘制各个角色 4 } 5 6 //每50ms执行一次gameLoop()函数,所产生的帧频为每秒20帧(=1000/50) 7 setInterval(gameLoop,50);
使用setInterval()函数来运行游戏主循环会有一个问题,那就是,如果下一次初始化出现前,浏览器还没有及时完成为游戏主循环函数代码的处理,则积压的代码会聚集并使得浏览器像是被锁定了一样(这种现象称为假死),或会引用所有动画效果出现频,卡的现象。
这可是个大问题。幸运的是,浏览器制造厂商已经在围绕这个问题研究解决之道了。与其由你自己去开发能够不受代码本身对浏览器的影响的情况下运行的代码,不如让浏览器来告诉你,它在什么时候可供使用并能够继续处理更多的指令。这是通过调用在window对象上的requestAnimationFrame()方法来实现的。该方法会被传入一个函数作为其参数,并会下一个合适的时机,由浏览器执行。将这个方法与计时器结合起来,可以确保各指令能够根据一个固定的帧频执行。我们赋予浏览器更多的控制权,从而使得更加流畅的动画效果得以实现,如代码清单11-8所示。因为一些对于跨浏览器在命名处理上的差异(直至规范确认下来),我们需要加一段简单的保障代码(polyfill)来确保跨浏览器操作正常,如代码清单11-8开始部分所示。代码清单11-8演示了一个简单的游戏主循环例子。
代码清单11-8 使用requestAnimationFrame方法运行游戏主循环
1 //为实现各种现代浏览器的requestAnimationFrame()方法,创建一段简单的跨浏览器保障代码(polyfill), 2 //以实现流畅,高效的动画。由Paul Irish编写,网址为:https://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ 3 window.requestAnimationFrame=(function(){ 4 5 return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame ||function(callback){ 6 window.setTimeout(callback,1000/60); 7 } 8 })(); 9 10 //用一个局部变量保存上一次游戏主循环开始的时间。初始化时使用当前时间 11 var lastTimeGameLoopRan=(new Date()).getTime(), 12 13 //定义我们希望游戏主循环来重新渲染canvas内容的刷新频率。20ms的刷新频率所实现的帧频是 14 //每秒50帧(=1000/20) 15 refreshRate=20; 16 17 //定义一个函数,用作游戏的主循环 18 function gameLoop(){ 19 20 //获取当前时间,并由当前时间与上一次游戏主循环运行的时间计算出时间差 21 var currentTime=(new Date()).getTime(), 22 timeDifference=currentTime -lastTimeGameLoopRan; 23 24 //当下一动画帧准备好可供浏览器使用时,再次执行此函数。在浏览器的性能和约束范围内使游戏主循环 25 //持续进行。最终,从玩家角度来说,这是所能实现的最佳效果 26 window.requestAnimationFrame(gameLoop); 27 28 //对于执行本次gameLoop()函数的时间与上一轮游戏主循环执行的时间,这两者之间的事件差如果大于或 29 //等于之前所定义的刷新频率,则运行一些典型游戏主循环操作 30 if(timeDifference>=refreshRate){ 31 32 //更新角色的位置,检测各种碰撞的发生,在新位置绘制各个角色 33 34 //更新记录上次游戏主循环发生的时间的变量值。这样,下一次游戏主循环才能在正确的时间发生 35 lastTimeGameLoopRan=currentTime; 36 } 37 } 38 39 //开始第一遍游戏主循环 40 gameLoop();
11.3.6 分层Canvas以提高性能
对<canvas>元素的每一个绘制操作都会耗用一定的执行时间。如果你设计的是一个很多角色的复杂游戏,则将这些绘制操作乘以很多次,你就会开始意识到,每一个绘制操作都会与性能相关。因此,只要有可能,就要尽量避免重新绘制游戏中的静态部分。例如,对于背景是静态的那些游戏,创建2个<canvas>元素是很有用的。一个用于绘制背景,另一个用于所有的角色移动和动画效果的定时更新。可以使用css来把这2个<canvas>元素放置在一起。包含着所有移动动作的<canvas>元素放在包含背景的<canvas>元素的上方。背景只需绘制一次,而且不再需要更新。
11.4 在Canvas中制作Frogger游戏
https://en.wikipedia.org/wiki/Frogger
字体:http://www.dafont.com/arcade-classic-pizz.font
代码清单11-9 一个HTML页面,用来放置用Canvas制作的Frogger游戏
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <title></title> 6 <meta name="viewport" content="width=480,initial-scale=1.0"/> 7 <style type="text/css"> 8 @font-face { 9 font-family:"Arcade Classic"; 10 src: url("ARCADECLASSIC.TTF") format("woff"); 11 font-weight: normal; 12 font-style: normal; 13 } 14 .canvas{ 15 position: absolute; 16 top: 0; 17 left: 0; 18 border: 2px solid #000; 19 width: 480px; 20 height: 640px; 21 } 22 </style> 23 </head> 24 <body> 25 <canvas id="background_canvas" class="canvas" width="960" height="1280"></canvas> 26 <canvas id="canvas" class="canvas" width="960" height="1280"></canvas> 27 28 </body> 29 </html>
准备好HTML页面后,我们可以着手处理JavaScript了。代码清单11-10演示了游戏代码是如何开始编写的。我们建立一个命名空间来放置代码。定义一些关键属性和方法,以便在其余的代码编写中在需要处使用。采用观察者设计模式,用于在游戏代码中实现各代码模块之间的通信。
代码清单11-10 定义命名空间、关键属性和方法以便在整体游戏代码中使用
1 //在一个单独的全局变量下定义一个命名空间来包含我们的游戏代码 2 var Frogger=(function(){ 3 4 //找出页面中的主<canvas>元素 5 var canvas=document.getElementById("canvas"), 6 7 //获取对<canvas>元素的2D绘制屏幕上下文引用 8 drawingSurfase=canvas.getContext("2d"), 9 10 //找出页面中的背景<canvas>元素 11 backgroundCanvas=document.getElementById("background_canvas"), 12 13 //获取对背景<canvas>元素的2D绘制平面上下文引用 14 backgroundDrawingSurfase=backgroundCanvas.getContext("2d"), 15 16 //获取对<canvas>元素的width和height的引用,以像素计量 17 drawingSurfaseWidth=canvas.width, 18 19 drawingSurfaseHeight=canvas.height; 20 21 return { 22 23 //暴露以下内容,以供其他代码模块使用:<canvas>元素、它的2D绘制平面上下文引用,它的宽和高 24 canvas:canvas, 25 drawingSurfase:drawingSurfase, 26 drawingSurfaseWidth:drawingSurfaseWidth, 27 drawingSurfaseHeight:drawingSurfaseHeight, 28 29 //暴露背景元素的2D绘制平面上下文引用 30 backgroundDrawingSurfase:backgroundDrawingSurfase, 31 32 //定义一个对象,其中包含游戏中各角色可以移动方向的引用。我们在这里进行全局化的定义, 33 //以便在整体代码中都能使用 34 direction:{ 35 UP:"up", 36 DOWN:"down", 37 LEFT:"left", 38 RIGHT:"right" 39 }, 40 41 //定义观察者模式方法subscribe()和publish(),来实现应用程序内的通信,避免紧解耦合模块的 42 //使用。参考第7章来了解该设计模式的更详细内容 43 observer:(function(){ 44 var events={}; 45 46 return { 47 subscribe:function(eventName,callback){ 48 49 if(!events.hasOwnProperty(eventName)){ 50 events[eventName]=[]; 51 } 52 53 events[eventName].push(callback); 54 }, 55 56 publish:function(eventName){ 57 var data=Array.prototype.slice.call(arguments,1), 58 index=0, 59 length=0; 60 61 if(events.hasOwnProperty(eventName)){ 62 length = events[eventName].length; 63 64 for (;index<length; index++) { 65 events[eventName][index].apply(this,data); 66 } 67 } 68 } 69 }; 70 }()), 71 //定义一个方法,来判断在游戏面板中的2个物体是否在水平方向发生相交。传入2个对象作为参数, 72 //每个对象都有一个left和right属性,以像素指出该物体在游戏面板中位置的最左和最右点位。这样 73 //我们就可以确定两者是否出现水平相交。如果是,并且它们在游戏面板中都处于同一行,那么可以 74 //认为这两个物体发生了碰撞 75 intersects:function(position1,position2){ 76 var doesIntersect=false; 77 78 if((position1.left> position2.left && position1.lefth<position2.right)|| (position1.right > position2.left && position1.left < position2.right) ){ 79 doesIntersect=true; 80 } 81 return doesIntersect; 82 } 83 }; 84 }());
对于接下来的每一个代码清单,需要在HTML页面(见代码清单11-9)中,依次在<script>中引用,以查看完成的结果。
现在,让我们建立游戏的核心逻辑,包括游戏状态、游戏主循环、分数出来,以及处理玩家的剩余生命条数和完成关卡的剩余时间,如代码清单11-11所示。
代码清单11-11 Frogger的核心游戏逻辑