文章首发于我的博客

目录

  • Chrome 小恐龙游戏源码探究一 -- 绘制静态地面
  • Chrome 小恐龙游戏源码探究二 -- 让地面动起来
  • Chrome 小恐龙游戏源码探究三 -- 进入街机模式
  • Chrome 小恐龙游戏源码探究四 -- 随机绘制云朵
  • Chrome 小恐龙游戏源码探究五 -- 随机绘制障碍
  • Chrome 小恐龙游戏源码探究六 -- 记录游戏分数
  • Chrome 小恐龙游戏源码探究七 -- 昼夜模式交替
  • Chrome 小恐龙游戏源码探究八 -- 奔跑的小恐龙
  • // TODO

前言

当 Chrome 处于离线情况下,会显示以下页面:

当按下空格键或者 ↑ 键,小恐龙游戏彩蛋就触发啦 (•ㅂ•)✧

游戏虽然简单,但源码却有三千多行,代码严谨且富有逻辑,值得拿来学习研究。这个教程将会从零开始,一步步解读源码并最终实现这个游戏。

获取源码、素材

要获取游戏的源码,可以通过下面几种方式:

  • 断网后,访问任意网址,进入小恐龙页面,用开发者工具获取源码
  • 在浏览器地址栏输入 chrome://dino,进入小恐龙页面,用开发者工具获取源码
  • 官方提供的源码网址
  • 有人将源码提取出来放在了 GitHub 上:t-rex-runner
游戏用到的雪碧图,音频文件可以在官方提供的源码网址里获取到。

为了方便食用,我将雪碧图中各个小图片的坐标信息标了出来(W: Width, H: Height, L: Left, T: Top):

关于上面雪碧图的坐标信息,我是用一个在线工具获取的:http://www.spritecow.com/,个别坐标信息通过这个网站获取的不太准,这里我已经通过参考源码里的数据进行了修正。

戳这里获取上面这张图片的 PSD 原图。

开始探究

游戏源码主要包括九个类:

  • 游戏的主体类 Runner
  • 背景类 Horizon

    • 地面类 HorizonLine
    • 云朵类 Cloud
    • 障碍物类 Obstacle
    • 昼夜更替类 NightMode
  • 小恐龙类 Trex
  • 分数类 DistanceMeter
  • 游戏结束面板类 GameOverPanel

这个教程并不会完全按照源码来,而是抽取主要的内容来一步步实现这个游戏。这样做并不意味着改变源码的思路,而是去除了一些目前可以先不考虑的代码,比如:去除了适配 HDPI 和 LDPI、适配移动端等。

这个游戏源码的探究已经有前辈 @逐影 写了系列教程。在这里,我写这个教程的目的,一是当做学习笔记,二是提供与前辈不一样的源码解读思路。

游戏主体搭建

游戏文件结构目录:

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;var DEFAULT_WIDTH = 600;var FPS = 60;// 游戏配置参数Runner.config = {  SPEED: 6, // 移动速度};// 游戏画布的默认尺寸Runner.defaultDimensions = {  WIDTH: DEFAULT_WIDTH,  HEIGHT: 150,};// 游戏用到的 classNameRunner.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));    }  },};/**  * 生成 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 类:

/** * Horizon 背景类 * @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 类。

背景类 Horizon

定义 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);  },};

最后通过以下代码调用:

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 小恐龙游戏源码探究二 -- 让地面动起来