首页 / 入职工作清单 / 死亡与重生家族

死亡与重生家族

一条线管四件事:死亡复活(濒死/救援/复活全流程)、 坠落重生、边界重生。 共同骨架:死亡/出界判定全在服务端,客户端跑一个四态表现状态机, 复活位置与朝向由服务端下发。本页重点讲死亡复活(模块 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,异常路径(流送失败)也会消费缓存保证不泄漏。
讲一个你印象最深的 bug难点
「断线重连后角色永远无法复活」。重连保留实体,但复活通知在断链窗口丢了, 本地还停在死亡态——倒地表现、输入锁全在,玩家彻底卡死。
修复不是等协议重发,而是对账补偿:重连时拿「服务端血量>0 且本地仍死亡态」的 状态矛盾当证据,识别出通知丢失,然后按五层顺序(死亡组件强复位 → 复活收尾 → 退出救援 → 关 UI → 状态机回 NotStart)手工补跑一遍复活。 上线后再没复发。通知驱动的系统,必须有对账补偿兜底断链窗口,这是那次的核心教训。
客户端和服务端的职责边界怎么划边界
状态跳变、复活点选择、救援判定全在服务端;客户端做表现时序和防御。 边界上有个细节:服务端曾发过一条协议把濒死状态逆向重置回预濒死(服务端 bug), 客户端在状态机里做了拦截——已知态是濒死就不接受回退,打 Warn 日志不动状态。 「信任但校验」:协议默认信,但状态机不允许非法跳变,脏包拦下来留证据, 联调期定位两边谁的锅非常快。