Screeps Arena 实战编程:从零构建你的RTS对战AI
1. 从零开始理解Screeps Arena的核心循环如果你玩过《星际争霸》或者《帝国时代》那你对RTS即时战略游戏一定不陌生。但Screeps Arena把这一切都交给了代码。想象一下你不是用鼠标和键盘去框选单位、点击建造而是坐在指挥官的位置上用JavaScript写下一套规则和策略然后看着你的“代码军团”自动执行采集、建造、进攻。这就是Screeps Arena的魅力所在它是一款纯粹的编程RTS游戏。很多刚接触的朋友会有点懵觉得这门槛太高了。我刚开始也这么想但上手后发现它的核心其实就一个东西游戏循环Game Loop。你可以把它理解为你AI大脑的“心跳”。游戏世界以“tick”游戏刻为单位向前推进每过一 tick服务器就会执行一遍你写的loop()函数。你的所有决策、所有单位的行动指令都必须在这个函数里安排好。这和我们平时写网页或者做后端服务很不一样。那里的事件驱动模型是“有事发生我才响应”。而在Screeps Arena里你是“主动思考持续决策”。每一 tick 你都得重新审视战场我的采集工在哪儿敌人有没有偷袭资源够不够造新兵然后根据当前情况给你的单位下达新的指令。指令不是永久的只持续当前这一 tick下一 tick 你得重新判断重新下令。这种“实时决策”的感觉正是编程对战的精髓。那么这个核心的loop()函数长什么样呢它就在你的主文件main.mjs里。一个最简单的、能通过第一关的AI可能只有两行代码import { getTicks } from game/utils; export function loop() { console.log(Current tick:, getTicks()); }别小看这几行。import语句引入了游戏提供的工具函数getTicks用来获取当前是第几个游戏刻这对于调试和制定基于时间的策略非常有用。export function loop()是游戏引擎寻找的入口你必须导出这个函数。里面的console.log会在游戏内的控制台面板输出信息是你调试AI的眼睛。我刚开始写的时候经常在里面写死循环或者阻塞操作结果就是AI直接“卡死”一个单位都不动。切记loop()函数必须快速执行完毕它只是下达命令真正的移动、攻击等动作会在函数执行完毕后由游戏引擎统一处理。如果你的loop()里有个没完没了的while循环那你的AI在这一 tick 就“掉线”了。提示强烈建议使用 VS Code 等本地编辑器编写代码而不是游戏内嵌的编辑器。将游戏模式文件夹作为项目根目录打开并安装官方提供的类型定义文件你会获得非常棒的代码补全和API提示效率能提升好几倍。理解并驾驭这个每秒可能执行几十上百次的loop()是你从“教程学习者”迈向“AI指挥官”的第一步。接下来我们就要在这个循环里塞进我们的智慧和策略了。2. 单位操控基础让你的爬虫动起来在Screeps Arena的世界里你的基本作战单位叫做“爬虫”Creep。它们可以是工人、士兵、医生角色完全由你赋予。而操控它们的第一步就是让它们听你的话走到该去的地方。游戏提供了creep.moveTo(target)方法来让爬虫向目标移动一步。注意是“一步”。如果你想让爬虫从地图左下角走到右上角就需要在连续多个 tick 的loop()里每次都对这个爬虫调用moveTo。这听起来很麻烦但正是这种精细控制为复杂的战术比如“hit-and-run”边打边跑提供了可能。那么在loop()里我们怎么找到“我的爬虫”呢游戏提供了一个强大的函数getObjectsByPrototype。你可以把它理解为一个全局对象扫描器。比如getObjectsByPrototype(Creep)会返回地图上所有的爬虫对象数组。但这里面有你的也有敌人的。怎么区分每个爬虫对象都有一个my属性如果为true就代表它是你的单位。import { getObjectsByPrototype } from game/utils; import { Creep, Flag } from game/prototypes; export function loop() { // 找到我所有的爬虫 const myCreeps getObjectsByPrototype(Creep).filter(creep creep.my); // 找到地图上所有的旗帜可能是目标点 const flags getObjectsByPrototype(Flag); // 假设我们让第一个爬虫走向第一面旗帜 if (myCreeps.length 0 flags.length 0) { myCreeps[0].moveTo(flags[0]); } }上面这段代码就是一个最简单的移动AI。但它在实战中非常脆弱。如果flags[0]不存在比如被摧毁了或者myCreeps[0]在移动过程中死掉了你的代码就可能报错导致整个loop()执行中断其他单位也会停止行动。所以健壮的错误处理是AI稳定性的基石。在实际编写中我会习惯性地检查目标是否存在、单位是否存活。更高级的移动会涉及到寻路。游戏地图上有平原、沼泽、道路和墙壁。不同的地形对移动速度影响巨大。一个身体臃肿CARRY部件装满了资源或者非MOVE部件很多的爬虫在沼泽里会寸步难行。这时候你可以使用爬虫自带的findClosestByPath方法它会自动计算当前地形下的最短路径返回一个最近的目标对象比你自己手动算坐标要方便和准确得多。for (let creep of myCreeps) { // 为每个爬虫寻找最近的可攻击敌人 const enemies getObjectsByPrototype(Creep).filter(c !c.my); const closestEnemy creep.findClosestByPath(enemies); if (closestEnemy) { creep.moveTo(closestEnemy); } }让单位动起来是基础但让它们“聪明地”动起来就需要你根据战场形势动态计算目标。是去采集资源还是回防基地还是集火某个敌人这就要引出我们下一个话题如何为爬虫设计不同的身体和角色。3. 兵种设计与角色分配打造你的专属军团在Screeps Arena里没有预设的“机枪兵”或“护士”。你想要的任何兵种都需要自己用“身体部件Body Parts”来组装。这就像给你的机器人选择装备模块。每个部件都有特定功能并且消耗不同数量的能量来生产。MOVE移动让爬虫能移动。没有MOVE部件爬虫就是固定炮台。移动速度取决于MOVE部件数量与总“负重”其他所有非MOVE部件的比例。WORK工作用于采集能量Harvest和建造建筑Build。部件越多单次采集或建造效率越高。CARRY携带增加资源携带容量。空的CARRY部件不增加负重但装满资源后会变重。ATTACK近战攻击允许进行贴身格斗。每个ATTACK部件增加固定伤害。RANGED_ATTACK远程攻击允许攻击3格范围内的敌人。同样部件越多伤害越高。HEAL治疗可以治疗自身或友方单位。TOUGH坚韧最便宜的部件只增加生命值没有特殊功能常用于充当“肉盾”。假设你现在有550点能量你可以选择造一个强大的战士[MOVE, MOVE, ATTACK, ATTACK, ATTACK]5050808080340能量也可以造一个快速的采集工[MOVE, MOVE, WORK, WORK, CARRY]505010010050350能量或者造五个廉价的侦察兵[MOVE, ATTACK]x 5。不同的配比决定了爬虫在战场上的角色。生产爬虫是通过你的主基地Spawn完成的。代码大概长这样import { MOVE, ATTACK, WORK, CARRY } from game/constants; import { StructureSpawn } from game/prototypes; let soldier; // 在loop外声明用于在tick间记住这个单位 export function loop() { const mySpawns getObjectsByPrototype(StructureSpawn).filter(s s.my); if (mySpawns.length 0) return; // 没有基地就别造了 const mySpawn mySpawns[0]; // 如果士兵还没造出来且基地能量够 if (!soldier mySpawn.store.energy 340) { const result mySpawn.spawnCreep([MOVE, MOVE, ATTACK, ATTACK, ATTACK]); if (result.ok) { soldier result.object; // 记住新造出来的爬虫对象 soldier.role soldier; // 可以给它自定义属性比如角色 } } // 如果士兵已存在就命令它 if (soldier) { // ... 这里写士兵的战斗逻辑 } }这里有个关键点spawnCreep不是瞬间完成的它需要时间。result.ok只表示指令下达成功result.object才是那个正在被孵化的爬虫对象你可以立刻持有它的引用并为其分配任务比如让它一出生就去某个位置站岗但它真正能行动要等到下一个tick。角色分配通常通过给爬虫对象添加自定义属性来实现比如creep.role harvester采集者、creep.role builder建造者。然后在loop()里根据角色来执行不同的行为模块。我常用的一个简单框架是这样的export function loop() { const myCreeps getObjectsByPrototype(Creep).filter(c c.my); for (const creep of myCreeps) { switch (creep.role) { case harvester: runHarvester(creep); // 执行采集者逻辑 break; case soldier: runSoldier(creep); // 执行士兵逻辑 break; case builder: runBuilder(creep); // 执行建造者逻辑 break; default: // 没有角色给它分配一个 if (creep.body.some(p p.type WORK)) { creep.role harvester; } else if (creep.body.some(p p.type ATTACK)) { creep.role soldier; } } } } function runHarvester(creep) { // 具体的采集逻辑... }通过身体部件的组合和角色分配你的AI就从控制“一个单位”升级为指挥“一支各司其职的军队”。接下来这支军队需要资源才能运转这就是我们经济系统的核心。4. 资源管理与经济系统运营是胜利之本无论你的战术多么精妙没有资源一切都是空谈。在Screeps Arena里核心资源是能量RESOURCE_ENERGY。它用于生产爬虫、建造建筑、为防御塔充能。建立一个稳定、高效、能抗干扰的经济系统是AI能否在持久战中胜出的关键。能量主要来自地图上的能量源Source。一个带有WORK部件的爬虫可以执行creep.harvest(source)来采集能量。这里有个细节能量源的能量是无限的但采集有冷却时间WORK部件越多单次采集量越大但冷却时间也相应变长。你需要根据能量源的再生速度来优化采集工的数量和身体配置避免“人多手杂”效率反而降低。采集到的能量会存储在爬虫自身的store里。爬虫需要把能量运回基地Spawn或者容器Container、扩展Extension等存储建筑中使用creep.transfer(target, RESOURCE_ENERGY)。一个经典的采集-运输逻辑闭环如下import { RESOURCE_ENERGY, ERR_NOT_IN_RANGE } from game/constants; function runHarvester(creep) { // 如果爬虫的存储没满 if (creep.store.getFreeCapacity(RESOURCE_ENERGY) 0) { // 寻找最近的能量源 const sources getObjectsByPrototype(Source); const closestSource creep.findClosestByPath(sources); if (!closestSource) return; // 尝试采集如果距离不够就移动过去 if (creep.harvest(closestSource) ERR_NOT_IN_RANGE) { creep.moveTo(closestSource); } } else { // 存储满了寻找最近的存储点比如主基地 const spawns getObjectsByPrototype(StructureSpawn).filter(s s.my); const closestSpawn creep.findClosestByPath(spawns); if (!closestSpawn) return; // 尝试转移能量如果距离不够就移动过去 if (creep.transfer(closestSpawn, RESOURCE_ENERGY) ERR_NOT_IN_RANGE) { creep.moveTo(closestSpawn); } } }这个逻辑很简单没能量就去采采满了就回去存。但在实战中你需要考虑更多。比如存储点满了怎么办你应该让采集工原地等待还是去寻找其他存储建筑我通常的做法是维护一个“能量需求列表”采集工会优先将能量运送到最需要能量的建筑比如正在孵化的Spawn或者能量见底的防御塔。中期开始你需要建造容器Container来充当能量中转站。采集工把能量存到容器里专门的运输工身体主要是MOVE和CARRY再把能量从容器分发到各个需求点。这样能提高采集工的效率避免它长途跋涉。经济系统的另一个维度是扩张与防御。你不能把所有资源都用来造采集工还需要留出资源生产士兵保卫基地以及升级科技比如建造更高级的建筑。我常用的一个简单策略是设置一个“能量缓冲区”当总能量超过某个阈值比如800时才允许生产士兵或建造建筑低于某个阈值比如200时则全力保障经济单位的生产。这能防止你一时上头把能量全造了兵结果经济崩盘。资源管理AI写得好不好直接体现在你的“每分钟采集量”和“资源分配效率”上。一个常见的坑是“交通堵塞”多个采集工挤在一个能量源或者运输工堵在存储建筑门口。解决方法是使用更智能的寻路findClosestByPath会考虑拥堵或者为每个单位分配独立的工作目标。5. 建筑学与防御体系构筑你的钢铁阵地当你的经济初步运转起来后就要考虑把资源转化为长期优势了这就是建造建筑。在Screeps Arena里你可以建造多种建筑初期最重要的是防御塔StructureTower。它能耗能进行远程范围攻击是防守基地、抵御早期骚扰的利器。建造建筑分两步。第一步是创建建筑工地ConstructionSite。你需要指定坐标和建筑类型。import { createConstructionSite } from game/utils; import { StructureTower } from game/prototypes; // 在坐标 (30, 25) 创建一个防御塔的建筑工地 const result createConstructionSite({x: 30, y: 25}, StructureTower); if (result.ok) { const site result.object; // 获取工地对象 }第二步是派遣带有WORK部件的爬虫去建造Build。建造会消耗爬虫携带的能量。function runBuilder(creep) { // 优先建造未完成的建筑工地 const sites getObjectsByPrototype(ConstructionSite); const closestSite creep.findClosestByPath(sites); if (closestSite) { // 如果爬虫没能量了就去取能量 if (creep.store[RESOURCE_ENERGY] 0) { // ... 去容器或能量源取能量的逻辑 } else { // 有能量尝试建造 if (creep.build(closestSite) ERR_NOT_IN_RANGE) { creep.moveTo(closestSite); } } return; // 有工地就专心建造不干别的 } // 没有工地建造工可以暂时去干点别的比如升级控制器或者当临时运输工 }建筑学Building Placement是个大学问。防御塔建在哪里覆盖范围最大存储容器放在能量源旁边还是基地旁边道路能加速单位移动该如何铺设来优化物流我踩过的坑是早期随手把塔建在了基地正中心结果射程覆盖不到关键的入口敌人几个远程兵就能在外面白嫖我的采集工。后来我学乖了会用代码动态计算最佳位置比如寻找能覆盖多个资源点或交通要道的路口。防御体系不光是建筑也包括城墙StructureWall。虽然教程里没细说但城墙在高级对战中非常重要。你可以用城墙把基地围起来只留一个用防御塔重点防守的入口形成“瓮城”。敌人近战单位会被卡住远程单位则会暴露在你的塔火下。建造城墙同样需要建筑工地和建造工。一个完整的防御循环是这样的侦察单位可能是廉价的移动爬虫发现敌人接近 - 预警系统可以通过检查一定范围内是否有非我方单位实现触发 - 所有空闲的建造工立刻在受威胁方向补建城墙或维修受损建筑 - 防御塔自动锁定进入射程的敌人开火 - 战斗单位向遇袭地点集结。把这套逻辑用代码实现出来看到你的基地自动应对攻击那种成就感是无与伦比的。6. 战斗与战术AI从脚本到智能终于到了最激动人心的部分战斗。让你的爬虫移动和攻击很简单但让它们像一支军队一样协同作战就需要战术AI了。战斗AI的层次可以大致分为个体脚本、小队协同、全局战略。个体脚本就是给每个战斗单位写死一套行为。比如近战兵发现敌人就冲上去砍远程兵保持距离射击治疗兵跟着受伤最重的单位跑。这通过检查身体部件很容易实现function runSoldier(creep) { const enemies getObjectsByPrototype(Creep).filter(c !c.my); const closestEnemy creep.findClosestByPath(enemies); if (!closestEnemy) { // 没有敌人去巡逻或者回防 creep.moveTo(mySpawn); return; } // 根据身体部件决定行为 if (creep.body.some(p p.type ATTACK)) { // 近战兵 if (creep.attack(closestEnemy) ERR_NOT_IN_RANGE) { creep.moveTo(closestEnemy); } } else if (creep.body.some(p p.type RANGED_ATTACK)) { // 远程兵尝试保持3格距离 const distance Math.max(Math.abs(creep.x - closestEnemy.x), Math.abs(creep.y - closestEnemy.y)); if (distance 3) { // 距离大于3靠近敌人 creep.moveTo(closestEnemy); } else if (distance 2) { // 距离太近远离敌人防止被近战贴脸 // 计算远离的方向这里简化处理 const fleeDir ...; // 需要根据坐标计算 creep.move(fleeDir); } // 在2-3格距离时原地攻击 if (distance 3) { creep.rangedAttack(closestEnemy); } } else if (creep.body.some(p p.type HEAL)) { // 治疗兵寻找受伤的友军 const injuredAllies getObjectsByPrototype(Creep).filter(c c.my c.hits c.hitsMax); const target injuredAllies.length 0 ? creep.findClosestByPath(injuredAllies) : closestEnemy; // 没伤员就跟着部队 if (target) { if (creep.heal(target) ERR_NOT_IN_RANGE) { creep.moveTo(target); } } } }小队协同就更进一步了。比如你可以设计一个“坦克输出治疗”的铁三角。让带有TOUGH和ATTACK的肉盾顶在前面RANGED_ATTACK单位在后面输出HEAL单位专心治疗坦克。这需要单位之间能互相识别和配合。我常用的方法是给同一小队的单位设置一个共同的squadId然后在loop()里让它们根据小队ID集结、选择共同的目标、保持阵型。// 假设我们有一个小队 const squad { id: 1, members: [tankCreep, dpsCreep1, dpsCreep2, healerCreep], target: null, formation: triangle // 阵型 }; // 在loop中更新小队行为 if (!squad.target) { // 寻找小队目标比如最近的敌人建筑或高价值单位 squad.target findSquadTarget(squad.members); } // 每个成员根据自己在小队中的角色和位置行动 for (const member of squad.members) { actAsSquadMember(member, squad); }全局战略则是最高层级。你的AI需要判断现在是应该爆经济快速发展还是应该早期Rush快攻敌人主力在哪里是应该正面决战还是派小分队去偷袭对方经济这需要收集战场信息侦察、分析敌我实力对比、并做出决策。你可以用有限状态机FSM来模拟AI的“心态”比如状态 { 早期发展 中期扩张 全军出击 防守反击 }根据当前游戏刻、资源数量、单位数量、发现的敌人强度等信息来切换状态每个状态对应一套不同的生产优先级和单位行为模式。写战斗AI最有趣也最头疼的就是调试。你写了复杂的逻辑一开战你的部队却像无头苍蝇一样乱跑。这时候多用console.log输出关键变量的状态比如squad.target是什么某个单位为什么选择逃跑或者用Game.map.visual系列API在游戏地图上画线、画圈来可视化你的AI的决策过程比如画出单位的索敌范围、移动路径能极大提升调试效率。7. 调试、优化与进阶思考当你把采集、建造、战斗的模块都拼凑起来一个能自动运行的AI就初具雏形了。但这时候它可能笨笨的效率低下或者有各种奇怪的bug。接下来就是打磨和优化的阶段。调试是你的最佳伙伴。除了console.log一定要善用游戏提供的Game.map.visual。比如你可以让每个采集工在头顶显示它的状态harvestingtransporting或者用线画出它打算移动的路径。对于战斗单位可以画出它的攻击范围和治疗范围。视觉化的反馈能让你一眼看出AI的逻辑哪里出了问题。我经常在代码里写这样的调试函数function debugVisuals() { const visuals Game.map.visual; const myCreeps getObjectsByPrototype(Creep).filter(c c.my); for (const creep of myCreeps) { // 在爬虫头上显示其角色和状态 visuals.text(creep.role || ?, creep.x, creep.y - 1, {color: cyan, fontSize: 10}); // 如果是士兵显示其目标 if (creep.memory.targetId) { const target Game.getObjectById(creep.memory.targetId); if (target) { visuals.line(creep.x, creep.y, target.x, target.y, {color: red}); } } } // 画出所有能量源和存储的位置 const sources getObjectsByPrototype(Source); sources.forEach(s visuals.circle(s.x, s.y, {fill: yellow, opacity: 0.2})); }性能优化在游戏后期单位众多时至关重要。getObjectsByPrototype是一个相对较慢的操作尤其是在地图上对象很多的时候。避免在每一 tick 为每个爬虫都调用它来查找敌人或资源。一个常见的优化模式是“缓存”在loop()开头一次性获取所有你需要查询的对象数组然后在循环中复用这些数组。let cachedEnemies null; let cacheTick 0; export function loop() { const currentTick getTicks(); // 每5 tick更新一次敌人缓存避免每tick都扫描 if (!cachedEnemies || currentTick - cacheTick 5) { cachedEnemies getObjectsByPrototype(Creep).filter(c !c.my); cacheTick currentTick; } // 现在所有单位都可以使用 cachedEnemies 了 for (const creep of myCreeps) { // 使用 cachedEnemies 而不是重新扫描 const target creep.findClosestByPath(cachedEnemies); // ... } }代码结构也会随着AI变复杂而越来越重要。不要把所有的逻辑都堆在loop()函数里。按照功能模块化比如role.harvester.js、role.soldier.js、manager.spawn.js、strategy.offensive.js。使用ES6的模块化语法导入导出让代码清晰可维护。这样当你想到一个新的战术时只需要修改或新增一个模块而不是在几千行的main.mjs里大海捞针。最后多看、多学、多对战。Screeps Arena社区有很多高手分享他们的AI代码。去观摩他们的思路看看他们是如何解决寻路优化、资源分配、动态编队这些复杂问题的。然后把你学到的技巧融入自己的AI中。最直接的提升方式就是去多人天梯对战输了一局后不要急着改代码先完整地看一遍回放用对手的视角看看你的AI在哪里露出了破绽。是经济被骚扰崩了还是主力部队被地形卡住了还是决策犹豫被各个击破了找到问题根源再有的放矢地修改。从写出一行让爬虫移动的代码到指挥一支能自主决策、协同作战的智能军团这个过程充满了挑战和乐趣。你的AI就像你的数字分身它在竞技场中的每一次胜利都是对你编程和策略思维的一次肯定。记住没有“完美”的AI只有不断进化的AI。每一次失败都是让它变得更强的机会。现在打开你的编辑器开始构建属于你的第一个RTS对战AI吧。