角色移动:冲量必须进入 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 后仍只发生一次,才算真正接入移动框架;否则它只是正常网络下恰好能工作的本地修改。