死亡与重生家族
一条线管四件事:死亡复活(濒死/救援/复活全流程)、 坠落重生、边界重生。 共同骨架:死亡/出界判定全在服务端,客户端跑一个四态表现状态机, 复活位置与朝向由服务端下发。本页重点讲死亡复活(模块 8 文件 + 实体组件 2700+ 行), 坠落/边界链接到站点深页。
四态状态机
23 条通知双通道
断线重连补跑
PlayerDeath 1635 行 Controller
1. 工作清单
| 内容 | 性质 |
|---|---|
| 死亡复活:濒死救援(倒地/扶人/救援进度)、复活倒计时 UI、复活传送黑屏、死亡表现(蒙太奇/灰屏) | 功能开发 |
| 边界重生:出边界后拉回安全区(深页见 边界检查重生) | 功能开发 |
| 坠落重生:坠落重生深页;与传送共用黑屏/流送收尾链路 | 复用底座 |
2. 理论:四态表现状态机 + 双通道协议
服务端维护死亡状态真源,客户端状态机是它的镜像:
1
PreNearDeath 预濒死
进入濒死倒计时前的过渡(可被治疗拉回)
2
NearDeath 濒死
倒地,队友可救援(有救援进度条),超时进入死亡
3
TotallyDeath 死亡
播死亡表现,进入复活等待/复活传送
4
NotStart 复位
复活收尾完成,回到初始态
协议分双通道:「WorldRole*」管自己,「WorldTeamMember*」管队友—— 自己死了要锁输入开死亡 UI,队友死了只更新救援列表,两条通道的处理重量完全不同。
2.1 复活位置:服务端权威
复活点位置 position 和相机朝向 dirPosition 都是 WorldReviveRoleSCNotify 下发的字段, 客户端只负责应用与旧朝向清理。复活分原地复活与远距离复活传送(带流送+黑屏)两种, 后者由服务端选择安全点。
3. 代码片段
3.1 协议注册全景:23 条通知一次看全
Src\Game\Module\PlayerDeath\PlayerDeathController.ts:94-119(节选)
protected override OnRegisterNetEvent(): void {
Net.Register(ENotifyMessageId.WorldTeamMemberRescueWaitSCNotify, this.OnTeamMemberPreNearDeathNotify);
Net.Register(ENotifyMessageId.WorldTeamMemberNearDeathSCNotify, this.OnTeamMemberNearDeathNotify);
Net.Register(ENotifyMessageId.WorldTeamMemberDeathSCNotify, this.OnTeamMemberDeathNotify);
Net.Register(ENotifyMessageId.WorldTeamMemberReviveSCNotify, this.OnTeamMemberReviveNotify);
Net.Register(ENotifyMessageId.WorldTeamMemberRescueSCNotify, this.OnTeamMemberRescueNotify);
Net.Register(ENotifyMessageId.WorldTeamMemberRescueProgressSCNotify, this.OnMemberRescueProgressNotify);
// ……本实体通道:WorldRoleNearDeath / WorldRoleDeath / WorldRescueRole
// WorldReviveRole / WorldCancelRescueRole / WorldRoleEnterReviveWait ……
Net.Register(ENotifyMessageId.WorldRoleDeathSCNotify, this.OnRoleDeathNotify);
Net.Register(ENotifyMessageId.WorldReviveRoleSCNotify, this.OnReviveRoleNotify);
// ……新版救援:WorldRoleEnterBeRescuedState / WorldRescueProgressUpdate /
// WorldExitNewRescue / WorldNewRescueTimeOut / WorldResumeNewRescueCountDown ……
Net.Register(ENotifyMessageId.WorldTeamDeadCntSCNotify, this.OnTeamDeadCntNotify);
}
23 条协议是「功能分期」的化石
数得出来的分层:本实体通道、队伍通道、新版救援(NewRescue 系列)、团灭计数。
面试讲协议设计时,拿这页说「同一领域的协议按订阅者分通道」——
队友死亡不需要触发本地输入锁,双通道避免客户端做无关过滤。
3.2 看门狗:黑屏必须能自己死掉
Src\Game\Module\PlayerDeath\PlayerDeathController.ts:1565-1586
private AddReviveBlackScreen(): void {
if (this.IsReviveBlackScreenActive) { return; }
this.IsReviveBlackScreenActive = true;
// 瞬时黑屏(无渐入动画),立刻遮住画面
void ControllerHolder.BlackScreenController.AddBlackScreenAsync(
EBlackScreenAnimType.None, 'PlayerDeathController_ReviveBlackScreen',
);
// 保底超时:防止流程中断导致黑屏永远不消失
this.ReviveBlackScreenTimeoutTimer = this.OwnedTimers.Delay(
() => {
Log.Warn(ELogModule.PlayerDeath, ELogAuthor.ZMK, '[PlayerDeathController] 复活黑屏保底超时,强制移除');
this.ForceRemoveReviveBlackScreen();
},
PlayerDeathControllerConstants.ReviveBlackScreenTimeout, // 60000ms
'PlayerDeathController.AddReviveBlackScreen',
);
}
长流程铁律:开遮罩必须配超时
复活黑屏依赖「流送收尾事件」来关,一旦服务端没发收尾或流程被打断,
黑屏永挂是 P0 级事故。60 秒看门狗强制移除 + Warn 日志,
宁可玩家看到闪一下画面,也不能永久黑屏。传送、结算的遮罩同款处理。
3.3 断线重连补跑复活:本模块最硬的设计
Src\Game\Module\PlayerDeath\PlayerDeathController.ts:1457-1485(节选)
/**
* 断线重连/无缝传送期间服务端已复活本地玩家,但 WorldReviveRoleSCNotify 在断链窗口丢失
* 补跑本地复活:同关卡重连保留实体、组件不重置,若不补收尾,
* 死亡表现(变火/倒地/输入锁)会残留导致角色无法复活。
*/
private ReviveSelfOnMissedNotify(handle: EntityHandle): void {
// 自己复活,团灭次数清空(与 OnReviveRoleNotify 一致)
ModelManager.DungeonModel!.ResetLastDungeonDefeatCount();
// 强制收尾死亡组件:无条件退出濒死 Loco / 清死亡上下文 / 切回 NotStart
handle.GetComponent(EComponent.RoleDeathComponent)?.ForceReviveReset();
// 复活收尾:解锁输入 / 恢复特效 / 广播 CharOnRevive(notify 传 undefined,不播被救演出)
handle.GetComponent(EComponent.RoleReviveComponent)?.HandleRevive(undefined);
// 结束死亡追踪 + 退出被救援态 + 取消救援他人 + UI 收尾
this.ExitDeathTrackAndRescue(handle);
handle.GetComponent(EComponent.RoleRescueComponent)?.TryCancelRescue(ERescueInterruptReason.PlayerDead);
if (this.IsPlayerDeathViewOpen()) {
UiInterfaceHelper.CloseView(EUiViewName.PlayerDeathView);
}
this.UpdateSelfDeathState(ECharacterDeathState.NotStart, undefined, true);
this.ShowMainUiChildPanels();
}
触发证据是「服务端血量 > 0 且本地仍死亡态」
不是等协议,而是用状态矛盾推断通知丢失,然后按
「死亡组件 → 复活组件 → 追踪/救援 → UI → 状态机」五层顺序手工补齐收尾。
顺序不能乱:先解锁实体层(输入/姿态),再收 UI,最后状态机复位。
这是「通知驱动系统必须有对账补偿」的完整代码证据,面试值得花两分钟讲。
3.4 复活通知缓存:流送未完先别播表现
Src\Game\World\Entity\Character\Role\RoleReviveComponent.ts:50-88(节选)
public HandleRevive(notify: Game.Protocol.WorldReviveRoleSCNotify | undefined): void {
// 本地玩家走"带流送等待的远距离复活传送"时,整个流送结束后再复活
// 此处先缓存本次复活通知,待 EndReviveTeleport 调 FlushPendingRevive 时再执行。
const isLocalPlayer = ownerPlayerId === localPlayerId;
if (isLocalPlayer && ControllerHolder.PlayerDeathController?.IsReviveTeleportInProgress()) {
this.PendingReviveNotify = notify;
this.HasPendingRevive = true;
return;
}
this.DoHandleRevive(notify);
}
Pending 模式 + 双路径消费
复活通知可能先于流送落位到达——直接播表现会「人在黑屏里突然站起来又瞬移」。
缓存后由 EndReviveTeleport 触发 Flush,且 flush 有两条路径:
正常落位 / 流送 Bail-Fail-Timeout 异常收尾。异常路径也消费缓存,
否则缓存变泄漏——又是「异常路径也要收尾」的重复主题。
3.5 死亡蒙太奇三重兜底
Src\Game\World\Entity\Character\Role\RoleDeathComponent.ts:820-857(节选)
// 起播失败时 uid 为 0n,订阅收不到任何东西 —— 直接兜底收尾,避免卡在不能移动的窗口。
// 原判据是 Montage_IsPlaying:那时起播失败会在 EndCallback 挂上之前就同步广播,事后订阅收不到。
if (this.DeathMontageUid === 0n) {
this.ClearDeathMontageTimer();
this.HandleDeathFinish();
return;
}
// 保底:Stop 之后结束事件不一定立即到(引擎入队统一回调),用 Delay(时长) 兜底
const totalLength = montage.GetPlayLength();
this.DeathMontageTimer = this.OwnedTimers.Delay(
() => { this.OnDeathMontageCompleted(); },
totalLength * TimerSystemConstants.SecondToMillisecond,
'RoleDeathComponent.PlayDeathMontage',
);
「播个动画」有三重防线 + 踩坑注释
① 起播失败(拿不到实例/蒙太奇)直接收尾;② uid 认领制——起播前先把旧 uid 作废成 0,
防止新蒙太奇的结束事件被误认领(注释写了为什么弃用 Montage_IsPlaying 判据);
③ 时长 Delay 保底,引擎回调入队延迟也不会卡死。
Bug 修复时把根因写进注释,是可复用的工程习惯。
4. 面试怎么讲
死亡复活流程怎么设计的设计
服务端权威的四态机(预濒死→濒死→死亡→复位),客户端镜像 + 表现。
协议按订阅者分双通道:本实体通道管输入锁/死亡 UI/相机,队友通道管救援列表,
23 条通知覆盖救援进度、超时恢复、团灭计数。
复活分原地和远距离传送两种,位置与相机朝向都由 WorldReviveRoleSCNotify 下发。 远距离复活的难点是时序:复活通知可能先于流送落位到达, 用 Pending 缓存 + 落位后 Flush,异常路径(流送失败)也会消费缓存保证不泄漏。
复活分原地和远距离传送两种,位置与相机朝向都由 WorldReviveRoleSCNotify 下发。 远距离复活的难点是时序:复活通知可能先于流送落位到达, 用 Pending 缓存 + 落位后 Flush,异常路径(流送失败)也会消费缓存保证不泄漏。
讲一个你印象最深的 bug难点
「断线重连后角色永远无法复活」。重连保留实体,但复活通知在断链窗口丢了,
本地还停在死亡态——倒地表现、输入锁全在,玩家彻底卡死。
修复不是等协议重发,而是对账补偿:重连时拿「服务端血量>0 且本地仍死亡态」的 状态矛盾当证据,识别出通知丢失,然后按五层顺序(死亡组件强复位 → 复活收尾 → 退出救援 → 关 UI → 状态机回 NotStart)手工补跑一遍复活。 上线后再没复发。通知驱动的系统,必须有对账补偿兜底断链窗口,这是那次的核心教训。
修复不是等协议重发,而是对账补偿:重连时拿「服务端血量>0 且本地仍死亡态」的 状态矛盾当证据,识别出通知丢失,然后按五层顺序(死亡组件强复位 → 复活收尾 → 退出救援 → 关 UI → 状态机回 NotStart)手工补跑一遍复活。 上线后再没复发。通知驱动的系统,必须有对账补偿兜底断链窗口,这是那次的核心教训。
客户端和服务端的职责边界怎么划边界
状态跳变、复活点选择、救援判定全在服务端;客户端做表现时序和防御。
边界上有个细节:服务端曾发过一条协议把濒死状态逆向重置回预濒死(服务端 bug),
客户端在状态机里做了拦截——已知态是濒死就不接受回退,打 Warn 日志不动状态。
「信任但校验」:协议默认信,但状态机不允许非法跳变,脏包拦下来留证据,
联调期定位两边谁的锅非常快。