在 TeleportController 的 LoadTransitionLevelAsync() 和 ExitLoadLevelAsync() 中,Seq1→Seq2→Seq3 的切换通过 OnFinished 回调 + LoadRoleLevelSequence 异步加载嵌套实现。这条链路存在两个严重问题:
| 问题 | 级别 | 位置 | 一句话描述 |
|---|---|---|---|
| 嵌套回调 | P1 | LoadTransitionLevelAsync |
Seq1 的 OnFinished 内异步加载 Seq2,回调层级达到 5 层深 |
| OnFinished 循环重入 | P0 | LoadTransitionLevelAsync L1570 |
OnFinished 处理器在异步窗口内未清除,可被重复触发→无限循环 |
| Seq2 缺少 OnFinished | 非 Bug | LoadTransitionLevelAsync L1579 |
Seq2 是循环序列(LoopCount=1),不应加 OnFinished — 3s 定时器是正确的 resolve 方式 |
| 3s 硬定时器与 Seq 脱节 | 合理 | LoadTransitionLevelAsync L1605 |
循环序列的结束只能靠定时器控制,这是正确做法 |
以下是 LoadTransitionLevelAsync() 中 Seq1→Seq2 的完整嵌套结构,已标注问题点:
// ┌─────────────────────────────────────────────────────────────────┐ // │ OnLevelShown 回调 (depth 0) │ // └─────────────────────────────────────────────────────────────────┘ // 加载 Seq1 资源 (异步) this.LoadRoleLevelSequence(path1, (levelSequence) => { // depth 1 if (levelSequence) { this.LevelSequenceActor!.SetSequence(levelSequence); this.ApplyCameraToLevelSequence(this.LevelSequenceActor!); const actorPlayer = this.LevelSequenceActor!.GetSequencePlayer(); if (actorPlayer) { // ┌──────────────────────────────────────────────────────┐ // │ ⚠️ Clear 旧回调,但 Add 新回调后没在进入时立即 Clear │ // └──────────────────────────────────────────────────────┘ actorPlayer.OnFinished.Clear(); actorPlayer.OnFinished.Add(() => { // depth 2: Seq1 播完回调 [A] // ❌ 没有在这里 Clear 掉自身! // 进入异步加载窗口,[A] 仍然挂在 OnFinished 上 this.LoadRoleLevelSequence(path2, (seq2) => { // depth 3: 异步加载 Seq2 [B] if (seq2) { this.LevelSequenceActor!.SetSequence(seq2); this.ApplyCameraToLevelSequence(this.LevelSequenceActor!); actorPlayer.OnFinished.Clear(); // [C] 清 [A] — 但在异步回调里! actorPlayer.PlaybackSettings.LoopCount = new UE.MovieSceneSequenceLoopCount(1); // ❌ 没有给 Seq2 补 OnFinished 处理器! this.ApplyAvatarToLevelSequence(this.LevelSequenceActor!, true, () => { // depth 4 this.SafePlaySequencePlayer(actorPlayer); // depth 5: Play Seq2 }); } else { reject(new Error('传送-加载退出序列失败')); } }); }); this.ApplyAvatarToLevelSequence(this.LevelSequenceActor!, true, () => { this.SafePlaySequencePlayer(actorPlayer); // Play Seq1 }); } } else { reject(new Error('传送-加载退出序列失败')); } }); // ┌─────────────────────────────────────────────────────────────────┐ // │ ⚠️ 3 秒硬定时器,与 Seq 播放完全无关 │ // └─────────────────────────────────────────────────────────────────┘ TimerSystem.Delay(() => { resolve(); // ← 不管 Seq 播到哪,3 秒后强制 resolve }, 3000);
| 层级 | 代码位置 | 说明 |
|---|---|---|
| depth 0 | OnLevelShown.Add | 关卡加载完成回调 |
| depth 1 | LoadRoleLevelSequence(path1, cb) | Seq1 资源异步加载回调 |
| depth 2 | actorPlayer.OnFinished.Add | Seq1 播放完成回调 [A] — 这里是循环重入点 |
| depth 3 | LoadRoleLevelSequence(path2, cb) | Seq2 资源异步加载回调 [B] |
| depth 4 | ApplyAvatarToLevelSequence(cb) | Seq2 换装回调 |
| depth 5 | SafePlaySequencePlayer | 实际 Play Seq2 |
当前 Seq1→Seq2 的切换嵌套在 OnFinished 回调内完成,形成了典型的"回调地狱"(Callback Hell):
OnLevelShown.Add(() => { // depth 0
LoadRoleLevelSequence(path1, (seq1) => { // depth 1
SetSequence(seq1)
ApplyAvatarToLevelSequence(cb => { // depth 2
Play(seq1)
})
OnFinished.Add(() => { // depth 2 (并行)
LoadRoleLevelSequence(path2, (seq2) => { // depth 3
SetSequence(seq2)
ApplyAvatarToLevelSequence(cb => { // depth 4
Play(seq2) // depth 5
})
})
})
})
TimerSystem.Delay(resolve, 3000) // ← 完全脱离回调链
})
// 最多 5 层缩进,每一层都是一个异步回调
// 错误处理散落在各层,无法统一 catch
// resolve 靠 3s 定时器而非回调链驱动
这是最严重的问题。OnFinished 处理器 [A] 在进入异步加载窗口后未及时清除自身,可被重复触发,导致循环。
actorPlayer.OnFinished.Clear(); // 清掉旧的 actorPlayer.OnFinished.Add(() => { // [A] 注册 Seq1 完成处理器 // ❌ 进入 [A] 时没有 Clear 自身! // → [A] 在整个异步加载窗口内仍然挂在 OnFinished 上 this.LoadRoleLevelSequence(path2, (seq2) => { // 异步,可能耗时数百ms // ... actorPlayer.OnFinished.Clear(); // [C] 这里才清 [A] // ... Play(seq2) }); });
[A] 被触发 到 [B] 异步回调执行 [C] Clear 之间,[A] 仍然注册在 OnFinished 上。
OnFinished 再次触发(原因见下表),[A] 会再次执行 → 又发起一次 LoadRoleLevelSequence(path2) → 产生第二个 [B] 回调 → 多次 SetSequence + Play → 循环。
| 触发原因 | 说明 |
|---|---|
SetSequence 副作用 |
UE SetSequence 可能重置 Player 状态并触发 OnFinished |
LoopCount 残留 |
Seq1 的 LoopCount 如果非 0,Seq1 可能重播并再次完成 |
| UE 内部重评估 | 引擎 Tick 中的 Sequence 评估可能在资源切换时触发 |
| 连续传送 | 玩家快速连续传送时,旧 [A] 残留 + 新 OnFinished 叠加 |
第 1 轮:
Seq1 播完 → OnFinished 触发 → [A] 执行
→ LoadRoleLevelSequence(path2) 异步发起 [B1]
← (异步窗口,[A] 仍在)
★ OnFinished 再次触发(SetSequence 副作用 / 重播 / ...)
→ [A] 又执行 → LoadRoleLevelSequence(path2) 异步发起 [B2]
← (两个 B 都在加载)
第 2 轮:
[B1] 回调 → SetSequence(seq2) → Clear → Play(seq2)
[B2] 回调 → SetSequence(seq2) → Clear → Play(seq2) ← 重复操作
Seq2 循环播放(LoopCount=1),OnFinished 空触发 ← 无害但无意义
★ 如果 [A] 在 [B2] Clear 之前又被触发 → 回到第 1 轮
→ 无限循环
最终只有 3s 硬定时器能打破循环 → resolve()
但此时可能有多组 Play/Stop 叠加 → 表现混乱
LoopCount = 1),不能加 OnFinished — 否则每次循环结束都会触发 → resolve 时机错误。LoopCount = 1(循环播放 2 遍)。UE 的 OnFinished 会在每次循环结束时触发:
OnFinished 触发 → 如果有 handler 就会执行 resolve() → 提前 resolve,第 2 遍还没播Clear 了自身,时机也不对 — 观众只看了 1 遍resolve() 被调第二次(虽然 Promise 幂等,但后续逻辑可能重复执行)this.LoadRoleLevelSequence(path1, (levelSequence) => { if (!levelSequence) { reject(new Error('传送-加载 Seq1 失败')); return; } this.LevelSequenceActor!.SetSequence(levelSequence); this.ApplyCameraToLevelSequence(this.LevelSequenceActor!); const actorPlayer = this.LevelSequenceActor!.GetSequencePlayer(); if (!actorPlayer) { reject(new Error('传送-Seq1 无播放器')); return; } // Seq1 → Seq2 actorPlayer.OnFinished.Clear(); actorPlayer.OnFinished.Add(() => { // ✅ 进入时立即清除自身,杜绝异步窗口内重入 actorPlayer.OnFinished.Clear(); this.LoadRoleLevelSequence(path2, (seq2) => { if (!seq2) { reject(new Error('传送-加载 Seq2 失败')); return; } this.LevelSequenceActor!.SetSequence(seq2); this.ApplyCameraToLevelSequence(this.LevelSequenceActor!); actorPlayer.PlaybackSettings.LoopCount = new UE.MovieSceneSequenceLoopCount(1); // 循环播放,保持原设计 // ✅ Seq2 是循环序列,不加 OnFinished // 循环播放的结束由外部 3s 定时器控制 // 如果加 OnFinished → 每次循环结束都会触发 → resolve 时机错误 this.ApplyAvatarToLevelSequence(this.LevelSequenceActor!, true, () => { this.SafePlaySequencePlayer(actorPlayer); }); }); }); // Play Seq1 this.ApplyAvatarToLevelSequence(this.LevelSequenceActor!, true, () => { this.SafePlaySequencePlayer(actorPlayer); }); }); // ✅ 保留 3s 定时器 — Seq2 是循环序列,只能靠定时器控制 resolve 时机 TimerSystem.Delay(() => { Log.Debug(ELogModule.Debug, ELogAuthor.ZZB, '传送-加载场景处理完成'); resolve(); }, 3000);
actorPlayer.OnFinished.Clear(); actorPlayer.OnFinished.Add(() => { // ✅ 进入时立即清除自身 actorPlayer.OnFinished.Clear(); Log.Info(ELogModule.Debug, ELogAuthor.ZZB, '传送-Seq3 OnFinished 触发'); ControllerHolder.CameraController.SetCameraCollisionEnabled(true); if (!this.HasResetCameraByLevelSeq) { this.HasResetCameraByLevelSeq = true; ControllerHolder.CameraController.ResetCameraByLevelSeq(); } this.UnLoadTransitionLevel(); EventSystem.Emit(EEventName.TeleportDownSkillComplete); });
| 对比项 | ❌ 现状 | ✅ 修复后 |
|---|---|---|
| OnFinished 进入时 | 不清除自身,异步窗口内可重入 | 立即 Clear(),杜绝重入 |
| Seq2 OnFinished | ❌ 没有(且不应有 — 循环序列) | ✅ 仍然不加 — LoopCount = 1 循环播放,OnFinished 会每次循环触发 |
| resolve 驱动方式 | 3s 硬定时器 | 保留 3s 定时器(循环序列只能靠定时器控制结束时机) |
| 循环重入风险 | P0 [A] 异步窗口内可无限重入 | 消除 [A] 进入即 Clear,Seq2 不挂 OnFinished |
| Seq2 循环误触 | 无 OnFinished,循环结束空触发 | 无 OnFinished,循环结束空触发(不变,无副作用) |
| 嵌套深度 | 5 层 | 5 层(可进一步用 async/await 扁平化) |
| 错误处理 | 分散在各层 reject | 每层 reject,但结构更清晰 |
async/await 将 LoadRoleLevelSequence 和 ApplyAvatarToLevelSequence 包装为 Promise,彻底消除回调嵌套:
const seq1 = await this.LoadLevelSequenceAsync(path1); this.LevelSequenceActor!.SetSequence(seq1); await this.ApplyAvatarAsync(...); await this.PlayAndWaitAsync(actorPlayer); // 等 OnFinished // → Seq1 完成,继续 Seq2...