
字节笔记本
2026年10月7日 · 约 9 分钟读完
复刻 Chrome 小恐龙:Canvas 游戏开发核心拆解
断开网络时,Chrome 会自动弹出那只像素小恐龙,按一下空格键,它就开始在仙人掌之间跳跃。这个 2014 年上线的离线彩蛋,官方名称是 T-Rex Runner,可能是有史以来被玩得最多、却最没有存在感的网页游戏。对开发者来说,它的价值远不止消磨时间:一个最小的 2D 游戏需要的全部要素,游戏循环、实体状态、跳跃物理、碰撞检测、难度曲线,在这个不到两百行的项目里一个不缺,堪称游戏开发的"Hello World"。
本文用原生 JavaScript 和 HTML5 Canvas 从零复刻它,不依赖任何游戏引擎,并把每个环节背后的原理讲透。这些概念不挑语言、不挑框架,换到任何 Canvas 游戏乃至可视化项目里都直接适用。
一、最小骨架:一块画布,两个实体
第一步是拿到画布和绘图上下文,然后定义游戏实体。恐龙游戏的实体只有两个对象:
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = 800;
canvas.height = 200;
const dino = {
x: 50, y: 150, width: 40, height: 60,
jumping: false, jumpCount: 0
};
const obstacle = {
x: canvas.width, y: 150, width: 30, height: 50
};
let score = 0; // 计分
let gameSpeed = 5; // 全局移动速度实体就是普通的对象字面量:位置、尺寸,外加一两个状态位。dino.jumping 标记恐龙是否在空中;障碍物初始 x 等于画布宽度,也就是埋在右边缘外,进场后随 gameSpeed 向左移动。HTML 侧只需要一个 <canvas id="gameCanvas"></canvas> 标签,页面就能跑起来。
这种"数据即对象、对象即实体"的写法是游戏开发最朴素的起点。等实体多起来,再把它们重构成类、用数组统一管理,是后话。
二、游戏循环:requestAnimationFrame 驱动一切
游戏和普通网页程序最大的区别,是它必须"连续地动"。实现方式就是游戏循环:每一帧先清空画布,更新所有实体状态,重新绘制,然后请求下一帧,如此往复:

function update() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 更新跳跃、移动障碍物、碰撞检测、绘制
requestAnimationFrame(update);
}
update();节拍器选的是 requestAnimationFrame(rAF)而非 setInterval,这个选择值得单独一提:rAF 与浏览器渲染周期同步,稳定对齐 60fps,不会像固定间隔的定时器那样在页面负载高时漂移丢帧;更重要的是,标签页切到后台时,rAF 回调会被浏览器自动暂停,既省资源,也避免玩家不在场时游戏"自己跑死"。这两点让它成为 Web 游戏循环的事实标准。
三、跳跃"物理":15 帧上升,15 帧下降
恐龙的跳跃没有引入任何物理引擎,靠的是一个帧计数器:
if (dino.jumping) {
dino.jumpCount++;
if (dino.jumpCount < 15) {
dino.y -= 5; // 前 15 帧,每帧上移 5px
} else if (dino.jumpCount < 30) {
dino.y += 5; // 第 15 到 30 帧,每帧下移 5px
} else {
dino.jumping = false; // 落地复位
dino.jumpCount = 0;
}
}起跳后 30 帧走完一上一下,轨迹是一条对称折线而非真正的抛物线,但 60fps 下肉眼几乎无法分辨,视觉上足够自然。想要更真实的手感,可以换成"速度加重力"的经典模型:每帧 v += g、y += v,起跳时给一个向上的初速度,y 回到地面线即落地。重力模型还能顺手做出"提前松手提前落地"的可变跳跃高度,那正是原版恐龙游戏手感的关键之一。
这里还埋着一个更隐蔽的坑:上升、下降、障碍物移动全部按"每帧固定像素"计算,游戏节奏被和帧率死死绑住。在 60Hz 屏幕上一切正常,换到 144Hz 高刷屏,恐龙和障碍物会以 2.4 倍速狂奔。标准解法是引入帧间时间差 delta time:把"每帧移动 5px"改写成"每秒移动 300px",用位移等于速度乘 dt 来计算,帧率再高也不会走样。几乎所有游戏引擎都要内置时间步长处理,原因就在这里。
四、AABB 碰撞检测:四个不等式
碰撞判定用的是最经典的 AABB(轴对齐包围盒)算法:判断两个矩形是否重叠,只需四个条件同时成立。

if (
dino.x < obstacle.x + obstacle.width &&
dino.x + dino.width > obstacle.x &&
dino.y < obstacle.y + obstacle.height &&
dino.y + dino.height > obstacle.y
) { /* 撞上了 */ }前两条管水平方向:恐龙左缘越过障碍物右缘,同时恐龙右缘还没退到障碍物左缘之外;后两条把同样的判断套在垂直方向上。四条同时为真,两个矩形必然相交。它只适用于轴对齐的矩形,遇到圆形或旋转体就得换成圆相交判断或分离轴定理,但对跑酷类游戏绰绰有余。
顺带一提,原版恐龙游戏的真实碰撞盒比屏幕上的像素图案小一圈。判定故意"松"一点,玩家撞死时会觉得是差之毫厘,手感反而更公平。这是碰撞检测里很实用的一课:数学上精确的判定,未必是体验上最好的判定。
五、难度曲线与游戏状态
难度递增只有一行:每越过一个障碍物,分数加一,gameSpeed 增加 0.1。线性加速让玩家始终停留在"刚好能应付"的边缘,这正是跑酷类游戏让人停不下来的核心设计。死亡后 resetGame 把障碍物归位、分数清零、速度回退,实体对象里存的就是全部状态,重置只需覆盖这几个变量。
原实现用 alert 弹窗报告成绩,这是个典型的反模式:alert 会阻塞主线程,rAF 循环被打断,弹窗一关游戏瞬间复位,体验非常生硬。更好的做法是引入一个简单的状态机(playing / gameover),死亡时切换状态、把结算画面直接画在画布上,等待按键再重开。原版恐龙游戏正是这么做的。
六、从"能玩"到"好玩":五个扩展方向
最小版本跑通之后,这个项目还有相当大的延伸空间:
- 精灵图动画:用 Sprite Sheet 替换色块矩形,切换 drawImage 的取图坐标就能让恐龙跑起来、跳起来,画面质感立上一个档次;
- 视差滚动:云朵、地面线条按不同速度移动,两层背景就能拉出纵深;
- 移动端适配:监听 touchstart 等价于按下空格,再让画布随窗口自适应缩放;
- 程序合成音效:不必加载音频文件,用 Web Audio API 的振荡器就能合成跳跃与碰撞音;
- 障碍物多样化:高低不同的仙人掌、会飞的翼龙,配合对象池复用实体,避免高频创建销毁带来的 GC 抖动。
代码组织上,随着实体增多,建议把恐龙、障碍物、计分器各自抽象成类,让游戏循环只调用它们的 update 与 draw,用事件驱动取代过程式堆叠。这一步重构是"能实现"与"实现得好"的分水岭,也为接入更复杂的游戏逻辑打好地基。
写在最后
恐龙游戏是游戏开发的完美标本:不到两百行代码,覆盖了循环、物理、碰撞、状态、难度五个核心概念,而且每一个都能单独拿出来深挖。比起调库跑通,更重要的是理解这些机制为什么这样设计:rAF 为什么优于定时器,delta time 解决什么问题,碰撞盒为什么要故意放宽。想清楚这些,换任何引擎、任何语言,套路都是相通的。
实践上还有一条低成本的路径:让 AI 生成第一版,自己负责逐段拆解,找出帧率耦合、alert 阻塞这类隐患再动手改进。能看懂并能改好,才算真正把游戏开发的第一课学完。



