角色移动:冲量必须进入 SavedMove,不能只改 Velocity

自动关联目录:角色移动:冲量必须进入 SavedMove,不能只改 Velocity

核心判断:在 UE 预测移动里,冲量、持续力、Dash 和特殊移动不是某一帧对 Velocity 的修改,而是必须可被记录、合包判断、上行、重放和纠正的时间线输入。没有进入 FSavedMove_Character 的效果,在正常运行看似正确,却会在 Combine 或 Replay 时被吞掉、重复消费或覆盖。

一、业务冲突:即时手感要求本地生效,DS 又必须能复算

行走、跳跃、击退、冲刺、速度修正、死亡禁用和游泳都需要立即反馈。本地主控如果等待服务器确认,操作会迟钝;完全相信客户端,又会使碰撞、战斗位移和跨端状态失去权威。UE 的 UCharacterMovementComponent 已提供预测、SavedMove、合包、ServerMove 与纠正,但业务效果只有遵守这套时间模型才真正被预测系统理解。

实际问题出现在冲量:第一帧调用 AddImpulse 时角色移动正确,随后 SavedMove 合包会把 Velocity 恢复为旧 Move 的起始速度,冲量被覆盖;收到服务器纠正后重放历史 Move,UE 默认 SavedMove 又不保存业务 PendingImpulse,导致重放时该帧没有冲量。反过来,若把“本帧新产生的待上行冲量”留在共享字段中,Replay 旧 Move 又可能提前或多次消费它。

这不是网络参数调优能解决的问题,而是业务动作没有被建模为预测历史的一部分。移动框架的核心边界必须是:Intent 进入可重放命令,CMC 产出物理事实,状态机和动画只消费事实,不能反向各自改速度。

二、实际实现:按 Move 保存外部速度,并把物理事实投影给状态机

实现扩展了 FSavedMove_Character:在 SetMoveFor 时保存当帧 PendingImpulseToApply,同时保存主控端业务冲量与力换算出的速度增量。只要新旧 Move 任一包含外部速度,就设置 bForceNoCombine 并在 CanCombineWith 再次拒绝,避免合包改变离散事件的时间位置。

Replay 前,PrepMoveFor 把该历史 Move 保存的外部冲量恢复到 CMC,由正常 ApplyAccumulatedForces 在正确步骤消费。整个 ClientUpdatePositionAfterServerUpdate 外层又临时保存“当前真实帧刚产生、尚未形成 SavedMove”的冲量与力,Replay 期间清空,结束后恢复。这两层保护处理的是不同数据:前者重现历史,后者隔离未来。

上行数据增加单调 MoveId、上一次已收到重要纠正的 MoveId、压缩后的朝向和可选 ControlledVelocity。服务器先验证时间戳,再把主控速度增量合入 PendingImpulse,执行 MoveAutonomous,最后检查单帧位置误差。一次重要纠正产生稳定 ACK 目标;客户端未确认前,服务器不会把后续重发当成新的目标,避免确认线不断前移。

物理执行后,Host 形成只读 Movement Snapshot:Grounded、垂直速度、输入强度、速度、上一帧是否腾空和地面对象身份。层级状态机消费该快照决定 Running、Jumping、Falling 等玩法状态,动画再消费状态与速度。自定义游泳同样只在本地执行角色或 Authority 执行,Simulated Proxy 直接返回,避免复制位移与本地 SafeMoveUpdatedComponent 形成双写。

flowchart LR
    I[输入/技能 Intent] --> M[可重放 Move Command]
    M --> P[CMC 预测与碰撞求解]
    P --> F[只读 Movement Facts]
    F --> S[玩法状态机]
    F --> A[动画参数]
    M --> U[ServerMove 上行]
    U --> D[DS 复算/校验]
    D --> C[纠正 + ACK]
    C --> P

三、值得保留:修复对准了 UE 预测语义,而不是绕开 CMC

最好的选择是没有用 Teleport 实现 Dash 或击退,而是继续走 CMC 的 Sweep、碰撞和预测链。外部冲量作为 SavedMove 的一部分被保存,且含冲量的 Move 不合并,明确保护了离散事件的时序。这比在纠正后再猜“刚才是否打中过”可靠得多。

历史冲量与当前帧待消费冲量分开保护也很关键。Replay 是过去时间线的重算,不能读取未来帧共享暂存区;保存、清空、重放、恢复的边界体现了这一点。MoveId 与稳定 ACK 目标则让“服务器已经发过纠正”和“客户端确实基于该纠正继续上行”成为两个可观察事实。

Movement Snapshot 作为物理层到状态机的只读接口同样值得保留。Grounded 来自 CMC/Floor 事实,状态机不需要反向查询散落的 Actor 属性;动画不拥有 MovementMode。水体采样集中为每帧一次,并把浸没、密度和流速预聚合,也避免状态判断与 PhysCustom 各自遍历 Volume、使用两套比例公式。

四、结构性缺陷:一个 MovementComponent 正在承担过多策略

当前自定义 CMC 同时拥有属性 Setter、Dash、速度偏移、死亡停机、游泳采样与求解、暂停恢复、网络协议、误差纠正、显示平滑和调试日志。它利用 UE 挂点实现了功能,但任何新移动能力都可能同时修改 SavedMove、序列化、ServerMove、Replay、状态机和表现平滑,回归面过大。

单帧位置阈值能拦截明显误差,却不能解释误差来源,也无法按移动模式、速度、网络抖动与碰撞上下文自适应。只在位置超过阈值时纠正,可能放过持续小漂移;阈值过紧又会把合法的动态地板、浮力或网络量化误差变成频繁回拉。纠正需要同时观察位置、速度、MovementMode、Base 与命令序列,而不是只有一个距离。

状态事实仍偏少。Grounded、速度和输入足以驱动基础状态,但移动 Base Generation、Floor Normal、实际加速度、外力来源、Dash 命令、Water Sample Generation 和 Authority Time 若不进入统一 Observation,状态、动画和诊断仍会分别回读 CMC。现有只读快照是正确开端,还未成为完整边界。

五、目标设计:把运动拆成 Command、Simulation、Observation、Presentation

业务能力实现为 MovementCommand,明确 CommandId、开始 MoveId、持续时间、参数、预测权限与可合并策略。Command 在本地与 DS 使用同一 Apply 接口;需要离散时序的命令拒绝 SavedMove 合并,需要持续效果的命令保存剩余时间或确定性曲线参数,而不是只保存某帧 Velocity。

CMC Adapter 只负责把命令映射到 UE 移动、Sweep 和 Floor;Prediction Adapter 负责 Save/Restore/Serialize;Observation Builder 在物理步骤完成后生成不可变事实;玩法状态和动画是两个并列消费者。纠正比较同一 MoveId 下的 Observation,并按误差类别选择 Ignore、Smooth、Replay 或 Hard Snap。

void SimulateMove(const MoveFrame& frame, PredictionContext& ctx)
{
    for (const MovementCommand& cmd : frame.commands)
        cmd.ApplyTo(ctx.characterMovement);

    ctx.characterMovement.PerformMovement(frame.dt);
    MovementObservation actual = ObserveAfterPhysics(ctx, frame.moveId);

    if (ctx.isServer) {
        Validation v = Compare(actual, frame.clientObservation);
        if (v.requiresCorrection)
            SendStableCorrection(frame.moveId, actual, v.reason);
    }
    PublishFacts(actual); // 状态机和动画只读
}

六、决策规则:任何移动效果都必须回答“重放时发生什么”

效果预测建模纠正与验证
连续行走输入可合并的加速度 Intent位置、速度、Mode、Floor/Base
跳跃/击退冲量离散 Command,保存到 SavedMove,禁止跨事件合包Replay 恢复一次;Authority 冲量触发重要纠正
Dash带 CommandId 与时长的可重放 Swept Movement服务器复算轨迹;取消必须幂等
持续速度偏移保存参数与剩余时间,不逐帧复制结果起止 MoveId、叠加规则、上限
Dead/暂停清空所有速度、加速度、RequestedVelocity 与待施力恢复时丢弃暂停输入,重新建立权威基线
游泳/特殊移动集中环境采样,Authority/Owner 执行,Proxy 不二次求解浸没与速度上限、模式切换、采样代际
远端表现消费复制状态并平滑 Mesh,不拥有玩法状态Capsule 真相与 Mesh Offset 分离

这段实践揭示了 UE 角色移动最容易踩的坑:预测不是“客户端先跑一下”,而是一套可保存、可重放、可确认的时间机器。一个效果只有在 Combine、丢包、ServerMove、纠正与 Replay 后仍只发生一次,才算真正接入移动框架;否则它只是正常网络下恰好能工作的本地修改。