电脑断网的时候我瞟了一眼Chrome右下角那只像素小恐龙心里想的本来是“跑个几分就关了”。结果手太残连两百分都没撑到就在鸟和仙人掌之间反复去世。那会儿我越想越气干脆打开开发者工具做了一件所有前端程序员看到小游戏都会干的事把这只恐龙的底裤扒了。折腾了一晚上之后我不但把最高分刷到了99999还给小恐龙写了一套自动跳跃脚本顺带研究出了换皮肤、改速度、强制夜间模式之类的骚操作。这篇文章就是那次完整“破解”过程的记录。先说清楚我这里说的破解不是攻击服务器也不涉及任何外挂软件就是纯粹通过浏览器自带的DevTools去观察和修改一个前端小游戏的运行逻辑。看完这篇文章你也能用同样的思路去拆解浏览器里的任意一款HTML5小游戏。说白了它就是一场很生动的JavaScript运行时调试实战课特别适合前端入门、闲得发慌的程序员以及所有被这只小恐龙羞辱过的人。1. 小恐龙游戏的底细它到底躲在哪里1.1 断网页和chrome://dino的真实关系很多人以为小恐龙游戏只会在断网的时候出现其实它有一个独立地址在地址栏输入chrome://dino再回车就能直接进入完整的游戏页面。这个出处在Chrome内部所以哪怕网络完全正常你想玩也能随时玩。断网页里那个小恐龙本质上是一个内嵌的iframe或独立脚本上下文。不同版本的Chrome实现方式不太一样有的直接在错误页里挂了一段内联代码有的是嵌入一个子frame。这也是为什么很多人断网时打开开发者工具在Console里输入命令却半天找不到游戏对象——因为你的命令默认作用在错误页本身而不是小恐龙所在的那个内部页面。我实际操作的时候一般不会去断网页里折腾太容易被上下文问题绊住。直接用新标签页打开chrome://dino右键点击游戏画面选择“检查”DevTools会自动定位到和游戏相关的DOM节点。到这一步调试环境就干净了。1.2 游戏源码里都有哪些“角色”小恐龙游戏虽然看起来简单代码量也就一个JS文件的事但内部结构其实相当完整。整个游戏核心就是一个叫Runner的类它负责游戏主循环、计分、碰撞检测、实例创建这些对外接口。你可以把Runner当成整个游戏公司的总经理。总经理下面还有几个关键部门Trex小恐龙本身管跳跃、下落、低头、跑步动画切换。所有“龙设”参数都挂在它身上。Horizon场景管理部门负责地面、云、月亮以及各种障碍物的生成与移动。DistanceMeter财务部专门算分、显示距离、更新最高纪录。GameOverPanel挂了以后弹出来的那个小墓碑专门负责收尸。这种划分方式在源码里非常清晰。如果你打开Sources面板在JavaScript文件里搜索“Runner”“gameOver”“obstacles”这些关键词很快就能把整个游戏的骨架拼出来。我第一次读到这份源码的时候挺感慨的一个断网彩蛋性质的游戏内部设计居然这么规范Chrome团队的工程师是真没把这当玩具写。1.3 为什么这类游戏特别容易被“破解”如果非要用四个字概括原因那就是“毫无防备”。小恐龙游戏是一个纯前端、纯本地逻辑的游戏。它的分数没有经过任何服务端验证最高纪录就存你浏览器本地的localStorage里碰撞判定就靠gameOver那个方法触发。甚至代码里变量名、方法名都保留得很完整几乎没有做混淆处理。这和商业游戏的“加固”思路完全是两个极端。这带来一个结果你只要能在运行时拿到Runner实例理论上就能篡改这个游戏里的一切。改速度、改分数、改恐龙尺寸甚至让游戏永远不结束统统可行。我们不需要逆向汇编不需要抓包改协议需要的只是打开Console把当前内存中的对象属性改一改。这种行为说“破解”可能有点夸张更准确的说法是对浏览器运行时的直接操作实验。2. 第一层破解用DevTools直接改游戏状态2.1 找到小恐龙游戏的关键实例对象在所有修改开始之前第一步是拿到那个总管全局的Runner实例。进入chrome://dino页面后按F12打开DevTools切到Console选项卡直接输入console.dir(Runner);如果运气正常你会看到一个函数定义这就是游戏的核心类。但我们需要的是当前正在运行的那个具体实例而不是类本身。Chrome的开发者们在Runner构造函数里留下了一个静态属性用来保存实例所以直接输入console.dir(Runner.instance_);就能看到当前游戏实例的所有属性和方法。我在不同版本的Chrome上都试过大部分版本都能直接通过Runner.instance_访问到。万一你打出来是undefined也别慌后面第五节我会专门写排查方法。拿到实例之后你可以先点开几个属性感受一下currentSpeed是当前游戏速度tRex是那只恐龙horizon是场景distanceMeter是计分器。这一眼看过去基本就知道哪里能下手了。2.2 修改速度、距离和分数从入门到刷爆先说最简单的改游戏速度。默认游戏跑一会儿之后速度会从6开始逐渐往上加每得100分明显加快一档。如果你觉得快到反应不过来可以在Console里把速度调慢Runner.instance_.setSpeed(3);这个setSpeed是官方封装好的方法传入的数字就是游戏速度。调成3之后障碍物移动速度肉眼可见地变慢属于手残党的练习模式。不过这里有个坑游戏自带自动加速逻辑每过一段时间或每隔一定分数它会把速度重新加回去。你手动设成3可能跑几秒钟它又恢复成原来的速度。想要持续锁住速度更粗暴的方法是开一个定时器每隔100毫秒强制设置一次setInterval(() { Runner.instance_.setSpeed(3); }, 100);我管这个叫“脚本式脚刹”简单粗暴但极其有效。再来说改分数。游戏表面上的得分其实和恐龙跑过的距离严格挂钩内部有一个distanceRan字段在不断增加。最直接的方式就是修改这个距离值Runner.instance_.distanceRan 999999;但这样改完画面上方的得分数字未必立刻刷新因为分数显示是由DistanceMeter这个类单独管理的。更优雅一点的做法是调用计分器的刷新接口Runner.instance_.distanceMeter.setScore(999999);这行代码会直接让顶部计分区显示出现在的高分。还有更狠的直接改最高纪录。游戏把历史最高分存在localStorage里键名就是highscorelocalStorage.setItem(highscore, 999999);下次重新开始游戏右上角显示的最高纪录就会变成你设定的值。这里提醒一句chrome://dino页面也是能访问localStorage的和普通网页一样。想确认有没有写进去切到Application面板在Local Storage下面就能看到。2.3 直接清空障碍物和手动操控恐龙除了改数字之外你还可以直接操纵游戏里的实体。比如被一堆仙人掌堵得走投无路的时候直接清空所有障碍物Runner.instance_.horizon.obstacles [];这个数组就是场景里所有障碍物的集合包括仙人掌、翼龙都放在里面。把它清空渲染循环里没有东西可画碰撞检测也没有对象可以判断恐龙自然就畅行无阻。不过你别高兴太早新的障碍物很快会从右边再生成出来这招只能救急。想主动跳起来同样有接口Runner.instance_.tRex.startJump(1);startJump的参数是跳跃力度传得越大跳得越高。传一个2的话恐龙能跳出一种接近原地起飞的效果。配合手动清空障碍物你就能在Console里亲自“弹奏”这只恐龙想什么时候跳就什么时候跳。我第一次试这些命令的时候感觉就像突然拿到了游戏里的后台管理系统。明明上一秒还在被仙人掌追着跑下一秒我已经能凭空变出各种效果。这种“因为我读懂了游戏运行结构所以我能操控它”的满足感其实比单纯刷一万分爽得多。3. 第二层破解注入代码实现无敌和自动操作3.1 最经典的无敌方案给gameOver动手术如果说改分数是“资金外挂”那无敌就是“防弹衣”。先想一个问题游戏是怎么判定你输了的答案很简单当恐龙头部撞上障碍物的时候代码里会调用一个叫gameOver的方法。所以最省事的无敌方案就是把gameOver换成空函数Runner.instance_.gameOver function() {};就这么一行。你再去撞仙人掌撞是撞上了但游戏结束后台弹不出来画面不会定格恐龙也不会变成撞击状态。表面上看起来就是“我穿过障碍物了”实际上你是在碰撞发生的瞬间把死亡流程给掐了。这个方案还有一个更讲究的版本可以随时开关无敌状态。比如定义一个全局开关window.__godMode true; const originalGameOver Runner.instance_.gameOver.bind(Runner.instance_); Runner.instance_.gameOver function() { if (window.__godMode) return; originalGameOver(); };这样当你把window.__godMode设为false游戏又会恢复正常死亡逻辑。这个设计主要是为了演示“运行时补丁”的常规思路实际玩起来没太大必要但理解这个模式对做前端调试很有帮助。需要提醒一个很隐蔽的坑游戏一旦重新开始新的Runner实例可能会被创建你打的补丁就失效了。我在测试时候就遇到过第一次碰仙人掌没死游戏重新开局以后又变成一碰就碎。解决办法很简单别只打一次补丁用定时器持续“巡查”setInterval(() { const r Runner.instance_; if (r) r.gameOver function() {}; }, 500);这相当于一个补丁守护进程每隔500毫秒就检查一次游戏实例发现gameOver被换回来就立刻再覆盖。实测下来非常稳。3.2 给恐龙装上自动跳跃大脑刷分和无敌本质上还是改数据不够“智能”。接下来来点更有意思的写一段脚本让恐龙自己判断什么时候该跳。核心思路也不复杂。游戏循环里障碍物列表是Runner.instance_.horizon.obstacles每帧都会更新位置。我们只需要定期检查第一个障碍物和恐龙之间的距离一旦距离够近就调用tRex.startJump()让它跳起来。我在Console里用的是这套脚本setInterval(() { const runner Runner.instance_; if (!runner) return; const obstacles runner.horizon.obstacles; const tRex runner.tRex; if (!obstacles.length) return; const obstacle obstacles[0]; // 障碍物xPos小于150说明离恐龙足够近 // 恐龙此时既没有跳也没有低头才触发跳跃 if (obstacle.xPos 150 !tRex.jumping !tRex.ducking) { tRex.startJump(1); } }, 30);这段脚本每30毫秒跑一次差不多每个渲染帧都会检查一遍。当障碍物从右边靠近时xPos会不断变小越过150这个阈值就触发跳跃。这里150不是一个固定标准你可以自己调数值越大起跳越早越小起跳越晚。如果发现恐龙老是起跳太早甚至撞上去先把数字改成100试试。后续如果你想让脚本更智能可以根据当前速度动态调整阈值。速度越快起跳时机越要提前因为恐龙飞行的水平距离也变远了。最简单的方式是让阈值跟着速度走const threshold 50 runner.currentSpeed * 15; if (obstacle.xPos threshold !tRex.jumping !tRex.ducking) { tRex.startJump(1); }这种写法让脚本在高分段依然有效恐龙即便面对暴走级别的障碍物也能精准跳跃。把这段代码丢进Console之后切回游戏你会看到恐龙像长了大脑一样自己蹦蹦跳跳完全不需要你干预。配合老鼠拖木锨后面刷分基本就只是时间问题了。3.3 把脚本封装成书签小工具Console里复制粘贴虽然快但每次重新打开游戏页面都要再粘一遍实在麻烦。所以我后来把自动脚本打包成了一个书签小工具点一下就能注入。新建一个书签在网址那一栏填上以javascript:开头的一整段压缩代码javascript:(function(){setInterval(function(){var rRunner.instance_;if(!r)return;var or.horizon.obstacles;var tr.tRex;if(!o.length)return;if(o[0].xPos150!t.jumping){t.startJump(1);}},30);})();存好之后等游戏页面打开点一下这个书签自动跳跃脚本就注入了。这一招对于懒人来说特别实用。不过有一点要提前说清楚chrome://dino属于浏览器内部页面出于安全策略书签里的javascript:脚本在部分Chrome版本里会被拦下点了没反应。这个问题不是代码写得不对是页面自身的CSP限制。我平时的做法很简单要么直接把代码粘进Console执行要么把整个dino游戏做成一个本地网页副本再来玩这样书签就能正常生效了。如果你只是为了自己爽玩Console粘贴完全够用。4. 第三层玩法换皮肤、改场景、解锁隐藏效果4.1 给恐龙“整容”改尺寸和跳跃高度改分数、无敌、自动跳都还停留在“作弊”阶段。等这几个功能玩腻了我开始琢磨更有技术含量的事给小恐龙整容。小恐龙的造型不是外部图片贴上去的而是由Canvas代码一笔一笔画出来的各种尺寸和运动参数放在Trex.config这个配置对象里。想验证这一点在Console里输入console.dir(Runner.instance_.tRex.config);你会看到WIDTH、HEIGHT、MIN_JUMP_HEIGHT、INITIAL_JUMP_VELOCITY一堆参数。既然都在这里改起来就没有压力了。想让恐龙变大就把宽高一起调大const tRex Runner.instance_.tRex; tRex.config.WIDTH 120; tRex.config.HEIGHT 120;改完你会发现画面里的恐龙瞬间从一只小蜥蜴变成了一只哥斯拉撞到普通仙人掌跟踩树桩一样轻松。当然身体变大了碰撞判定也会变大更容易寄这时候就把前面写的无敌脚本一并开起来。想让它跳得高调这个参数Runner.instance_.tRex.config.INITIAL_JUMP_VELOCITY -20;这是一个初速度参数数值越小起跳瞬间向上的力量越大。原来的值大概在-12左右改成-20之后恐龙能直接飞出屏幕顶你自己试试就知道了。我第一次把恐龙跳出台面的时候笑到差点把水杯打翻。4.2 修改场景云朵、月球和夜间模式小恐龙游戏里有一个经典设计当得分超过700分左右场景会从白天切换成黑夜出月亮然后翼龙登场整个游戏难度陡然上升。这个夜间模式也是由运行参数控制的。想跳过700分的漫长等待直接强制切换到夜间模式可以找Horizon里的夜晚状态控制字段。不同版本字段名可能略有区别你可以先用console.dir(Runner.instance_.horizon);浏览一下里面有哪些属性眼睛重点扫过nightMode、config这两块。找到类似nightMode的布尔属性之后直接设成true再用horizon.update或者任意一个场景更新方法强行刷一帧月亮和黑夜效果通常就会立刻出现。云朵就更简单了。场景里的云都放在Runner.instance_.horizon.clouds数组里你可以直接全部清空Runner.instance_.horizon.clouds [];清完之后天朗气清一只云都看不见。想造点云出来也行源码里云对象有固定的构造方式你可以往数组里塞新的实例不过说实话这个玩法属于“明白了原理但没什么实际意义”的范畴我就没深入研究。但恰恰是这种无意义操作最能验证你脑子里对游戏结构的理解是不是对的。4.3 关于小恐龙游戏的那些冷知识折腾这一圈之后我还顺便查了查关于这只小恐龙的背景。它最初是Chrome团队在2014年前后做的离线彩蛋为了让用户在断网的时候不至于对着报错页面发呆。游戏本体占用空间极小整个逻辑几乎都封装在两个脚本文件里设计得相当克制。比较广为人知的彩蛋包括跑过700分进入夜间模式恐龙从躲避仙人掌进化到躲避翼龙最高分记录存在本地浏览器里和登录账号基本没啥关系甚至连恐龙扭头左顾右盼、撞到障碍物之后的眨眼特效都是通过不同帧贴图实现的。这些细节你在玩的时候很少注意但一查源码全都能对上号。我当时最大的感受是你看一个东西觉得它平平无奇只是因为你还没有凑近看。凑近之后满眼都是设计。5. 常见问题与排查技巧实录5.1 为什么我在Console里找不到Runner这是新手最常见的问题。打开断网页面输入Runner却返回undefined第一反应是“教程是不是骗人的”。真不是多半是代码执行上下文没切对。断网页里的小恐龙游戏如果嵌在iframe中Console默认执行的是外层页面的环境当然找不到游戏里的变量。解决办法是在Console顶部的“JavaScript context”下拉菜单里切换成和chrome://dino或游戏iframe相关的上下文。如果你直接打开的是chrome://dino独立页面这个问题基本不会出现所以我强烈建议所有实验都在这个标签页里做。5.2 修改分数后刷新就没了如果你是用setScore(999999)改的分数刷新之后显示的数字会回落到原始值。这太正常了因为你改的是内存中的对象属性这相当于给运行中的程序打了一个临时补丁关掉程序自然就没了。想要把分数永久的保留下来需要改localStoragelocalStorage.setItem(highscore, 999999);但也要留个心眼如果你把脚本写错了存入了一个无效值游戏读取的时候可能会出问题表现为最高分不显示或者显示NaN。这时候直接调用localStorage.removeItem(highscore)把它清掉游戏就会恢复正常。5.3 无敌代码失效、变速失效的原因这两种情况原因很相似基本都是因为游戏自己会刷新实例或重新赋值。比如自动加速逻辑会在每个一定分数后调用setSpeed把你手动调的速度盖掉游戏重开后gameOver方法会恢复成原始实现。我的经验就是别去猜它什么时候覆盖直接用定时器去“抢”。前面提过的每500毫秒补丁一次实测下来非常管用。变速如果想长期生效也照着这个思路搞setInterval(() { const r Runner.instance_; if (r r.currentSpeed ! 3) r.setSpeed(3); }, 100);这相当于把你的设置变成一条持续有效的策略而不是临时碰运气。5.4 小恐龙破解操作速查表我自己整理了一份速查表照着输入就能复现效果。表格里的代码都默认你在chrome://dino页面Console里执行。想实现的效果核心代码注意事项查看游戏实例console.dir(Runner.instance_)需要在dino页面上下文执行调慢/锁速度Runner.instance_.setSpeed(3)会被自动加速覆盖配合定时器更稳直接刷分Runner.instance_.distanceMeter.setScore(999999)只改内存刷新失效改最高分localStorage.setItem(highscore,999999)永久保留清空用removeItem无敌Runner.instance_.gameOver function(){}重开会失效用定时器持续补丁清空障碍物Runner.instance_.horizon.obstacles []新障碍物稍后会再生成手动跳跃Runner.instance_.tRex.startJump(1)传参越大跳越高恐龙变大Runner.instance_.tRex.config.WIDTH120碰撞体积也会变大跳得更高Runner.instance_.tRex.config.INITIAL_JUMP_VELOCITY-20初速度绝对值越大跳得越高最后分享一个我自己的习惯每次折腾完这些“作弊”操作我都会把Console里跑过的代码清空再老老实实正常玩几局。倒不是为了什么“尊重游戏”而是想确认自己对源码的理解是真的通了而不是靠死记命令碰运气。后来我去调试公司项目里的前端性能问题思路也明显顺了不少。毕竟浏览器在你面前完全打开的时候你会发现自己不是在看一个黑盒而是在看一个可以对话的系统。这种手感建议你也试试看。