The server ticks along at the fixed rate, never rewinding or rolling back. Player inputs must arrive in time for the frame they are associated with to be simulated.
The server runs the game simulation, applying forces to player objects in response to inputs, then runs the physics update sets, then broadcasts the new position of objects.
PreUpdate | Replicon reads from network, writes to Events<IncomingMessage> |
||||||||||
FixedUpdate |
|
||||||||||
FixedUpdate |
|
||||||||||
PostUpdate | Replicon broadcasts entity/component changes (the post-physics values for this frame) | ||||||||||
PostUpdate | Rendering (if server is built with "gui" feature) |
The client tries to be a few frames ahead of the server's simulation, such that inputs for frame F arrive by frame F-1 on the server.
This means inputs from the server arriving at the client are, from the client's POV, in the past.
PreUpdate |
Replicon reads from network, deserializes and applies the replication data to the client.
This can include spawning new entities and updating components.
For timewarp-specific components, the new updates are written to the ServerSnapshot<Component> at
the RepliconTick . Timewarp applies the updates to the components at the correct frame during rollback, as required.
New custom events are written to the bevy event queue, to be consumed by my game's systems. |
|||||||||||||||||||||
FixedUpdate |
|
|||||||||||||||||||||
FixedUpdate |
|
|||||||||||||||||||||
FixedUpdate |
|
|||||||||||||||||||||
FixedUpdate |
|
|||||||||||||||||||||
PostUpdate | Messages "sent" in OutgoingMessages are sent now by Replicon. | |||||||||||||||||||||
PostUpdate | Rendering |
Systems that initiate rollbacks write a RollbackRequest
to an event queue, specifying the frame they
wish to start resimulating from. These are in the NotInRollback
set.
All rollback requests are consolidated, and a Rollback
resource is written. The RollbackConsolidationStrategy
from TimewarpConfig
determines if the oldest or newest frame from the list of requests is used.
If you only receive entire-world updates at a time, taking the newest frame requested is optimal.
This is how replicon currently works, and is the default.
If we need to resimulate from frame N
onwards, before we start simulating that frame, we load in
stored component values from frame N - 1
.
We also unwrap any blueprints (ABAF) for frame N
.
The server sends replicon data containing component values in PostUpdate, after physics.
So when the client receives a packet saying that a component value is X at frame N
, that means
the value was X on the server, after frame N
was simulated.
So if the client receives this, they can resimulate from frame N+1
, and set the component to X
before starting - representing the correct state at the end of frame N
.
Say the server spawns a new player during frame 100. It inserts a PlayerShip blueprint, and then the blueprint assembly fn for players adds a Position, Collider, etc. At the end of the frame in postupdate, replicon sends this data out to clients.
That player entity might have been given a position of X,Y during server's frame 100, but during physics that position might have changed to X',Y' before replication data was sent.
On clients, when we get a player blueprint for frame 100 we'll be rolling back to 101, and inserting the replicated Pos value@100 before we start simulating 101. But we'll only end up adding the (non-replicated) collider during bp assembly in frame 101. Although not optimal, this is correct – the replicated components are correct for that frame.
Anything the player's collider interacted with during physics on frame 100 on the server may end up requiring a correction on the client (since client couldn't have simualted that). but that's fine.
if we embed the pos/whatever into the BP we could actually assemble on the correct frame?
REALLY, the actual bundles on the server are what we need to replicate, like we do for BPs at the mo. so the current blueprint comps need all the stuff that's in the bundle, then the assembly fn creates the comps. then we can rollback and insert blueprints on the same frame the server did - even though replicon will have also received the post-physics replicated data for them at the end of that frame. our bp assembly fn should overwrite it on the client.
HOWEVER then we get stale data left over in the blueprint (like the pos from when it was spawned) and we can't really filter or control how replicon sends these bp compoents to clients. we don't want to continually update the BPs copying in fresh data, just so new players can spawn stuff from existing blueprints. kinda want a way to dynamically create a blueprint-bundle just for replicating to new clients.
that's annoying.
that's why we spawn blueprints the next frame on the client.
This isn't a concern for bullets that get predicted and spammed, so it's ok 🥺
| WARNING unedited ravings below this line that haven't yet necessarily coalesced into useful code
As a client it's possible to get a player's input for a future frame before getting the component data, ie before you simulates that frame.
A low lag player with a 3 frame input delay will send inputs for frame 103 to the server while the server is doing frame 101, and the server will rebroadcast, so you might get them a frame before needed on a remote client.
so perhaps we should be locally simulating the bullet spawn for remote players too somehow?
when the server gets a fire command, before broadcasting it to others, it should immediately spawn an entity, even if input is for a future frame. associate the spawned entity with the input command on the server, and replicate the entity with a FutureBullet(f=100, client_id=X) component.
it's possible the client coule receive this right before they even simulate f100. in which case they can assemble on the correct frame, set the pos using the same local prediction logic as firing yourself (because we wouldn't have comps from the server's assembly of the bp yet), and wait on normal server updates to arrive to correct it.
when the server does the bp assembly, it will have the prespawned entity associated with the input, and it just assembles into that entity.
Maybe all firing should work this way, rather than apply_inputs doing it? apply inputs makes sense for thrusting and rotating etc, but when you need a new entity it's different.
FireIntent(client_id, frame, bullet_blueprint) component ? could do the same on the client, since the client also prespawns entities to send them to the server, for matching up. can add the Prediction comp to that too to clean up misfires.
intent to fire is locked in at the time of sampling inputs for a future frame (input delay..), and immediately sent to the server.
local client A, simulating f100 with input delay of 3. so it's sampling inputs for 103 at f100. presses fire. spawns an entity with Predicted component with entity id of af_100_103 sends fire input to server, with associated entity of af_100_103.
server receives the input in time for frame 101. server spawns an entity sf_101_103_a with Replicated and FireIntent(bullet_bp, f=103, client_id=A). (this entity doesnt have physics components, or even a transform yet. invisible.) server adds mapping for client A between af_100_103 <--> sf_101_103_a, which is sent back to A next packet. server replicates entity sf_101_103_a to all players.
client B, about to simulate frame 103, (so in prefix clock still 102) receives the sf_101_103_a entity. server has not yet delivered replication data for 103 to this client, so no pos@100 for the new bullet. notices the FireIntent with a f=103, and unwraps it into a normal Bullet blueprint component. client B simulates f 103, assembles bullet using normal prediction logic, into the fireintent entity. will receive updates to it normally since it's already a server entity.
client C, on a very low lag connection, is about to simulate frame 102, (so in prefix clock still 101) receives the sf_101_103_a entity. the entity gets created on the client with the fireintent, but not unwrapped, because fireintent.frame=103, and next client frame will be 102. ... NEXT frame, the client unwraps it at frame=103. all good, as per the B client.
client D, on a high lag connection, receives the sf_101_103_a entity as it's about to simulate f 108. issues RollbackRequest(104) in prefix, while clock was wound back to 103, the fireintent bp is