资讯专栏INFORMATION COLUMN

Chrome 小恐龙游戏源码探究一 -- 绘制静态地面

lixiang / 1741人阅读

摘要:首先是绘制静态的地面。上一篇下一篇无小恐龙游戏源码探究二让地面动起来

文章首发于我的 GitHub 博客
目录

Chrome 小恐龙游戏源码探究一 -- 绘制静态地面

Chrome 小恐龙游戏源码探究二 -- 让地面动起来

Chrome 小恐龙游戏源码探究三 -- 进入街机模式

Chrome 小恐龙游戏源码探究四 -- 随机绘制云朵

Chrome 小恐龙游戏源码探究五 -- 随机绘制障碍

Chrome 小恐龙游戏源码探究六 -- 记录游戏分数

Chrome 小恐龙游戏源码探究七 -- 昼夜模式交替

Chrome 小恐龙游戏源码探究八 -- 奔跑的小恐龙

Chrome 小恐龙游戏源码探究九 -- 游戏碰撞检测

Chrome 小恐龙游戏源码探究完 -- 游戏结束和其他要素

前言

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

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

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

获取源码、素材

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

断网后,访问任意网址,进入小恐龙页面,用开发者工具获取源码

在浏览器地址栏输入 chrome://dino,进入小恐龙页面,用开发者工具获取源码

官方提供的源码网址

有人将源码提取出来放在了 GitHub 上:t-rex-runner

游戏用到的雪碧图,音频文件可以在官方提供的源码网址里获取到。

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

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

戳这里获取上面这张图片的 JPG 原图和 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 就不过多解释,直接贴代码:



  
    
    
    
    Chrome Dino
    
    
  
  
    
    
* {
  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");
};

到这里,不出意外的话,就可以绘制出静态的地面,如图:

查看完整的代码:戳这里

这里各个方法和类之间的调用逻辑是(箭头代指调用):

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

简单来说就是:游戏主体类 Runner 控制背景类 Horizon,再由背景类 Horizon 控制地面类 HorizonLine

遵循的思想就是把游戏层层抽象,由抽象程度高的类一层一层向下调用抽象程度低的类。这样做的好处是,思路清晰并且易于扩展。

上一篇 下一篇
无                       Chrome 小恐龙游戏源码探究二 -- 让地面动起来

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/103890.html

相关文章

  • Chrome 恐龙游戏源码探究二 -- 让地面动起来

    摘要:文章首发于我的博客前言上一篇文章小恐龙游戏源码探究一绘制静态地面中定义了游戏的主体类,并实现了静态地面的绘制。 文章首发于我的 GitHub 博客 前言 上一篇文章:《Chrome 小恐龙游戏源码探究一 -- 绘制静态地面》 中定义了游戏的主体类 Runner,并实现了静态地面的绘制。这一篇文章中,将实现效果:1、地面无限滚动。2、刚开始地面不动,按下空格后地面滚动。 地面无限滚动 ...

    siberiawolf 评论0 收藏0
  • Chrome 恐龙游戏源码探究八 -- 奔跑的恐龙

    摘要:例如,将函数修改为小恐龙眨眼这样小恐龙会不停的眨眼睛。小恐龙的开场动画下面来实现小恐龙对键盘按键的响应。接下来还需要更新动画帧才能实现小恐龙的奔跑动画。 文章首发于我的 GitHub 博客 前言 上一篇文章:《Chrome 小恐龙游戏源码探究七 -- 昼夜模式交替》实现了游戏昼夜模式的交替,这一篇文章中,将实现:1、小恐龙的绘制 2、键盘对小恐龙的控制 3、页面失焦后,重新聚焦会重置...

    paulquei 评论0 收藏0
  • Chrome 恐龙游戏源码探究三 -- 进入街机模式

    摘要:文章首发于我的博客前言上一篇文章小恐龙游戏源码探究二让地面动起来实现了地面的移动。街机模式的效果就是游戏开始后,进入全屏模式。例如可以看到,进入街机模式之前,有一段开场动画。 文章首发于我的 GitHub 博客 前言 上一篇文章:《Chrome 小恐龙游戏源码探究二 -- 让地面动起来》 实现了地面的移动。这一篇文章中,将实现效果:1、浏览器失焦时游戏暂停,聚焦游戏继续。 2、开场动...

    yeooo 评论0 收藏0
  • Chrome 恐龙游戏源码探究五 -- 随机绘制障碍

    摘要:文章首发于我的博客前言上一篇文章小恐龙游戏源码探究四随机绘制云朵实现了云朵的随机绘制,这一篇文章中将实现仙人掌翼龙障碍物的绘制游戏速度的改变障碍物的类型有两种仙人掌和翼龙。 文章首发于我的 GitHub 博客 前言 上一篇文章:《Chrome 小恐龙游戏源码探究四 -- 随机绘制云朵》 实现了云朵的随机绘制,这一篇文章中将实现:1、仙人掌、翼龙障碍物的绘制 2、游戏速度的改变 障碍物...

    tomorrowwu 评论0 收藏0
  • Chrome 恐龙游戏源码探究四 -- 随机绘制云朵

    摘要:文章首发于我的博客前言上一篇文章小恐龙游戏源码探究三进入街机模式实现了开场动画和街机模式。 文章首发于我的 GitHub 博客 前言 上一篇文章:《Chrome 小恐龙游戏源码探究三 -- 进入街机模式》 实现了开场动画和街机模式。这一篇文章中,将实现云朵的随机绘制。 云朵类 Cloud 定义云朵类 Cloud: /** * 云朵类 * @param {HTMLCanvasEle...

    svtter 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<