A system that keeps mouse feeling fast on a client server setup
The system to be implemented is a client prediction and server reconciliation architecture, it consists of:
- A client
- A server
It will consist of a system which processes mouse inputs, which are received using glfw which gives us as input the (x, y) coordinates of the mouse, note that regularly these values would be confined to the rectangle (0, 0) - (1080, 1920) or whatever resolution the monitor is, but when the mouse is "grabbed" ie the mouse cursor is not visible on the screen, the values may technically become negative and have no restriction on their values.
A client also consists of a monitor and a mouse, both of which have their own poll rates, for our purposes, suppose the monitor is 144-256hz monitor and that we want to be able to provide frames up to and exceeding the the monitors resolution, note that if we only processed mouse inputs at a rate of 60hz then when looking around, the game would not be feeling smooth in the sense that the view angle of the player would only change at a rate of 60hz, thus it's important to be able to process mouse inputs at a rate which is equal or exceeding the monitor refresh rate.
Also there is a rate at which the client will send the keyboard and mouse updates to the server.
At the same time, we have a server that simulates the game world at its own rate, suppose that it updates the physics world at 60hz, the physics world is what simulates explosions, interactions between players and movement changes, the output from the server is the position of the player, what their view angles are (yaw pitch) etc...
Note that there is a mouse submodule which includes a function to to process these mouse (x, y) coordinates and convert them to yaw pitch values.
When talking about this entire client server system to speed things up we have these abbreviations to help speed up communication:
network_space
: updates travel between client and server, there are four places information can be at any moment it can either be on the client, it can be travelling from the client to the server, it can be on the server, or it can be travelling from the server to client, which we denote by|C|
,|C|->|S|
,|S|
,|S|->|C|
tick
: a tick generally refers to the action of a system performing the logic contained in one looptick_rate
: the rate at which a specific system runs at, note that we prefer to be specific egclient_mouse_freq
instead of just saying thetick_rate
of the mouse.km_update
: mouse x and y position along with keys currently pressed on the keyboard and mouseclient_monitor_freq
: the refresh rate mesured in Hz of the clients monitorclient_mouse_freq
: the poll rate of the mouseclient_km_update_avg_bundle_size
: as mentioned before multiple km_updates may be generated between two send events, these events get bundled together and sent in the next tick, this value represents the average number of km_updates which are being sent out per km_update send event.client_keyboard_freq
: the poll rate of the keyboardclient_km_update_send_freq
: the rate at which the client sends km_updates to the serverserver_simulation_freq
: the rate at which the servers simulation runsserver_game_update_send_freq
: the ratw at which the server sends game updates to the clientscurrent_game_state
on tickn
: the game state on the server on tickn
where
Authorative Server (AS)
: the server is authorative, if we don't use the game updates to update the clients view, and only do client side predictions, the view can get entirely off the rails, and where you're currently looking could be 90deg away from your actual look direction making it impossible to aim. Thus we need to make sure that the client is sychronized to the reality of the server.min_subsystem_rate_rule (msrr)
: if you have subsystems each of which create information, and that information is passed into the next subsystem and if each subsystem processes data at a certain rate, then the rate at which information can be processed through the entire system is the minimum processing rate over all subsystems, if no modifications are made to the system.- it is a guarentee that
client_mouse_freq > client_km_update_send_freq
, this means that there will be multiple mouse updates between km_updates being sent out, therefore we need a way to deal with this, here's what valve does: (we should probably do the same because their games feel good)
The client creates user commands from sampling input devices with the same tick rate that the server is running with. A user command is basically a snapshot of the current keyboard and mouse state. But instead of sending a new packet to the server for each user command, the client sends command packets at a certain rate of packets per second (usually 30). This means two or more user commands are transmitted within the same packet. Clients can increase the command rate with cl_cmdrate. This will increase responsiveness but requires more outgoing bandwidth, too.
- it is a guarentee that
client_monitor_freq > server_game_update_send_freq
therefore bymsrr
if we don't make any modifictions, our monitor would only see changes at a rate less than or equal toserver_game_update_send_freq
, which is bad, because we are no longer using all possible frames available to us on the monitor
The solution to solve msrr
in the client server model as specified above is to render mouse inputs at a rate execeeding the monitor refresh rate, on the client side, with the following extremely important modifiction
- for each km_update it is enumerated, which we can call it's
id
when a bundle of km_updates arrive on the server, they are processed in order and the when the server sends a game update out, it also sends out the lastid
that has been processed by the server - when the game update is received by the client, it immediately replaces the current game state, whatever it may be with what the server told us it is, but while things were flying over the network, the client has processed more km_updates in the mean time, and thus it's moved forward with things, perhaps changing the look direction, so if you just slam the new update onto the client, their view "rubberbands" back to where it was before causing a jerky effect, so we need reconcile this, the way that reconciliation is done is by collecting all km_updates that occurred after the one last processed on the server, and re-apply them all in the same frame that the game update is received to "get us back to" where we were looking, most of the time this look direction is the same as what we predicted, but sometimes it might not be, meaning that the client and server simulation have diverged a bit, and it has been corrected for, which is good and hopefully if there is a delta it is not large.
- Also at this point it becomes important to think about the
game_state
for eachkm_update
and agame_state
we say that it is applied if thekm_update
has already been used to update thegame_state
, otherwise it is not yet applied
predicted_km_update (pkm_update)
: akm_update
on the client which is applied on the current client game state, and possibly applied to the server game state but importantly, we have not run the reconciliation process on it yet, therefore thiskm_packet
is in any of the|C|
,|C|->|S|
, but not yet|C|->|S|->|C|
reconciled_km_update (rkm_update)
: akm_update
which has been applied on the server, and then reconciled against on the server, ie thiskm_update
has completed the travel path|C|->|S|->|C|
last_predicted_client_game_state
: the game state right before the reconciliation algorithm runsreconciled_client_game_state
: the game state after the reconcilation algorithm runsgame_state_delta
: Given two game states, this is a function which returns a real number representing how much the two game states differ by, one way of doing this by measuring the distance between all of the same objects, and their look directions.prediction_delta
:game_state_delta(last_predicted_client_game_state, reconciled_client_game_state)
pkm_update_avg_bundle_size
: Every time we receive a game update, we have to re-applypkm_updates
over time we can measure on average how manypkm_updates
are getting repplied evertime a game update comes in from the server, this metric is important because the larger this value is the higher (potientially) theprediction_delta
when a game update (gu) is received on the client:
set_state(gu)
for each pkm_update such that pkm_update.id > gu.id:
update_state(pkm_update)
note the above looks simple but making sure everything is operating correctly is hard.