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:
Impulseknockbacks,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
| Actor | On this page |
|---|---|
schema-author | declares movement models, constraints and bindings |
room-owner | applies impulse, teleport and modifiers from the host |
player | submits 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
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)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[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:
Motion.Drive sent at input rate, stepped server-sideroom.My<Tank>().Motion.Drive(throttle: 1f, steer: -0.4f); // cl — sent at input rateroom.my<Tank>().motion.drive({ throttle: 1, steer: -0.4 }); // cl — sent at input rateroom.my(Tank).motion.drive(throttle=1.0, steer=-0.4) # cl — a bot brain drives the same wayAttribute-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.room.My<Tank>().Motion.Drive(throttle: 1f, steer: -0.4f); // cl — sent at input rateServer-side verbs:
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)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.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.
| Declares | What it is |
|---|---|
movement model | one 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 |
parameters | declared 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 |
limits | maximum 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 input | stop, 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 tolerance | how 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 cap | how often the step runs, and how many steps may be taken in one go when the server is behind |
impulse kinds | each with its magnitude and its manner of decay |
What is true of every step.
| Always | What it is |
|---|---|
the module owns | an 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 |
authority | is the server's: under the our simulation mode a client sends an intent, never a result |
collisions | are not resolved here: it asks collision whether a position is admissible and what the response is, and keeps no response table of its own |
input | is an intent: "forward", "right", "turn the turret there" — accepted as it comes, because it asserts nothing about the world |
a claimed pose | is 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 sequencing | is required: the same sequence number is never applied twice, and a lower one is discarded |
a limit | clamps, 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 constraints | recoil, 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 bits | no bit-identical result across platforms is promised. What is promised is the same rules, and reproducibility within one authority |
the step | is 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.