在上一篇《Chrome自帶恐龍小游戲的源碼研究(七)》中研究了恐龍與障礙物的碰撞檢測,這一篇主要研究組成游戲的其它要素。
游戲分數記錄
如圖所示,分數及最高分記錄顯示在游戲界面的右上角,每達到100分就會出現閃爍特效,游戲第一次gameover時顯示歷史最高分。分數記錄器由DistanceMeter構造函數實現,以下是它的全部代碼:

1 DistanceMeter.dimensions = { 2 WIDTH: 10, //每個字符的寬度 3 HEIGHT: 13, //每個字符的高 4 DEST_WIDTH: 11 //間隙 5 }; 6 DistanceMeter.config = { 7 // 初始時記錄的分數上限為5位數,即99999 8 MAX_DISTANCE_UNITS: 5, 9 10 // 每隔100米距離記錄器的數字出現閃動特效 11 ACHIEVEMENT_DISTANCE: 100, 12 13 // 將移動距離轉化為合理的數值所用的轉化系數 14 COEFFICIENT: 0.025, 15 16 // 每250ms閃動一次 17 FLASH_DURATION: 1000 / 4, 18 19 // 閃動次數 20 FLASH_ITERATIONS: 3 21 }; 22 /** 23 * 距離記錄器 24 * @param {HTMLCanvasElement} canvas 25 * @param {Object} spritePos 雪碧圖上的坐標. 26 * @param {number} canvasWidth 27 * @constructor 28 */ 29 function DistanceMeter(canvas, spritePos, canvasWidth) { 30 this.canvas = canvas; 31 this.canvasCtx = canvas.getContext('2d'); 32 this.image = imgSprite; 33 this.spritePos = spritePos; 34 //相對坐標 35 this.x = 0; 36 this.y = 5; 37 38 //最大分數 39 this.maxScore = 0; 40 //高分榜 41 this.highScore = 0; 42 43 this.digits = []; 44 //是否進行閃動特效 45 this.acheivement = false; 46 this.defaultString = ''; 47 //閃動特效計時器 48 this.flashTimer = 0; 49 //閃動計數器 50 this.flashIterations = 0; 51 this.invertTrigger = false; 52 53 this.config = DistanceMeter.config; 54 //最大記錄為萬位數 55 this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS; 56 this.init(canvasWidth); 57 } 58 59 DistanceMeter.prototype = { 60 /** 61 * 初始化距離記錄器為00000 62 * @param canvasWidth canvas的寬度 63 */ 64 init: function(canvasWidth) { 65 var maxDistanceStr = ''; 66 67 this.calcXPos(canvasWidth); 68 for (var i = 0; i < this.maxScoreUnits; i++) { 69 this.draw(i, 0); 70 this.defaultString += '0'; 71 maxDistanceStr += '9'; 72 } 73 74 //99999 75 this.maxScore = parseInt(maxDistanceStr); 76 }, 77 /** 78 * 計算出xPos 79 * @param canvasWidth 80 */ 81 calcXPos: function(canvasWidth) { 82 this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH * (this.maxScoreUnits + 1)); 83 }, 84 draw: function(digitPos, value, opt_highScore) { 85 var sourceWidth = DistanceMeter.dimensions.WIDTH; 86 var sourceHeight = DistanceMeter.dimensions.HEIGHT; 87 var sourceX = DistanceMeter.dimensions.WIDTH * value; 88 var sourceY = 0; 89 90 var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH; 91 var targetY = this.y; 92 var targetWidth = DistanceMeter.dimensions.WIDTH; 93 var targetHeight = DistanceMeter.dimensions.HEIGHT; 94 95 sourceX += this.spritePos.x; 96 sourceY += this.spritePos.y; 97 98 this.canvasCtx.save(); 99 100 if (opt_highScore) { 101 // 將最高分放至當前分數的左邊 102 var highScoreX = this.x - (this.maxScoreUnits * 2) * DistanceMeter.dimensions.WIDTH; 103 this.canvasCtx.translate(highScoreX, this.y); 104 } else { 105 this.canvasCtx.translate(this.x, this.y); 106 } 107 108 this.canvasCtx.drawImage(this.image, sourceX, sourceY, sourceWidth, sourceHeight, targetX, targetY, targetWidth, targetHeight); 109 110 this.canvasCtx.restore(); 111 }, 112 /** 113 * 將像素距離轉化為“真實距離” 114 * @param distance 像素距離 115 * @returns {number} “真實距離” 116 */ 117 getActualDistance: function(distance) { 118 return distance ? Math.round(distance * this.config.COEFFICIENT) : 0; 119 }, 120 /** 121 * 更新距離記錄器 122 * @param {number} deltaTime 123 * @param {number} distance 124 * @returns {boolean} 是否播放聲音 125 */ 126 update: function(deltaTime, distance) { 127 var paint = true; 128 var playSound = false; 129 130 if (!this.acheivement) { 131 distance = this.getActualDistance(distance); 132 // 分數超過最大分數時增加至十萬位999999 133 if (distance > this.maxScore && this.maxScoreUnits === this.config.MAX_DISTANCE_UNITS) { 134 this.maxScoreUnits++; 135 this.maxScore = parseInt(this.maxScore + '9'); 136 } 137 138 if (distance > 0) { 139 // 每100距離開始閃動特效並播放聲音 140 if (distance % this.config.ACHIEVEMENT_DISTANCE === 0) { 141 this.acheivement = true; 142 this.flashTimer = 0; 143 playSound = true; 144 } 145 146 // 讓數字以0開頭 147 var distanceStr = (this.defaultString + distance).substr( - this.maxScoreUnits); 148 this.digits = distanceStr.split(''); 149 } else { 150 this.digits = this.defaultString.split(''); 151 } 152 } else { 153 // 到達目標分數時閃動分數 154 if (this.flashIterations <= this.config.FLASH_ITERATIONS) { 155 this.flashTimer += deltaTime; 156 157 if (this.flashTimer < this.config.FLASH_DURATION) { 158 paint = false; 159 } else if (this.flashTimer > this.config.FLASH_DURATION * 2) { 160 this.flashTimer = 0; 161 this.flashIterations++; 162 } 163 } else { 164 this.acheivement = false; 165 this.flashIterations = 0; 166 this.flashTimer = 0; 167 } 168 } 169 170 // 非閃動時繪制分數 171 if (paint) { 172 for (var i = this.digits.length - 1; i >= 0; i--) { 173 this.draw(i, parseInt(this.digits[i])); 174 } 175 } 176 177 this.drawHighScore(); 178 return playSound; 179 }, 180 //繪制高分榜 181 drawHighScore: function() { 182 this.canvasCtx.save(); 183 this.canvasCtx.globalAlpha = .8; //讓字符看起來顏色稍淺 184 for (var i = this.highScore.length - 1; i >= 0; i--) { 185 this.draw(i, parseInt(this.highScore[i], 10), true); 186 } 187 this.canvasCtx.restore(); 188 }, 189 setHighScore: function(distance) { 190 distance = this.getActualDistance(distance); 191 var highScoreStr = (this.defaultString + distance).substr( - this.maxScoreUnits); 192 //10和11分別對應雪碧圖中的H、I 193 this.highScore = ['10', '11', ''].concat(highScoreStr.split('')); 194 }, 195 //重置記錄器為00000 196 reset: function() { 197 this.update(0); 198 this.acheivement = false; 199 } 200 };
GameOver
恐龍和障礙物碰撞后,游戲結束,游戲界面顯示gameover面板,該功能由GameOverPanel構造函數實現:

1 GameOverPanel.dimensions = { 2 TEXT_X: 0, 3 TEXT_Y: 13, 4 TEXT_WIDTH: 191, 5 TEXT_HEIGHT: 11, 6 RESTART_WIDTH: 36, 7 RESTART_HEIGHT: 32 8 }; 9 10 function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) { 11 this.canvas = canvas; 12 this.canvasCtx = canvas.getContext('2d'); 13 this.canvasDimensions = dimensions; 14 this.textImgPos = textImgPos; 15 this.restartImgPos = restartImgPos; 16 this.draw(); 17 } 18 19 GameOverPanel.prototype = { 20 draw: function() { 21 var dimensions = GameOverPanel.dimensions; 22 23 var centerX = this.canvasDimensions.WIDTH / 2; 24 25 // Game over text 26 var textSourceX = dimensions.TEXT_X; 27 var textSourceY = dimensions.TEXT_Y; 28 var textSourceWidth = dimensions.TEXT_WIDTH; 29 var textSourceHeight = dimensions.TEXT_HEIGHT; 30 31 var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2)); 32 var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3); 33 var textTargetWidth = dimensions.TEXT_WIDTH; 34 var textTargetHeight = dimensions.TEXT_HEIGHT; 35 36 var restartSourceWidth = dimensions.RESTART_WIDTH; 37 var restartSourceHeight = dimensions.RESTART_HEIGHT; 38 var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2); 39 var restartTargetY = this.canvasDimensions.HEIGHT / 2; 40 41 textSourceX += this.textImgPos.x; 42 textSourceY += this.textImgPos.y; 43 44 // Game over text from sprite. 45 this.canvasCtx.drawImage(imgSprite, textSourceX, textSourceY, textSourceWidth, textSourceHeight, textTargetX, textTargetY, textTargetWidth, textTargetHeight); 46 47 // Restart button. 48 this.canvasCtx.drawImage(imgSprite, this.restartImgPos.x, this.restartImgPos.y, restartSourceWidth, restartSourceHeight, restartTargetX, restartTargetY, dimensions.RESTART_WIDTH, dimensions.RESTART_HEIGHT); 49 } 50 };

1 function gameOver() { 2 cancelAnimationFrame(raq); 3 raq = 0; 4 crashed = true; 5 trex.update(0, Trex.status.CRASHED); 6 7 distanceMeter.acheivement = false; 8 if (distanceRan > highestScore) { 9 highestScore = Math.ceil(distanceRan); 10 distanceMeter.setHighScore(highestScore); 11 } 12 13 if (!gameOverPanel) { 14 gameOverPanel = new GameOverPanel(c, spriteDefinition.TEXT_SPRITE, spriteDefinition.RESTART, dimensions); 15 } else { 16 gameOverPanel.draw(); 17 } 18 }
游戲重新開始
GameOver后,按下Spacebar
游戲重新開始,restart方法負責將游戲各個元素或數據重置:

1 function restart() { 2 trex.reset(); 3 Obstacle.obstacles = []; 4 h.reset(); 5 night.reset(); 6 crashed = false; 7 time = performance.now(); 8 distanceRan = 0; 9 ctx.clearRect(0, 0, 600, 150); 10 distanceMeter.reset(); 11 raq = requestAnimationFrame(draw, c); 12 }
游戲暫停
當游戲窗口失去焦點時,游戲暫停,得到焦點時游戲繼續。游戲通過注冊三個事件來實現:
document.addEventListener('visibilitychange',onVisibilityChange); window.addEventListener('blur',onVisibilityChange); window.addEventListener('focus',onVisibilityChange);

1 onVisibilityChange: function(e) { 2 if (document.hidden || document.webkitHidden || e.type == 'blur' || document.visibilityState != 'visible') { 3 this.stop(); 4 } else if (!this.crashed) { 5 this.tRex.reset(); 6 this.play(); 7 } 8 }, 9 stop: function() { 10 this.activated = false; 11 this.paused = true; 12 cancelAnimationFrame(this.raqId); 13 this.raqId = 0; 14 }, 15 play: function() { 16 if (!this.crashed) { 17 this.activated = true; 18 this.paused = false; 19 this.tRex.update(0, Trex.status.RUNNING); 20 this.time = getTimeStamp(); 21 this.update(); 22 } 23 }
開場動畫
第一次開始游戲時,會有一個過渡動畫,效果是地面逐漸展開,並且恐龍向前移動50像素。

1 // CSS animation definition. 2 var keyframes = '@-webkit-keyframes intro { ' + 'from { width:' + Trex.config.WIDTH + 'px }' + 'to { width: ' + this.dimensions.WIDTH + 'px }' + '}'; 3 document.styleSheets[0].insertRule(keyframes, 0); 4 5 this.containerEl.addEventListener('webkitAnimationEnd', this.startGame.bind(this)); 6 7 this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both'; 8 this.containerEl.style.width = this.dimensions.WIDTH + 'px'; 9 10 11 //向前移動50像素 12 if (this.playingIntro && this.xPos < this.config.START_X_POS) { 13 this.xPos += Math.round((this.config.START_X_POS / this.config.INTRO_DURATION) * deltaTime); 14 }
游戲音效
游戲准備了三種音效,分別是游戲點擊空格鍵開始時、與障礙物碰撞時、每到達100分時。游戲在代碼中放置了三個audio標簽來存放音效,並且是base64形式,所以在播放時要經過解碼,可以查閱文檔了解AudioContext API
的用法:

1 function decodeBase64ToArrayBuffer(base64String) { 2 var len = (base64String.length / 4) * 3; 3 var str = atob(base64String); 4 var arrayBuffer = new ArrayBuffer(len); 5 var bytes = new Uint8Array(arrayBuffer); 6 7 for (var i = 0; i < len; i++) { 8 bytes[i] = str.charCodeAt(i); 9 } 10 return bytes.buffer; 11 }

1 var data = '........base64String.......'; 2 var soundFx = {}; 3 var soundSrc = data.substr(data.indexOf(',')+1); 4 var buffer = decodeBase64ToArrayBuffer(soundSrc); 5 var audioContext = new AudioContext(); 6 audioContext.decodeAudioData(buffer,function(index,audioData) { 7 soundFx[index] = audioData; 8 }.bind(this,'audio1')); 9 10 function playSound(soundBuffer) { 11 if (soundBuffer) { 12 var sourceNode = audioContext.createBufferSource(); 13 sourceNode.buffer = soundBuffer; 14 sourceNode.connect(audioContext.destination); 15 sourceNode.start(0); 16 } 17 } 18 19 window.onload = function() { 20 playSound(soundFx['audio1']); 21 };
對移動設備的處理
游戲還專門對移動設備進行了處理,包括屏幕大小的自適應,游戲速度調節,為高清屏加載高清素材等等。具體代碼就不一一列出了。
至此,對這個小游戲的代碼研究結束,下面是完整的游戲:
總結
通過對這個游戲的源碼進行研究,從中收獲了不少干貨,對2d游戲的制作思路有一定的啟發,特別是基於時間的運動有了進一步的認識。游戲大致可以划分為以下功能:
大部分構造函數里都包含了一個名為update的方法,在每次GameLoop里調用以更新該游戲元件的狀態,並根據條件判斷是否在畫布上繪制(draw)。
暫時就想到這么多,接下來就是以開發一個2D游戲為目標努力了。