Phaser AI 导演难度曲线:压力值、刷怪节奏和喘息窗口

用 Phaser 做关卡 AI 导演时,如何根据玩家状态调整压力、刷怪、资源投放和喘息窗口,避免机械堆数值。

为什么要把它当成系统来做

合作生存射击游戏里,玩家小队在废弃医院搜集样本。导演系统不能只每 20 秒刷一波敌人,而要观察血量、弹药、倒地次数、推进速度和房间结构,在紧张与喘息之间摆动。

难度动态调整如果只偷偷改敌人血量,会让玩家觉得不公平。更好的 AI 导演调整的是节奏、资源和组合,并把压力曲线控制在可解释的范围内。 本文不把它写成一个一次性 Demo,而是按可上线、可维护的小系统拆开。重点不是堆 API,而是回答几个真实问题:数据从哪里来,谁有权修改状态,失败时玩家看到什么,调试时程序能看到什么,内容增加后系统还能不能承受。

核心架构

flowchart TD
  Ne78ea9e5ae["玩家状态采样"] --> N5072657373["PressureMeter"]
  N5072657373["PressureMeter"] --> N4469726563["DirectorState"]
  N4469726563["DirectorState"] --> N537061776e["SpawnBudget"]
  N4469726563["DirectorState"] --> N5265736f75["ResourceDropper"]
  N537061776e["SpawnBudget"] --> N456e636f75["EncounterPlanner"]
  N456e636f75["EncounterPlanner"] --> N5068617365["Phaser 刷怪点"]
  N5265736f75["ResourceDropper"] --> Ne8a1a5e7bb["补给投放"]

这张图的关键,是把 PressureMeter、DirectorState、SpawnBudget、ResourceDropper、PacingWindow、EncounterPlanner 放在单向流里。玩家输入或系统 tick 进入核心模型,模型产出结果,Phaser 再把结果转成动画、粒子、声音和界面。不要让显示对象反向决定规则。只要核心模型能在没有 Canvas 的环境中运行,就能写测试、做编辑器预览,也能在以后接入服务端校验或云存档。

压力值要来自多维信号

压力不是剩余血量一个数字。可以综合队伍平均血量、弹药比例、最近受伤频率、倒地次数、敌人存活数量、目标推进速度和玩家停留时间。每个信号归一化到 0 到 1,再加权合成。权重应可配置,调试面板显示分项贡献,策划才知道导演为什么加压或减压。

在实现这一点时,建议先写一组可以复现的输入数据,再接 Phaser 场景。先让控制台打印出正确状态,再让 Graphics、Sprite、Text 和 Tween 去表现它。这样的顺序不炫,但能减少非常多“画面看起来对,规则其实错”的问题。

导演状态需要滞后

如果压力一高就立刻减少敌人,一低就立刻加怪,体验会像温控器抖动。DirectorState 可以分成 build、peak、relief、ambient。状态切换有最短持续时间和阈值滞后。比如压力低于 0.35 持续 10 秒才进入 build,高于 0.75 后进入 relief,而不是每帧跳。

在实现这一点时,建议先写一组可以复现的输入数据,再接 Phaser 场景。先让控制台打印出正确状态,再让 Graphics、Sprite、Text 和 Tween 去表现它。这样的顺序不炫,但能减少非常多“画面看起来对,规则其实错”的问题。

刷怪预算比固定波次灵活

每个敌人有 cost,导演根据当前状态给出预算。普通感染者 cost 1,远程怪 cost 3,精英怪 cost 6。EncounterPlanner 在可用刷怪点中选择组合,避免在玩家视野内凭空出现。预算没有用完可以保留一部分,但要有上限,防止后面突然倾泻。

在实现这一点时,建议先写一组可以复现的输入数据,再接 Phaser 场景。先让控制台打印出正确状态,再让 Graphics、Sprite、Text 和 Tween 去表现它。这样的顺序不炫,但能减少非常多“画面看起来对,规则其实错”的问题。

喘息窗口要真喘息

relief 阶段不只是少刷怪,还要停止尖叫音效、降低音乐层级、投放少量资源、让目标路径清晰。玩家需要感到系统给了空间。喘息窗口太短会让人疲惫,太长会无聊,通常 8 到 20 秒更容易调。

在实现这一点时,建议先写一组可以复现的输入数据,再接 Phaser 场景。先让控制台打印出正确状态,再让 Graphics、Sprite、Text 和 Tween 去表现它。这样的顺序不炫,但能减少非常多“画面看起来对,规则其实错”的问题。

资源投放要避免露馅

缺弹时刷一盒子弹很明显,但每次都刷在脚边会破坏沉浸。ResourceDropper 可以选择玩家即将经过但不在视线中心的位置,或者让箱子从场景原有容器里出现。资源也应有预算,避免越菜补给越多导致经济失衡。

在实现这一点时,建议先写一组可以复现的输入数据,再接 Phaser 场景。先让控制台打印出正确状态,再让 Graphics、Sprite、Text 和 Tween 去表现它。这样的顺序不炫,但能减少非常多“画面看起来对,规则其实错”的问题。

导演不能覆盖关卡承诺

剧情 Boss、固定伏击和教学节点应有更高优先级。AI 导演可以调节小怪组合和补给,但不能取消关卡设计的关键事件。为每个区域设置 directorPolicy:自由、轻度、锁定。锁定区域只采样压力,不主动刷怪。

在实现这一点时,建议先写一组可以复现的输入数据,再接 Phaser 场景。先让控制台打印出正确状态,再让 Graphics、Sprite、Text 和 Tween 去表现它。这样的顺序不炫,但能减少非常多“画面看起来对,规则其实错”的问题。

调试曲线要记录整局

把压力值、导演状态、刷怪预算、资源投放和玩家倒地时间记录成曲线,复盘一局就能看到哪里过载。Phaser 调试面板可以实时画小图,正式版把摘要上报。难度设计靠感觉很难稳定,曲线能让讨论落到证据上。

在实现这一点时,建议先写一组可以复现的输入数据,再接 Phaser 场景。先让控制台打印出正确状态,再让 Graphics、Sprite、Text 和 Tween 去表现它。这样的顺序不炫,但能减少非常多“画面看起来对,规则其实错”的问题。

TypeScript 实现骨架

interface PlayerSample { hp: number; ammo: number; recentDamage: number; downs: number; enemyCount: number }
type DirectorMode = "ambient" | "build" | "peak" | "relief";
function pressure(s: PlayerSample) {
  const lowHp = 1 - Phaser.Math.Clamp(s.hp, 0, 1);
  const lowAmmo = 1 - Phaser.Math.Clamp(s.ammo, 0, 1);
  const damage = Phaser.Math.Clamp(s.recentDamage / 100, 0, 1);
  const crowd = Phaser.Math.Clamp(s.enemyCount / 24, 0, 1);
  return Phaser.Math.Clamp(lowHp * 0.35 + lowAmmo * 0.15 + damage * 0.25 + crowd * 0.2 + Math.min(s.downs, 3) * 0.05, 0, 1);
}
class DirectorStateMachine {
  mode: DirectorMode = "ambient";
  private elapsed = 0;
  update(dt: number, p: number) {
    this.elapsed += dt;
    if (this.mode === "build" && p > 0.72 && this.elapsed > 6000) this.enter("peak");
    else if (this.mode === "peak" && p > 0.82 && this.elapsed > 5000) this.enter("relief");
    else if (this.mode === "relief" && this.elapsed > 14000) this.enter("ambient");
    else if (this.mode === "ambient" && p < 0.45 && this.elapsed > 8000) this.enter("build");
  }
  private enter(mode: DirectorMode) { this.mode = mode; this.elapsed = 0; }
}

这段代码只是骨架,真正项目里还要加事件派发、错误码、配置校验和日志。但骨架已经表达了方向:核心概念是普通 TypeScript 对象,Phaser 类型只出现在输入适配或表现需要的地方。若你发现某个函数越来越依赖 Scene、Camera 或 Sprite,就应该停下来判断它是不是被放错层了。

落地步骤

  1. 第一,确认 PressureMeter 的输入输出是否是纯数据。若需要 Phaser.GameObjects 才能计算结果,说明边界还没有切开。
  2. 第二,给 DirectorState 或同等级的核心概念写三个最小样例:正常路径、边界路径、失败路径。样例要能在没有浏览器画面的情况下运行。
  3. 第三,把 UI 上每个可点击动作都映射成明确意图,不要让按钮直接修改深层状态。意图里带 requestId,便于防重复和追踪。
  4. 第四,失败反馈要比成功反馈更早接入。成功时玩家通常愿意接受,失败时才会质疑系统是否可靠。
  5. 第五,内容配置要有默认值和校验脚本。字段缺失时宁可在启动时报错,也不要在玩家操作到一半才静默失败。
  6. 第六,性能指标要提前量化:每帧最多处理多少对象、单次刷新允许多少毫秒、低端机是否需要降级显示。

常见坑

  • 最容易踩的坑,是让表现层过早成为事实来源。比如动画播完才算成功、按钮亮着就代表可用、某个 Sprite 存在就说明状态存在。这些判断在演示机上没问题,一到跳过、暂停、断线、切场景和重连就会变成隐性故障。
  • 第二个坑是只为第一关写逻辑。第一关对象少、路径短、输入慢,任何写法都像是正确的。等内容增加到几十张地图、几百个配置和各种活动修正时,临时判断会互相覆盖。写系统时要假设它会被复用、被误用、被配置错。
  • 第三个坑是没有留下证据。玩家反馈“刚才没生效”时,如果没有事件日志、状态快照或 requestId,只能靠猜。哪怕是单机项目,也可以保留最近 50 条关键事件,开发包里导出文本,定位速度会快很多。

项目里的验证方式

把这套系统放进 Phaser 项目时,我会先建一个不依赖 Scene 的核心目录,例如 src/gameplay/ai-director-difficulty-curves。里面只放模型、求解器、状态机和测试夹具。Scene 只负责输入适配、对象池、摄像机、音效和 UI。这个边界看似多写几行代码,但它换来的是可测试、可回放和可迁移。等项目进入内容生产阶段,最值钱的不是某个特效多漂亮,而是当策划说某个状态不对时,程序能在五分钟内复现并解释。

数据格式要尽量像内容团队会填写的表,而不是像程序临时拼出来的对象。每个 id 都要稳定,每个状态都要能序列化,每个失败原因都要有明确枚举。Phaser 的优势是快速把反馈做出来,但反馈越快,越容易掩盖规则层的混乱。先把规则层写清楚,再接动画,后续加新模式、新活动或新平台才不会反复拆墙。

在调试阶段,建议给这个系统加一个小面板:显示当前输入、核心状态、最近事件、最后一次失败原因和关键耗时。面板不需要好看,但要准确。很多客户端问题在日志里只是一句 undefined,在调试面板里却能看到完整链路。尤其是多人、存档、复杂 UI 或长时间运行的玩法,调试可见性直接决定维护成本。

最后检查

做完第一版后,不要只看一次演示是否顺滑。至少准备三组数据:一组正常流程,一组边界流程,一组故意配置错误的流程。正常流程证明体验成立,边界流程证明状态不会漂移,错误流程证明系统会给出可理解的失败原因。AI 导演难度曲线:压力值、刷怪节奏和喘息窗口 的质量不取决于第一眼多热闹,而取决于玩家反复操作、内容不断扩张、版本持续迭代时,它还能保持清楚、稳定和可解释。

继续阅读

探索更多技术文章

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

全部文章 返回首页