第一次遇见谷歌小恐龙,应该是绝大多数Chrome用户共同的记忆:断网时那只像素小恐龙出现在页面上,按一下空格它就开始狂奔,越过一颗颗仙人掌。很多人的最高纪录可能就十几分,但我在一次偶然中打开了开发者工具,才发现这只小恐龙根本不是什么“离线彩蛋”,而是一个完整的JavaScript游戏,游戏对象全部挂在页面的全局作用域里。这也意味着:只要往控制台里输入几行代码,就可以做到无敌、加速、换皮肤,甚至直接改掉你的最高分。
这篇文章我会从底层对象结构讲起,把无敌、加速、换皮肤这三件事分别拆开,给大家一套可以直接复制粘贴的作弊代码,也会解释每段代码为什么有效、失效了怎么排查。无论你只是想在朋友面前炫耀一个十万分,还是想深入玩玩Chrome的JS游戏机制,这篇都适用。
1. 破译Chrome小恐龙游戏的对象架构——代码从哪里介入
1.1 游戏到底在哪里运行
谷歌小恐龙在官方文档里的名字叫 T-Rex Runner,是Chrome内置的离线网页游戏。很多人以为它只会在断网时出现,其实不需要断网,直接在地址栏输入chrome://dino,回车就能进入游戏。这个彩蛋页面存在已久,绝大多数Chrome版本都保留了入口。
游戏本身是一个完全用JavaScript编写的Canvas小程序,所有对象以实例属性的形式挂在Runner这个顶层对象下面。由于Chrome没有给它做模块化隔离,所以打开开发者工具输入Runner,就能看到整个游戏对象的构造函数和原型方法。
这里有个很重要的概念:Canvas只是负责把画面画出来,游戏真正的逻辑在Runner里面。碰撞检测、计分规则、障碍物生成这些环节都暴露在前端代码里,修改它们不需要任何底层手段,只要在控制台执行JavaScript就行。对玩惯了大型单机游戏修改器的人来说,这相当于把存档文件夹直接摆在桌面上。
1.2 Runner.instance_:打开游戏后门的钥匙
游戏初始化时会创建Runner实例,并赋值给静态属性Runner.instance_。这个静态属性就是作弊的入口。在控制台输入:
Runner.instance_返回的是当前活动的游戏实例,里面包含tRex(小恐龙对象)、horizon(障碍物管理器)、distanceRan(累计距离)、currentSpeed(当前速度)等字段。理解了这个结构,作弊的本质就可以概括为三件事:
- 让
gameOver不再触发,也就是无敌; - 直接修改
distanceRan、currentSpeed,也就是加速和改分; - 控制
horizon里的障碍物数组,从物理层面消灭威胁。
1.3 游戏循环与作弊触发时机
小恐龙的整个逻辑循环由requestAnimationFrame驱动,每一帧都会执行Runner.prototype.update()。这里有个关键点:作弊代码在什么时候注入。
如果在页面还没有开始游戏时输入代码,Runner.instance_可能不存在,因为游戏尚未实例化。最好的做法是先按一下空格开始游戏,或用鼠标点击页面触发启动,然后再输入代码。相比之下,Runner.prototype.gameOver重写不需要实例存在,因为原型方法始终存在,这就是为什么有些代码需要“先开始再注入”,而有些代码任何时候都能生效。
另外提一句,Chrome把runner-canvas这个class给了画布元素,很多CSS选择器方案都需要用到它,后面换皮肤时会经常用到。
2. 无敌机制:不只一种写法,关键在拦截“Game Over”
2.1 最简做法:重写gameOver方法
无敌的经典写法是重写游戏结束方法。在控制台输入:
Runner.prototype.gameOver = function() {};运行之后,不管恐龙撞上仙人掌还是飞龙,游戏都不会结束。因为所有死亡逻辑都会经过gameOver这个方法,一旦把它替换成空函数,整条死亡判定链就断了。恐龙会直接穿过障碍物继续跑,体感上像是“虚无模式”。
但只输入这一条有一个副作用:gameOver原本还会执行保存分数、显示Game Over面板这些逻辑,替换掉之后都不会执行。如果你想保留“撞上之后暂停一下”的感觉,可以改成:
Runner.prototype.gameOver = function() { Runner.instance_.playing = false; };这样恐龙撞上障碍物后会停在原地,不会弹出结束面板,也不会丢分,按空格还能继续跑。这种写法比直接清空函数更接近正常游戏体验。
2.2 物理级作弊:直接清理障碍物数组
相比重写死亡函数,更“物理”的方案是从根上消灭障碍物。控制台执行:
Runner.instance_.horizon.obstacles = [];这条命令会把当前地平线上所有障碍物清空,恐龙立刻获得一片开阔地。但有个问题:游戏每一帧都可能生成新的障碍物,所以只执行一次不够。通常配一个定时器循环执行:
setInterval(() => { Runner.instance_.horizon.obstacles = []; }, 100);这样恐龙永远跑在一片干净的路上,视觉上没有紧张感,但确实是无敌。如果你既想无敌又不想画面太单调,可以把定时器的间隔调整到300~500毫秒,让障碍物偶尔出现一下又被清掉,看起来像在畅行无阻地跑。
2.3 为什么某些写法只生效一次
不少人在尝试时发现,刚粘完上述代码很好用,跑了一阵后障碍物又冒出来了。原因在于游戏存在画面暂停再恢复的机制,暂停恢复时可能会重新构建horizon对象,或者重新生成一个障碍物数组。这时候之前定时器里引用的Runner.instance_.horizon.obstacles指向的可能是旧数组,新的数组里照样有仙人掌。
解决思路是不要直接操作障碍物数组,而是重写障碍物生成方法。很多Chrome版本里,障碍物生成相关的方法名字大概是createObstacle、spawnObstacle这个风格。你可以先打印原型看看:
Object.keys(Runner.prototype).filter(k => /obstacle/i.test(k));这会列出所有和 obstacle 有关的方法,找到当前版本实际存在的那一个,把它置空:
Runner.prototype.createObstacle = function() {};置空生成方法后,即使地平线重新构建,也不会凭空冒出仙人掌。这个方法更稳健,适配不同Chrome版本时,只需要先跑一下过滤命令确认方法名即可。
2.4 组合操作:不死+持续跳跃的观赏模式
无敌和跳跃结合可以做出一种“弹簧恐龙”效果,适合演示给别人看:
Runner.prototype.gameOver = function() {}; setInterval(() => { Runner.instance_.tRex.startJump(); }, 500);每半秒触发一次跳跃,恐龙像装了弹簧一样不断起跳。如果觉得节奏太快,可以把间隔调成800;如果版本不支持startJump,在Runner.instance_.tRex对象里看一眼方法名,通常还有jump()这个候选。这种组合方式很适合录屏,看起来完全没有作弊痕迹,像是一只在自嗨的恐龙。
3. 加速与控分:读懂distanceRan和setSpeed的内部逻辑
3.1 distanceRan:距离即分数
游戏的核心参数是distanceRan,单位是像素。你没看错,就是像素。T-Rex Runner这个游戏没有复杂的计分系统,直接以水平移动距离作为分数基数,每往前跑一段这个值就会增加。
在控制台执行:
Runner.instance_.distanceRan = 99999;结算时的分数会变成九万多,但游戏画面不会立即变化。严格来说,这种改法“画面感”不强,只是把最终成绩改了,游戏节奏照样慢悠悠。很多人用这种方式刷分后看不出效果,以为代码失效了,其实只是没理解它的作用点。
3.2 setSpeed:真正意义上让恐龙跑起来
要真正“加速”,得用速度接口:
Runner.instance_.setSpeed(20);默认速度大概在6~10之间,设成20后恐龙的前进节奏明显加快,地面纹理每秒滑过的距离肉眼可见。这个方法是T-Rex Runner提供的官方接口之一,相当于游戏自己留的后门。
但有个细节要注意:setSpeed不是永久锁定。游戏逻辑每一帧都会按加速度把速度往上推,如果你设成20,它可能继续涨到21、22。想要“锁速”,需要定时覆盖:
setInterval(() => { Runner.instance_.setSpeed(20); }, 500);这样速度基本固定,不会无限增高。实测下来,速度锁定在15~25之间观感最好,既有明显的加速快感,又不会快到看不清障碍物。
3.3 从“改分”到“改排行榜”:localStorage的利用
当你跑到一个高分,Chrome会把最高纪录存在localStorage里。在游戏页面打开控制台,输入:
localStorage就能看到存储的键值,其中典型键名有highscore、dino/score之类的命名。不同版本可能不一样,直接按下表对照排查:
| 现象 | 可能键名 | 处理方式 |
|---|---|---|
| 新游戏读取旧纪录 | highscore | localStorage.setItem('highscore', '99999') |
| 刷新后分数被重置 | dino/score | 检查键名后再写入 |
| 修改后不立刻生效 | 内存缓存 | 改完刷新页面,重新进入游戏 |
直接修改localStorage后,通常刷新页面再进游戏就能看到新纪录。但有一些版本会把高分绑定在游戏内部实例的内存变量上,页面刷新前不一定能立刻反映出来,这种时候就要配合内存修改。
const game = Runner.instance_; game.setHighScore && game.setHighScore(99999);setHighScore并不是全版本通用的方法,所以先用&&做存在性判断,如果存在就调用。这个方法的好处是不需要刷新页面,当前局内就能生效。
3.4 加速偏高时的表现与建议
当速度超过30,你会发现恐龙跳跃高度相对变低,因为跳跃的垂直速度没有跟着水平速度同步调整。这是很有意思的物理现象:跳跃加速度是恒定值,水平速度增加后,滞空时间不变但行进距离变长,看起来就像“跳不远”。速度太高时还可能出现地面纹理闪烁,因为Canvas重绘跟不上。
所以给大家一个实际建议:展示用的速度值定在15~25之间,既能明显看出加速,又不至于让人看不清画面。真想追求极限可以把速度放到40,但那就不是游戏体验,而是单纯看画面闪动了。
4. 换皮肤实战:从CSS滤镜到像素级重绘
4.1 最快换装法:canvas滤镜
想让默认的黑色像素恐龙换个颜色,最快的方案是使用CSSfilter,不用碰任何游戏内部逻辑:
document.querySelector('.runner-canvas').style.filter = 'hue-rotate(90deg)';hue-rotate可以把整个画面的色相偏移,原本的黑白像素变成有颜色的像素。你可以试试不同角度:
hue-rotate(90deg):青色系hue-rotate(180deg):紫色系hue-rotate(270deg):红橙色系
如果你觉得整条地面、云朵都变色太夸张,可以先用最基础的效果感受一下。反正CSS滤镜是即时生效的,不满意就改参数重新执行,完全不影响游戏成绩。
4.2 组合滤镜做出霓虹风格
CSS过滤器支持叠加,把反色、棕褐色调、饱和度、色相旋转串在一起,能组合出非常夸张的视觉效果:
const canvas = document.querySelector('.runner-canvas'); canvas.style.filter = 'invert(1) sepia(1) saturate(6) hue-rotate(60deg) brightness(1.2)';拆开看的话:
invert(1)会把黑色像素变成白色,白色背景变成黑色;sepia(1)把所有像素染上棕褐色基调;saturate(6)拉高饱和度,让颜色更鲜艳;hue-rotate调整最终色相;brightness控制整体明暗。
实测下来,亮度过高时仙人掌轮廓会看不清,容易误判。我刚上手时喜欢saturate(10),结果仙人掌和背景糊成一片。后来保留了一个比较耐看的参数:invert(1) hue-rotate(120deg),白色背景变成淡紫,黑色恐龙变成青色,不伤眼而且很有辨识度。
4.3 像素级重绘:真正意义上的换皮肤
CSS滤镜实现方便,但它是“整屏换色”。如果只想把恐龙变成粉红色、仙人掌保持原样,就需要 pixel 级别的操作。思路是拿到画布的 ImageData,遍历每个像素,把属于深色物体的像素替换成目标颜色。
以下是一个简化版实现:
const canvas = document.querySelector('canvas'); setInterval(() => { const ctx = canvas.getContext('2d'); const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imgData.data; for (let i = 0; i < data.length; i += 4) { if (data[i] < 60 && data[i+1] < 60 && data[i+2] < 60) { data[i] = 255; data[i+1] = 0; data[i+2] = 150; } } ctx.putImageData(imgData, 0, 0); }, 100);这段代码每100ms把深色像素染成粉紫色,恐龙、仙人掌、文字都会变色。这里用setInterval的原因在于,游戏每帧都会重新绘制整个画布,如果不反复覆盖,颜色只会持续几百毫秒就恢复。
必须提醒:遍历加写入非常吃CPU,100ms的间隔已经是兼容性较好的折中。如果硬要按16ms一帧的频率执行,低配电脑在长跑后很容易掉帧,风扇可能直接起飞。想要流畅还是优先用CSS滤镜方案。
4.4 换肤与无敌、加速组合示例
把前面的技术组合成一个“一次粘贴,全套生效”的脚本:
// ====== 小恐龙全开模式 ====== // 1. 无敌 Runner.prototype.gameOver = function(){}; // 2. 清理障碍物,每250ms执行一次 setInterval(() => { Runner.instance_.horizon.obstacles = []; }, 250); // 3. 锁速18 setInterval(() => { Runner.instance_.setSpeed(18); }, 500); // 4. 整屏紫色调 document.querySelector('canvas').style.filter = 'hue-rotate(180deg)';粘贴运行后,恐龙不会再死、速度稳定在18、画面变成紫色调,可以作为演示专用模式。如果你嫌颜色不够炫,把第4段代码替换成前面说的组合滤镜即可。
5. 失效、恢复与自定义扩展:高手才需要知道的坑
5.1 为什么Chrome更新后代码会失效
前面所有代码都基于游戏源码里的方法名,而Chrome每次大版本更新都可能调整内部实现。比如某次更新后,Runner.prototype.gameOver仍然存在,但新游戏流程改为在按键逻辑里先判断playing状态,然后才调用gameOver,这时候只重写gameOver就不够用了。
遇到这种情况,先别急,用控制台打印原型:
console.log(Runner.prototype);展开原型对象,对比方法名是否变化。如果gameOver确实消失了,就改用实例方法:
const instance = Runner.instance_; instance.gameOver = function(){};注意,这种写法只对当前游戏会话有效,刷新后新一局会重新从原型链读取原始方法,所以只适合临时演示。想稳定生效,还是得从原型层面动手。
5.2 让代码自动执行:Snippets保存大法
不想每次都手动复制粘贴,可以把代码保存为Chrome DevTools的Snippets。按F12打开开发者工具,切到Sources(源代码)面板,左侧找到Snippets,右键新建一个脚本,把组合代码粘进去。之后每次进入chrome://dino,先点击游戏让实例创建,再在Snippets里右键运行即可。
这个方案比每次都从记事本复制代码优雅得多。还有一个常见习惯是先把脚本拆成几段,无敌和加速分开保存,因为有时候只想开加速不想无敌,整体执行的话会改变游戏手感。
5.3 常见报错与排查思路
输入代码后没有任何反应,通常有这几个原因:
- 控制台所在的页面不是
chrome://dino。开发者工具和网页是一一对应的,在普通标签页打开的控制台里操作Dino是无效的。 Runner.instance_为 undefined,说明游戏尚未启动。按一下空格或点击一次页面开始游戏后再试。- 有浏览器插件干扰了页面脚本。正常Dino页面不会阻止控制台注入,但某些安全类扩展可能会,临时禁用即可。
还有一种情况是代码生效了但看不出效果,比如只执行Runner.instance_.distanceRan = 99999,画面毫无变化,很容易误以为失效。要验证代码是否真的生效,最直接的方式是打印相关属性:
console.log(Runner.instance_.distanceRan);如果输出随游戏跳动并变成一个新值,说明命令执行成功。如果报错,在控制台复制错误信息时,优先看是不是undefined is not a function,这类错误基本都是方法名在当前版本不存在。
5.4 在此基础上还能玩出什么花
除了无敌、加速、换皮肤,这套对象体系还支持更多扩展玩法:
- 自动跳跃:监听障碍物距离,靠近时自动跳。先读
Runner.instance_.horizon.obstacles数组,取第一个障碍物的x坐标,判断接近阈值后调用Runner.instance_.tRex.jump()。 - 假死按钮:把原始
gameOver方法暂存下来,在特定条件下手动调用,做一个键盘触发“自杀”。 - 自定义按键:监听
keydown事件,绑定加速、减速、换肤三组功能键。这样可以把作弊代码变成一个外置控制器。
有一点要提醒:其他浏览器的恐龙彩蛋不一定会用同一套对象命名。Firefox离线页面的游戏对象名、Edge内置冲浪游戏的对象结构都和Chrome不同,需要重新分析。对着错误页面干烧时间没有意义,认准Chrome就好。
真正玩下来你会发现,谷歌小恐龙的高分不过是一串能被随意修改的数字,有趣的是在开发者工具里一层层拆开游戏对象的探索过程。尤其是当我第一次看着那只像素小恐龙顶着紫色滤镜,在锁定18的速度下一路狂奔还不死时,我意识到所有看似封闭的小程序,其实都留着足够聪明的调试入口。希望这篇攻略能帮你在无聊的时候,找到和那只小恐龙重新认识的方式。