尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

自动玩Chrome小恐龙游戏:基于CDP的浏览器自动化实战

自动玩Chrome小恐龙游戏:基于CDP的浏览器自动化实战 某天我发现 Chrome 断网后那只像素小恐龙又出现在屏幕上跑着跑着就撞上了仙人掌。旁边同事随口一句“这玩意儿要是能自动跑就好了”我倒真的动了心思。研究了两天之后我用脚本让小恐龙自己跳过了所有障碍物速度拉满也能跑十几万分不翻车。整个过程没碰任何逆向破解纯粹是借助浏览器自带的调试通道读取游戏内部状态、调用游戏自己的接口来操作角色。今天就把这套思路、代码和调参经验完整写出来关注 Chrome、小恐龙游戏这类自动化玩法的朋友可以直接照着抄。先说清楚这个项目到底在做什么。所谓“自动玩小恐龙游戏”本质上就是把人类靠眼睛判断、靠手指按键的过程换成程序去判断、去触发。它能解决的真实需求有两个一是纯粹想娱乐一下看看最高能跑多少分二是练习浏览器自动化技术用最轻量的方式把“CDP 控制 Chrome”这条链路跑通。不管你是编程新手还是老手这套方案的核心都值得学因为从里面可以延伸出页面自动操作、数据采集、UI 自动化等一系列玩法。1. 内容整体设计与思路拆解1.1 自动玩小恐龙的四条技术路线先说说整体设计。自动玩小恐龙本质就一件事不断判断前方障碍物离恐龙多远、长什么样然后决定起跳、下蹲还是原地等。但实现这件事有四种常见路线我一开始全都试过各有取舍。第一种是纯图像识别加模拟按键。用 OpenCV 截取游戏画面通过模板匹配找到恐龙和障碍物的位置距离够近就调用pyautogui发一个空格键。优点是不碰游戏内部逻辑原理最简单甚至不用理解代码。缺点也明显截图和识别有延迟游戏速度一快就反应不过来而且窗口焦点一旦被抢走模拟按键直接失效。我实测在速度达到中等水平以后这种方案失误率非常高只适合做演示玩具。第二种是浏览器扩展注入脚本。写一个 Chrome 扩展在 content script 里往页面上下文塞 JS直接读取游戏内部变量。听起来很优雅但实际很折腾要打开chrome://extensions/开启开发者模式要处理 manifest 版本兼容还会遇到“无法安装扩展程序”这类权限问题更麻烦的是chrome://dino这种内置页面很多扩展 API 不生效需要额外绕路。如果你遇到的是旧版 Chrome 或者受限环境这条路很容易卡死。第三种是 Chrome DevTools ProtocolCDP加 JavaScript 注入也是我最终采用的方案。CDP 是 Chrome 原生提供的远程调试协议通过 WebSocket 连接调试端口就能远程执行脚本、模拟输入、读取页面状态。它不需要安装扩展不依赖窗口焦点而且从老版本 Chrome 开始就一直存在兼容性非常好。缺点是理解门槛稍高你要搞清楚协议里几个关键方法但学一次能用在无数自动化场景里。第四种是用 Puppeteer 这类封装库。Puppeteer 底层就是 CDP只是把连接、命令、事件都封装成了好用的 API。用起来最省事但如果你想把自动玩做成长驻脚本或者想用别的语言比如 vba、Python远程控制一个已经打开的 Chrome还是得回到 CDP 本身。这四种路线我做了个对比方便你按需要选方案原理优点缺点图像识别 模拟按键OpenCV 找障碍物pyautogui 按空格不碰游戏内部逻辑思路直观反应慢、受窗口焦点影响高速段必翻车浏览器扩展注入content script 注入 JS 操作游戏对象不依赖调试端口门槛中扩展加载麻烦chrome:// 内置页限制多CDP JS 注入调试协议远程执行脚本快、稳、可控适合长期跑需要理解 WebSocket 和协议调用Puppeteer 封装同一套 CDPAPI 更友好开发效率高代码直观依赖 Node 库排查问题时多一层封装1.2 为什么最终选用 CDP 页面内自循环选 CDP 方案最关键的原因是响应速度。小恐龙游戏的速度会随着分数不断上涨最高速的时候恐龙左右两侧的可用反应距离从最初的几十个像素压缩到十几个像素。如果每一步判断都要“页面执行脚本—把结果传回 Node—Node 再发下一个命令”来回的 WebSocket 延迟足够让恐龙撞上一百次。所以我的设计是通过 CDP 只做一次“启动注入”把完整的自动玩家脚本塞进页面上下文里让它在页面内部用requestAnimationFrame自循环。这样每一帧游戏状态的读取、判断、动作触发全部在浏览器进程内部完成几乎没有额外延迟。CDP 只在两件事上发挥作用注入前定位页面以及游戏撞车后重新拉起脚本。这种做法还有一个好处就是可以任意扩展逻辑。因为脚本跑在页面上下文里可以直接调用游戏源码里所有公开的方法和属性比如读取当前速度、判断恐龙是否在跳跃、拿到障碍物的精确坐标甚至可以修改游戏加速度来制造训练场景。这些能力用图像识别方案是完全做不到的。1.3 先拆开游戏看看Runner 对象里藏着什么小恐龙游戏本质上是一个 Canvas 跑酷游戏逻辑都在trex-runner这套脚本里。页面加载后游戏会创建一个Runner实例并保存在全局变量Runner.instance_里。这个实例是整套实现的核心入口上面挂了几个关键属性。tRex是恐龙对象控制着角色的一切horizon是障碍物管理器所有仙人掌和翼龙都挂在它下面的obstacles数组里currentSpeed是当前游戏速度速度越大障碍物移动越快跳跃的提前量也要越大playing和crashed是运行状态标志distanceRan是已经跑过的距离可以用来做进阶的统计。恐龙对象tRex提供了两个核心动作方法startJump(speed)让恐龙起跳setDuck(true)让恐龙下蹲。这两个方法我在后面会重点讲。障碍物对象则暴露了xPos、yPos、width、typeConfig等字段其中xPos代表障碍物在画布上的水平坐标值越小说明离恐龙越近typeConfig里存着障碍物类型判断是不是翼龙就靠它。知道了这些结构自动玩家的核心逻辑就非常简单了每帧找到离恐龙最近的、还没越过去的障碍物算出距离决定是跳还是蹲然后调用对应方法。2. 核心逻辑解析与动作决策要点2.1 游戏状态判断开始、运行中、撞车写自动玩家之前先把游戏状态机理清楚。小恐龙游戏打开后并不会自动开跑它会显示一个“Press Space to play”的等待画面。页面里监听的是键盘按键收到空格才会进入运行状态。我在注入脚本里直接用 JavaScript 模拟空格键代码是这样的document.dispatchEvent(new KeyboardEvent(keydown, { keyCode: 32, code: Space, key: , which: 32 })); setTimeout(() { document.dispatchEvent(new KeyboardEvent(keyup, { keyCode: 32, code: Space, key: , which: 32 })); }, 50);这里有两个坑。第一个是KeyboardEvent的keydown和keyup都得触发否则游戏可能认为按键没松开第二个是构建事件对象时最好把keyCode、code、key、which都带上因为不同版本 Chrome 对事件字段的依赖有细微差别。游戏运行过程中Runner.instance_.playing为true撞上障碍物后crashed会变成true同时playing变成false。自动玩家需要在这两种状态间切换撞车后等一小段时间调用Runner.instance_.restart()重新开始。这样脚本就能从游戏开始一直跑下去不用人工干预。2.2 障碍物检测别被“看不见”的障碍物骗了障碍物检测是自动玩的核心也是最容易踩坑的地方。一开始我写的是“遍历所有障碍物找 xPos 最小的那个”结果恐龙经常在已经越过一个障碍物后又回头去跳后面的旧障碍物表现就是莫名其妙地原地乱跳。原因很简单障碍物的 xPos 在游戏中是逐渐减小的已经跑到恐龙左边的障碍物xPos 依然存在于数组里但它已经没有任何威胁了。正确的过滤条件是只保留那些obs.xPos obs.width tRex.xPos的障碍物也就是障碍物的右边缘还没有越过恐龙左边缘然后再从中找 xPos 最小的一个作为目标。这样每帧都能锁定“下一个真正会撞到我的障碍物”判断起来非常干净。得到目标后计算距离dist target.xPos - tRex.xPos。这个距离就是决策的依据。还有一个细节小恐龙的 xPos 实际不是 0头部还占着一定宽度所以判断时机时把恐龙自身宽度考虑进去会更准。我的做法是把过滤阈值放宽 10 个像素给判断留一点余量实测下来失误率明显下降。2.3 跳跃与下蹲的阈值到底怎么定决策逻辑是整段脚本的“大脑”。对普通仙人掌和地面障碍物策略很简单距离小于阈值就起跳。但这个阈值不能写死因为游戏速度会逐渐加快同样的像素距离在低速时很安全在高速时就是“看到再按已经晚了”。我最初的实现是threshold speed * 12然后用Math.min(220, Math.max(55, threshold))限制范围。意思是最慢的时候大约 55 像素开外就跳中速在大约 120 像素起跳极速则提前到 200 像素以上。具体数值我整理成了一张参考表当前速度 currentSpeed跳跃触发距离像素我的说明6 ~ 7开局低速60 ~ 80速度慢跳跃滞空时间长离近一点跳也能过10中速120 左右大约在恐龙前方一个身位时起跳13较快150 ~ 160预留反应时间否则起跳瞬间已到障碍物头顶16 以上极速200 以上宁可跳早了落地再跳也不要晚跳翼龙的处理要单独说。翼龙有高飞和低飞两种形态判断依据是target.yPos。在游戏坐标系里障碍物越靠上yPos的负值越大低飞翼龙和地面距离比较近yPos通常靠近 0。我的策略是低飞翼龙到了阈值距离就下蹲高飞翼龙不用管如果是普通地面障碍物一律起跳。3. 完整实操从启动 Chrome 到注入自动玩家3.1 环境准备一条命令启动带调试端口的 Chrome先准备环境。理论上任何带有 DevTools 的 Chrome 版本都能跑我这边用的 Chrome 109 测试稳定老版本和离线安装包也能工作。需要提前装好 Node.js随便一个较新版本都行Windows 和 macOS 操作方式一样。第一步是关闭正在运行的 Chrome然后打开命令行执行这条命令启动带调试端口的 Chromechrome.exe --remote-debugging-port9222 --user-data-dirD:\chrome-dino-profilemacOS 的命令稍有不同执行的是 Chrome 应用目录里的二进制/Applications/Google Chrome.app/Contents/MacOS/Google Chrome --remote-debugging-port9222 --user-data-dir/tmp/chrome-dino-profile这里必须强调--user-data-dir参数不能省。如果不指定独立目录Chrome 会直接复用你现有的默认用户目录然后命令行参数里的调试端口会失效后面连不上 9222 端口白折腾半天。指定一个独立的临时目录相当于开了一个“干净的新浏览器”和日常使用的 Chrome 互不干扰。启动后访问chrome://dino确认能看到小恐龙游戏页面。这时候你已经可以在浏览器里手动玩一把确认页面无误。注意如果地址栏访问chrome://dino时提示无法访问多半是当前 Chrome 版本不支持直接访问彩蛋页可以换成访问chrome://network-error/-106或者打开一个任意网页然后断网效果相同。3.2 用 Node.js 连接 CDP 并定位游戏页Chrome 开启远程调试后默认会在本机 9222 端口开一个 HTTP 接口访问http://127.0.0.1:9222/json可以拿到所有标签页的列表。列表里每个标签页有url、title、webSocketDebuggerUrl等字段后者就是连接 CDP 的 WebSocket 地址。先建一个项目目录初始化 npm 并安装 ws 库mkdir dino-auto-play cd dino-auto-play npm init -y npm install ws然后写一段 Node 脚本用来定位游戏页、拿到 WebSocket 地址const WebSocket require(ws); async function findDinoPage() { const tabs await (await fetch(http://127.0.0.1:9222/json)).json(); const target tabs.find((t) t.url.includes(dino)) || tabs[0]; if (!target) { throw new Error(没有找到可调试的页面先打开 chrome://dino); } return target.webSocketDebuggerUrl; }拿到 WebSocket 地址后就可以向 CDP 发送命令了。最常用的是Runtime.evaluate它能在页面上下文里执行任意 JavaScript 字符串。我写了一个封装函数每次调用都会新建一个 WebSocket 连接执行完表达式后关闭连接async function evaluate(wsUrl, expression) { return new Promise((resolve, reject) { const ws new WebSocket(wsUrl); ws.on(open, () { ws.send(JSON.stringify({ id: 1, method: Runtime.evaluate, params: { expression, returnByValue: true } })); }); ws.on(message, (data) { const msg JSON.parse(data.toString()); if (msg.id 1) { ws.close(); resolve(msg.result.result.value); } }); ws.on(error, (err) reject(err)); }); }这个函数对低频操作足够了。但我要再强调一次自动玩家的主循环一定不要通过这种“每帧一条 CDP 命令”的方式跑延迟太高。CDP 在这里只负责两件事一是确认页面正常二是把自动玩家脚本注入进去。3.3 注入自动玩家脚本可直接复制下面这段是核心把它保存成一个inject.js文件内容是一个 IIFE 字符串。它的作用是在页面内部启动一个requestAnimationFrame循环每一帧检查游戏状态、障碍物距离并执行跳跃或下蹲。const autoPlayerCode (() { const GAME window.Runner; if (!GAME || !GAME.instance_) return game not ready; const run GAME.instance_; let restartTimer null; function simulateSpace() { document.dispatchEvent(new KeyboardEvent(keydown, { keyCode: 32, code: Space, key: , which: 32 })); setTimeout(() { document.dispatchEvent(new KeyboardEvent(keyup, { keyCode: 32, code: Space, key: , which: 32 })); }, 50); } function tick() { if (run.crashed) { if (!restartTimer) { restartTimer setTimeout(() { restartTimer null; if (run !run.playing) run.restart(); }, 400); } requestAnimationFrame(tick); return; } if (!run.playing) { simulateSpace(); requestAnimationFrame(tick); return; } const obstacles run.horizon.obstacles; const tRex run.tRex; const speed run.currentSpeed; let target null; for (const obs of obstacles) { if (obs.xPos obs.width tRex.xPos 10) { if (!target || obs.xPos target.xPos) target obs; } } if (target) { const dist target.xPos - tRex.xPos; const threshold Math.min(220, Math.max(55, speed * 12)); const isPterodactyl target.typeConfig target.typeConfig.type pterodactyl; if (isPterodactyl target.yPos -10 dist threshold) { tRex.setDuck(true); setTimeout(() tRex.setDuck(false), 220); } else if (!isPterodactyl dist threshold !tRex.jumping) { tRex.startJump(speed); } } requestAnimationFrame(tick); } requestAnimationFrame(tick); return auto player injected; })(); ;注入的方式很简单把autoPlayerCode作为表达式传给刚才写的evaluate函数就行const wsUrl await findDinoPage(); const result await evaluate(wsUrl, autoPlayerCode); console.log(result); // 正常情况下输出 auto player injected注入成功后你会看到页面里的小恐龙自己开始跑了。如果当前页面处于等待开始状态脚本会自动触发空格键撞车后脚本会等 400 毫秒然后自动重开。这套逻辑可以一直跑下去。这里特别说一下tRex.startJump(speed)。它是游戏源码里自带的起跳方法参数是当前速度。源码内部会根据速度算出起跳速度和滞空时间所以不需要我们自己写物理模型。如果这段代码运行时报startJump is not a function说明你打开的页面版本比较旧改用tRex.jump()也能达到类似效果。3.4 数据观察与调参怎么知道它是不是要撞了自动玩家跑起来之后你大概率会遇到一个问题恐龙还是经常撞。这时候第一件事不是瞎改阈值而是把决策过程“可视化”。CDP 允许我们用Runtime.consoleAPICalled事件监听页面里的console.log但更省事的方式是直接把调试信息写到页面的document.title上目光一扫就能看到。我调参时会在脚本里临时加一行document.title dist Math.round(dist) speed speed target (target ? target.xPos : -);然后在 Chrome 标签页顶部实时观察距离和速度的变化。撞车前的那一帧dist到多少了是不是每次撞的时候速度都在一个区间把这些数据记录下来再微调threshold公式里的系数比闭着眼睛猜靠谱得多。如果你连 Chrome 的标签页都不想盯着看可以把同样的信息写入localStorage每 10 帧存一条撞车后拉出来复盘。这种“先记录、再调整”的习惯在做任何浏览器自动化项目时都非常值钱。4. 常见问题与排查技巧实录4.1 调试端口连不上、页面打不开最先遇到的坑大概率是http://127.0.0.1:9222/json打不开或者访问报错。绝大多数原因是你启动 Chrome 时没有指定独立的--user-data-dir导致新启动的 Chrome 进程把自己让给了后台已有的老进程调试端口根本没有建立。解决办法是彻底退出所有 Chrome 进程再带上独立参数重新启动。还有一种情况是端口被占用。如果你之前跑过调试服务9222 端口可能被别的程序占着启动 Chrome 时静默失败。可以换成别的端口比如 9223再重新打开地址栏测试。最后如果访问列表里找不到chrome://dino标签页大概率是页面没有打开成功直接在地址栏再访问一次即可。4.2 起跳时机不准老撞仙人掌如果脚本注入成功但恐龙频繁撞仙人掌问题多半出在阈值公式上。记住一个原则速度越快提前量越多。Math.max(55, speed * 12)这个下限是在低速时试出来的不同分辨率和窗口大小下游戏画布的逻辑像素可能不同所以数值需要按实际环境微调。调参建议是先把公式里的 12 改成 10观察撞车时机如果发现“距离太近来不及跳”就把系数上调到 13 或 14如果发现“跳得太早落下来正好砸在仙人掌上”就往下调。每次只改一个系数跑几轮看记录不要一次动多个变量。另外如果你发现恐龙经常会“空中顿一下”或者连续跳跃时有间隔可能是tRex.jumping的判断和实际状态不一致把起跳条件改成if (dist threshold !tRex.jumping tRex.yPos 0)会更稳。4.3 翼龙低飞处理不好下蹲判断失效翼龙的判断依赖target.yPos但这个数值在不同游戏版本里不完全一样。我在一个版本里写死yPos -10是低飞换到本地部署的开源版本后又完全不生效打印出来发现翼龙的yPos是另一套坐标系。遇到这种情况先把所有障碍物的typeConfig.type和yPos打印到document.title跑一次游戏看真实数值再重新设定高度阈值。另一个容易忽略的点是下蹲的持续时长。翼龙比仙人掌宽你下蹲 200 毫秒可能不够翼龙还没飞过去恐龙就站起来了。给setDuck(false)的延迟设成 300 到 350 毫秒再观察是否还会撞到翼龙的尾部。如果还撞可以在下蹲期间持续检测翼龙的xPos等它完全越过恐龙后再松开。4.4 速度拉满后失误率升高怎么办小恐龙游戏到后期速度很快连续两个甚至三个障碍物挨得很近脚本经常是刚跳过一个还没落地就必须再跳。我实测在速度 16 以上失误率会明显上升。排查下来发现一个问题起跳动作本身有冷却时间如果上一跳还没落地startJump不会立即生效等落地再调用就已经晚了。解决思路是我测试下来比较有效的“连续跳跃排队”维护一个queuedJump标志当检测到障碍物时无论恐龙是否在跳跃都先把“我要跳”这个意图记下来然后在requestAnimationFrame里循环检查一旦tRex.jumping变成false且tRex.yPos 0立刻执行下一跳。这份代码我标注一下属于需要额外接入的进阶逻辑网上很多版本没提过这个细节if (dist threshold !isPterodactyl) { queuedJump true; } if (queuedJump !tRex.jumping tRex.yPos 0) { tRex.startJump(speed); queuedJump false; }另外如果把自动玩家当成长期运行的“挂机脚本”游戏最高速度其实可以通过修改代码参数压低把run.config.ACCELERATION改大或改小就能控制速度爬升曲线。这算是我个人爱用的一个调试技巧想快速验证脚本稳定性时把加速度调大让游戏很快进入极速阶段想看脚本在低速里刷分时把加速度调成接近 0让它在极低速度下无限跑分数能刷到很高。我自己实测的记录是 13 万 6 千分最后是在一次翼龙贴脸加两连仙人掌的组合里翻的车。从“能跑”变成“稳定地跑”最花时间的其实不是代码而是阈值和容错逻辑的打磨。如果你也想折腾这个项目建议耐着性子把决策过程记录下来一次只改一个参数。这套流程练熟之后你会发现不只是小恐龙任何基于浏览器运行的循环类游戏只要你找得到它的内部变量和状态机都能用同样的思路实现自动操作。
返回列表