-
Notifications
You must be signed in to change notification settings - Fork 77
Networking model
This is the reference the implementation should follow.
The game runs in deterministic lockstep mode, which means that given the same initial state and the same inputs the game will turn out the same. This gives the replay function for free: We can simply record the initial state and all inputs and replay them any time we want to yield the same game.
We have 3 different types of frames in the game:
-
GameFrames (GF): This updates the game by 1 step. The game objects register events to be executed x GFs later and if the given GF is reached, the object gets notified. This allows us to skip most of the object notifications as the actual changes are rather infrequent. Examples are: Place me 1 Node further in 20 GFs, enter next production state in 500 GFs etc. The alternative would be an update call on every object which can then decide what to do. In the above example that would mean 19 ignored updates before the last one actually changes something.
-
NetWorkFrames (NWF): aka command frame. After a given amount of GFs a NWF is executed before the next GF. This means the execution of the aggregated commands from all players in-order. These might be evenly spaced (e.g. after 20 GFs) or not to allow adjustments based on the players ping and PC performance. The important part is: We must have the commands from all players for a NWF before we can execute this. This means we might need to wait, if any are missing and there are also empty commands as placeholders when the player did not do anything. In a replay we can omit the empty commands and simply tag the commands by the GF they have to be executed to save space.
-
Rendering frames (RF or generic frame): Here the game is drawn to the screen. It is completely unrelated to the other 2 because we might have none, 1 or multiple RFs between 2 GFs. When a RF is drawn, the game state is interpolated between the current and the next GF. This is done by keeping track when the last GF started and determine how much time has passed since.
Example: Normal speed -> 50ms/GF, 250ms ping -> 5 GF/NWF, 60 FPS -> 16.6ms We execute 1 GF, render 3 times, execute the next GF, render 3 times, next GF, ... after 5 GF we execute 1 NWF before executing the next GF
The following is loosely based on 1500 Archers on a 28.8: Network Programming in Age of Empires and Beyond where the ideas are explained well.
For the game the rendering is irrelevant, so we focus on the other 2 here.
For every NWF we need to have the commands from all players for that NWF. So although every command might be sent immediately we need a 'I'm done sending my commands for that NWF'
message. So for simplicity we batch up all commands on the client and send them together which is implicitly the 'done' message. It follows that a good time to send the commands is when executing the NWF which (best case) is evenly spaced.
If these were the commands to be executed on that NWF we would have to wait for a reply of all other players which could take some time and result in a lag. So we send the commands to be executed in a future NWF and execute the ones sent from a past NWF.
We use the following: When NWF n is reached and all commands are received, send out commands to be executed in NWF n+m, execute current commands and continue executing GFs till NWF n+m is reached.
The server has a controlling role. It relays commands received to all clients and if a client did not send its commands for a given time, it will be kicked. It will also send the new GF length, and the GF of the next NWF the same way as players send their 'done'-msg.
Putting this together:
- Each client has a queue of future NWFs with: new GF length, next NWF, commands (and checksum) per player
- There is always at least 1 entry in that queue (the next NWF) which might be empty (no commands, no next NWF)
- We always know the GF for the next NWF
- Clients cannot execute a NWF if it did not receive all commands AND the server message with GF length and next NWF
- The server will send that when it executes a NWF
- Commands sent in NWF n are execute in NWF n + m, where m is the command delay which is fixed for the whole game
- There can never be more than 2*m pending commands from any player
- For the start the server must send the current GF which is the first NWF
- Each client must reply with an empty command for the start NWF and the server must send
m - 1
empty commands for all players for the past NWFs that never got executed along its own "commands" (length msgs)
Note on the maximum pending commands:
The maximum amount is right before executing our NWF n. We have not yet send the commands for NWF n + m, so no one can execute NWF n + m. Assume client A is at NWF n and client B is at NWF n+m (both haven't executed that yet). Client A has the following commands from client B: n, n+1, ..., n+m, n+m+1 (sent at n+1), .. n+m+m-1 (sent at n+m-1). This equals m*2 commands.