共计 6659 个字符,预计需要花费 17 分钟才能阅读完成。
文章首发于我的博客
目录
- 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,
};
// 游戏用到的 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));
}
},
};
/**
* 生成 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 小恐龙游戏源码探究二 — 让地面动起来 |