文章首發於個人 GitHub 博客
當 Chrome 處於離線狀況下,會顯示如下頁面:css
當按下空格鍵
或者 ↑ 鍵
,小恐龍遊戲彩蛋就觸發啦 (๑•̀ㅂ•́)و✧
html
遊戲雖然簡單,但源碼卻有三千多行,代碼嚴謹且富有邏輯,值得拿來學習研究。這個教程將會從零開始,一步步解讀源碼並最終實現這個遊戲。git
要獲取遊戲的源碼,能夠經過下面幾種方式:github
chrome://dino
,進入小恐龍頁面,用開發者工具獲取源碼遊戲用到的雪碧圖,音頻文件能夠在官方提供的源碼網址裏獲取到。
爲了方便食用,我將雪碧圖中各個小圖片的座標信息標了出來(W: Width, H: Height, L: Left, T: Top
):chrome
關於上面雪碧圖的座標信息,我是用一個在線工具獲取的:http://www.spritecow.com/,個別座標信息經過這個網站獲取的不太準,這裏我已經經過參考源碼裏的數據進行了修正。canvas
戳這裏獲取上面這張圖片的 JPG 原圖和 PSD 原圖。
遊戲源碼主要包括九個類:segmentfault
背景類 Horizon瀏覽器
這個教程並不會徹底按照源碼來,而是抽取主要的內容來一步步實現這個遊戲。這樣作並不意味着改變源碼的思路,而是去除了一些目前能夠先不考慮的代碼,好比:去除了適配 HDPI 和 LDPI、適配移動端等。app
這個遊戲源碼的探究已經有前輩 @逐影 寫了系列教程。在這裏,我寫這個教程的目的,一是當作學習筆記,二是提供與前輩不同的源碼解讀思路。
遊戲文件結構目錄:工具
chrome-dino - index.html - index.css - index.js // JS 入口文件 - offline.js // 遊戲邏輯實現 - imgs - sounds
想要獲取整個教程的源代碼,戳這裏: GitHub
HTML、CSS 就不過多解釋,直接貼代碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Chrome Dino</title> <link rel="stylesheet" href="./index.css" /> <script src="./offline.js"></script> </head> <body> <!-- 遊戲的 「根」 DOM節點,用來容納遊戲的主體部分 --> <div id="chrome-dino"></div> <!-- 遊戲用到的雪碧圖,音頻資源 --> <div id="offline-resources"> <img id="offline-resources-1x" src="./imgs/100-offline-sprite.png" alt="sprite" /> </div> <script src="./index.js"></script> </body> </html>
* { margin: 0; padding: 0; } *, *::before, *::after { box-sizing: border-box; } #chrome-dino { width: 100%; max-width: 600px; margin: 0 auto; } #offline-resources { display: none; } .offline .runner-container { position: absolute; top: 35px; width: 100%; max-width: 600px; height: 150px; overflow: hidden; } .offline .runner-canvas { z-index: 10; position: absolute; top: 0; height: 150px; max-width: 600px; overflow: hidden; opacity: 1; }
下面來分析 JS 代碼:
首先看一下游戲的主體類 Runner
,這個類用於控制遊戲的主要邏輯:
/** * 遊戲主體類,控制遊戲的總體邏輯 * @param {String} containerSelector 畫布外層容器的選擇器 * @param {Object} opt_config 配置選項 */ function Runner(containerSelector, opt_config) { // 獲取遊戲的 「根」 DOM 節點,整個遊戲都會輸出到這個節點裏 this.outerContainerEl = document.querySelector(containerSelector); // canvas 的外層容器 this.containerEl = null; this.config = opt_config || Runner.config; this.dimensions = Runner.defaultDimensions; this.time = 0; // 時鐘計時器 this.currentSpeed = this.config.SPEED; // 當前的速度 this.activated = false; // 遊戲彩蛋是否被激活(沒有被激活時,遊戲不會顯示出來) this.playing = false; // 遊戲是否進行中 this.crashed = false; // 小恐龍是否碰到了障礙物 this.paused = false // 遊戲是否暫停 // 加載雪碧圖,並初始化遊戲 this.loadImages(); } window['Runner'] = Runner; // 將 Runner 類掛載到 window 對象上
相關的數據和配置參數:
var DEFAULT_WIDTH = 600; // 遊戲畫布默認寬度 var FPS = 60; // 遊戲默認幀率 // 遊戲配置參數 Runner.config = { SPEED: 6, // 移動速度 }; // 遊戲畫布的默認尺寸 Runner.defaultDimensions = { WIDTH: DEFAULT_WIDTH, HEIGHT: 150, }; // 遊戲用到的 className Runner.classes = { CONTAINER: 'runner-container', CANVAS: 'runner-canvas', PLAYER: '', // 預留出的 className,用來控制 canvas 的樣式 }; // 雪碧圖中圖片的座標信息 Runner.spriteDefinition = { LDPI: { HORIZON: { x: 2, y: 54 }, // 地面 }, }; // 遊戲中用到的鍵盤碼 Runner.keyCodes = { JUMP: { '38': 1, '32': 1 }, // Up, Space DUCK: { '40': 1 }, // Down RESTART: { '13': 1 }, // Enter }; // 遊戲中用到的事件 Runner.events = { LOAD: 'load', };
在 Runner
原型鏈上添加的方法:
Runner.prototype = { // 初始化遊戲 init: function () { // 生成 canvas 容器元素 this.containerEl = document.createElement('div'); this.containerEl.className = Runner.classes.CONTAINER; // 生成 canvas this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH, this.dimensions.HEIGHT, Runner.classes.PLAYER); this.ctx = this.canvas.getContext('2d'); this.ctx.fillStyle = '#f7f7f7'; this.ctx.fill(); // 加載背景類 Horizon this.horizon = new Horizon(this.canvas, this.spriteDef); // 將遊戲添加到頁面中 this.outerContainerEl.appendChild(this.containerEl); }, // 加載雪碧圖資源 loadImages() { // 圖片在雪碧圖中的座標 this.spriteDef = Runner.spriteDefinition.LDPI; // 獲取雪碧圖 Runner.imageSprite = document.getElementById('offline-resources-1x'); // 當圖片加載完成(complete 是 DOM 中 Image 對象自帶的一個屬性) if (Runner.imageSprite.complete) { this.init(); } else { // 圖片沒有加載完成,監聽其 load 事件 Runner.imageSprite.addEventListener(Runner.events.LOAD, this.init.bind(this)); } }, };
其中 createCanvas
方法定義以下:
/** * 生成 canvas 元素 * @param {HTMLElement} container canva 的容器 * @param {Number} width canvas 的寬度 * @param {Number} height canvas 的高度 * @param {String} opt_className 給 canvas 添加的類名(可選) * @return {HTMLCanvasElement} */ function createCanvas(container, width, height, opt_className) { var canvas = document.createElement('canvas'); canvas.className = opt_className ? opt_className + ' ' + Runner.classes.CANVAS : Runner.classes.CANVAS; canvas.width = width; canvas.height = height; container.appendChild(canvas); return canvas; }
定義好 Runner
類以後,爲了方便探究,接下來從簡單的背景開始提及。首先是繪製靜態的地面。
定義地面類 HorizonLine
:
/** * 地面類 * @param {HTMLCanvasElement} canvas 畫布 * @param {Object} spritePos 雪碧圖中的位置 */ function HorizonLine(canvas, spritePos) { this.canvas = canvas; this.ctx = this.canvas.getContext('2d'); this.dimensions = {}; // 地面的尺寸 this.spritePos = spritePos; // 雪碧圖中地面的位置 this.sourceXPos = []; // 雪碧圖中地面的兩種地形的 x 座標 this.xPos = []; // canvas 中地面的 x 座標 this.yPos = 0; // canvas 中地面的 y 座標 this.bumpThreshold = 0.5; // 隨機地形係數,控制兩種地形的出現頻率 this.init(); this.draw(); } HorizonLine.dimensions = { WIDTH: 600, HEIGHT: 12, YPOS: 127, // 繪製到 canvas 中的 y 座標 };
在 HorizonLine
原型鏈上添加方法:
HorizonLine.prototype = { // 初始化地面 init: function () { for (const d in HorizonLine.dimensions) { if (HorizonLine.dimensions.hasOwnProperty(d)) { const elem = HorizonLine.dimensions[d]; this.dimensions[d] = elem; } } this.sourceXPos = [this.spritePos.x, this.spritePos.x + this.dimensions.WIDTH]; this.xPos = [0, HorizonLine.dimensions.WIDTH]; this.yPos = HorizonLine.dimensions.YPOS; }, // 繪製地面 draw: function () { // 使用 canvas 中 9 個參數的 drawImage 方法 this.ctx.drawImage( Runner.imageSprite, // 原圖片 this.sourceXPos[0], this.spritePos.y, // 原圖中裁剪區域的起點座標 this.dimensions.WIDTH, this.dimensions.HEIGHT, this.xPos[0], this.yPos, // canvas 中繪製區域的起點座標 this.dimensions.WIDTH, this.dimensions.HEIGHT, ); this.ctx.drawImage( Runner.imageSprite, this.sourceXPos[1], this.spritePos.y, this.dimensions.WIDTH, this.dimensions.HEIGHT, this.xPos[1], this.yPos, this.dimensions.WIDTH, this.dimensions.HEIGHT, ); }, };
背景類 Horizon
負責管理 HorizonLine
、Cloud
、Obstacle
、NightMode
這幾個類。
因此接下來須要經過 Horizon
類來調用 HorizonLine
類。
定義背景類 Horizon
:
/** * 背景類 * @param {HTMLCanvasElement} canvas 畫布 * @param {Object} spritePos 雪碧圖中的位置 */ function Horizon(canvas, spritePos) { this.canvas = canvas; this.ctx = this.canvas.getContext('2d'); this.spritePos = spritePos; // 地面 this.horizonLine = null; this.init(); }
在 Horizon
原型鏈上添加方法:
Horizon.prototype = { // 初始化背景 init: function () { this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON); }, };
最後,經過調用 Runner
類來運行遊戲:
index.js:
window.onload = function () { var chromeDino = document.getElementById('chrome-dino'); chromeDino.classList.add('offline'); new Runner('#chrome-dino'); };
到這裏,不出意外的話,就能夠繪製出靜態的地面,如圖:
查看完整的代碼: 戳這裏
這裏各個方法和類之間的調用邏輯是(箭頭代指調用):
new Runner() -> loadImage() // Runner -> init() // Runner -> new Horizon() -> init() // Horizon -> new HorizonLine() -> init() // HorizonLine -> draw() // HorizonLine
簡單來講就是:遊戲主體類 Runner
控制背景類 Horizon
,再由背景類 Horizon
控制地面類 HorizonLine
。
遵循的思想就是把遊戲層層抽象,由抽象程度高的類一層一層向下調用抽象程度低的類。這樣作的好處是,思路清晰而且易於擴展。
上一篇 | 下一篇 |
無 | Chrome 小恐龍遊戲源碼探究二 -- 讓地面動起來 |