在上一篇《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(睜眼)),這樣我們看到的效果就是睜眼的時間稍長。
下面是運行后的效果: