In my previous news post "
Client features
revisited", I talked about several important client features and how
they have recently been improved.
As promised there, although this is rather a technical documentation than
suitable prose for a news post, I would still like to provide a brief overview
on how the previously covered features work and how they interact with each
other.
The Basics
As implemented in class
ClientStateInGameT
, a client
that has joined a game world has these crucial tasks to complete in each video
frame:
Render()
{
// Draw the current state of the game world on screen.
}
MainLoop()
{
// Everything else that is important in each game loop:
// - process any network packets that might have arrived from the server
// (apply server updates and run reprediction as described below),
// - collect and process player input.
}
For a start, we can assume that the
Render()
method (and any nested
Draw()
methods) that
are responsible for rendering the client world are "constant". As explained
later, this is not exactly true in the strict sense of the C++
const
keyword, but it
is true in the sense that when the drawing is done and the
CaClientWorldT::Draw()
method returns, the client's world state is
exactly the same as it was
when the drawing began.
That leaves the client's main loop (
ClientStateInGameT::MainLoop()
)
to be considered.
If an
SC1_FrameInfo
message arrived from the server, all game entities (that are relevant for the
client) are updated to the state of server frame "X". As the server also
indicates the growing ID of our client's player command that it has accounted
for up to server frame "X", the locally available player commands that have not
yet been accounted for since frame "X" are re-applied ("reprediction").
Player input from mouse and keyboard is collected in each video frame and
stored in a
PlayerCommandT
struct. It then
is:
- sent to the server (for the server's authoritative processing),
- "predicted": applied locally and stored for future reprediction,
- cleared (for the next loop iteration).
Interim Result
At this point, it is important to note that the above described client
implements full networking support with prediction and reprediction, but not
yet with any of the extra features mentioned in my
previous
post.
Especially note that the client's game world is always in state "X" as last
received from the server, where additionally the local player input is applied
to the client's player entity.
Adding Interpolation, Reconciliation and Client
Effects
The crucial idea is that our client world is generally
always kept in
the above described state: latest server state "X" + prediction.
This is a very important invariant of the prediction feature, because
otherwise, if interpolation or other effects modified the world state, the
prediction would find a different state when locally applying player input than
the server would, which in turn introduces stuttering as it breaks the
prediction feature.
Therefore, in
ClientStateInGameT::MainLoop()
we make sure to (only)
take note of anything that might affect the
effects. Most prominently, if a new
SC1_FrameInfo
server update
arrives, new target values for the interpolations are recorded – but the values
themselves are updated as if interpolation didn't exist.
The last piece of the puzzle is in
CaClientWorldT::Draw()
, which is
actually responsible for rendering the world:
- it activates the interpolation, reconciliation and client effects,
updating all variables to their proper values,
- renders the world in the prepared state,
- restores the previous values back to "latest server state X +
prediction".
Summary
Besides the processing of incoming server updates and outgoing player input,
the client has a lot more to accomplish before a good game experience can be
rendered. The key idea is to keep the client's world state always at the latest
received server state "X", with local prediction applied.
Interpolation, reconciliation and client effects are kept and maintained
independently. Although the code for these features affects several source
files, this encapsulation helps with readability and correctness.
In this regard, we now have a very good and powerful framework. It is used to
implement several important examples already, and I hope to utilize these
facilities more often and more extensively in the future – I can still think of
a lot of effects that I would like to see but that aren't there yet, e.g. head
bopping when the player is walking or running, rotating items, swinging and
flickering light source, and so on.
As always, your comments, feed-back or help are very much appreciated!