为什么这个系统值得单独设计
合作动作游戏中,队友被精英敌人击倒,倒计时还剩 18 秒。另一个玩家冲过去救援,但敌人转身逼近。救还是打,读条是否会被中断,倒地玩家能否爬向安全区,这些都决定合作体验是否紧张而公平。
倒地救援不是把死亡延迟几十秒。它改变玩家能力、敌人仇恨、镜头提示、UI 优先级、失败条件和奖励结算。Phaser 客户端要把这些状态表现清楚,同时避免重复救援、读条错位和结算不一致。 本文按实际项目会遇到的问题来拆,不停留在“能跑”的 Demo 层。重点会放在数据边界、状态流、玩家反馈、调试方式和后续维护成本上。Phaser 很适合快速做出手感,但越是能快速表现,越需要把规则层写清楚。
核心架构
flowchart TD
N1["HealthState"] --> N2["DownedState"]
N2["DownedState"] --> N3["ThreatRedirect"]
N2["DownedState"] --> N4["HUDSignal"]
N5["救援输入"] --> N6["ReviveChannel"]
N7["InterruptRule"] --> N6["ReviveChannel"]
N6["ReviveChannel"] --> N8["OutcomeResolver"]
N8["OutcomeResolver"] --> N1["HealthState"]
这套结构的原则是单向流动:输入或场景事件进入 HealthState,核心模型完成计算,再由 Phaser 表现层消费结果。DownedState、ReviveChannel、ThreatRedirect、InterruptRule、OutcomeResolver、HUDSignal 都应尽量保持可序列化、可测试、可回放。不要让某个 Tween 完成回调、某个 Sprite 是否可见、某个按钮是否高亮成为玩法事实。
倒地状态要限制能力
倒地玩家通常不能攻击,只能缓慢移动、标记敌人或使用有限道具。DownedState 记录倒计时、可爬行速度、是否已被救援、是否可自救。不要直接复用正常 PlayerController,否则输入、碰撞和动画会出现半死不活的混合状态。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
救援是通道行为
救援读条类似施法通道:需要距离、视线、按住输入、没有被强控,并且目标仍可救。ReviveChannel 每帧检查条件,任何中断都返回具体原因。UI 根据原因显示读条暂停、取消或失败。不要只在开始时检查距离,否则玩家离开后读条仍会完成。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
敌人仇恨要转移但不作弊
队友倒地后,敌人可以短暂转向仍站着的玩家,给救援制造压力。但不要让所有敌人瞬间换目标,这会显得机械。ThreatRedirect 可以按敌人类型、距离和当前行为决定是否转移。某些敌人继续压制倒地目标,某些敌人追击救援者,组合会更有戏。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
中断规则要公平可读
受到轻微擦伤是否中断救援,取决于游戏节奏。硬核项目可以任何伤害中断,休闲项目可以只让重击中断。规则必须写清楚,并有对应反馈:读条闪红、角色后退、救援音效中断。玩家需要知道是自己松手、距离太远,还是被攻击打断。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
镜头和 UI 要提高优先级
队友倒地是团队关键信息。HUD 应显示倒计时、方向提示和距离。镜头可以轻微偏向倒地点,但不能夺走控制。多人同屏时,要避免倒地提示遮挡技能冷却;分屏或远距离场景要提供边缘箭头。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
失败结算要区分原因
倒地倒计时结束、全队倒地、任务目标失败、玩家主动放弃,都可能进入失败流程。OutcomeResolver 统一结算:是否扣复活资源、是否保留掉落、是否重开检查点。结算规则不要写在倒计时结束回调里,否则全队同时倒地时容易重复触发。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
联机项目要有权威边界
如果是多人同步,救援完成必须由权威端确认。客户端可以预测读条和动画,但不能自行恢复血量。每次救援带 reviveId,重复包和延迟包都能被去重。单机合作也建议保留这个结构,后续扩展在线模式更容易。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
TypeScript 实现骨架
type PlayerHealthState = "alive" | "downed" | "reviving" | "dead";
interface ReviveTarget { id: string; x: number; y: number; state: PlayerHealthState; downedMsLeft: number }
class ReviveChannel {
progress = 0;
activeTarget?: string;
constructor(private requiredMs: number, private maxDistance: number) {}
update(dt: number, rescuer: Phaser.Math.Vector2, target: ReviveTarget, inputHeld: boolean) {
if (target.state !== "downed") return this.cancel("target-not-downed");
if (!inputHeld) return this.cancel("input-released");
const dist = Phaser.Math.Distance.Between(rescuer.x, rescuer.y, target.x, target.y);
if (dist > this.maxDistance) return this.cancel("too-far");
this.activeTarget = target.id;
this.progress += dt;
if (this.progress >= this.requiredMs) return { done: true as const, targetId: target.id };
return { done: false as const, progress: this.progress / this.requiredMs };
}
cancel(reason: string) {
this.progress = 0;
this.activeTarget = undefined;
return { done: false as const, progress: 0, reason };
}
}
这段代码不是完整框架,而是把关键边界先立出来。实际项目里应继续补上配置加载、错误码、事件派发、性能统计和单元测试。只要骨架保持清楚,后续接入 Phaser 的 Graphics、Sprite、Matter、Tilemap 或 Sound 都不会污染规则层。
具体落地步骤
- 第一步,把 HealthState 和 DownedState 从 Scene 中拆出来,写成可以直接用 TypeScript 调用的模型。这个模型只接收普通对象,不接收 Sprite、Camera 或 Tween。只要这一步做到,后面的测试、调试、存档和工具预览都会简单很多。
- 第二步,在 Phaser Scene 里建立很薄的适配层。输入事件、物理回调、计时器和资源加载都可以在适配层发生,但它们只提交意图,不直接改核心状态。核心系统产出快照后,适配层再更新显示对象、音效、粒子和 HUD。
- 第三步,给每个关键状态准备调试可视化。不要等 QA 报问题才补日志。开发模式下至少能看到当前状态、最近输入、失败原因、候选列表、耗时和重要阈值。对复杂玩法来说,能看见中间状态比多写一层封装更重要。
- 第四步,用三类样例保护系统:正常流程、边界流程、错误配置。正常流程证明体验能跑通,边界流程证明快速输入、暂停、切场景和重复触发不会破坏状态,错误配置证明系统会给出明确报告,而不是静默失败。
项目检查清单
- 确认 HealthState 的输入输出能被 JSON 记录,便于复现玩家操作。
- 确认 DownedState 的配置有默认值、版本号和校验错误信息。
- 确认快速点击、暂停、切后台、重开场景和读档不会重复提交关键状态。
- 确认失败反馈比成功反馈更具体,玩家能理解自己为什么没有成功。
- 确认低端机或高负载场景有降级策略,而不是等帧率下降后再猜瓶颈。
- 确认调试面板能在不改代码的情况下打开,并能导出最近关键事件。
常见误区
第一类误区,是把 Phaser 的显示对象当成状态来源。显示对象适合表达结果,却不适合保存规则事实。它可能被对象池回收、被摄像机隐藏、被动画临时修改,也可能因为画质档变化而不存在。核心状态必须独立存在。
第二类误区,是只为当前关卡写逻辑。当前关卡对象少、节奏慢、输入简单,临时判断看起来没有问题。等到内容增加、节奏加快、平台变多,临时逻辑会互相覆盖。每个系统至少要提前考虑配置错误、重复触发和性能上限。
第三类误区,是没有把失败当成流程设计。复杂系统一定会失败:条件不满足、资源缺失、网络超时、玩家中断、配置非法。失败不应该只是 console 里的一行错误,而应该是玩家、QA 和内容团队都能理解的状态。
倒地玩家也要有选择
倒地状态如果只剩等待,合作体验会变得被动。可以给倒地玩家有限但有意义的选择:缓慢爬向掩体、标记精英敌人、消耗一次自救道具、短暂吸引敌人注意,或把身上的资源丢给队友。每个动作都要受倒地规则限制,不能比存活状态更强。这样倒地不是失败动画,而是团队决策的一部分。
救援奖励要服务团队节奏
救援成功后的状态也要细调。直接满血复活会削弱压力,只给一滴血又可能让玩家立刻再次倒地。可以按难度配置恢复比例、短暂无敌、移动减速和复活后技能锁定。救援者是否获得护盾、是否吸引仇恨、是否消耗团队资源,也会改变队伍策略。把这些规则写进 OutcomeResolver,结算才不会散落在角色脚本里。
结语
合作倒地与救援:仇恨转移、读条、中断和失败结算 的难点不在某个 API,而在边界。把数据、规则、表现和调试分开后,Phaser 的优势会更明显:你可以很快做出反馈,也可以放心迭代规则。反过来,如果所有逻辑都散落在 Scene 的回调里,第一版越快,后续越难维护。
额外实践建议
- 倒地救援最重要的是反馈具体,玩家要知道读条为什么断。
- 仇恨转移需要按敌人类型配置,不要全局一刀切。
- 结算流程要幂等,多个失败条件同帧触发时只能进入一次结果。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。