跳到主要内容
实时游戏

本页尚未翻译,当前显示英文版本。

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 predicted
即将推出 —— Go

Go 中基于特性的声明方式尚未确定(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
Unity C# is the same C# API here — 同样的特性可以在 Unity 中编译,基线为 2021.3,这里没有用到 C# 12 语法
[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)的判定回答的是“开这一枪时,每个人都在哪”:

用一个 hook 校验命中:ResolveAt(shooterViewTick) 把 hitbox 回退到射击者的视角
[After(Projectiles.HitReported)]
public static void Validate(HitReport hit) =>
    hit.ResolveAt(hit.ShooterViewTick);    // rewinds hitboxes, sub-tick interpolated
export 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 interpolated
即将推出 —— Go

Go 中基于特性的声明方式尚未确定(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-aware
const arc = room.prediction.trajectory(from, velocity, { steps: 30 });   // collision-aware
arc = room.prediction.trajectory(origin, velocity, steps=30)   # collision-aware
即将推出 —— Go

Go 中基于特性的声明方式尚未确定(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.
Unity C# is the same C# API here — 在 Unity 中基于生成的类型可直接运行
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。两者都不可能进入正在进行的对局。
  • 有两种情况不是错误,而且都可以观察到。 输入缓冲区满时,预测会暂停到收到确认为止,不会静默丢弃输入;偏差超过阈值时直接采用权威方的状态,这是声明好的校正方式,不是故障。

限制

具体数值会随平台限制一章给出;每个上限触发后的行为现在就已确定。

  • 补偿窗口:超出时拒绝,从不外推。
  • 其他玩家的外推窗口:实体被标记为过期,停止外推。
  • 未确认输入缓冲区:预测暂停到收到确认为止;输入绝不静默丢弃。
  • 回退所需的历史深度:不小于补偿窗口,部署时检查。
  • 携带视角时间的操作频率:限流,并附带时间点。
  • 每个客户端同时预测的实体数:超出上限的部分不做预测,这是声明好的降级,而不是拒绝。