你有没有过这种经历:电脑断网,Chrome 浏览器跳出一只像素霸王龙,你百无聊赖地按空格跳仙人掌,然后死在第三只鸟上,越想越气,又按一下空格重新开始。我就是这么入坑的。作为一个喜欢折腾脚本的人,我很快产生了“歪念头”——能不能给这个谷歌 Chrome 恐龙小游戏写个外挂,让霸王龙自动跳,甚至让我想看它跑多远就能跑多远?
这篇文章就把我折腾“Chrome 恐龙小游戏外挂”的完整过程记录下来。不吹牛,不引流,就是纯技术拆解。内容包括:这个游戏的内核是怎么跑的、为什么几个 JavaScript 命令就能改游戏逻辑、我写的三版外挂脚本分别怎么实现、以及实际操作中踩过的一堆坑。适合对浏览器脚本、Canvas 游戏实现、Chrome DevTools 感兴趣的人参考,也适合把“游戏外挂”当成前端技术练习的入门者。
1. 先想清楚要做什么:外挂需求拆解与方案选型
1.1 常见的“外挂”需求有哪些
我一开始以为恐龙小游戏的外挂只有“自动跳跃”一种,真正上手拆解后发现,需求其实可以拆成好几类,每一类对应着不同的技术切入点。
- 自动跳跃:检测前方障碍物位置,在合适的时机触发霸王龙跳跃,解决手残党的痛点。这个需求最主流,玩家人群最大。
- 无限复活与无敌:让霸王龙撞到障碍物也不死,或者说直接绕过碰撞检测逻辑。这类需求适合“我就想看看游戏能跑到多少分”的人。
- 变速与加速:把游戏速度调快或调慢,既能制造变态难度,也能把游戏降速分析每一帧的障碍物生成规律。
- 清空障碍物:一键抹除当前画布上的所有仙人掌和翼龙,体验“无阻力跑酷”。
- 刷分与修改距离:直接修改计分变量,让分数瞬间爆表,满足晒截图的需求。
- 锁定白天/黑夜模式:正常游戏白天黑夜会交替,有些人不喜欢画面闪变,可以强制固定模式。
- 自动化测试辅助:把游戏变成一个天然的前端性能测试场,通过脚本控制人物行为,然后观察 Canvas 渲染帧率,这个需求比较硬核,但同样有价值。
你要做“外挂”,第一件事不是写代码,而是先明确到底要解决哪一个需求。因为不同需求对应的改造点完全不同,有的改方法,有的改变量,有的改循环逻辑,混在一起容易乱。以我这次实践为例,主线需求是“自动跳跃 + 无敌 + 刷分”,这样既能跑得快,又死不了,还能快速验证效果。
1.2 技术选型:控制台注入、油猴脚本与扩展的取舍
明确了需求,第二个问题是脚本以什么形式注入到游戏里。我对比了三种主流方案,各有各的适用场景。
| 注入方式 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| DevTools 控制台手动注入 | 零安装,打开就能用,调试方便 | 每次刷新页面后需要重新执行,代码量大时容易乱 | 快速验证思路、临时使用 |
| 油猴/Tampermonkey 等用户脚本 | 自动注入,可持久化,带规则匹配 | chrome:// 页面默认拦截扩展脚本,需要额外设置或绕行 | 配合本地镜像页长期使用 |
| Chrome 扩展插件 | 正规、能力最强,可以做到按钮式控制 | 开发门槛稍高,chrome:// 页面同样受限 | 想做成工具分发给别人用 |
| CDP 远程调试协议 | 可完全接管浏览器,能模拟按键、读取状态 | 需要外部程序配合,无法单独在页面内运行 | 自动化测试、Python/Node 控制浏览器 |
我做第一版时直接用的控制台。因为恐龙小游戏在 chrome://dino 页面里运行,打开 DevTools 就可以直接访问页面上下文里的 JavaScript 对象,效率最高。油猴脚本对 chrome:// 这类浏览器内部页面默认不生效,扩展也需要额外权限,属于进阶方案,我在后文会专门讲怎么处理。这里先记住一个原则:先控制台,后自动化,不要一上来就写一堆配置框架,容易把简单的事搞复杂。
2. 这个游戏到底是怎么实现的
2.1 找到游戏本体与实例
想要给游戏做外挂,第一步不是写脚本,而是找到游戏代码里的关键对象。Chrome 恐龙小游戏本质上是一个基于 Canvas 的 JavaScript 单机游戏,所有逻辑都在浏览器里跑,没有服务器校验,这给了我们很大的操作空间。
在 chrome://dino 页面里按 F12 打开 DevTools,切到 Console 标签,输入一行命令:
console.dir(Runner.instance_);这个Runner.instance_就是整个游戏的核心实例。它能被访问是因为游戏源码里有一个全局的单例引用,恰好没有设置成不可访问,这在许多早期 Canvas 小游戏里非常常见。运行后你会看到一个巨大的对象,里面包含了霸王龙(tRex)、地平线系统(horizon)、距离计数器(distanceRan)、速度(speed)等等。
我试过用Object.keys(Runner.instance_)去列所有的属性,常见的直接可用的字段包括:
tRex:霸王龙对象,里面有跳跃状态、位置坐标、跳跃起始函数horizon:整个场景系统,包含云、障碍物、地面纹理horizon.obstacles:当前画布上所有障碍物的数组currentSpeed/speed:游戏当前速度distanceRan:已经奔跑的距离,直接对应分数setSpeed():运行时修改速度的方法checkForCollisions():碰撞检测方法
找到这个对象,等于拿到了游戏的控制台钥匙。
2.2 帧循环、速度与碰撞检测
理解游戏实现机制,核心要看三个逻辑:帧循环、速度计算、碰撞检测。
帧循环用的是浏览器标准的requestAnimationFrame,每一帧都会更新画面状态。游戏为了性能,会根据当前速度动态调整每次更新的步长,所以当速度数值变大时,你会发现霸王龙的前进速度明显加快,但帧率并没有变,这种感觉就像跑步时步幅变大而不是脚步变快。
速度计算很有意思。正常游戏里,速度会随着奔跑距离缓慢增加,这也是为什么越跑越难。Runner实例里的update()方法会按时间累计距离,再换算成当前速度。我们可以直接调用setSpeed(30)强制塞一个很大的速度,游戏就会立刻进入“疯狗模式”。
碰撞检测的核心就是checkForCollisions(),它遍历当前所有障碍物,检查霸王龙和障碍物之间有没有发生矩形重叠。如果重叠了,就触发死亡逻辑,播放动画,结束本局。这个逻辑非常典型,很多 2D 小游戏都是同样的套路。
哪天真要“无敌”,最简单的办法就是让这个碰撞检测函数什么都不做。因为游戏逻辑全部在客户端,没有后端做任何校验,我们修改函数体不会被任何人拦。
2.3 哪些属性改了就能生效
为了不让你对着巨大的对象一脸懵,我整理了一份“改造点速查表”,都是实测过可以直接用的。
| 目标和效果 | 关键操作 | 说明 |
|---|---|---|
| 无敌 | Runner.instance_.checkForCollisions = function(){} | 覆盖实例上的碰撞检测方法,撞到障碍物也不会死 |
| 变速 | Runner.instance_.setSpeed(50) | 调到 50 后游戏瞬间变快,跳跃时机需要重新适应 |
| 清障 | Runner.instance_.horizon.obstacles = [] | 清空当前所有障碍物数组,画布上立刻干干净净 |
| 刷分 | Runner.instance_.distanceRan += 1000 | 直接加距离,分数跟着变,但会被每帧更新覆盖,需要循环执行 |
| 强制跳跃 | Runner.instance_.tRex.startJump() | 让霸王龙立刻起跳,不依赖按键 |
| 强制蹲下 | Runner.instance_.tRex.setDuck(true) | 模拟按下蹲键,可以躲过飞鸟 |
| 锁定黑夜 | Runner.instance_.horizon.nightMode = true | 强制夜晚模式,画面色调变深 |
看到这里你应该明白了:对这类单机 Canvas 小游戏做外挂,本质上不是“破解”,而是“运行时修改 JavaScript 对象”,跟调试普通前端项目几乎没区别。理解了这一点,后面写脚本就是水到渠成的事。
3. 手写脚本:从“自动跳”到“定制挂”
3.1 第一版:直接绕开碰撞检测
我先写了最暴力的“无敌版”,代码逻辑极其简单。
(function () { const runner = Runner.instance_; if (!runner) { console.error('未找到游戏实例'); return; } runner.checkForCollisions = function () {}; console.log('无敌模式已开启'); })();把这段代码粘贴到控制台,立刻生效。霸王龙撞到仙人掌时不会再停下,游戏会一直跑下去,直到你手动刷新页面。这个脚本的核心在于覆盖了实例方法,而不是修改原型方法。这里有个细节值得注意:游戏每帧更新都会调用this.checkForCollisions(),这里this指向的是Runner实例本身,所以实例上的方法优先级高于原型链上的同名方法,我们覆盖实例方法就能生效。
但不要高兴太早,第一版有个致命缺点。如果你在游戏已经 Game Over 之后运行这段代码,场景里霸王龙可能已经倒地,游戏不会自动恢复运行。正确姿势是先按空格开一局,或者调用runner.restart()重新开始,再注入脚本。
还有一个隐藏问题:页面刷新后变量全部重置,之前打的“补丁”会全部消失。所以我一开始就建议,开发阶段用控制台,想要长期使用就要做持久化,后文会讲。
3.2 第二版:自己判断该不该跳
无敌模式虽然爽,但少了“跑酷”的刺激感。我决定做一个更智能的版本:自动跳跃。这需要读取障碍物位置,判断距离,再触发跳跃,本质上是一个简单的 AI 决策。
(function () { const runner = Runner.instance_; if (!runner) { console.error('未找到游戏实例'); return; } const trex = runner.tRex; const horizon = runner.horizon; setInterval(() => { const obstacles = horizon.obstacles; if (!obstacles.length) return; // 找到离霸王龙最近的障碍物 const next = obstacles.reduce((a, b) => (a.xPos < b.xPos ? a : b)); const distance = next.xPos - trex.xPos; // 满足距离条件且霸王龙不在空中时起跳 if (distance < 120 && distance > 20 && !trex.jumping && !trex.ducking) { trex.startJump(); } }, 20); })();这段代码的核心逻辑是“找最近障碍物 -> 算距离 -> 跳”。每个参数都经过我的实测:
setInterval(..., 20)代表每 20 毫秒检测一次,对应大约 50 帧的检测频率,足够实时。distance < 120是触发距离。速度较低时,120 像素留出的反应时间很宽裕;速度很高时,最好把阈值提高到 150 左右,否则跳得太晚容易撞上。distance > 20防止霸王龙已经跟障碍物重叠时还在不停触发跳跃。!trex.jumping && !trex.ducking避免跳跃过程中重复起跳导致动作卡顿。
实测下来的效果是:普通速度下基本能自动跳过所有仙人掌和翼龙,跑几千分没有问题。但这个版本对“飞鸟低头”场景没有优化。正常游戏里,翼龙飞得低时需要按蹲键,而不是跳。我偷懒没有做蹲的逻辑,因为跳跃的高度有时候也能擦着飞过去,只是偶尔会被打中。如果你要完美版,需要在检测到翼龙且高度较低时调用trex.setDuck(true),延迟几百毫秒后再设置setDuck(false)恢复站立。
3.3 第三版:变速、清障、刷分
自动跳跃搞定后,我继续往里面塞新功能,最后做成了一个小工具箱。核心思路是把多个独立功能封装成函数,需要哪个调哪个。
(function () { const runner = Runner.instance_; if (!runner) return; window.dinoHack = { godMode() { runner.checkForCollisions = function () {}; }, setSpeed(speed) { runner.setSpeed(speed || 60); }, clearObstacles() { runner.horizon.obstacles = []; runner.horizon.obstacleTimer = 0; }, addScore(score) { runner.distanceRan += score || 1000; }, forceJump() { runner.tRex.startJump(); }, forceNight() { runner.horizon.nightMode = true; } }; // 周期刷分:因为 distanceRan 每帧都会被更新,所以要不断追加 window.dinoHack.autoScore = function (interval) { setInterval(() => { runner.distanceRan += 500; }, interval || 100); }; console.log('恐龙外挂工具箱已加载'); })();这个版本里有一个我特别想强调的坑:直接往runner.distanceRan赋值往往没有效果,或者只闪一下就被重置。原因在于游戏每一帧都会在update()里重新计算距离,把distanceRan当成一个基础变量来覆盖。想稳定刷分,要么不断往上面“加”,要么拦截游戏的距离更新逻辑,前者简单粗暴,后者复杂度高,对咱们写外挂来说没必要。
变速也一样。setSpeed(50)生效后,游戏速度在新的一局开始时大概率会被重置。如果你想实现“每次开局都 50 迈”,需要监听游戏的重置事件,或者干脆用一个定时的setInterval循环强制设置速度。这种“抗重置”设计是外挂能否稳定工作的关键。
4. 常见问题与排查技巧实录
4.1 “改了没反应”到底卡在哪
我做测试时最常遇到的情况是:脚本贴进去,控制台没有任何报错,但游戏纹丝不动。这种问题多到我已经条件反射地按顺序排查了。
第一,确认页面是 chrome://dino。Chrome 新标签页会加载各种组件,有些版本也会出现类似的小游戏入口,但它的内部变量可能不是Runner.instance_。不要在错误的页面上浪费时间。
第二,确认游戏实例存在。在控制台输入Runner.instance_,如果返回undefined,说明页面还没初始化完成或者游戏没有开始。按一下空格开始游戏再试。
第三,确认代码执行时机。如果你在游戏已经 Game Over 后覆盖checkForCollisions,此时霸王龙倒在地上,不会自动站起来继续跑,看起来就像“没反应”,其实方法已经改了。此时调用Runner.instance_.restart()重新开局,效果立刻出来。
第四,确认是否被游戏周期性还原。部分版本的游戏在调用restart()时会重建某些对象状态,实例上的方法覆盖不会丢失,但速度、距离、障碍物数组都会被重置。所以刷分脚本如果只执行一次,肯定会被覆盖;变速脚本只执行一次,重开后速度也会归位。解决思路就是循环执行,或者挂钩到对应的事件时机。
4.2 Chrome 更新之后脚本失效
Chrome 浏览器有过多次内部代码重构,恐龙小游戏这个模块也在变化。我最早用的变量名和现在用的就有些许差异,早期版本里碰撞检测函数是checkForCollisions,这个到现在没变,但一些内部属性如obstacleTimer的存放位置不同版本之间不一定相同。
遇到脚本失效,不要慌,先打开 DevTools 查看Runner.instance_的真实结构,看关键字段是否重命名,然后按新字段名改写。这也是为什么我反复建议先把console.dir(Runner.instance_)打出来看一遍,而不是死记硬背代码。游戏的实现在变,调试思路不能变。
4.3 想让脚本每次自动加载:书签脚本与扩展
控制台粘贴的缺点很明显:每次刷新都要重新粘贴。我后续试了两种持久化方式。
最简单的方式是“书签脚本”(bookmarklet)。把 JavaScript 代码压缩成一行,保存为浏览器书签,打开 chrome://dino 后点击书签即执行。示例:
javascript:(function(){if(Runner.instance_){Runner.instance_.checkForCollisions=function(){};}else{alert('未找到恐龙实例');}})();优点是无需安装任何工具,缺点是每次开局仍然要手动点一下,而且代码不能太长。Chrome 地址栏会过滤粘贴的 javascript 代码,但收藏夹里点击执行是允许的,实际测试可用。
更进阶的方式是 Chrome 扩展。扩展的能力远强于书签,但有一个很关键的限制:Chrome 默认不允许扩展脚本在 chrome:// 开头的浏览器内部页面里运行。我折腾了一套方案,思路是拉取恐龙游戏的离线版本,放到本地静态服务器上,然后在扩展的 manifest 里配置对应的域名权限,再用 content script 注入自动跳跃脚本。这种方案稳定、可分发,但已经绕了一圈,严格来说改的是“互联网上的恐龙游戏镜像”,不是 chrome://dino 本身。如果你只是自己玩,书签脚本足够,扩展适合想把工具分享给别人时再考虑。
4.4 操作细节与踩坑经验
这里整理几条我实际踩过的坑,很多都是血的教训。
- 不要在控制台把
Runner.instance_整个对象覆盖掉。比如执行Runner.instance_ = {},会导致所有内部方法失效,页面直接卡死,只能刷新。正确做法是修改这个对象上的属性或方法,而不是替换整个对象。 - 自动跳跃的检测间隔不要太短。我试过 5 毫秒的
setInterval,CPU 占用明显变高,而且跳跃指令可能在一帧里被触发多次,导致动作异常。20 毫秒是性能与响应速度的平衡点。 - 清空障碍物数组后,游戏会在几百毫秒内立刻生成新的障碍物,因为障碍物生成定时器并没有停止。想长时间保持清空,必须同时把
obstacleTimer改成一个很大的值,或者也用一个定时器不停清空。 - 刷分时不要一次加太多。
distanceRan = 999999999这样的操作可能导致记分板渲染溢出,表现为数字变成诡异的字符串,或者画面卡顿。用循环小步追加更稳定。 - 部分脚本需要在游戏开始后运行。
Runner.instance_.restart()可以在任何状态下调用,但某些内部状态(比如horizon的初始化)只有在游戏启动时才会完整创建。不确定时就先按空格开一局。
5. 进阶玩法:从外挂到自动化测试
5.1 做成 Chrome 扩展的具体步骤
如果你想把这套脚本扩展成真正的浏览器插件,我可以给你一个最小可运行的结构。这里假设你已经把恐龙小游戏跑在一个允许扩展注入的页面里,比如本地开发的镜像站。
扩展根目录下创建三个文件:
manifest.json:
{ "manifest_version": 3, "name": "Dino Auto Runner", "version": "1.0", "content_scripts": [ { "matches": ["http://localhost/*"], "js": ["content.js"] } ] }content.js:
(function () { const checkAndInject = () => { if (window.Runner && Runner.instance_) { Runner.instance_.checkForCollisions = function () {}; console.log('扩展注入成功'); } }; window.addEventListener('load', checkAndInject); setInterval(checkAndInject, 1000); })();popup.html可以做一个开关按钮,通过chrome.tabs.sendMessage与 content script 通信,这里不再展开。关键在于提取了“加载即监听、定时检测、注入后保持”这个稳定的生命周期逻辑。扩展的 content script 本质上还是在页面上下文里修改对象,跟前文的原理一致,只是触发时机和持久化由浏览器来保证。
5.2 用 CDP 远程控制浏览器
还有一个更底层的路子,不需要在页面里执行代码,而是从外部通过 Chrome DevTools Protocol(CDP)控制浏览器,模拟按键、执行脚本、读取日志。以前我见过有人用 VBA 通过 CDP 操控 Chrome 做自动化表格填报,原理完全一样,只是当初那套玩法被用在了办公自动化上。
用浏览器自动化库来做,代码量会小很多。比如 Node.js 环境中使用 puppeteer 连接一个带远程调试端口的 Chrome 实例,打开恐龙游戏页面,然后用page.keyboard.down('Space')模拟按键,再通过page.evaluate读取页面里的障碍物信息,做闭环判断。这套方案的优点是不污染页面代码,缺点是依赖外部进程,不适合纯休闲场景。
无论哪种方式,核心思路都是一样的:游戏的运行状态在浏览器内存里,谁能读取并修改这些状态,谁就掌握了“控制权”。这也是自动化测试、爬虫、辅助脚本的共同底层逻辑。
5.3 把“外挂”变成学习工具
往回看,这次折腾最大的收获不是刷了多少分,而是我借这个机会把 Canvas 游戏开发里帧循环、碰撞检测、对象生命周期这些概念都实际摸了一遍。平时看文档觉得枯燥,但当你发现改一个函数就能让霸王龙不死时,你会忍不住去研究它旁边的结构体里还有什么好玩的。
我建议你试试把自动跳跃脚本改造成“性能分析器”,比如定期记录Runner.instance_.currentSpeed和distanceRan,看游戏速度的增长曲线。你会发现它并不是线性的,而是阶梯式上升,到了某个距离后触发夜间模式,速度再加一档。这种细节光靠看代码很难体会,只有亲手打印日志才能建立起直观印象。
也可以把它改成“无障碍辅助”。比如在检测到前方有障碍物时,通过控制台输出一条可视化提示,或者用 Web Audio API 播放一个短音。这样的思路做出来的工具可以帮助视障玩家或者单纯想闭眼听声音的玩家。外挂不只有作弊一个落点,把技术能力用在正向场景里会更有成就感。
最后再分享一个小技巧:如果你经常玩这个游戏,我建议把常用的脚本压缩成一行,存在本地笔记里,不要每次上网搜索占用时间。我现在的用法是打开 chrome://dino,按 F12,粘贴一行无敌代码,然后调setSpeed(30),看霸王龙狂奔。等哪天电脑没有网络,我又能对着这个像素小恐龙玩一下午。