⚡ 传送落地「弹起几帧」根因分析与保底方案 · 角色1014

2026-08-31 · 分支 zhaozibo_071 · 现象由 QA 录屏报告,根因经本人实测确认 · 本文为留档,保底方案设计完成、尚未实施

概述

轻度传送(LightTeleport)落地阶段,只有角色 1014 会被向上弹起几帧再恢复正常起立。 定位结论:落地 Seq(Seq3)的配置长度大于下落技能的蒙太奇长度——下落技能 1018 先播完、先结束, 技能结束时的清理动作与仍在驱动角色的 Seq3 打架,落地姿态被弹回。已用「两侧长度对齐」临时修复并验证。

项内容
现象角色 1014 传送落地瞬间,深蹲最低点后向上回弹几帧,再进入正常起立
复现范围QA 报告只有 1014 明显复现(大体型角色落地蹲得深,回弹更醒目;时序问题是全局性的)
根因Seq3 配置长度 > 下落技能(1018)蒙太奇长度 → 技能先于 Seq3 结束(本人实测确认)
直接动因技能结束的「三连击」:硬切蒙太奇 + 恢复重力 + Phase 切换,全部落在 Seq3 还在播的窗口里
已实施修复已验证 两侧长度对齐(配置修复,无代码改动)
保底方案待实施 双驱动完成闸门 + 长度校验(本文 §6)

现象(QA 录屏)

录屏逐帧可见的完整过程:

  1. 角色从高空下落,调试面板 Phase: Skill(下落技能 1018 生效中)
  2. 触地瞬间深蹲到最低点(Seq3 驱动的落地缓冲)
  3. 最低点后躯干上弹几帧——不是起立动画的平滑回升,是姿态被瞬间拽回(弹起后画面出现角色名「执火人」)
  4. 随后正常起立,Phase: Skill → Grounded、CurrentState: NotMoving
诊断面板的关键价值:「弹起」发生在 Phase 从 Skill 切到 Grounded 的瞬间—— 说明弹起不是动画资产问题,而是技能结束时机与Seq 播放进度不同步。

根因(实测确认)

传送落地有两个并行的视觉驱动者:

驱动者是什么由谁结束
下落技能 1018 LevelSkillLogicLightTeleportDown,播放技能蒙太奇(下落姿态),结束时恢复重力、切 Phase 蒙太奇播完回调 OnCompleted
Seq3 落地表演 TeleportController.ExitLoadLevelAsync 加载的 LevelSequence,绑定真实战斗角色驱动落地动画与位移 OnFinished → emit TeleportDownSkillComplete

当 Seq3 配置长度 > 下落技能蒙太奇长度时:蒙太奇先播完 → 技能结束。技能结束瞬间做了三件事, 每一件都在和还在播放的 Seq3 抢角色:

蒙太奇先播完(T_m)T_m < T_s(Seq3 还剩一段没播)
↓
EndSkill(1018)
↓
① 硬切蒙太奇
Montage_Stop(0.2s)DefaultMontageBlendTime 固定混出
② 恢复重力
ApplyPlayerGravityScale(true)物理开始参与下坠/支撑
③ 切换 Phase
Skill → Groundedlocomotion 状态机接管姿态
↓
三股力量 vs 仍在驱动角色的 Seq3同一帧内双写动画/位移 → 弹起几帧
↓
Seq3 播完(T_s),角色恢复正常起立
验证方法(已执行):把两侧长度调成一致后弹起消失。这同时证明了: 弹起来自「技能先结束」而非动画资产本身。

完整链路

事故链TeleportController.OnStartDown → 技能 1018 → EndSkill
// TeleportController.OnStartDown(顺序严格:先 Seq 后技能)
await this.ExitLoadLevelAsync();      // ① Seq3 加载 → BindRealRoleToSeq3 → 播放(时长 T_s)
await this.StartTeleportDown();       // ② BeginLevelSkill(1018)

// LevelSkillLogicLightTeleportDown.OnStart(蒙太奇路径)
const playResult = this.SkillInstance?.PlaySkillMontage(
    0, 1, 0,
    () => { this.OnTeleportDownComplete(); },   // ← 蒙太奇播完(T_m)直接结束技能,没有等 Seq3
    startSection,
);

// OnTeleportDownComplete
this.SkillComp?.EndSkill(skillId, '传送-轻度传送下降完成', ESkillEndReasonType.SkillEnd);

// Skill.ClearSkillMontage / OnEnd
this.AnimComp!.MainAnimInstance!.Montage_Stop(0.2, montage);  // ① 硬切
this.ApplyPlayerGravityScale(true);                        // ② 重力恢复
// ③ Phase: Skill → Grounded(状态机接管)

代码现状:闸门只盖了一半

LevelSkillLogicLightTeleportDown.OnStart 有三条结束路径,只有一条有「等 Seq3」的闸门:

路径结束触发等 Seq3?现状
主控 + SkipLocalPlayerDownMontage=true
(默认开关:主控跳过蒙太奇,Seq3 独占落地)
Seq3 完成事件 ✅ WaitSeq3ThenComplete() 正确 事件 + 15s 兜底定时器
蒙太奇播放路径
(skip=false 的并行分支 / 模拟端)
蒙太奇 OnCompleted ❌ 无 事故路径 蒙太奇先完 → 直接结束技能
起播失败路径 TimerSystem.Next 下一帧 ❌ 无 同类隐患 下一帧就结束,更早
「等 Seq3 再结束」本来就是这套代码的设计意图。 WaitSeq3ThenComplete 的注释写着:立即结束会让 end 上报同步到模拟端,把模拟端还在播的下落蒙太奇 Montage_Stop 掉。只是这个闸门只覆盖了 skip 分支,蒙太奇路径漏了。

另一个帮凶:事件发射点散落

TeleportDownSkillComplete(Seq3 完成的唯一信号)在 TeleportController 里有 7 处 emit(OnFinished 正常路径 + 6 处失败收闸:配置缺失 / 绑定失败 / 播放器缺失 / 资源加载失败 / Actor 缺失 / 淡入淡出路径)。 排查「闸门为什么提前/延后关」时需要逐处找,是典型的多点收闸结构。

已实施修复 配置修复 · 已验证

两侧长度对齐:把下落技能蒙太奇长度与 Seq3 落地段配置长度调成一致, 技能结束与 Seq3 完成同帧到达,双驱动窗口消失,弹起不再出现。

为什么这只是临时修复:靠的是「两个长度碰巧相等」这个脆弱不变量。 之后任何人改任一侧的动画/配置(加长落地蹲、改 Seq 剪辑),错配就会回来——而且 QA 只会在某个角色身上看到几帧弹跳, 很难第一时间联想到配置长度。需要运行时保底 + 编辑期校验(下节)。

保底方案(设计完成 · 待实施)

核心原则:技能结束条件 = 自身蒙太奇播完 AND 落地 Seq3 完成(双驱动完成闸门), 复用已有的 WaitSeq3ThenComplete 模式,不是新发明。

① LevelSkillLogicLightTeleportDown.ts

② TeleportController.ts

③ 长度校验(防再犯)

无竞态论证

时序保证OnStartDown 的顺序是硬保证
await this.ExitLoadLevelAsync();   // Seq3 流程先建立 → IsDownSeqFlowActive 必然先为 true
await this.StartTeleportDown();  // 技能后启动 → 读标志时 Seq3 流程必然已存在

// 标志读取 + Once 注册在同一个同步块内,事件 emit 也是同步的
// JS 单线程 → 不存在「读到 false 但事件还没来得及发」的窗口
为什么不担心「技能永不结束」:Lyra 同款结构——正常路径等事件, 兜底路径保证一定收尾(15s 定时器 + 6 处失败收闸 emit)。 「不能提前结束」和「必须能结束」两条都要有,这套设计里都有。

业界对照

「多个异步视觉驱动者 + gameplay 状态机抢角色控制权」这类问题,业界三种模式:

模式代表做法本工程对应
1. 技能编排
(完成屏障)
Epic GAS / Lyra 技能持有多个异步任务(蒙太奇任务 + 等事件任务),全部完成才 EndAbility; 同时保证被打断时也必须能结束(防技能永不结束) 本方案 双 AND 闸门即此模式; Lyra 换弹技能(播蒙太奇+等事件,事件没来用蒙太奇结束兜底)与之同构
2. 序列独占 UE 过场惯例 序列期间锁输入、DisableMovement,seq 全权驱动角色;极端形式 = loading screen SkipLocalPlayerDownMontage=true 默认已是此方向(主控 seq 独占); 事故的并行分支是遗留路径
3. 编辑期校验 各厂 CI / 表校验 上线前比对 seq 长度 vs 技能/蒙太奇长度,超阈值报错 待补 长度校验(§6-③)
结论:运行时层面双驱动闸门就是业界标准姿势(GAS/Lyra 同款), 且本工程已有半个(WaitSeq3ThenComplete),实施是「补完」而非「改造」。 与最理想的差距只在缺预防层——闸门(止血)+ 长度校验(防再犯)双层才是完整答案。 曾评估过更简变体(主控只认 Seq3 完成、无视蒙太奇时序),但反向场景(Seq 比蒙太奇短)会切蒙太奇; 双 AND 两个方向都安全,维持双 AND。

结论与待办

#事项状态
1根因定位:Seq3 配置长度 > 下落技能蒙太奇长度,技能先结束与 Seq3 双驱动冲突实测确认
2临时修复:两侧长度对齐已验证
3保底:蒙太奇路径接入 WaitSeq3ThenComplete 双驱动闸门待实施
4保底:7 处 emit 收口 + IsDownSeqFlowActive 查询待实施
5预防:配置加载时长度比对校验(Log.Error)待实施

相关代码:Src/Game/World/Entity/Skill/SkillLogic/LevelSkillLogic/LevelSkillLogicLightTeleportDown.ts · Src/Game/Module/Teleport/TeleportController.ts · Src/Game/World/Entity/Skill/Skill.ts(ClearSkillMontage) · 下落技能 ID 1018(TeleportDefineConstants.TeleportDownSkillId)