在上一篇《Chrome自带恐龙小游戏的源码研究(四)》中实现了障碍物的绘制及移动,从这一篇开始主要研究恐龙的绘制及一系列键盘动作的实现。
会眨眼睛的恐龙
在游戏开始前的待机界面,如果仔细观察会发现恐龙会时不时地眨眼睛。这是通过交替绘制这两个图像实现的:
可以通过一张图片来了解这个过程:
为实现图片的切换,需要一个计时器timer,并且需要知道两张图片切换的时间间隔msPerFrame。当计时器timer的时间大于切换的时间间隔msPerFrame时,将图片切换到下一张,到达最后一张时又从第一张开始,如此反复。下面是实现代码:
1 Trex.config = { 2 BLINK_TIMING:3000, //眨眼间隔 3 WIDTH: 44, //站立时宽度 4 WIDTH_DUCK: 59, //闪避时宽度 5 HEIGHT: 47, //站立时高度 6 BOTTOM_PAD: 10, 7 MIN_JUMP_HEIGHT: 30 //最小起跳高度 8 }; 9 //状态 10 Trex.status = { 11 CRASHED: ‘CRASHED‘, //与障碍物发生碰撞 12 DUCKING: ‘DUCKING‘, //闪避 13 JUMPING: ‘JUMPING‘, //跳跃 14 RUNNING: ‘RUNNING‘, //跑动 15 WAITING: ‘WAITING‘ //待机 16 }; 17 //元数据(metadata),记录各个状态的动画帧和帧率 18 Trex.animFrames = { 19 WAITING: {//待机状态 20 frames: [44, 0],//动画帧x坐标在44和0之间切换,由于在雪碧图中的y坐标是0所以不用记录 21 msPerFrame: 1000 / 3 //一秒3帧 22 }, 23 RUNNING: { 24 frames: [88, 132], 25 msPerFrame: 1000 / 12 26 }, 27 CRASHED: { 28 frames: [220], 29 msPerFrame: 1000 / 60 30 }, 31 JUMPING: { 32 frames: [0], 33 msPerFrame: 1000 / 60 34 }, 35 DUCKING: { 36 frames: [262, 321], 37 msPerFrame: 1000 / 8 38 } 39 }; 40 41 function Trex(canvas,spritePos){ 42 this.canvas = canvas; 43 this.ctx = canvas.getContext(‘2d‘); 44 this.spritePos = spritePos; //在雪碧图中的位置 45 this.xPos = 0; //在画布中的x坐标 46 this.yPos = 0; //在画布中的y坐标 47 this.groundYPos = 0; //初始化地面的高度 48 this.currentFrame = 0; //初始化动画帧 49 this.currentAnimFrames = []; //记录当前状态的动画帧 50 this.blinkDelay = 0; //眨眼延迟(随机) 51 this.animStartTime = 0; //动画开始的时间 52 this.timer = 0; //计时器 53 this.msPerFrame = 1000 / FPS; //默认帧率 54 this.config = Trex.config; //拷贝一个配置的副本方便以后使用 55 this.jumpVelocity = 0; //跳跃的初始速度 56 57 this.status = Trex.status.WAITING; //初始化默认状态为待机状态 58 59 //为各种状态建立标识 60 this.jumping = false; //角色是否处于跳跃中 61 this.ducking = false; //角色是否处于闪避中 62 this.reachedMinHeight = false; //是否到达最小跳跃高度 63 this.speedDrop = false; //是否加速降落 64 this.jumpCount = 0; //跳跃次数 65 66 this.init(); 67 }
首先还是和以往一样,对Trex这个构造函数进行基本的配置,然后在原型链中添加操作方法:
1 Trex.prototype = { 2 init:function() { 3 this.groundYPos = DEFAULT_HEIGHT - this.config.HEIGHT - this.config.BOTTOM_PAD; 4 this.yPos = this.groundYPos; 5 //计算出最小起跳高度 6 this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT; 7 8 this.draw(0,0); 9 this.update(0,Trex.status.WAITING); 10 }, 11 setBlinkDelay:function () { 12 //设置随机眨眼间隔时间 13 this.blinkDelay = Math.ceil(Math.random() * Trex.config.BLINK_TIMING); 14 }, 15 update:function (deltaTime,opt_status) { 16 this.timer += deltaTime; 17 18 if(opt_status) { 19 this.status = opt_status; 20 this.currentFrame = 0; 21 //得到对应状态的帧率 e.g. WAITING 1000ms / 3fps = 333ms/fps 22 this.msPerFrame = Trex.animFrames[opt_status].msPerFrame; 23 //对应状态的动画帧 e.g. WAITING [44,0] 24 this.currentAnimFrames = Trex.animFrames[opt_status].frames; 25 26 if(opt_status === Trex.status.WAITING) { 27 //开始计y时 28 this.animStartTime = getTimeStamp(); 29 //设置延时 30 this.setBlinkDelay(); 31 } 32 } 33 34 //计时器超过一帧的运行时间,切换到下一帧 35 if (this.timer >= this.msPerFrame) { 36 this.currentFrame = this.currentFrame === this.currentAnimFrames.length - 1 ? 37 0 : this.currentFrame + 1; 38 this.timer = 0; //重置计时器 39 } 40 41 //待机状态 42 if(this.status === Trex.status.WAITING) { 43 //执行眨眼动作 44 this.blink(getTimeStamp()); 45 } 46 }, 47 blink:function (time) { 48 var deltaTime = time - this.animStartTime; 49 50 if(deltaTime >= this.blinkDelay) { 51 this.draw(this.currentAnimFrames[this.currentFrame],0); 52 53 if (this.currentFrame === 1) {//0闭眼 1睁眼 54 //设置新的眨眼间隔时间 55 this.setBlinkDelay(); 56 this.animStartTime = time; 57 } 58 } 59 }, 60 draw:function (x,y) { 61 var sourceX = x; 62 var sourceY = y; 63 var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ? 64 this.config.WIDTH_DUCK : this.config.WIDTH; 65 var sourceHeight = this.config.HEIGHT; 66 sourceX += this.spritePos.x; 67 sourceY += this.spritePos.y; 68 69 this.ctx.drawImage(imgSprite, 70 sourceX, sourceY, 71 sourceWidth, sourceHeight, 72 this.xPos, this.yPos, 73 this.config.WIDTH, this.config.HEIGHT); 74 } 75 };
先来看update方法中的这段代码:
1 if (this.timer >= this.msPerFrame) { 2 this.currentFrame = this.currentFrame === this.currentAnimFrames.length - 1 ? 3 0 : this.currentFrame + 1; 4 this.timer = 0; 5 }
这段代码实现了两个帧之间的切换,但如果只是单纯地以相同时间间隔来切换两张图片,那么得到的效果是不正确的,会出现频繁眨眼的情况。而实际情况是,闭眼只是一瞬间,睁开眼睛的时间则比较长。Chrome开发人员非常巧妙地解决了这个问题:
1 blink:function (time) { 2 var deltaTime = time - this.animStartTime; 3 4 if(deltaTime >= this.blinkDelay) { 5 this.draw(this.currentAnimFrames[this.currentFrame],0); 6 7 if (this.currentFrame === 1) {//0闭眼 1睁眼 8 //设置新的眨眼间隔时间 9 this.setBlinkDelay(); 10 this.animStartTime = time; 11 } 12 } 13 }
只要计时器没有超过blinkDelay就不绘制新的图片,这样图片就会停留在上一次绘制的状态,恐龙此时是睁着眼睛的。当时间超过了blinkDelay,即执行眨眼的时间到了,这时会绘制this.currentFrame这一帧。如果这一帧是0(闭眼),由于之前设置了this.timer >= this.msPerFrame时会切换帧,当时间再次超过blinkDelay时,这时就会绘制帧1(睁眼),我们看到的效果就是眼睛闭上只有一瞬然后立刻睁开了。 如果当前帧是1(睁眼),重新设置blinkDelay,于是在deltaTime没有超过重新设置blinkDelay的情况下,都不会绘制新图片(始终保持在帧1(睁眼)),这样我们看到的效果就是睁眼的时间稍长。
下面是运行后的效果: