资讯动态

Chrome恐龙游戏外挂实战:从Runner逆向到Canvas像素检测

发布时间:2026/9/16 18:27:26 来源:尧图企业网站定制
Chrome断网时那只小恐龙大概是这个星球上被玩次数最多的跑酷游戏了。作为一个常年折腾浏览器插件的开发者我第一次看到别人用几十行代码让恐龙自己无限跳跃的时候第一反应是“这也太作弊了”第二反应是“这玩意到底怎么做到的”。后来自己动手研究了一圈才发现给这只像素恐龙做外挂本质上就是一次对Chrome内部游戏架构的逆向工程技术含量不高但坑绝对不少。这篇就把我实测过的几条路线、核心代码和踩坑过程完整写出来给想自己动手折腾的朋友做个参考。1. 恐龙小游戏的技术底细它并不是一个普通的网页游戏恐龙小游戏T-Rex Runner虽然藏在一个浏览器的彩蛋页面里但它的技术栈比想象中要复杂一些。它不是用DOM元素拼出来的而是基于Canvas 2D绘制整个游戏逻辑封装在一个全局对象Runner中游戏循环、碰撞检测、背景滚动全部由JavaScript驱动。这意味着如果你能拿到这个全局对象的引用就等于拿到了游戏的后台管理权限。1.1 先搞清楚游戏是怎么跑起来的当你打开chrome://dino或者断网时看到那只灰白色小恐龙浏览器其实已经加载了一个完整的游戏页面。这个页面由内嵌脚本初始化创建了一个Runner实例它内部维护了当前速度currentSpeed已跑距离distanceRan障碍物数组horizon.obstacles小恐龙对象tRex整个游戏循环通过requestAnimationFrame驱动每一帧会更新距离、检测碰撞、滚动背景。碰撞检测的逻辑很直白恐龙、障碍物、地面三者的边界矩形是否相交相交就判定游戏结束。1.2 为什么“外挂”在这个游戏上这么容易实现相比真正复杂的游戏恐龙小游戏的外挂难度低得惊人原因有两个第一它是纯JavaScript写的所有运行状态都挂在全局对象上你可以直接在控制台里访问第二它没有做任何反作弊保护没有加密脚本、没有变量混淆连全局变量名都直接叫Runner.instance_。所以你只需要在控制台或者通过扩展脚本拿到这个实例想改速度改速度想绕碰撞绕碰撞。提示Runner.instance_这个全局变量一旦游戏初始化完毕就会存在但页面刚打开时可能还没有初始化完成访问时需要做判空处理。1.3 我为什么推荐从“改状态”入手而不是“改DOM”很多人第一反应是去操作Canvas里的像素或者监听键盘事件模拟按键。这些办法可行但绕了远路。直接操作Runner.instance_才是最优雅的路径——你等于直接拿到了游戏引擎内部的数据结构改距离、改速度、触发跳跃其实都是改一个对象的属性或调用一个方法的事。下面两节我会把这两条路线都讲透。2. 动手前先选路线操作内部状态与像素级识别的优劣对比做恐龙外挂网上随手一搜能搜出一堆代码片段但大多数都没说清楚背后的原理和适用场景。我把它们分成两类一类是改内部状态另一类是模拟玩家操作。两者思路完全不同踩过的坑也完全不同我分别展开说。2.1 改内部状态直接让恐龙“开挂”这条路线的主旨是既然游戏状态都在Runner.instance_里那直接改这些状态不就行了。具体能做这几件事修改distanceRan让计分器瞬间跑出几十万米将currentSpeed调大让背景飞速后退视觉上极其刺激清空horizon.obstacles数组所有障碍物直接消失调用tRex.jump()或tRex.startJump()方法主动触发跳跃这些操作本质上是绕过游戏本身的逻辑直接篡改内存数据和很多PC单机游戏的修改器原理完全一样。它最大的优势是简单、稳定、不依赖画面渲染不管屏幕上的恐龙长什么样数据层改对了就一定生效。2.2 模拟玩家操作像素检测与事件模拟另一种思路是“模仿人”用脚本盯着Canvas画面一旦发现前方出现障碍物就模拟按下空格键或向上键让恐龙跳起来。这也是一种外挂而且更接近真实玩家操作。流程大致是获取Canvas画布实例调用getContext(2d)拿到绘图上下文定时截取画布上某个区域比如恐龙前方一段矩形区域的像素数据通过分析像素的颜色值判断是否有障碍物轮廓出现一旦检测到明显的前景变化触发document.dispatchEvent(new KeyboardEvent(...))来模拟跳跃键这个方案的最大优势是“不挑实现”即使游戏内部结构变了只要画面还在渲染就能工作。但劣势也很明显性能开销大、坐标校准时容易受窗口缩放影响、像素采样的区域需要反复试验才能定准。2.3 我最终选择了“状态修改为主、像素检测为辅”的组合拳在实测过程中我发现单纯靠像素检测会出现一个很尴尬的问题当游戏速度越来越快障碍物从出现到抵达恐龙的时间窗口会急剧缩短像素检测的轮询频率不一定能跟上。而直接修改状态的方式不存在这种问题因为它是直接从数据层拿信息不会受到“画面是否刷新到位”的影响。所以我的最终方案是核心逻辑基于Runner.instance_像素检测只作为补充验证手段用来确游戏确实在正常运行而不是卡死。这也是我建议后来者采用的思路——两套方案不算浪费因为它们解决的是不同层面的问题。3. 实战用 Runner 实现自动跳跃、无限速度与无敌模式这是整篇最有价值的部分我会把我在控制台里一行一行敲过的代码全部放出来并解释每个关键点为什么这么写。3.1 初始环境如何稳定拿到 Runner 实例在chrome://dino页面按F12打开开发者工具切换到Console面板。理论上你可以直接输入Runner.instance_查看游戏实例但有个细节必须注意如果你在游戏尚未开始或者游戏已经结束Game Over画面的时候访问部分属性可能不可用或者正在重置。最稳妥的做法是先在页面上点击一下或者按一下空格让游戏先跑起来然后再执行脚本。// 先确认 Runner 是否存在 if (window.Runner Runner.instance_) { console.log(游戏实例已就绪); console.log(当前速度, Runner.instance_.currentSpeed); console.log(已跑距离, Runner.instance_.distanceRan); } else { console.log(Runner 还没初始化先让游戏跑起来再试); }我第一次执行时就踩了个栽直接输Runner.instance_返回undefined还以为这个版本没有全局变量后来发现只是因为没有先启动游戏。这个顺序问题浪费了我十分钟写在这里省得大家再踩。3.2 自动跳跃的两种方式与调优细节自动跳跃是外挂的核心功能有两个实现层级。第一个层级是调用内置跳跃方法也是最简单的方式const runner Runner.instance_; // 每 50 毫秒检查一次前方障碍物情况 setInterval(() { const obstacles runner.horizon.obstacles; if (obstacles.length 0 obstacles[0].xPos 100) { // 当第一个障碍物的 x 坐标小于 100 时触发跳跃 runner.tRex.jump(); } }, 50);这里的xPos是障碍物的X坐标数值越小离恐龙越近。实测中我发现阈值设为100比较合适跳得太早会落在障碍物前面跳得太晚直接撞上。具体阈值还和游戏速度有关所以我更推荐用下面这个动态版本const runner Runner.instance_; setInterval(() { const obstacles runner.horizon.obstacles; if (obstacles.length 0) { const obstacle obstacles[0]; // 结合速度动态调整触发距离 const threshold 50 runner.currentSpeed * 1.2; if (obstacle.xPos threshold) { runner.tRex.jump(); } } }, 30);逻辑很简单速度越快提前量越大。这个版本的实测效果好了很多基本覆盖了从低速到高速的所有场景。而且我在jump()之前加了!tRex.jumping的判断避免连续触发跳跃导致二次跳跃游戏中双击会导致二段跳有时候会反而撞上去。第二个层级是完全绕过障碍物直接让障碍物消失这个更暴力// 定时清除所有障碍物 setInterval(() { Runner.instance_.horizon.obstacles []; }, 100);注意这个方法不能只用一次因为游戏每隔几秒会生成新障碍物所以要用定时器反复清空。实测下来这样玩虽然不会死但画面上会出现一种很鬼畜的效果——仙人掌刚露头就闪没了翼龙刚飞出来就消散了。适合拿来炫但观赏性一般。3.3 无限速度与锁定距离让计分器彻底疯狂想刷高分最直接的办法是让distanceRan数值飙升。但这个属性在游戏内部会被每帧重写直接赋值一次很快就会被覆盖掉。正确做法是Object.defineProperty改写它的setter强制让它的值只增不减或者直接锁死在一个天文数字。const runner Runner.instance_; // 将 distanceRan 锁定在 999999 Object.defineProperty(runner, distanceRan, { get: () 999999, set: () {} }); console.log(距离已锁定为 999999);同样的思路也可以应用到速度上Object.defineProperty(runner, currentSpeed, { get: () 60, // 正常最高速度大约在 13 左右60 已经是疯狂模式 set: () {} });不过这里要提醒一句速度直接调到60画面会快得很难看清而且恐龙的跳跃高度和重力参数是固定的速度越快意味着空中移动距离越远很容易出现明明跳过了前面的障碍物却因为位移过大撞上后面刚生成的障碍物。所以我个人推荐速度改到 20~30 之间既有视觉冲击力又不至于完全失控。3.4 无敌模式把碰撞检测直接关掉自动跳跃再完美也会有漏网之鱼而且速度极高时几乎必漏。这时候就需要无敌模式兜底。游戏的碰撞检测每帧都会执行我们没法直接改源码但可以曲线救国在碰撞发生前把恐龙移到安全位置或者定期修改恐龙的状态让它免疫碰撞。最简单的方式是劫持tRex对象的碰撞相关属性。通过断点调试可以发现小恐龙的碰撞盒由tRex.collisionBoxes决定里面的 x、y、width、height 决定了矩形碰撞区域。把宽度和高度改为0碰撞检测就永远碰不到它const runner Runner.instance_; const originalBoxes runner.tRex.collisionBoxes; runner.tRex.collisionBoxes [ { x: 0, y: 0, width: 0, height: 0 } ]; // 为了应对每帧可能的重置用定时器锁定 setInterval(() { if (runner.tRex.collisionBoxes[0].width ! 0) { runner.tRex.collisionBoxes [{ x: 0, y: 0, width: 0, height: 0 }]; } }, 100);实测这个方法非常稳定恐龙会变成“幽灵状态”所有障碍物都从它身体里穿过去画面显示上恐龙完好无损。这个方案比单纯清空障碍物优雅得多因为场景中依然会有仙人掌和翼龙飞过只是它们已经伤不到你。4. 进阶纯像素检测方案不碰游戏内部数据也能实现自动跳跃如果你对“改内部状态”这种作弊方式不感兴趣或者你想把同一套思路用到别的Canvas游戏上那这个像素检测方案值得仔细看。它能让你对游戏画面做实时分析完全通过“视觉”驱动操作不依赖任何内部变量。4.1 拿到Canvas并分析像素数据的完整流程游戏页面的画布元素是canvas.runner-canvas我们要拿它来做像素级检测const canvas document.querySelector(.runner-canvas); const ctx canvas.getContext(2d, { willReadFrequently: true });这里给getContext传willReadFrequently: true是一个性能优化告诉浏览器我们后续会频繁读取像素数据。如果不传这个参数高频调用getImageData时有的浏览器会明显卡顿。接下来是核心逻辑在恐龙前方划分一个检测区域持续读取该区域内的像素如果发现“非天空色”的连续像素块就判定为障碍物出现了。const WIDTH 200; // 检测区域宽度像素 const HEIGHT 40; // 检测区域高度像素 const DINO_X 60; // 恐龙在画布上的大致X坐标 function detectObstacle() { // 读取画布上恐龙前方的一块区域 const imageData ctx.getImageData(DINO_X, 40, WIDTH, HEIGHT); const pixels imageData.data; for (let i 0; i pixels.length; i 4) { const r pixels[i]; const g pixels[i 1]; const b pixels[i 2]; // 背景色接近白色如果出现灰黑色像素说明有障碍物 if (r 200 g 200 b 200) { return true; } } return false; } // 每 50ms 检测一次 setInterval(() { if (detectObstacle()) { document.dispatchEvent(new KeyboardEvent(keydown, { key: , code: Space, keyCode: 32, which: 32, bubbles: true })); } }, 50);这里最关键的是getImageData的三个细节坐标起点要根据实际页面调整、区域宽度和高度的选择直接决定性能、颜色阈值要排除天空渐变和树木背景的干扰。4.2 高分屏与窗口缩放导致的坐标偏移问题这个方案我测试时最大的坑出现在高分屏Retina显示屏上。Chrome的恐龙游戏Canvas有一个内部逻辑分辨率大约 600×150但物理像素会根据设备的devicePixelRatio缩放。如果你直接用getImageData(x, y, w, h)在原画布上读取坐标是逻辑像素还是物理像素会对不上导致你画的检测区域可能根本不在恐龙前方甚至跑到画布外面去了。我踩完这个坑之后摸索出的处理方法是先不检测只做一个字段校准脚本把画布上从左上角开始的一整行像素扫描一遍输出颜色分布的边界然后反向推断画布的实际逻辑尺寸再据此调整检测区域坐标。// 校准画布实际坐标 const rect canvas.getBoundingClientRect(); const scaleX canvas.width / rect.width; const scaleY canvas.height / rect.height; console.log(画布物理尺寸, canvas.width, canvas.height); console.log(CSS显示尺寸, rect.width, rect.height); console.log(缩放比, scaleX, scaleY);有了缩放比之后你的所有检测坐标都应该乘以这个比例再传给getImageData这样才能保证在高分屏上依然准确。4.3 像素检测方案和内部状态方案的实际对比我用同一台电脑把两种方案都跑了一遍列个表给你们参考对比维度内部状态方案像素检测方案实现难度低几行代码中需要校准坐标和调阈值运行开销几乎为零每次 getImageData 有内存分配频率高时有卡顿兼容性依赖 Runner 内部结构版本更新可能失效不依赖内部实现只要能渲染就能跑速度适应无压力直接从数据层判断高速时轮询频率可能跟不上可移植性只能用在恐龙游戏换一个Canvas游戏也能用同一套思路对我个人来说平时自己玩我会首选内部状态方案简单直接但在研究其他网页游戏或Canvas动画时像素检测的思路更有通用价值。5. 踩坑实录这些细节大概率也会绊倒你最后一部分是纯实战排坑。我和这个游戏外挂死磕的过程中积累了一批“复盘才知道怎么办”的细节挑几个最有价值的写出来。5.1 为什么Runner.instance_一直拿不到这个问题不止我一个人遇到。原因是游戏页面最开始只有一个静态的加载画面Runner脚本并不会立刻执行。你要等恐龙和那个灰白的地平线完全渲染出来才能读取到Runner.instance_。如果你的访问时机太早它压根还没被赋值为实例。另外一个原因是如果你在游戏结束的瞬间执行代码Runner.instance_指向的实例可能正处于重置过程中部分属性会被重新初始化。我新建了一个简单的重试机制来解决function getRunner(callback, maxAttempts 20) { let attempts 0; const timer setInterval(() { attempts; if (Runner Runner.instance_) { clearInterval(timer); callback(Runner.instance_); } else if (attempts maxAttempts) { clearInterval(timer); console.warn(获取 Runner 实例失败); } }, 500); }5.2 自动跳跃在高速场景下为什么会偶尔失效这是一个典型的滞后问题。当速度只有初始值6的时候障碍物从出现到恐龙面前大约有两秒的缓冲期不管怎么跳都能跳过去。但当速度爬到13以上这个窗口会急剧缩短到几百毫秒。如果你的轮询频率是50ms一次理论上还够用但再加上setInterval在标签页后台被节流Chrome 对后台页面每分钟最多只触发一次定时器那就根本来不及了。我试过的解法是用requestAnimationFrame替代setInterval。因为游戏本身也是用requestAnimationFrame驱动的这样我的检测逻辑和游戏逻辑就绑定在同一个帧循环上天然消除了后台节流的影响。但代价是每秒执行60次检测对性能压力更大所以我在函数内部加了个次数计数每两帧才做一次真正的像素检测。let frameCount 0; function loop() { frameCount; if (frameCount % 2 0) { // 隔帧检测降低性能压力 const obstacles Runner.instance_.horizon.obstacles; if (obstacles.length 0 obstacles[0].xPos 100) { Runner.instance_.tRex.jump(); } } requestAnimationFrame(loop); } requestAnimationFrame(loop);这套方案实测在最高速度下也能稳定跳过去。5.3 游戏结束后代码失效如何写一套自动重启的完整外挂按上面所有代码一旦恐龙撞到障碍物游戏进入Game Over画面Runner.instance_的内部状态会重置一部分你的定时器可能还在跑但很多属性已经不可用了。如果你想要的是一个“全自动挂机刷分”的脚本就需要把重启也纳入逻辑。核心思路是监听游戏状态。游戏内部有一个runner.crashed属性碰撞后变成true。通过定时检测这个值一旦发现为true就模拟点击重启按钮或按空格键重启游戏并在新回合开始时重新注入我们的修改setInterval(() { const runner Runner.instance_; if (runner runner.crashed) { // 触发游戏重启 runner.restart(); console.log(游戏已自动重启); // 重新注入锁距离和无敌配置 Object.defineProperty(runner, distanceRan, { get: () 999999, set: () {} }); } }, 200);用runner.restart()这个官方方法重启游戏比模拟键盘输入要优雅得多而且不会出现按键事件没被识别的问题。整个脚本闭环之后你就可以开着页面去干别的让它自己刷到天荒地老。5.4 一个很多人忽略的问题别在正式场合用“锁距离”写到这里我突然想加一句和代码无关的经验。这个外挂做出来之后确实很有成就感但它的本质是篡改游戏状态属于“修改器”性质的工具和正常的休闲娱乐是两码事。如果你只是想在自己电脑上看看效果、研究下Canvas游戏原理那完全没问题但别把它用在一些涉及比分、奖励或者排名的场景里——Chrome自带的这个小游戏本身没有这些机制但同样的技术思路如果用到别的游戏上后果就完全不一样了。我不评价对错只是提醒你技术研究和技术滥用之间的边界在哪里自己心里要清楚。说到底给恐龙做外挂这件事最有价值的不是最后那个“999999分”的截图而是你在折腾过程中对JavaScript全局对象、Canvas像素操作、浏览器渲染机制的理解。我这几段代码写完读者只要能跟着跑通就已经比一小时前的自己多懂了不少东西。这个项目当成周末小实验刚刚好既不会太难到劝退又足以让你体验一把“破解游戏引擎”的快乐。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价