文章首發於個人 GitHub 博客
上一篇文章:《Chrome 小恐龍遊戲源碼探究七 -- 晝夜模式交替》實現了遊戲晝夜模式的交替,這一篇文章中,將實現:一、小恐龍的繪製 二、鍵盤對小恐龍的控制 三、頁面失焦後,從新聚焦會重置小恐龍的狀態git
定義小恐龍類 Trex
:github
/** * 小恐龍類 * @param {HTMLCanvasElement} canvas 畫布 * @param {Object} spritePos 圖片在雪碧圖中的座標 */ function Trex(canvas, spritePos) { this.canvas = canvas; this.ctx = canvas.getContext('2d'); this.spritePos = spritePos; this.xPos = 0; this.yPos = 0; this.groundYPos = 0; // 小恐龍在地面上時的 y 座標 this.currentFrame = 0; // 當前的動畫幀 this.currentAnimFrames = []; // 存儲當前狀態的動畫幀在雪碧圖中的 x 座標 this.blinkDelay = 0; // 眨眼間隔的時間(隨 機) this.blinkCount = 0; // 眨眼次數 this.animStartTime = 0; // 小恐龍眨眼動畫開始時間 this.timer = 0; // 計時器 this.msPerFrame = 1000 / FPS; // 幀率 this.status = Trex.status.WAITING; // 當前的狀態 this.config = Trex.config; this.jumping = false; // 是否跳躍 this.ducking = false; // 是否閃避(俯身) this.jumpVelocity = 0; // 跳躍的速度 this.reachedMinHeight = false; // 是否達到最低高度 this.speedDrop = false; // 是否加速降低 this.jumpCount = 0; // 跳躍的次數 this.jumpspotX = 0; // 跳躍點的 x 座標 this.init(); }
相關的配置參數:canvas
Trex.config = { GRAVITY: 0.6, // 引力 WIDTH: 44, // 站立時的寬度 HEIGHT: 47, WIDTH_DUCK: 59, // 俯身時的寬度 HEIGHT_DUCK: 25, MAX_JUMP_HEIGHT: 30, // 最大跳躍高度 MIN_JUMP_HEIGHT: 30, // 最小跳躍高度 SPRITE_WIDTH: 262, // 站立的小恐龍在雪碧圖中的總寬度 DROP_VELOCITY: -5, // 下落的速度 INITIAL_JUMP_VELOCITY: -10, // 初始跳躍速度 SPEED_DROP_COEFFICIENT: 3, // 下落時的加速係數(越大下落的越快) INTRO_DURATION: 1500, // 開場動畫的時間 START_X_POS: 50, // 開場動畫結束後,小恐龍在 canvas 上的 x 座標 }; Trex.BLINK_TIMING = 7000; // 眨眼最大間隔的時間 // 小恐龍的狀態 Trex.status = { CRASHED: 'CRASHED', // 撞到障礙物 DUCKING: 'DUCKING', // 正在閃避(俯身) JUMPING: 'JUMPING', // 正在跳躍 RUNNING: 'RUNNING', // 正在奔跑 WAITING: 'WAITING', // 正在等待(未開始遊戲) }; // 爲不一樣的狀態配置不一樣的動畫幀 Trex.animFrames = { WAITING: { frames: [44, 0], msPerFrame: 1000 / 3 }, RUNNING: { frames: [88, 132], msPerFrame: 1000 / 12 }, CRASHED: { frames: [220], msPerFrame: 1000 / 60 }, JUMPING: { frames: [0], msPerFrame: 1000 / 60 }, DUCKING: { frames: [264, 323], msPerFrame: 1000 / 8 }, };
補充本篇文章中會用到的一些數據:segmentfault
Runner.config = { // ... BOTTOM_PAD: 10, // 小恐龍距 canvas 底部的距離 MAX_BLINK_COUNT: 3, // 小恐龍的最大眨眼次數 }; Runner.spriteDefinition = { LDPI: { // ... TREX: {x: 848, y: 2}, // 小恐龍 }, };
而後來看下 Trex
原型鏈上的方法。咱們首先來繪製靜態的小恐龍:dom
Trex.prototype = { // 初始化小恐龍 init: function() { // 獲取小恐龍站在地面上時的 y 座標 this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT - Runner.config.BOTTOM_PAD; this.yPos = this.groundYPos; // 小恐龍的 y 座標初始化 this.draw(0, 0); // 繪製小恐龍的第一幀圖片 }, /** * 繪製小恐龍 * @param {Number} x 當前幀相對於第一幀的 x 座標 * @param {Number} y 當前幀相對於第一幀的 y 座標 */ draw: function(x, y) { // 在雪碧圖中的座標 var sourceX = x + this.spritePos.x; var sourceY = y + this.spritePos.y; // 在雪碧圖中的寬高 var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ? this.config.WIDTH_DUCK : this.config.WIDTH; var sourceHeight = this.config.HEIGHT; // 繪製到 canvas 上時的高度 var outputHeight = sourceHeight; // 躲避狀態. if (this.ducking && this.status != Trex.status.CRASHED) { this.ctx.drawImage( Runner.imageSprite, sourceX, sourceY, sourceWidth, sourceHeight, this.xPos, this.yPos, this.config.WIDTH_DUCK, outputHeight ); } else { // 躲閃狀態下撞到障礙物 if (this.ducking && this.status == Trex.status.CRASHED) { this.xPos++; } // 奔跑狀態 this.ctx.drawImage( Runner.imageSprite, sourceX, sourceY, sourceWidth, sourceHeight, this.xPos, this.yPos, this.config.WIDTH, outputHeight ); } this.ctx.globalAlpha = 1; }, };
前面進入街機模式那一章中,用到了 Trex 類中的數據,臨時定義了 Trex 類,別忘了將其刪除。
接下來須要經過 Runner
類調用 Trex
類。添加屬性用於存儲小恐龍類的實例:函數
function Runner(containerSelector, opt_config) { // ... + this.tRex = null; // 小恐龍 }
初始化小恐龍類:動畫
Runner.prototype = { init: function () { // ... + // 加載小恐龍類 + this.tRex = new Trex(this.canvas, this.spriteDef.TREX); }, };
這樣在遊戲初始化時就繪製出了靜態的小恐龍,如圖:this
遊戲初始化以後,小恐龍會隨機眨眼睛。默認的是最多隻能眨三次。下面將實現這個效果。google
添加更新小恐龍的方法:spa
Trex.prototype = { /** * 更新小恐龍 * @param {Number} deltaTime 間隔時間 * @param {String} opt_status 小恐龍的狀態 */ update: function(deltaTime, opt_status) { this.timer += deltaTime; // 更新狀態的參數 if (opt_status) { this.status = opt_status; this.currentFrame = 0; this.msPerFrame = Trex.animFrames[opt_status].msPerFrame; this.currentAnimFrames = Trex.animFrames[opt_status].frames; if (opt_status == Trex.status.WAITING) { this.animStartTime = getTimeStamp(); // 設置眨眼動畫開始的時間 this.setBlinkDelay(); // 設置眨眼間隔的時間 } } if (this.status == Trex.status.WAITING) { // 小恐龍眨眼 this.blink(getTimeStamp()); } else { // 繪製動畫幀 this.draw(this.currentAnimFrames[this.currentFrame], 0); } if (this.timer >= this.msPerFrame) { // 更新當前動畫幀,若是處於最後一幀就更新爲第一幀,不然更新爲下一幀 this.currentFrame = this.currentFrame == this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1; // 重置計時器 this.timer = 0; } }, // 設置眨眼間隔的時間 setBlinkDelay: function() { this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING); }, // 小恐龍眨眼 blink: function (time) { var deltaTime = time - this.animStartTime; // 間隔時間大於隨機獲取的眨眼間隔時間才能眨眼 if (deltaTime >= this.blinkDelay) { this.draw(this.currentAnimFrames[this.currentFrame], 0); // 正在眨眼 if (this.currentFrame == 1) { console.log('眨眼'); this.setBlinkDelay(); // 從新設置眨眼間隔的時間 this.animStartTime = time; // 更新眨眼動畫開始的時間 this.blinkCount++; // 眨眼次數加一 } } }, };
而後將小恐龍初始更新爲等待狀態:
Trex.prototype = { init: function () { // ... this.update(0, Trex.status.WAITING); // 初始爲等待狀態 }, };
最後在 Runner
的 update
方法中調用 Trex
的 update
方法來實現小恐龍眨眼:
Runner.prototype = { update: function () { // ... // 遊戲變爲開始狀態或小恐龍尚未眨三次眼 - if (this.playing) { + if (this.playing || (!this.activated && + this.tRex.blinkCount < Runner.config.MAX_BLINK_COUNT)) { + this.tRex.update(deltaTime); // 進行下一次更新 this.scheduleNextUpdate(); } }, };
效果以下:
能夠看到,眨眼的代碼邏輯觸發了 3 次,可是實際小恐龍只眨眼了 1 次。這就是前面說的,小恐龍默認最多隻能眨三次眼。具體緣由以下:
先來看下 Trex
的 update
方法中的這段代碼:
if (this.timer >= this.msPerFrame) { // 更新當前動畫幀,若是處於最後一幀就更新爲第一幀,不然更新爲下一幀 this.currentFrame = this.currentFrame == this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1; // 重置計時器 this.timer = 0; }
這段代碼會將當前動畫幀不斷更新爲下一幀。對於小恐龍來講就是不斷切換睜眼和閉眼這兩幀。若是當前幀爲 「睜眼」,那麼執行 blink
函數後小恐龍仍是睜眼,也就是說實際小恐龍沒眨眼;同理,只有當前幀爲 「閉眼」 時,執行 blink
函數後,小恐龍纔會真正的眨眼。
至於這樣作的目的,就是爲了防止小恐龍不停的眨眼睛。例如,將 blink
函數修改成:
// 小恐龍眨眼 blink: function () { this.draw(this.currentAnimFrames[this.currentFrame], 0); },
這樣小恐龍會不停的眨眼睛。因此須要對其進行限制,這裏 Chrome 開發人員的作法就是:設置一個間隔時間,當小恐龍眨眼的間隔時間大於這個設置的間隔時間,而且當前動畫幀爲 「閉眼」 時,才容許小恐龍眨眼睛。而後每次眨完眼後,從新設置眨眼間隔(默認設置爲 0~7 秒),就實現了小恐龍的隨機眨眼。
下面來實現小恐龍對鍵盤按鍵的響應。
首先,當觸發遊戲彩蛋後,小恐龍會跳躍一次,並向右移動 50 像素(默認設置的是 50 像素)。
添加讓小恐龍開始跳躍的方法:
Trex.prototype = { // 開始跳躍 startJump: function(speed) { if (!this.jumping) { // 更新小恐龍爲跳躍狀態 this.update(0, Trex.status.JUMPING); // 根據遊戲的速度調整跳躍的速度 this.jumpVelocity = this.config.INITIAL_JUMP_VELOCITY - (speed / 10); this.jumping = true; this.reachedMinHeight = false; this.speedDrop = false; } }, };
進行調用:
Runner.prototype = { onKeyDown: function (e) { if (!this.crashed && !this.paused) { if (Runner.keyCodes.JUMP[e.keyCode]) { e.preventDefault(); // ... + // 開始跳躍 + if (!this.tRex.jumping && !this.tRex.ducking) { + this.tRex.startJump(this.currentSpeed); + } } } }, };
這樣,按下空格鍵後,小恐龍仍然會靜止在地面上。接下來還須要更新動畫幀才能實現小恐龍的奔跑動畫。
添加更新小恐龍動畫幀的方法:
Trex.prototype = { // 更新小恐龍跳躍時的動畫幀 updateJump: function(deltaTime) { var msPerFrame = Trex.animFrames[this.status].msPerFrame; // 獲取當前狀態的幀率 var framesElapsed = deltaTime / msPerFrame; // 加速下落 if (this.speedDrop) { this.yPos += Math.round(this.jumpVelocity * this.config.SPEED_DROP_COEFFICIENT * framesElapsed); } else { this.yPos += Math.round(this.jumpVelocity * framesElapsed); } // 跳躍的速度受重力的影響,向上逐漸減少,而後反向 this.jumpVelocity += this.config.GRAVITY * framesElapsed; // 達到了最低容許的跳躍高度 if (this.yPos < this.minJumpHeight || this.speedDrop) { this.reachedMinHeight = true; } // 達到了最高容許的跳躍高度 if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) { this.endJump(); // 結束跳躍 } // 從新回到地面,跳躍完成 if (this.yPos > this.groundYPos) { this.reset(); // 重置小恐龍的狀態 this.jumpCount++; // 跳躍次數加一 } }, // 跳躍結束 endJump: function() { if (this.reachedMinHeight && this.jumpVelocity < this.config.DROP_VELOCITY) { this.jumpVelocity = this.config.DROP_VELOCITY; // 下落速度重置爲默認 } }, // 重置小恐龍狀態 reset: function() { this.yPos = this.groundYPos; this.jumpVelocity = 0; this.jumping = false; this.ducking = false; this.update(0, Trex.status.RUNNING); this.speedDrop = false; this.jumpCount = 0; }, };
其中 minJumpHeight
的屬性值爲:
Trex.prototype = { init: function() { + // 最低跳躍高度 + this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT; // ... }, }
而後進行調用:
Runner.prototype = { update: function () { // ... if (this.playing) { this.clearCanvas(); + if (this.tRex.jumping) { + this.tRex.updateJump(deltaTime); + } this.runningTime += deltaTime; var hasObstacles = this.runningTime > this.config.CLEAR_TIME; // 剛開始 this.playingIntro 未定義 !this.playingIntro 爲真 - if (!this.playingIntro) { + if (this.tRex.jumpCount == 1 && !this.playingIntro) { this.playIntro(); // 執行開場動畫 } // ... } // ... }, };
這樣在按下空格鍵後,小恐龍就會跳躍一次並進行奔跑動畫。如圖:
下面來實現效果:小恐龍第一次跳躍後,向右移動 50 像素。
修改 Trex
的 update
方法。當判斷到正在執行開場動畫時,移動小恐龍:
Trex.prototype = { update: function(deltaTime, opt_status) { this.timer += deltaTime; // 更新狀態的參數 if (opt_status) { // ... } // 正在執行開場動畫,將小恐龍向右移動 50 像素 + if (this.playingIntro && this.xPos < this.config.START_X_POS) { + this.xPos += Math.round((this.config.START_X_POS / + this.config.INTRO_DURATION) * deltaTime); + } // ... }, };
能夠看出當 playingIntro
屬性爲 true
時,小恐龍就會向右移動。因此須要經過控制這個屬性的值來控制小恐龍第一次跳躍後的移動。
修改 Runner
上的 playIntro
方法,將小恐龍標記爲正在執行開場動畫:
Runner.prototype = { playIntro: function () { if (!this.activated && !this.crashed) { + this.tRex.playingIntro = true; // 小恐龍執行開場動畫 // ... } }, };
而後須要在開始遊戲後也就是執行 startGame
方法時,結束小恐龍的開場動畫:
Runner.prototype = { startGame: function () { this.setArcadeMode(); // 進入街機模式 + this.tRex.playingIntro = false; // 小恐龍的開場動畫結束 // ... }, };
效果以下:
能夠很明顯的看到,小恐龍在第一次跳躍後向右移動了一段距離(默認 50 像素)。
在這個遊戲中,當按下 ↓
鍵後,若是小恐龍正在跳躍,就會快速下落,若是小恐龍在地上,就會進入躲閃狀態,下面來實現這些效果。
加速下落:
Trex.prototype = { // 設置小恐龍爲加速下落,當即取消當前的跳躍 setSpeedDrop: function() { this.speedDrop = true; this.jumpVelocity = 1; }, };
設置小恐龍是否躲閃:
Trex.prototype = { // 設置小恐龍奔跑時是否躲閃 setDuck: function(isDucking) { if (isDucking && this.status != Trex.status.DUCKING) { // 躲閃狀態 this.update(0, Trex.status.DUCKING); this.ducking = true; } else if (this.status == Trex.status.DUCKING) { // 奔跑狀態 this.update(0, Trex.status.RUNNING); this.ducking = false; } }, };
在 onKeyDown
方法中調用:
Runner.prototype = { onKeyDown: function () { if (!this.crashed && !this.paused) { if (Runner.keyCodes.JUMP[e.keyCode]) { // ... + } else if (this.playing && Runner.keyCodes.DUCK[e.keyCode]) { + e.preventDefault(); + + if (this.tRex.jumping) { + this.tRex.setSpeedDrop(); // 加速下落 + } else if (!this.tRex.jumping && !this.tRex.ducking) { + this.tRex.setDuck(true); // 進入躲閃狀態 + } + } } }, };
這樣就實現了前面所說的效果。可是小恐龍進入躲閃狀態後,若是鬆開按鍵並不會從新站起來。由於如今尚未定義鬆開鍵盤按鍵時響應的事件。下面來定義:
Runner.prototype = { onKeyUp: function(e) { var keyCode = String(e.keyCode); if (Runner.keyCodes.DUCK[keyCode]) { // 躲避狀態 this.tRex.speedDrop = false; this.tRex.setDuck(false); } }, };
而後調用,修改 handleEvent
方法:
Runner.prototype = { handleEvent: function (e) { return (function (eType, events) { switch (eType) { // ... + case events.KEYUP: + this.onKeyUp(e); + break; default: break; } }.bind(this))(e.type, Runner.events); }, };
效果以下:
第一次跳是正常下落,第二次跳是加速下落
小恐龍的跳躍分爲大跳和小跳,如圖:
要實現這個效果,只須要在 ↑
鍵被鬆開時,當即結束小恐龍的跳躍便可。
修改 onKeyUp
方法:
Runner.prototype = { onKeyUp: function(e) { var keyCode = String(e.keyCode); + var isjumpKey = Runner.keyCodes.JUMP[keyCode]; + if (this.isRunning() && isjumpKey) { // 跳躍 + this.tRex.endJump(); } else if (Runner.keyCodes.DUCK[keyCode]) { // 躲避狀態 this.tRex.speedDrop = false; this.tRex.setDuck(false); } }, };
其中 isRunning
方法定義以下:
Runner.prototype = { // 是否遊戲正在進行 isRunning: function() { return !!this.raqId; }, };
這樣就實現了小恐龍的大跳和小跳。
最後是要實現的效果是:若是頁面失焦時,小恐龍正在跳躍,就重置小恐龍的狀態(也就是會當即回到地面上)。這個效果實現很簡單,直接調用前面定義的 reset
方法便可:
Runner.prototype = { play: function () { if (!this.crashed) { // ... + this.tRex.reset(); } }, };
效果以下:
查看添加或修改的代碼, 戳這裏
Demo 體驗地址:https://liuyib.github.io/blog/demo/game/google-dino/dino-gogogo/
上一篇 | 下一篇 |
Chrome 小恐龍遊戲源碼探究七 -- 晝夜模式交替 | Chrome 小恐龍遊戲源碼探究九 -- 遊戲碰撞檢測 |