Skip to main content

Replication

Read this properly if you are shipping multiplayer. The short version: it works out of the box, the server decides everything that matters, and there are two settings worth knowing about.

What goes over the wire

Two things, and nothing else.

FEDMountState, on the rider. One replicated struct: mount actor, seat handle, previous seat, phase, entry side, a sequence number, and the server time the transition started. Replicated to everyone rather than only the owner, because a spectator watching somebody else get into a car has to see the animation.

Held together as one struct so it replicates atomically. Split into separate properties, clients observe seated in seat 2 of nothing for a frame, and every OnRep has to defend against the other two not having landed.

FEDSeatArray, on the mountable. A fast array, so occupancy replicates one seat at a time. A plain replicated array resends every element when any of them changes: on a twelve-seat bus that is eleven redundant entries every time somebody sits down, to every client that can see the bus.

Everything else — which montage is playing, how far through it is, where the rider is being interpolated to — is derived locally on each machine from those two. Same inputs, same animation set, same answer.

Seat shape never replicates

Where a seat is, where its doors are, what plays and who is allowed are properties of a component every machine already has, because it is part of the actor. Sending any of it would be sending the actor's own construction back to itself.

The flow

CLIENT                                    SERVER
RequestMountNearest()
evaluate locally ── seat? door? in reach? ──┐
(no) → OnMountFailed, done │
(yes) │
predict: set state, start animation │
ServerRequestMount(request) ──────────────►│
│ evaluate again, with distance tolerance
│ ReserveSeat ← the race is decided here
│ publish FEDMountState (sequence + 1)
│ start the same transition locally
◄───────────── replicated state ───────────┤
sequence matches → prediction confirmed │
sequence differs → adopt the server's │

◄────────── ClientRejectRequest(reason) ───┤ (only when it refused)
roll back to the stored pre-request state │

The sequence number

A uint8 bumped on every committed transition. Without it a client cannot tell the server confirmed what I predicted from the server started a different transition that happens to end in the same phase — and the difference decides whether the local montage keeps playing or restarts.

Why rejection needs its own RPC

When the server refuses, its copy of FEDMountState never changed, so it has nothing new to send. And property replication compares against what it last sent, not against what the client did on its own — so it will never correct the client. ClientRejectRequest closes that hole; the client restores the copy of its pre-request state that it kept.

If even that RPC is lost, Prediction Timeout rolls the client back anyway. A player is never left stuck in a phase.

Prediction

Project Settings → Predict Transitions, on by default.

On, the animation starts on the frame the key is pressed and the server's answer arrives during it. A rejection reads to the player as the door not opening. Off, every mount costs a round trip — at 120 ms that is noticeable on a control players press constantly.

Prediction never touches authority. A predicting client sets its local phase and starts its local animation; the seat is only ever assigned by the server. Two players pressing the key on the same frame both predict, both play a get-in, and exactly one keeps it.

ED.Mounting.DisablePrediction 1 forces it off at runtime. If the problem survives with prediction off, it was never about prediction.

Distance tolerance

Server Distance Tolerance, default 0.35.

A client that was in range when it pressed the key can be out of range by the time the RPC lands, because both it and the vehicle moved. Validating on the server with the exact same number is how a system ends up rejecting perfectly honest requests on high-ping connections. The server allows 35% further — enough for a moving vehicle at a few hundred milliseconds, and still a refusal for the player boarding from across the street.

Late joiners

FEDMountState carries ServerStartTime, taken from the game state rather than from local world time. A client joining mid-transition starts the montage at now − ServerStartTime, so it sees the rest of the animation rather than all of it, a second late. A client joining after it finished simply sees Seated and attaches with no animation, which is correct.

Possession

Possession changes happen on the server only. Under PossessMount the controller possesses the mount at the attach moment and re-possesses the rider on detach; the rider component remembers which controller it was, so a mount destroyed mid-ride cannot strand it.

Under KeepRider — the default — no possession happens at all. That removes a whole class of bugs where a controller possesses a vehicle that is then destroyed, and it means nothing else in your game has to learn that "the pawn" is sometimes a car.

Driving input under KeepRider

The mount is not possessed, so nothing in the engine carries a client's input to the server. The rider component does: ApplyDriveInput applies the input to the local copy of the mount — so the wheel turns and the lights come on before any round trip — and, on a client, also sends ServerApplyDriveInput. It is unreliable on purpose: it is sent every frame a key is down and the next one supersedes it, so a lost packet costs a frame of steering rather than a stuck throttle. The server re-checks that the sender is seated, is the driver and is under KeepRider before it applies anything.

That means ApplyDriverInput runs on two machines for a client driver, and a mount has to know which it is on. The rule: simulate where the mount is simulated, show everywhere else. AEDMountableVehicle does this for you. The server simulates. So does the driver's own client, ahead of the server and corrected by replicated movement — the same arrangement a possessed vehicle has, and the only one that feels like driving rather than like watching a replicated car slide round a corner. Every other client is a spectator: its copy is replicated movement, and feeding its Chaos would have a local simulation fight the correction.

To make the driver's client a predicting client, the vehicle gives its ownership to the driver's controller for as long as they drive (SetOwner in OnRiderSeated, cleared in OnRiderLeft). Under KeepRider the car is never possessed, so without an owner it has no owning connection and the Chaos movement component's own input RPCs from that client are dropped. IsDrivenLocally() and SimulatesInputHere() are the two questions a vehicle subclass asks before touching physics.

One consequence of that ownership: Chaos's own input path comes alive. The owning client's movement component sends ServerUpdateState every tick, and on the server that overwrites the raw inputs with whatever the client's component last processed — which would silently zero a brake the rider applied through ApplyDriverInput on a machine where nobody holds the key locally. AEDMountableVehicle therefore remembers the last ApplyDriverInput and pushes it back into the movement component every tick where it simulates, after the network has been received and before the component ticks. The framework's input path is the one that counts; Chaos's is left alone.

Seat events on clients

OnRiderSeated and OnRiderLeft fire on clients as well, from the seat array's replication, with the same arguments the server got. A car that starts its engine when a driver sits down starts it on the passengers' machines too. Implementations that touch authority-only state should check for it.

Iris

The seat array declares Iris replication fragments and the module links IrisCore, so the framework replicates the same way under the legacy network driver and under Iris. There is nothing to turn on.

What is deliberately not replicated

  • Montage playback. Every machine plays its own from the same state.
  • Doors. Driven by anim notifies inside a montage every machine is playing.
  • Seat shape. Part of the actor.
  • Failure reasons. They exist where the request was made and nowhere else.