跳到主要内容
实时游戏

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

Locomotion

你声明物体如何移动,不需要任何人手写积分器。 移动模型把带序号的输入变成权威的运动:它与碰撞联动,为预测记录历史,并受增益、减益和地形影响。

何时使用

  • 实体(entity)由玩家输入驱动移动(坦克、角色、载具),并且运动必须以服务器为权威(authoritative)。
  • 你宁愿声明速度、加速度和转向速率的上限,也不想自己写积分器。
  • 玩法会推动物体:Impulse 击退、Teleport,以及像泥地那样带持续时间的修正效果。
  • 移动必须手感即时:同一个声明的模型既在服务器上步进,也在预测循环中步进。
  • 无需使用:如果位置只按离散的步骤变化。实体上的一个同步字段就够了。

分工

主体在本页中的职责
schema-author声明移动模型、约束和绑定
room-owner在主机上施加冲量、传送和修正效果
player提交带序号的输入;读取运动状态

适用于哪些房间

本模块只在由平台推进模拟的地方运行,即声明了 Host = "Backend" 的房间(room)。如果模拟归你自己的游戏服务器所有(PlayServ 作为元服务器),移动、碰撞和预测都留在引擎一侧,本页描述的是平台托管这一备选方案,而不是必须遵守的要求。

概览

声明 Tank 的移动模型:Locomotion.Tank,带速度、加速度和转向速率上限
[Entity("tank")]
public class Tank
{
    [Sync] public Vector3 Position;
    [Motion(Model.Tank, MaxSpeed = 8f, Acceleration = 14f, TurnRateDeg = 120f)]
    public Motion Motion;
}
@Entity('tank')
export class Tank {
  @Sync() position!: Vector3;
  @Motion({ model: 'tank', maxSpeed: 8, acceleration: 14, turnRateDeg: 120 }) motion: Motion;
}
@entity("tank")
class Tank:
    position: Vector3 = sync()
    motion = locomotion.motion(model="tank", max_speed=8.0, acceleration=14.0, turn_rate_deg=120.0)
即将推出 —— Go

Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。

UCLASS(PSEntity = "tank")
class UTank : public UObject
{
    GENERATED_BODY()

    UPROPERTY(PSSync) FVector3f Position;

    UPROPERTY(PSMotion = (Model = "Tank", MaxSpeed = "8.0", Acceleration = "14.0", TurnRateDeg = 120))
    FPSMotion Motion;
};
// 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")]
public class Tank
{
    [Sync] public Vector3 Position;
    [Motion(Model.Tank, MaxSpeed = 8f, Acceleration = 14f, TurnRateDeg = 120f)]
    public Motion Motion;
}

客户端输入是带序号的意图,运动由平台来步进:

客户端输入即带序号的意图:Motion.Drive 按输入频率发送,在服务器端步进
room.My<Tank>().Motion.Drive(throttle: 1f, steer: -0.4f);   // cl — sent at input rate
room.my<Tank>().motion.drive({ throttle: 1, steer: -0.4 });   // cl — sent at input rate
room.my(Tank).motion.drive(throttle=1.0, steer=-0.4)   # cl — a bot brain drives the same way
即将推出 —— Go

Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。

Room->Entities->Of<UTank>()->Select().GetMine().Then(
    TPSOnResult<UTank*>::CreateWeakLambda(this, [this](const TPSResult<UTank*>& Result)
    {
        if (!Result.HasValue()) { return; }
        // client — sent at input rate, numbered so the platform can acknowledge
        Result.Value()->Motion->SubmitInput(FPSMoveInput{ .Throttle = 1.f, .Steer = -0.4f }, InputSequence);
    }));
// 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 中基于生成的类型可直接运行
room.My<Tank>().Motion.Drive(throttle: 1f, steer: -0.4f);   // cl — sent at input rate

服务器端操作:

服务器端操作:击退冲量、持续 3 秒的泥地修正效果、一次干净的传送
tank.Motion.Impulse(knockback);
tank.Motion.Modify("mud", speedMultiplier: 0.6f, duration: 3.Seconds());
tank.Motion.Teleport(spawn);
tank.motion.impulse(knockback);
tank.motion.modify('mud', { speedMultiplier: 0.6, duration: seconds(3) });
tank.motion.teleport(spawn);
tank.motion.impulse(knockback)
tank.motion.modify("mud", speed_multiplier=0.6, duration=seconds(3))
tank.motion.teleport(spawn)
即将推出 —— Go

Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。

// on a dedicated server / master-client host
Tank->Motion->Impulse(EPSImpulseKind::Impulse, KnockbackVelocity);
Tank->Motion->Modify({ .Modifier = TEXT("mud"), .SpeedMultiplier = 0.6f, .For = FPSDuration::Seconds(3.f) });
Tank->Motion->Teleport(SpawnPosition, SpawnFacing);
// 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 — 主客户端 build 拥有同样的主机操作;普通客户端看到的结果是经过预测和校正的运动
tank.Motion.Impulse(knockback);
tank.Motion.Modify("mud", speedMultiplier: 0.6f, duration: 3.Seconds());
tank.Motion.Teleport(spawn);

模型

实体要移动,需要声明什么。

声明项含义
移动模型从内置集合中选一个:转向、坦克、角色、载具、飞行。它们是同一个步进的几种实现,选择条件和默认值都由声明给出。步进是一个纯函数 (pose, input, dt) → pose
参数客户端可读的声明值;没有它们,预测(prediction)会系统性地偏离。参数属于 seed:策划会调整它们,部署不能悄悄丢掉这些修改;而反作弊所依赖的上限可以设为 managed,这样从管理控制台修改会被拒绝
上限最大速度、加速度和制动,每次输入的最大转向,倒车倍率,以及部件单独的旋转速度。每次输入的转向与旋转速度特意分开声明:前者限制瞬时跳变,后者限制持续速率,是两道不同的防线
输入过期时的行为stop、continue until a declared deadline 或 continue indefinitely。没有“照旧继续”这种默认值,否则断网的玩家会一直开下去
位姿容差客户端声称的位姿与服务器的位姿最多能差多远,并且可以按状态区分:静止、移动和刚重生后是三种不同的容差
步进频率与追帧上限步进多久运行一次,以及服务器落后时一次最多能补多少步
冲量类型每种都有各自的大小和衰减方式

每次步进都成立的规则。

始终成立含义
本模块负责实体随时间变化的位置和朝向,别的都不管。位置历史由实体保存,不在这里,所以“300 ms 前玩家在哪”只有一个答案,而不是两个周期不同的缓冲区
权威属于服务器:在 平台模拟 模式下,客户端发送的是意图,从不发送结果
碰撞不在这里处理:本模块向碰撞询问某个位置是否可达、响应是什么,自己不维护响应表
输入是意图:“前进”“右转”“把炮塔转向那里”。输入来了就接受,因为它对世界不做任何断言
声称的位姿只是声称,从来不是事实。超出声明的容差时,它会被钳制到最近的可达位姿,并产生一个可观察的 pose_clamped
输入序号必须有:同一个序号绝不会应用两次,更小的序号直接丢弃
上限只钳制,不拒绝:“这个 tick 前进十米”会变成可达范围内的值,而不是一个错误。这样服务器不会产生非法位姿,客户端也不会每帧都被拒绝消息淹没
冲量遵守同样的约束后坐力、推搡、爆炸和击退都来自输入之外,但没有一个能绕过碰撞:后坐力不会把坦克推进岩石里
规则一致,而非逐位一致不保证跨平台结果逐位一致。保证的是规则相同,以及在同一权威方内可复现
步进是纯函数,由 tick 驱动:服务器和客户端预测循环用同一段代码步进运动,这正是校正能够精确的原因

错误

  • 实体缺少移动模型、预设只填了一部分、冲量没有声明衰减方式,都属于部署时的校验失败,不会拖到运行时。
  • 预期的代不匹配属于前置条件失败,重新读取后值得重试:重生前发送的输入不能在重生后应用。
  • 超出输入频率时返回限流类错误,并附带时间点。
  • 实体不可控制属于冲突,只有状态变化后重试才有意义。
  • 有三种情况都不算拒绝,而且都可以观察到。 过期输入会丢弃,超出上限的意图会钳制,超出容差的位姿会钳制并记为 pose_clamped。如果其中任何一种静默发生,客户端会以为自己的输入已经生效,从此与服务器越偏越远。

限制

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

  • 最大速度和加速度:钳制,从不拒绝。
  • 每次输入的最大转向:钳制。
  • 每个主体的输入频率:限流,并附带时间点。
  • 追帧步数:超出上限的步会丢弃,后果由声明给出。模拟时间会落后,而且可以观察到,而不是一下子追上来,看起来像所有人同时瞬移。
  • 冲量大小:钳制到声明的最大值。
  • 每个实体同时存在的冲量数:新的冲量挤掉最旧的一个,并且可以观察到;不存在静默的无上限叠加。
  • 声称位姿的有效期:超过声明时长的位姿不予考虑。