Chrome浏览器中“Dino游戏”背后的实现原理:深入探究Chromium的源代码
你肯定已经见过上百次这样的场景:Wi-Fi信号突然中断,Chrome浏览器陷入无反应状态,然后一个像素化的霸王龙图标就会出现,只要你按下空格键,它就会立刻在沙漠中开始奔跑。 这个自2014年以来就隐藏在“没有网络”错误提示背后的小游戏,每个月被玩大约2.7亿次。在谷歌内部,它的代号是“Project Bolan”,这个名字是为了纪念摇滚乐队T. Rex的主唱马克·博兰。 由于Chrome浏览器是基于开源的Chromium项目开发的,因此这个游戏的全部代码——包括所有的设计细节和编程技巧——都公开透明,任何人都可以查看。 所以我把它的全部代码都看了一遍。结果发现,这个看似“简单”的游戏其实蕴含着
你肯定已经见过上百次这样的场景:Wi-Fi信号突然中断,Chrome浏览器陷入无反应状态,然后一个像素化的霸王龙图标就会出现,只要你按下空格键,它就会立刻在沙漠中开始奔跑。
这个自2014年以来就隐藏在“没有网络”错误提示背后的小游戏,每个月被玩大约2.7亿次。在谷歌内部,它的代号是“Project Bolan”,这个名字是为了纪念摇滚乐队T. Rex的主唱马克·博兰。
由于Chrome浏览器是基于开源的Chromium项目开发的,因此这个游戏的全部代码——包括所有的设计细节和编程技巧——都公开透明,任何人都可以查看。
所以我把它的全部代码都看了一遍。结果发现,这个看似“简单”的游戏其实蕴含着丰富的游戏设计精髓:它会循序渐进地引导玩家上手,绝不会不公平地让玩家失败;它会模拟月亮的七种变化阶段;而且十年来,它一直带着一个拼写错误存在于数十亿台设备上。
在这篇文章中,我们将详细分析这个游戏的真实源代码,了解它的运作原理。如果你想在阅读文章的同时玩游戏,完全不需要关闭Wi-Fi连接。你可以在网上直接玩恐龙游戏,freeCodeCamp也提供了关于如何在线离线游玩的指南。
游戏的源代码保存在Chromium项目中,你可以在source.chromium.org网站上查看。这些代码其实只是一个包含大约3,000行纯JavaScript代码的文件,没有使用任何引擎或框架,甚至连jQuery都没有。
顺便说一下,这里还有这个游戏的全部美术资源。它们被保存在一个PNG文件中,所有的精灵图都通过坐标在文件中被精确地定位。
目录
时钟机制:为何游戏在所有设备上的运行速度都相同
每个游戏都必须首先解决的问题是:浏览器的重绘频率并不是固定的。一款支持144赫兹刷新率的游戏显示器会每秒发送144次`requestAnimationFrame`请求;而性能较差的笔记本电脑可能只能完成40次这样的请求。因此,如果你在每一帧中让角色移动固定数量的像素,那么在性能更好的硬件上,游戏的运行速度实际上会快3倍。
解决这个问题的标准方法就是被清晰、准确地执行出来。游戏会先以“每帧移动多少像素”来设定角色的移动速度,但这一数值是基于“60帧每秒”的假设来计算的;之后,游戏会根据实际经过的时间来调整角色的移动距离:
this.msPerFrame = 1000 / FPS;
// ...在每次更新时:
this.xPos -= Math.floor((currentSpeed * FPS / 1000) * deltaTime);
游戏中的所有移动对象,无论是恐龙的跳跃动作,还是仙人掌、云朵,甚至月亮的位置变化,都是通过`deltaTime`这个参数来计算的。正因为如此,无论你们使用的是哪种设备,你们的最高得分都是可以相互比较的。
如果从这篇文章中只记住一条工程原则的话,那就请记住这条:**永远不要用“每帧移动固定距离”的方式来控制角色的移动,而应该根据实际经过的时间来进行调整。**
霸王龙:四个常量与一个拼写错误
恐龙的所有物理行为都是由`Trex.config`和`Runner.config`文件中的一些数值来决定的:
GRAVITY: 0.6,
INITIAL_JUMP_VELOCITY: -10,
SPEED: 6,
ACCELERATION: 0.001,
MAX_SPEED: 13,
没错,你没看错:这里的`INITIAL_jump_VELOCITY`确实有三个字母“i”。这个拼写错误从一开始就被应用在了Chrome浏览器中,被安装在数十亿台设备上,而且一直存在了将近十年——因为重新命名这个变量所带来的风险实在不值得承担。下次当你在自己的代码中发现类似的拼写错误时,不妨用这个例子来安慰自己吧。
这些数值都是以“60帧每秒”为基准的像素值。只要把这些数值换算成实际的速度单位,物理规律就会变得一目了然了:重力的作用力是0.6像素/帧²,跳跃初速度是-10像素/帧。经过计算后可以发现,角色在起飞后大约0.28秒时,其跳跃轨迹的最高点会达到约83像素的高度——在150像素高的游戏场景中,这个高度已经超过了屏幕宽度的一半。
游戏的背景画面会以每帧6像素的速度开始滚动,然后这一速度会每帧增加0.001像素,直到最终达到13像素/帧的限制值。这个限制值非常重要——它确保了游戏会变得“有难度”,但永远不会让人觉得“根本无法通关”。
不过,大多数模仿这款游戏的开发者都忽略了一个细节:**跳跃的高度是可以变化的**。当你松开空格键时,游戏中的代码会如何运行呢?来看一下这段代码:
endJump: function () {
if (this.reachedMinHeight && this.jumpVelocity < this.config.DROP_VELOCITY) {
this.jumpVelocity = this.config.DROP_VELOCITY;
}
},
如果你只是短暂地按住空格键,恐龙就会进行一次短距离的跳跃;但如果你长时间按住这个键,恐龙就会完成一个完整的跳跃轨迹。如果提前松开按键,上升的速度就会被限制住,从而导致跳跃距离变短(不过只要达到最低高度要求,就不会让角色撞到仙人掌上)。而如果你在飞行过程中按下向下箭头键,`setSpeedDrop`函数会将下落速度增加3倍,从而使恐龙迅速重新落到地面。
仅仅通过这两个简单的机制,这款单键操作的游戏就具备了丰富的表现力:短跳、高跳、快速猛冲——这些动作使得顶尖玩家的玩法与你的截然不同。
公平性机制:游戏如何防止欺骗玩家
这是代码库中我最喜欢的部分,因为这些规则完全隐藏在背后,玩家只能“感受到”它们的存在。游戏中的每一个障碍物都是通过一小段配置信息来定义的,而这些配置实际上编码了一整套公平性规则。比如这个小仙人掌……
{
type: 'CACTUS_SMALL',
width: 17,
height: 35,
multipleSpeed: 4,
minGap: 120,
minSpeed: 0,
// ...
}
让我们来剖析一下这些隐藏在代码中的规则,以及它们在其他生成障碍物的逻辑中的体现:
规则1:游戏开始的前三秒内不会生成任何障碍物。 `CLEAR_TIME: 3000` 这一设置确保了在游戏开始后的前三秒内不会出现任何障碍物。这样玩家就有时间先熟悉操作方式,再开始正式游戏。 规则2:障碍物的出现频率会受到速度的影响。一个仙人掌最多可以以MAX_OBSTACLE_LENGTH: 3 个为单位成组出现,但只有当当前的游戏速度超过其 multipleSpeed 值时才会生成(小仙人掌为4,大仙人掌为7)。为什么呢?因为玩家的跳跃距离会随着游戏速度的增加而增加,所以地面移动得越快,玩家每次跳跃能跨越的距离就越远。
规则3:障碍物之间的间距也会根据速度进行调整。每个障碍物后的间距大致由 obstacleWidth × speed + minGap × 0.6 计算得出,其中还会加入一定的随机性。因此游戏速度越快,玩家可用的反应时间就越长。游戏的难度其实源自速度本身,而非不合理的障碍物布局。
规则4:没有任何一种障碍物会连续出现三次。游戏中有一个专门负责确保障碍物多样性的函数:
duplicateObstacleCheck: function (nextObstacleType) {
var duplicateCount = 0;
for (var i = 0; i < this.obstacleHistory.length; i++) {
duplicateCount = this.obstacleHistory[i] == nextObstacleType ?
duplicateCount + 1 : 0;
}
return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION;
},
MAX_OBSTACLE_DUPLICATION: 2 这一设置使得生成器会记录障碍物出现的历史,如果同一类型的障碍物连续第三次出现,就会重新随机生成。玩家根本不会注意到这条规则的存在,而这正是设计者的意图——因为如果这条规则不存在,玩家反而会立刻察觉到。
规则5:翼龙是游戏后期的终极BOSS。它的配置信息中明确规定 minSpeed: 8.5,这意味着在玩家的当前速度达到最大值的三分之二之前,它根本不会出现。翼龙会在三种不同的高度飞行,因此玩家有时需要跳跃躲避,有时则需要低头闪避;而且它永远不会以群体形式出现(multipleSpeed: 999),同时它的飞行速度还会比游戏界面的滚动速度稍快或稍慢——这个细节正好会在玩家逐渐适应游戏节奏的时候打破这种平衡,从而增加游戏的挑战性。这些规则共同解答了每一位游戏设计师都会面临的问题:如何让游戏变得更困难,同时又不会让它变得不公平?玩家能够清楚地感受到“我失败是因为动作太慢”与“我失败是因为游戏存在作弊行为”之间的区别。而这只恐龙,在经历了十年的发展以及无数次的运行之后,从未欺骗过任何人。
碰撞检测:恐龙由六个矩形构成
如果使用简单的碰撞检测方法,只需为恐龙设置一个边界框来检测是否发生重叠即可。但仔细看看这只恐龙吧——它的鼻子突出在外,还有尾巴以及下巴下方的一些空隙。如果只用一个边界框来检测碰撞,那么哪怕是一株仙人掌触碰到它下巴下方的空处,也会导致游戏角色死亡,而且这种体验会让人感到非常糟糕。
因此,实际上这只恐龙是由六个矩形构成的:
Trex.collisionBoxes = {
RUNNING: [
new CollisionBox(22, 0, 17, 16), // 头部
new CollisionBox(1, 18, 30, 9), // 躯干
new CollisionBox(10, 35, 14, 8), // 腿部
new CollisionBox(1, 24, 29, 5),
new CollisionBox(5, 30, 21, 4),
new CollisionBox(9, 34, 15, 4)
],
DUCKING: [
new CollisionBox(1, 18, 55, 25) // 一个长而低的矩形框
]
};
这六个小矩形精确地勾勒出了恐龙的实际轮廓:头部、躯干,以及腹部和腿部的一系列矩形结构。当恐龙低下头时,这些矩形会组合成一个长而低的矩形框来表示它的姿态。
障碍物的处理方式也是一样的:对于一小株仙人掌,只需用三个矩形来表示它的茎干和枝条;而对于翼龙,则需要五个矩形来模拟它的翅膀和喙部。
以下是这些矩形在实际游戏画面中的呈现效果:
这种碰撞检测算法采用了经典的两阶段检测机制:首先进行快速的外层矩形框检测(判断这些矩形是否相互接触),只有当这一阶段通过后,才会进一步详细地比较每个恐龙矩形的轮廓与每个障碍物的形状。在大多数情况下,这种检测方法速度很快;而在关键的时刻,它又能确保检测的准确性。如果仙人掌的刺看起来能够穿过恐龙下巴下方的空隙,那么游戏角色就不会死亡,因为代码的判断结果与你眼睛所看到的情况是一致的。
这种技术是最简单实用的方法之一,它的适用范围甚至可以扩展到格斗游戏中角色的碰撞区域和受击区域的检测。这个经验法则具有普遍性:碰撞检测的结果应该与玩家实际看到的景象相符,而不是为了让数学计算更加方便而随意设定。
夜间模式与七相月亮
当玩家的分数达到700分时(INVERT_DISTANCE: 700),整个游戏世界会发生反转:天空变暗,地面变浅,星星也会显现出来。12秒钟后(INVERT_FADE_DURATION: 12000),白天又会恢复。从数值上看,这只不过是一个简单的状态切换,再加上一些CSS样式来改变色彩搭配而已。但真正吸引人的是天空中发生的变化。
月亮并不是一个静止的图像。资源图中包含了它的七种不同形态,而代码会循环显示这些形态:
NightMode.phases = [140, 120, 100, 60, 40, 20, 0];
这些数值表示月亮在资源图中的位置偏移量——从细长的新月形态,到半圆月,最终变为满月:
每当天黑时,月亮的形态会向前推进一个阶段。星星则以自己的速度移动(STAR_speed: 0.3),这个速度比地面的移动速度要慢,因此夜景中会呈现出一种视差效果,营造出深度感。在浏览器的错误页面上,本来并不需要什么农历功能,但还是有人开发了这个功能——因为这些人明白,正是这样的细节让一个功能变得有趣、令人喜爱。
那些隐藏在显而易见之处的小细节
从源代码中还发现了一些值得注意的小细节:
恐龙会眨眼。当游戏等待玩家开始时,那只闲置的恐龙会以随机的时间间隔眨眼,而且眨眼的次数受到MAX_BLINK_COUNT: 3的限制。每次眨眼之间的间隔时间则是Math.ceil(Math.random() * Trex.BLINK_TIMING)。显然,是谷歌的某位工程师精心调整了恐龙眨眼的随机性。
你的得分并不是以像素为单位计算的。距离测量器会将实际移动的距离乘以COEFFICIENT: 0.025来得出最终结果。因此,得分为100意味着你实际上移动了4,000像素。每当你的得分达到100分时(ACHIEVEMENT_DISTANCE: 100),得分数字会以每秒四次的频率闪烁——这种设计让整数分数看起来更像是有意义的事件。
计数器的溢出显示方式也很特别。显示屏最多只能显示MAX_distance_units: 5位数字。当得分超过99,999时,视觉上的得分会重置为0,但内部的计数器会继续累加。这种设计明显是在向街机游戏机致敬。
移动设备玩家会有些“优势”。MOBILE-Speed_COEFFICIENT: 1.2这个数值使得游戏在移动设备上的运行速度稍快一些……不过实际上,这是为了适应较小的屏幕和触摸延迟,让游戏体验保持一致。显然,是有人仔细测量了手指在玻璃屏幕上操作与在键盘上操作之间的差异,并将这个差异通过一个常量体现在了代码中。
重新开始游戏是有保护的。在游戏崩溃后,会有GAMEOVERCLEAR_TIME: 750这段延迟时间。在四分之三秒的时间里,你按下的跳跃键不会立即重启游戏。这样设计的目的是为了防止你在死亡后立即重新开始游戏,从而错过查看自己得分的机会。这其实是一种“仁慈”的设计。
相关文章
技术实践
如何使用Pydantic AI构建具备生产级功能的智能代理
使用原始的LLM SDK来构建AI代理,在开发原型阶段确实可行,但一旦你需要结构化输出、可测试的代码以及具备生产环境可靠性的系统,这些问题就会显现出来。 这些问题的出现具有很强的规律性。你的笔记本代码可以正常运行,于是你将其应用到生产环境中,并开始添加各种补丁:比如为`json.loads`添加异常处理逻辑,编写辅助函数来去除Markdown格式的标记,使用`if`语句检查字段类型,设置重试机制,以及创建一个将工具名称与对应的可调用函数关联起来的映射函数。这些代码单独来看并不复杂,但当它们汇集在一起时,就会占据你代码库的大部分内容,而真正的代理逻辑反而被这些辅助代码所掩盖。 本文将按照这些问题
阅读全文
技术实践
如何利用人工智能对传统应用程序进行现代化改造,同时又避免对其进行彻底的重写?
我见过一些旧系统的迁移项目被认为取得了成功,因为那些旧的框架已经从代码库中消失了。 但六个月后,团队仍然在面对同样的耦合问题、同样不清晰的业务规则,以及几乎相同的部署难题。 虽然技术已经发生了变化,但整个系统本身并没有发生太大的改变。 人工智能让这个问题变得更加复杂了。 它能够比人类团队更快地翻译代码,能够解释那些不熟悉的类结构,生成测试用例,创建适配器,更新API接口,从而大大减少重复性工作。 但是,如果你让一个人工智能编码工具去处理一个旧应用程序,并简单地要求它将所有内容都迁移到现代的技术架构中,那么很可能会得到你想要的结果: 还是那个系统,只不过被更快地重新编写了一遍而已。 这并不一定算
阅读全文
技术实践
Astro 7:Rust编译器、Rust Markdown处理工具以及Vite 8框架,使得构建速度提升了高达61%。
Astro 7着重提升构建性能,它采用了原生工具以及用Rust语言重新编写的编译器。新版本在处理Markdown格式的内容时效率更高,同时对HTML代码的规范要求也更加严格。最近的更新为该工具添加了高级路由功能及增量构建机制;不过,在用户反馈中也有提到一些关于旧文件兼容性以及依赖项数量计算方面的问题。Astro主要适用于那些依赖少量JavaScript代码的内容驱动型网站。 作者:丹尼尔·柯蒂斯
阅读全文
技术实践
如何在没有服务器的情况下为静态网站添加动态功能
静态网站目前正受到人们的青睐,这是有原因的。一个包含HTML、CSS和JavaScript文件的文件夹,加载速度很快,托管成本也很低,而且几乎不可能出现故障。 像 Astro 、 Eleventy 和 Hugo 这样的工具,能够利用Markdown文件和模板帮您生成这样的网站结构。而Netlify、Vercel以及Cloudflare Pages等托管服务,则可以通过内容分发网络来提供这些生成的网站内容,而且通常还是免费的。 不过,您的网站还需要具备实际的功能。读者可能想要留下评论,或者有人想通过电子邮件与您联系。也许您还想实时显示价格信息,需要用户登录后才能查看某些页面,或者在网站发布之前收
阅读全文
如何使用Pydantic AI构建具备生产级功能的智能代理
使用原始的LLM SDK来构建AI代理,在开发原型阶段确实可行,但一旦你需要结构化输出、可测试的代码以及具备生产环境可靠性的系统,这些问题就会显现出来。 这些问题的出现具有很强的规律性。你的笔记本代码可以正常运行,于是你将其应用到生产环境中,并开始添加各种补丁:比如为`json.loads`添加异常处理逻辑,编写辅助函数来去除Markdown格式的标记,使用`if`语句检查字段类型,设置重试机制,以及创建一个将工具名称与对应的可调用函数关联起来的映射函数。这些代码单独来看并不复杂,但当它们汇集在一起时,就会占据你代码库的大部分内容,而真正的代理逻辑反而被这些辅助代码所掩盖。 本文将按照这些问题
阅读全文
如何利用人工智能对传统应用程序进行现代化改造,同时又避免对其进行彻底的重写?
我见过一些旧系统的迁移项目被认为取得了成功,因为那些旧的框架已经从代码库中消失了。 但六个月后,团队仍然在面对同样的耦合问题、同样不清晰的业务规则,以及几乎相同的部署难题。 虽然技术已经发生了变化,但整个系统本身并没有发生太大的改变。 人工智能让这个问题变得更加复杂了。 它能够比人类团队更快地翻译代码,能够解释那些不熟悉的类结构,生成测试用例,创建适配器,更新API接口,从而大大减少重复性工作。 但是,如果你让一个人工智能编码工具去处理一个旧应用程序,并简单地要求它将所有内容都迁移到现代的技术架构中,那么很可能会得到你想要的结果: 还是那个系统,只不过被更快地重新编写了一遍而已。 这并不一定算
阅读全文
Astro 7:Rust编译器、Rust Markdown处理工具以及Vite 8框架,使得构建速度提升了高达61%。
Astro 7着重提升构建性能,它采用了原生工具以及用Rust语言重新编写的编译器。新版本在处理Markdown格式的内容时效率更高,同时对HTML代码的规范要求也更加严格。最近的更新为该工具添加了高级路由功能及增量构建机制;不过,在用户反馈中也有提到一些关于旧文件兼容性以及依赖项数量计算方面的问题。Astro主要适用于那些依赖少量JavaScript代码的内容驱动型网站。 作者:丹尼尔·柯蒂斯
阅读全文
如何在没有服务器的情况下为静态网站添加动态功能
静态网站目前正受到人们的青睐,这是有原因的。一个包含HTML、CSS和JavaScript文件的文件夹,加载速度很快,托管成本也很低,而且几乎不可能出现故障。 像 Astro 、 Eleventy 和 Hugo 这样的工具,能够利用Markdown文件和模板帮您生成这样的网站结构。而Netlify、Vercel以及Cloudflare Pages等托管服务,则可以通过内容分发网络来提供这些生成的网站内容,而且通常还是免费的。 不过,您的网站还需要具备实际的功能。读者可能想要留下评论,或者有人想通过电子邮件与您联系。也许您还想实时显示价格信息,需要用户登录后才能查看某些页面,或者在网站发布之前收
阅读全文