本页尚未翻译,当前显示英文版本。
Prediction & Lag Compensation
向前预测,向后补偿,数据都带着事件真实发生的时间。 客户端手感即时,服务器始终正确,命中按射击者的时间线判定。
何时使用
- 有延迟时输入也要手感即时,同时服务器仍是权威(authority):向前预测,出现偏差时校正(reconciliation)。
- 命中必须按射击者的时间线判定:
ResolveAt把 hitbox 回退到上报的视角 tick。 - 瞄准弧线和落点标记必须与实际结果一致:客户端和服务器预报同一条
Trajectory。 - 关键玩法字段绝不能回滚:声明哪些字段参与预测(prediction),哪些等待服务器。
- 需要调优橡皮筋效应:按实体设置窗口和容差,并收集预测错误的遥测数据。
- 无需使用:如果延迟不影响体验。回合制或节奏慢的游戏用普通的 Data & Subscriptions 增量就能跑得很好。
分工
| 主体 | 在本页中的职责 |
|---|---|
schema-author | 声明哪些字段参与预测、哪些只由权威方决定;设置预测窗口 |
room-owner | 在历史状态上判定命中;回退世界 |
player | 预测并校正运动;订阅校正事件 |
适用于哪些房间
本模块只在由平台推进模拟的地方运行,即声明了 Host = "Backend" 的房间(room)。如果模拟归你自己的游戏服务器所有(PlayServ 作为元服务器),移动、碰撞和预测都留在引擎一侧,本页描述的是平台托管这一备选方案,而不是必须遵守的要求。参见由谁运行 tick。
概览
“预测”这个词涵盖三种不同的机制,它们的权威方和出错方式各不相同,每种都有单独的文章:
| 机制 | 作用 | 运行于 | 出错时的后果 |
|---|---|---|---|
| 预测自身移动 | 不等服务器,直接把声明的模型应用到你自己的输入上 | 客户端 | 一次校正,重放并平滑 |
| 显示其他玩家 | 在陆续到达的状态之间绘制其他实体(entity) | 客户端 | 一次肉眼可见的抖动 |
| Lag Compensation | 把目标回退到射击者看到的那一刻 | 服务器 | 有人死得不公平 |
四个预设,“no prediction”也是其中之一。
| 预设 | 预测自身 | 补偿 | 平滑显示他人 |
|---|---|---|---|
| shooter | 是 | 在约一秒半的窗口内 | 是 |
| arcade | 是 | 否 | 是 |
| observer | 否 | 否 | 是 |
| no prediction | 否 | 否 | 否:状态直接来自权威方,按声明的插值窗口显示 |
回合制游戏、策略游戏和大多数手游完全不需要预测;声明 no prediction,就是告诉客户端按原样显示状态,不要猜。
本模块自己没有移动模型、响应表、几何数据,也没有历史窗口。 它们分别属于 Locomotion、Collision、Map 和 Entity。预测只是提前应用它们,或者回头读取它们,从不另外声明一份副本。
Tank 上声明预测:参与预测的字段,只由权威方决定的 Hp,向前 8 / 回退 64 的窗口[Entity("tank")]
[Prediction(ForwardTicks = 8, MaxRewindTicks = 64)]
public class Tank
{
[Sync, Predicted] public Vector3 Position; // rolls back and replays
[Sync, Predicted] public Vector3 Velocity;
[Stat(Max = 100), AuthoritativeOnly] public Stat Hp; // never predicted
}@Entity('tank')
@Prediction({ forwardTicks: 8, maxRewindTicks: 64 })
export class Tank {
@Sync() @Predicted() position!: Vector3; // rolls back and replays
@Sync() @Predicted() velocity!: Vector3;
@Stat({ max: 100 }) @AuthoritativeOnly() hp: Stat; // never predicted
}@entity("tank")
@prediction(forward_ticks=8, max_rewind_ticks=64)
class Tank:
position: Vector3 = sync(predicted=True) # rolls back and replays
velocity: Vector3 = sync(predicted=True)
hp = stat(max=100, authoritative_only=True) # never predictedGo 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。
UCLASS(PSEntity = "tank", PSPrediction = (ForwardTicks = 8, MaxRewindTicks = 64))
class UTank : public UObject
{
GENERATED_BODY()
UPROPERTY(PSSync = (Predicted = "true")) FVector3f Position; // rolls back and replays
UPROPERTY(PSSync = (Predicted = "true")) FVector3f Velocity;
UPROPERTY(PSStat = (Max = 100, AuthoritativeOnly = "true")) FPSStat Hp; // never predicted
};
// declarations compile into the same pushed model — playserv push from the UE project or CI[Entity("tank")]
[Prediction(ForwardTicks = 8, MaxRewindTicks = 64)]
public class Tank
{
[Sync, Predicted] public Vector3 Position; // rolls back and replays
[Sync, Predicted] public Vector3 Velocity;
[Stat(Max = 100), AuthoritativeOnly] public Stat Hp; // never predicted
}带延迟补偿(lag compensation)的判定回答的是“开这一枪时,每个人都在哪”:
ResolveAt(shooterViewTick) 把 hitbox 回退到射击者的视角[After(Projectiles.HitReported)]
public static void Validate(HitReport hit) =>
hit.ResolveAt(hit.ShooterViewTick); // rewinds hitboxes, sub-tick interpolatedexport const validate = after(Projectiles.hitReported, (hit: HitReport) =>
hit.resolveAt(hit.shooterViewTick)); // rewinds hitboxes, sub-tick interpolated@after(projectiles.hit_reported)
def validate(hit: HitReport):
hit.resolve_at(hit.shooter_view_tick) # rewinds hitboxes, sub-tick interpolatedGo 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。
hook 是云函数:它在平台上执行,不在引擎里。用 C#、TypeScript 或 Python 编写,Unreal 订阅它产生的事件。在引擎内编写函数的方式已在计划中:Blueprint 图表,以及 C++(翻译成经平台校验的脚本目标)。两者都像其他声明一样经过分析后推送,执行仍然在平台上。
hook 是云函数:它在平台上执行,不在引擎里。用 C#、TypeScript 或 Python 编写,Unity 订阅它产生的事件。
轨迹预报由服务器和客户端共用(瞄准弧线、落点标记)。预报是本模块的操作,会结合地图的障碍物集合外推,所以双方用相同的输入画出同一条弧线:
Trajectory 调用:考虑碰撞的轨迹预报,服务器和瞄准预览共用var arc = room.Prediction.Trajectory(from, velocity, steps: 30); // collision-awareconst arc = room.prediction.trajectory(from, velocity, { steps: 30 }); // collision-awarearc = room.prediction.trajectory(origin, velocity, steps=30) # collision-awareGo 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。
// collision-aware: the arc the platform itself would walk
Room->Prediction->Trajectories->Get(LaunchPosition, LaunchVelocity, /*Steps*/ 30,
TPSOnResult<FPSTrajectory>::CreateWeakLambda(this, [this](const TPSResult<FPSTrajectory>& Result)
{
if (!Result.HasValue()) { return; }
DrawArc(Result.Value());
}));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.var arc = room.Prediction.Trajectory(from, velocity, steps: 30); // collision-aware模型
是否可预测声明在 aspect 上。如果某个 aspect 只由权威方按客户端没有的规则修改,它就不能标记为可预测:这属于部署时的校验失败,不会在运行时突然冒出来。
声明哪些内容。
| 声明项 | 含义 |
|---|---|
可预测的 aspect | 客户端可以抢在权威方之前步进哪些 aspect |
偏差阈值 | 低于阈值时平滑校正;高于阈值时直接采用权威方的状态。由声明给出,属于 managed,不是策划日常调节的旋钮 |
远端实体的显示模式 | 在已到达的状态之间插值,或者外推 |
插值延迟 | 其他玩家的显示滞后多少,由声明给出,而不是凭感觉调 |
外推窗口 | 超出后实体被标记为过期,停止外推 |
补偿窗口 | 最多能回退多远,属于 managed |
哪些会回退,哪些特意不回退。
| 含义 | |
|---|---|
回退 | 目标的位置和朝向,以及类型声明为带历史的动态障碍物的几何形状 |
不回退 | 生死状态(死者不会为了挨这一枪而复活),以及归属、分数和背包 |
划分依据 | 判定发生在过去,效果作用于现在 |
| 始终成立 | 含义 |
|---|---|
权威状态注明它见过的输入 | 它带着最后应用的输入序号,所以校正是精确的,而不是近似的 |
偏差 | 可以观察到:客户端知道自己的预测被校正了,而不是悄悄漂移 |
视角时间 | 是声称,不是事实:主体声称自己看到画面的那一刻。超出窗口时平台会拒绝,而不是外推:静默外推等于送给作弊者一份礼物,他只要发一个更早的时间就行 |
回退不保证浮点运算可复现 | 与契约中其他地方的约束相同 |
一个历史环形缓冲区,两个使用方 | 校正和延迟补偿查询都读取实体的瞬时历史轨道。回退由本模块负责,而不是由被回退的模 块负责:环形缓冲区恢复有争议的那个 tick 的位姿,再就这些位姿向碰撞提出普通的重叠查询。碰撞模块不保存历史,也不知道什么是“视角 tick” |
client tick: apply input locally (predict) → buffer it → send, tick-stamped
server tick: step the same movement model → authoritative state → delta out
client recv: authoritative state for tick T → if divergence beyond tolerance:
rewind to T → replay buffered inputs T+1..now → smooth
只有声明为参与预测的字段才会回滚;细微漂移做平滑,真正的偏差则回退并重放。
错误
- 视角时间超出窗口属于冲突:发送当前的时间。
- 历史请求超出窗口同样属于冲突。
- 两种声明缺陷在部署时就能发现:补偿窗口大于历史缓冲区,以及客户端没有规则可用、却标记为可预测的 aspect。两者都不可能进入正在进行的对局。
- 有两种情况不是错误,而且都可以观察到。 输入缓冲区满时,预测会暂停到收到确认为止,不会静默丢弃输入;偏差超过阈值时直接采用权威方的状态,这是声明好的校正方式,不是故障。
限制
具体数值会随平台限制一章给出;每个上限触发后的行为现在就已确定。
- 补偿窗口:超出时拒绝,从不外推。
- 其他玩家的外推窗口:实体被标记为过期,停止外推。
- 未确认输入缓冲区:预测暂停到收到确认为止;输入绝不静默丢弃。
- 回退所需的历史深度:不小于补偿窗口,部署时检查。
- 携带视角时间的操作频率:限流,并附带时间点。
- 每个客户端同时预测的实体数:超出上限的部分不做预测,这是声明好的降级,而不是拒绝。