为什么这个系统值得单独设计
关卡设计师想在废弃商场里做三段遭遇:入口少量弱敌,电梯厅混合远程敌,撤离时出现精英压迫。程序如果每次都改 Scene 代码,内容迭代会很慢。更好的方式是把刷怪规则做成可校验、可预演的数据。
数据驱动刷怪不是把随机表搬进 JSON 就结束。系统要知道出生点是否可用、权重是否合理、预算是否超标、敌人组合是否符合节奏、预演曲线是否过载。Phaser 负责运行,工具负责让内容在发布前就暴露问题。 本文按实际项目会遇到的问题来拆,不停留在“能跑”的 Demo 层。重点会放在数据边界、状态流、玩家反馈、调试方式和后续维护成本上。Phaser 很适合快速做出手感,但越是能快速表现,越需要把规则层写清楚。
核心架构
flowchart TD
N1["SpawnConfig"] --> N2["WeightTable"]
N1["SpawnConfig"] --> N3["SpawnPointQuery"]
N2["WeightTable"] --> N4["BudgetSimulator"]
N3["SpawnPointQuery"] --> N4["BudgetSimulator"]
N4["BudgetSimulator"] --> N5["PreviewTimeline"]
N5["PreviewTimeline"] --> N6["ValidationReport"]
N6["ValidationReport"] --> N7["RuntimeSpawner"]
这套结构的原则是单向流动:输入或场景事件进入 SpawnConfig,核心模型完成计算,再由 Phaser 表现层消费结果。WeightTable、SpawnPointQuery、BudgetSimulator、PreviewTimeline、ValidationReport、RuntimeSpawner 都应尽量保持可序列化、可测试、可回放。不要让某个 Tween 完成回调、某个 Sprite 是否可见、某个按钮是否高亮成为玩法事实。
配置要面向遭遇段落
不要只写每 5 秒随机一个敌人。SpawnConfig 应描述 encounter:开始条件、持续时间、预算曲线、允许敌人、出生点标签、最大同屏数量和结束条件。这样设计师能表达入口试探、压力上升、撤离爆发,而不是在数字里猜节奏。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
权重表要有上下文
敌人权重不能全关卡固定。狭窄走廊里远程敌权重低,开阔广场里突进敌权重高;玩家低血量时可以减少压迫组合。WeightTable 支持按区域、阶段和难度修正,但修正要可预览。否则设计师会发现运行时结果和表格直觉不一致。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
出生点要检查视野和路径
刷怪点不应在玩家视野中心突然生成,也不应生成后找不到路径。SpawnPointQuery 需要检查距离、视线、导航可达、冷却、标签和当前占用。运行时如果没有合格点,系统应延迟或降级,而不是硬刷在玩家脚下。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
预算模拟能提前发现过载
工具里按配置跑一段模拟,输出每 10 秒预计敌人数、预算消耗、精英数量和同屏峰值。即使模拟不等于真实对局,也能发现明显问题:某段预算永远用不完、某类敌人权重为 0、同屏数量超过性能预算。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
预演时间线服务沟通
PreviewTimeline 用图表展示刷怪节奏:哪个时刻可能出现哪些敌人,压力何时上升,补给何时插入。策划、程序和 QA 可以围绕同一张图讨论。没有预演,大家只能进游戏反复打,效率低且容易漏边界。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
运行时要记录抽样结果
真正对局中,RuntimeSpawner 每次选择敌人和出生点都记录原因:预算、权重、随机值、候选点数量、失败原因。线上反馈某关太难时,日志能告诉你是配置过重、点位太近,还是玩家路线触发了异常组合。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
发布检查要阻止坏配置
ValidationReport 应能阻止明显错误进入发布:引用不存在的敌人 id、出生点标签为空、预算为负、最大同屏低于必刷数量、结束条件不可达。内容工具不能只给警告,关键错误必须让构建或发布失败。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
TypeScript 实现骨架
interface EnemyWeight { id: string; cost: number; weight: number; tags?: string[] }
interface SpawnPoint { id: string; x: number; y: number; tags: string[]; cooldownUntil: number }
function weightedPick(enemies: EnemyWeight[], budget: number, rng = Math.random) {
const candidates = enemies.filter(e => e.cost <= budget && e.weight > 0);
const total = candidates.reduce((sum, e) => sum + e.weight, 0);
let roll = rng() * total;
for (const enemy of candidates) {
roll -= enemy.weight;
if (roll <= 0) return enemy;
}
return candidates[0];
}
function chooseSpawnPoint(points: SpawnPoint[], requiredTag: string, player: Phaser.Math.Vector2, now: number) {
return points
.filter(p => p.tags.includes(requiredTag))
.filter(p => p.cooldownUntil <= now)
.filter(p => Phaser.Math.Distance.Between(p.x, p.y, player.x, player.y) > 360)
.sort((a, b) => Phaser.Math.Distance.Between(a.x, a.y, player.x, player.y) - Phaser.Math.Distance.Between(b.x, b.y, player.x, player.y))[0];
}
function validateWeights(enemies: EnemyWeight[]) {
const errors: string[] = [];
if (!enemies.length) errors.push("empty enemy table");
for (const enemy of enemies) if (enemy.cost <= 0 || enemy.weight < 0) errors.push(`invalid enemy ${enemy.id}`);
return errors;
}
这段代码不是完整框架,而是把关键边界先立出来。实际项目里应继续补上配置加载、错误码、事件派发、性能统计和单元测试。只要骨架保持清楚,后续接入 Phaser 的 Graphics、Sprite、Matter、Tilemap 或 Sound 都不会污染规则层。
具体落地步骤
- 第一步,把 SpawnConfig 和 WeightTable 从 Scene 中拆出来,写成可以直接用 TypeScript 调用的模型。这个模型只接收普通对象,不接收 Sprite、Camera 或 Tween。只要这一步做到,后面的测试、调试、存档和工具预览都会简单很多。
- 第二步,在 Phaser Scene 里建立很薄的适配层。输入事件、物理回调、计时器和资源加载都可以在适配层发生,但它们只提交意图,不直接改核心状态。核心系统产出快照后,适配层再更新显示对象、音效、粒子和 HUD。
- 第三步,给每个关键状态准备调试可视化。不要等 QA 报问题才补日志。开发模式下至少能看到当前状态、最近输入、失败原因、候选列表、耗时和重要阈值。对复杂玩法来说,能看见中间状态比多写一层封装更重要。
- 第四步,用三类样例保护系统:正常流程、边界流程、错误配置。正常流程证明体验能跑通,边界流程证明快速输入、暂停、切场景和重复触发不会破坏状态,错误配置证明系统会给出明确报告,而不是静默失败。
项目检查清单
- 确认 SpawnConfig 的输入输出能被 JSON 记录,便于复现玩家操作。
- 确认 WeightTable 的配置有默认值、版本号和校验错误信息。
- 确认快速点击、暂停、切后台、重开场景和读档不会重复提交关键状态。
- 确认失败反馈比成功反馈更具体,玩家能理解自己为什么没有成功。
- 确认低端机或高负载场景有降级策略,而不是等帧率下降后再猜瓶颈。
- 确认调试面板能在不改代码的情况下打开,并能导出最近关键事件。
常见误区
第一类误区,是把 Phaser 的显示对象当成状态来源。显示对象适合表达结果,却不适合保存规则事实。它可能被对象池回收、被摄像机隐藏、被动画临时修改,也可能因为画质档变化而不存在。核心状态必须独立存在。
第二类误区,是只为当前关卡写逻辑。当前关卡对象少、节奏慢、输入简单,临时判断看起来没有问题。等到内容增加、节奏加快、平台变多,临时逻辑会互相覆盖。每个系统至少要提前考虑配置错误、重复触发和性能上限。
第三类误区,是没有把失败当成流程设计。复杂系统一定会失败:条件不满足、资源缺失、网络超时、玩家中断、配置非法。失败不应该只是 console 里的一行错误,而应该是玩家、QA 和内容团队都能理解的状态。
结语
数据驱动刷怪工具:权重表、预演和内容校验 的难点不在某个 API,而在边界。把数据、规则、表现和调试分开后,Phaser 的优势会更明显:你可以很快做出反馈,也可以放心迭代规则。反过来,如果所有逻辑都散落在 Scene 的回调里,第一版越快,后续越难维护。
额外实践建议
- 刷怪系统越数据化,越需要校验和预演,否则只是把 bug 从代码搬到表格。
- 运行时日志要记录抽样原因,不然随机系统出了问题很难复现。
- 工具里用同一套 RuntimeSpawner 逻辑做预演,避免工具预览和游戏实际行为脱节。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。