ByteNoteByteNote
复刻 Chrome 小恐龙:Canvas 游戏开发核心拆解
字

字节笔记本

2026年10月7日 · 约 9 分钟读完

复刻 Chrome 小恐龙:Canvas 游戏开发核心拆解

API中转
¥120

断开网络时,Chrome 会自动弹出那只像素小恐龙,按一下空格键,它就开始在仙人掌之间跳跃。这个 2014 年上线的离线彩蛋,官方名称是 T-Rex Runner,可能是有史以来被玩得最多、却最没有存在感的网页游戏。对开发者来说,它的价值远不止消磨时间:一个最小的 2D 游戏需要的全部要素,游戏循环、实体状态、跳跃物理、碰撞检测、难度曲线,在这个不到两百行的项目里一个不缺,堪称游戏开发的"Hello World"。

本文用原生 JavaScript 和 HTML5 Canvas 从零复刻它,不依赖任何游戏引擎,并把每个环节背后的原理讲透。这些概念不挑语言、不挑框架,换到任何 Canvas 游戏乃至可视化项目里都直接适用。

一、最小骨架:一块画布,两个实体

第一步是拿到画布和绘图上下文,然后定义游戏实体。恐龙游戏的实体只有两个对象:

js
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 驱动一切

游戏和普通网页程序最大的区别,是它必须"连续地动"。实现方式就是游戏循环:每一帧先清空画布,更新所有实体状态,重新绘制,然后请求下一帧,如此往复:

游戏循环:requestAnimationFrame 驱动的四步帧流程

js
function update() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // 更新跳跃、移动障碍物、碰撞检测、绘制
  requestAnimationFrame(update);
}
update();

节拍器选的是 requestAnimationFrame(rAF)而非 setInterval,这个选择值得单独一提:rAF 与浏览器渲染周期同步,稳定对齐 60fps,不会像固定间隔的定时器那样在页面负载高时漂移丢帧;更重要的是,标签页切到后台时,rAF 回调会被浏览器自动暂停,既省资源,也避免玩家不在场时游戏"自己跑死"。这两点让它成为 Web 游戏循环的事实标准。

三、跳跃"物理":15 帧上升,15 帧下降

恐龙的跳跃没有引入任何物理引擎,靠的是一个帧计数器:

js
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(轴对齐包围盒)算法:判断两个矩形是否重叠,只需四个条件同时成立。

AABB 碰撞检测:四个条件与重叠区域示意

js
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 阻塞这类隐患再动手改进。能看懂并能改好,才算真正把游戏开发的第一课学完。

相关文章

分享: