Phaser 敌人 AI 设计:巡逻、追击和行为树不要写成一堆 if

以 Phaser 动作和冒险类游戏为例,讲解敌人 AI 的状态机、行为树、感知范围、仇恨、技能选择和调试方式。

AI 最怕看起来聪明,实际不可控

敌人 AI 很容易从简单 if 开始:距离玩家小于 300 就追,大于 300 就巡逻,血量低于 30% 就逃跑。第一只怪可以这样写,第二只也能复制,等到第十种怪加入远程攻击、召唤、霸体、警戒、回巢和技能冷却时,代码就会变成一团条件判断。每次调一个怪,都可能影响另一种怪。

我在一个俯视角冒险 H5 项目里见过这种情况。早期怪物只有巡逻和追击,后来加了弓箭手、盾兵、治疗怪和小 Boss。代码里到处是 if (distance < x)if (hpRate < y)if (skillReady)。线上最难排查的问题是治疗怪偶尔站着不动,因为它同时满足“需要逃跑”和“需要治疗队友”,两个逻辑互相覆盖速度。

Phaser 不限制你怎么写 AI,但项目需要一个清楚的决策模型。轻量敌人可以用有限状态机,复杂敌人可以用行为树或分层状态机。关键是让“感知、决策、执行、表现”分开,而不是让 update 里的一堆 if 同时做所有事情。

flowchart TD
    A[感知系统] --> B[黑板数据]
    B --> C[决策层 状态机/行为树]
    C --> D[动作执行器]
    D --> E[移动/攻击/施法/逃跑]
    E --> F[Phaser 物理和动画]
    F --> G[反馈事件]
    G --> B

先做感知,不要直接做决策

敌人需要知道什么?玩家位置、距离、视线是否被挡住、自己血量、队友状态、技能冷却、是否在出生区域、最近受击时间。这些都属于感知或上下文数据。很多项目把这些判断直接写在行为逻辑里,导致每个行为都重复计算距离和状态。

更好的方式是每隔一定时间更新一次黑板数据。黑板里保存 targetdistanceToTargetcanSeeTargethpRatehomeDistancenearbyAlliesreadySkills 等字段。决策层只读取这些字段,不关心它们怎么计算。这样感知频率也可以独立优化,不必每个敌人每帧都做完整扫描。

感知还要允许误差。敌人不一定需要每帧都知道玩家精确位置。巡逻怪可以 200ms 更新一次目标,Boss 可以更频繁。低端机上,大量敌人同时做射线检测和距离排序会很贵。AI 的聪明程度要服务玩法,不要超出玩家能感知的范围。

简单敌人用状态机就够

有限状态机适合行为明确的敌人:Idle、Patrol、Alert、Chase、Attack、Return、Dead。每个状态有进入、更新、退出,状态之间有清楚条件。相比散落 if,状态机最大的好处是可解释。调试时看到敌人处于 Chase,就知道它不应该同时执行 Patrol。

状态机不要太细。不要把“向左走一秒”“停顿半秒”“转身”都做成全局状态,除非它们对外部规则有意义。巡逻内部可以有自己的子步骤,攻击内部可以有起手、有效、收招阶段。全局状态描述敌人当前意图,子阶段描述动作细节。

状态迁移要有优先级。死亡高于一切,强控制高于普通攻击,回巢可能高于追击,受击反应取决于怪物类型。优先级写清楚以后,敌人就不会在同一帧既想逃跑又想攻击。

复杂敌人可以用行为树

行为树适合组合行为多的敌人。它由选择器、序列、条件和动作节点组成。比如一个弓箭手可以先判断是否死亡,再判断是否需要拉开距离,再判断是否能射击,再判断是否需要追击,最后回到巡逻。每个节点只做一件事,组合起来表达复杂策略。

行为树的优势是可组合。治疗怪可以复用“寻找受伤队友”“走到施法距离”“释放治疗”节点;远程怪可以复用“保持距离”“检查射线”“释放投射物”节点。不同怪物通过不同树配置组合,而不是复制一份 update。

但行为树也会被滥用。小怪只有两三个状态时,用行为树反而增加复杂度。行为树配置也需要调试工具,否则策划看到一棵树却不知道哪个节点失败。选择状态机还是行为树,不是技术信仰,而是看行为复杂度。

移动和决策要分开

敌人决定追击玩家,不等于它应该直接设置速度。决策层输出意图,例如 moveTo(target)keepDistance(target, 180)castSkill(skillId)。动作执行器负责把意图转换成 Phaser 里的路径、速度、动画和碰撞处理。这样以后换寻路方式或物理系统,不需要重写 AI 决策。

简单场景可以直接朝玩家移动。复杂地图需要路径点、导航网格或基于 tile 的寻路。H5 小游戏里,不一定要上完整 A*。很多时候,敌人只需要在可见区域内绕开大障碍,或者用关卡设计避免复杂寻路。算法选择要贴合玩法。

移动执行器还要处理失败。目标不可达、被墙挡住、离出生点太远、平台高度不够,这些都不能让 AI 卡死。执行器应该把失败反馈给黑板或决策层,让敌人改为等待、回巢、远程攻击或重新选点。

技能选择要有节奏

敌人技能如果只按冷却好了就放,会显得机械。玩家更容易接受有节奏的敌人:先预警,后攻击,攻击后留空档。AI 选择技能时要考虑距离、角度、冷却、当前状态、玩家最近动作和场景位置。

Boss 更需要技能编排。可以用阶段表控制血量阶段,用权重表控制技能选择,用互斥标签避免连续释放同类技能。比如冲刺后短时间内不再冲刺,召唤后给玩家清怪窗口,远程连射前先给地面预警。AI 的目标不是赢玩家,而是制造可读的挑战。

技能执行期间,AI 决策层通常要锁定一段时间。起手、释放、收招不能每帧被新决策打断,否则 Boss 会像没有重量。是否允许打断,要由技能配置决定。比如受击打断普通施法,但不打断霸体大招。

仇恨和回巢要明确

多人或召唤物场景里,敌人目标选择不能只看最近距离。可能需要仇恨值:谁造成伤害高、谁治疗队友、谁进入警戒范围、谁使用嘲讽。仇恨表不需要很复杂,但要明确更新和衰减规则。

回巢也很重要。敌人追玩家追出太远,会破坏关卡节奏,还可能把怪拉到出生点外导致刷怪混乱。每个敌人应该知道 home position 或 home area。超过范围后进入 Return,回到区域前不再追击,必要时回血或重置。

回巢不是简单瞬移。玩家能看到敌人行为时,最好让它有清楚表现:脱战、转身、返回、无法被继续拉远。否则玩家会觉得敌人突然作弊。只有在卡死或越界时,才使用强制重置。

AI 调试要可视化

AI 问题如果没有可视化,排查会非常慢。开发版可以在敌人头顶显示状态、目标、距离、技能冷却和当前行为节点。还可以画出警戒范围、攻击范围、回巢范围、路径点和视线检测线。

行为树调试更需要显示当前 tick 走到哪个节点,哪个条件失败,哪个动作正在运行。这样当敌人站着不动时,工程能看到是 CanSeeTarget 失败、MoveTo 卡住,还是 SkillCooldown 没好。

线上也可以记录少量 AI 事件。比如 Boss 战失败率高,可以记录玩家死亡前 Boss 最近释放的技能序列。不要记录每帧状态,成本太高。记录关键语义事件就足够分析节奏。

一个轻量行为树示例

下面示例展示了思路。每个节点返回 success、failure 或 running。真实项目可以扩展成配置驱动,但一开始先用 TypeScript 类写清楚更容易调试。

type BtResult = 'success' | 'failure' | 'running';

class Selector {
  constructor(private children: Array<() => BtResult>) {}
  tick(): BtResult {
    for (const child of this.children) {
      const result = child();
      if (result !== 'failure') return result;
    }
    return 'failure';
  }
}

function createArcherBrain(enemy: Enemy) {
  return new Selector([
    () => enemy.dead ? 'success' : 'failure',
    () => enemy.keepDistanceIfTooClose(),
    () => enemy.shootIfReady(),
    () => enemy.chaseIfTargetVisible(),
    () => enemy.patrol()
  ]);
}

注意这里的行为节点仍然调用敌人动作接口,而不是直接操作 Phaser sprite。这样 AI 层可以测试,表现层也可以替换。节点失败原因也应该能输出到调试面板。

上线前检查清单

上线前检查:敌人是否有明确状态或行为树,感知是否有频率控制,目标选择是否可解释,技能是否有预警和空档,回巢是否稳定,移动失败是否有兜底,Boss 技能序列是否不会连续重复,调试面板是否能显示当前行为,低端机大量敌人时 AI tick 是否超预算。

还要做极端测试:玩家站在墙后、跳到高台、快速进出警戒区、同时引多只怪、Boss 切阶段时死亡、页面后台恢复后 AI 是否继续执行旧动作。AI Bug 往往出现在状态切换边界,不在正常巡逻路径上。

AI 配置要让策划敢改

敌人 AI 最终一定会变成调参工作。警戒范围、追击速度、技能冷却、回巢距离、技能权重、受击硬直、仇恨衰减,这些值如果都写死在代码里,策划每次调整都要找工程,迭代会很慢。比较好的方式是把数值放进 enemy config,把行为结构放进可复用 brain,把特殊怪物只暴露少量扩展点。

配置也要有限制。比如警戒范围不能小于攻击范围,回巢距离不能小于追击范围,技能冷却不能为负,Boss 阶段血量必须递减。配置加载时就检查这些条件。否则某次运营活动为了让 Boss 更刺激,把冲刺冷却调成 0.1 秒,玩家体验会直接崩掉。

还可以为每种敌人写一段“行为描述”,在调试面板里显示。比如弓箭手:保持距离、视线清楚时射击、过近时后撤。测试发现行为异常时,可以对照描述判断是配置不符合预期,还是代码执行错误。AI 系统越可解释,内容团队越敢用。

批量敌人的性能分层

当场上只有三五个敌人时,每个敌人每帧完整 tick 行为树问题不大。割草、塔防、刷怪房间就不同了,几十个敌人一起做感知、寻路和技能判断,会把主线程压满。AI 需要分层更新:近处敌人高频 tick,远处敌人低频 tick,屏幕外敌人只做简化移动或休眠。

可以给敌人分 active、near、far、sleep 四档。active 是正在攻击或被玩家关注的敌人,每帧更新;near 每 100ms 更新决策;far 每 300ms 更新目标;sleep 只等待玩家接近或事件唤醒。这样玩家感知到的敌人仍然聪明,远处看不到的敌人不会浪费预算。

分层更新要避免同一帧集中爆发。可以按 enemyId 做取模,把低频 AI 分散到不同帧。否则每 300ms 所有远处敌人同时 tick,也会形成卡顿尖峰。AI 性能优化不只是减少次数,还要把工作摊平。

如果敌人被分层降频,动画和移动也要保持连续。决策可以低频,表现插值仍然可以每帧执行。玩家看到的是平滑移动,系统内部只是少做几次昂贵判断。

结语

Phaser 做敌人 AI 不需要很重的框架,但需要清楚的层次。感知收集事实,黑板保存上下文,状态机或行为树做决策,动作执行器驱动 Phaser 对象,表现层播放动画和特效。把这些混在 update 里,项目越长越难调。

好 AI 不是看起来永远聪明,而是行为可读、节奏可信、失败可解释、性能可控。玩家需要的是有挑战但讲道理的敌人,工程需要的是能被调试和复用的行为系统。做到这两点,Phaser 小游戏里的怪物也能有足够的生命力。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页