Skip to main content
The live game

Not translated yet — showing English.

Locomotion

You declare how a thing moves; nobody writes an integrator. A movement model turns sequenced input into authoritative motion, integrated with collision, recorded for prediction, and modified by buffs, debuffs and terrain.

When to use it

  • Entities move under player input (tanks, characters, vehicles), and motion must be server-authoritative.
  • You would rather declare speed, acceleration and turn-rate limits than write an integrator.
  • Gameplay pushes bodies around: Impulse knockbacks, Teleport, and mud-style modifiers with durations.
  • Movement must feel instant: the same declared model steps on the server and in prediction's loop.
  • Skip it when positions change only in discrete steps. A synced field on the entity already covers it.

Who does what

ActorOn this page
schema-authordeclares movement models, constraints and bindings
room-ownerapplies impulse, teleport and modifiers from the host
playersubmits sequenced input; reads motion state

Which rooms this applies to

This module runs where the platform steps the simulation: rooms declared Host = "Backend". If your own game server owns the simulation (PlayServ as the metaserver), movement, collision and prediction stay engine-side, and this page describes the platform-hosted alternative rather than a requirement.

At a glance

Declaring the Tank movement model: Locomotion.Tank with speed, acceleration and turn-rate limits
[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)
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

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 — the same attributes compile in Unity, on the 2021.3 baseline — no C# 12 syntax here
[Entity("tank")]
public class Tank
{
    [Sync] public Vector3 Position;
    [Motion(Model.Tank, MaxSpeed = 8f, Acceleration = 14f, TurnRateDeg = 120f)]
    public Motion Motion;
}

Client input is a sequenced intent. The platform steps the motion:

Client input as sequenced intent: Motion.Drive sent at input rate, stepped server-side
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
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

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 — runs as-is in Unity against the generated types
room.My<Tank>().Motion.Drive(throttle: 1f, steer: -0.4f);   // cl — sent at input rate

Server-side verbs:

Server verbs: a knockback impulse, a 3-second mud modifier, a clean teleport
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)
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

// 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 — a master-client build holds the same host verbs; a plain client sees their results as predicted, reconciled motion
tank.Motion.Impulse(knockback);
tank.Motion.Modify("mud", speedMultiplier: 0.6f, duration: 3.Seconds());
tank.Motion.Teleport(spawn);

The model

What an entity declares in order to move.

DeclaresWhat it is
movement modelone of the shipped set — steering, tank, character, vehicle, flying — as several implementations of one step, with declared conditions of choice and a default. The step is a pure (pose, input, dt) → pose function
parametersdeclared values the client can read; without them prediction diverges systematically. They are a seed — a designer tunes them and a deploy must not silently lose the edits — while the limits anti-cheat rests on may be managed, and then an edit from the admin console is refused
limitsmaximum speed, acceleration and braking, maximum turn per input, the reverse multiplier, and a separate rotation speed for parts. The turn per input is declared apart from the rotation speed on purpose: one bounds an instantaneous jump, the other a continuous rate, and they are different defences
behaviour on stale inputstop, continue until a declared deadline, or continue indefinitely. There is no "carry on as before" default — a player whose network dropped would drive on
pose tolerancehow far a claimed pose may sit from the server's, and it may differ by state: standing, moving, and just after a respawn are three different tolerances
step rate and catch-up caphow often the step runs, and how many steps may be taken in one go when the server is behind
impulse kindseach with its magnitude and its manner of decay

What is true of every step.

AlwaysWhat it is
the module ownsan entity's position and orientation over time, and nothing else. The history of those positions is kept by entity rather than here, so "where was the player 300 ms ago" has exactly one answer instead of two buffers with different periods
authorityis the server's: under the our simulation mode a client sends an intent, never a result
collisionsare not resolved here: it asks collision whether a position is admissible and what the response is, and keeps no response table of its own
inputis an intent: "forward", "right", "turn the turret there" — accepted as it comes, because it asserts nothing about the world
a claimed poseis a claim: never a fact. Outside the declared tolerance it is clamped to the nearest admissible pose, and that produces an observable pose_clamped
input sequencingis required: the same sequence number is never applied twice, and a lower one is discarded
a limitclamps, it does not refuse: "ten metres forward this tick" becomes what is admissible rather than an error. So the server can't produce an illegal pose, and the client isn't flooded with refusals every frame
an impulse obeys the same constraintsrecoil, a shove, an explosion and knockback arrive from outside input, and none of them bypasses collision: recoil does not drive a tank into a rock
identical rules, not identical bitsno bit-identical result across platforms is promised. What is promised is the same rules, and reproducibility within one authority
the stepis pure and tick-driven: the same code steps motion on the server and inside the client's prediction loop, which is what makes reconciliation exact

Errors

  • A missing movement model on the entity, a partially filled preset, and an impulse with no declared decay are all validation failures at deploy, not at runtime.
  • The expected generation did not match is a precondition failure, worth repeating after re-reading: input sent before a respawn must not be applied after it.
  • The input rate exceeded answers in the rate-limit category with a deadline.
  • The entity is not controllable is a conflict, and repeating it makes sense only after the state changes.
  • Three things are refusals in neither direction, and all three are observable. Stale input is discarded, an intent beyond a limit is clamped, and a pose beyond the tolerance is clamped as pose_clamped. Doing any of them silently would leave the client believing it applied and diverging from the server for good.

Limits

The numbers arrive with the platform limits chapter; what happens at each ceiling is fixed now.

  • Maximum speed and acceleration: clamped, never refused.
  • Maximum turn per input: clamped.
  • Input rate per actor: a rate limit with a deadline.
  • Catch-up steps: beyond the cap, steps are discarded with a declared consequence. Simulation time falls behind, observably, instead of catching up in a jump that looks like everyone teleporting at once.
  • Impulse magnitude: clamped to the declared maximum.
  • Simultaneous impulses per entity: a new one evicts the oldest, and the eviction is observable; there is no silent unbounded summation.
  • The lifetime of a claimed pose: one older than the declared period is not considered.