Chrome 小恐龍遊戲源碼探究一 -- 繪製靜態地面

文章首發於個人 GitHub 博客

目錄

前言

當 Chrome 處於離線狀況下,會顯示如下頁面:css

clipboard.png

當按下空格鍵或者 ↑ 鍵,小恐龍遊戲彩蛋就觸發啦 (๑•̀ㅂ•́)و✧html

clipboard.png

遊戲雖然簡單,但源碼卻有三千多行,代碼嚴謹且富有邏輯,值得拿來學習研究。這個教程將會從零開始,一步步解讀源碼並最終實現這個遊戲。git

獲取源碼、素材

要獲取遊戲的源碼,能夠經過下面幾種方式:github

  • 斷網後,訪問任意網址,進入小恐龍頁面,用開發者工具獲取源碼
  • 在瀏覽器地址欄輸入 chrome://dino,進入小恐龍頁面,用開發者工具獲取源碼
  • 官方提供的源碼網址
  • 有人將源碼提取出來放在了 GitHub 上:t-rex-runner
遊戲用到的雪碧圖,音頻文件能夠在官方提供的源碼網址裏獲取到。

爲了方便食用,我將雪碧圖中各個小圖片的座標信息標了出來(W: Width, H: Height, L: Left, T: Top):chrome

關於上面雪碧圖的座標信息,我是用一個在線工具獲取的:http://www.spritecow.com/,個別座標信息經過這個網站獲取的不太準,這裏我已經經過參考源碼裏的數據進行了修正。canvas

戳這裏獲取上面這張圖片的 JPG 原圖和 PSD 原圖。

開始探究

遊戲源碼主要包括九個類:segmentfault

  • 遊戲的主體類 Runner
  • 背景類 Horizon瀏覽器

    • 地面類 HorizonLine
    • 雲朵類 Cloud
    • 障礙物類 Obstacle
    • 晝夜更替類 NightMode
  • 小恐龍類 Trex
  • 分數類 DistanceMeter
  • 遊戲結束面板類 GameOverPanel

這個教程並不會徹底按照源碼來,而是抽取主要的內容來一步步實現這個遊戲。這樣作並不意味着改變源碼的思路,而是去除了一些目前能夠先不考慮的代碼,好比:去除了適配 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;
}

地面類 HorizonLine

定義好 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 負責管理 HorizonLineCloudObstacleNightMode 這幾個類。

因此接下來須要經過 Horizon 類來調用 HorizonLine 類。

背景類 Horizon

定義背景類 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');
};

到這裏,不出意外的話,就能夠繪製出靜態的地面,如圖:

clipboard.png

查看完整的代碼: 戳這裏

這裏各個方法和類之間的調用邏輯是(箭頭代指調用):

new Runner()
-> loadImage() // Runner
-> init()      // Runner
-> new Horizon()
-> init()      // Horizon
-> new HorizonLine()
-> init()      // HorizonLine
-> draw()      // HorizonLine

簡單來講就是:遊戲主體類 Runner 控制背景類 Horizon,再由背景類 Horizon 控制地面類 HorizonLine

遵循的思想就是把遊戲層層抽象,由抽象程度高的類一層一層向下調用抽象程度低的類。這樣作的好處是,思路清晰而且易於擴展。

上一篇 下一篇
無                       Chrome 小恐龍遊戲源碼探究二 -- 讓地面動起來
相關文章
相關標籤/搜索