In our last posts we have been dedicating our efforts
to reduce the client perceived latency.
Let’s check what we have focused:
- We
have configured TCP to flush data as
soon as it has been generated.
- We
decided to mix TCP & UDP
whenever is possible.
- We
create control layers to sync client and
server to get a smooth gameplay.
- We
made our own Object Serialization .
All points above are about technical issues. Now, let’s turn inside our
app and let's check if we can enhance its performance.
Summarizing how the server works will show us some clues about which point can
be improved. Let’s look inside:
- The
server and the remote clients wait, initially,
the same gap of time (tick)
for each turn.
- The
server wakes up each time tick and consolidates
the players movements sent previously by the remote clients.
- The
server sends back a message containing the consolidated
movements and tells every client if they have advanced or delayed related
to the objective lag between them.
- The
remote clients make some adjustments on the client side to compensate changing latency
related to the server.
- In
order to make this work, clients
begin their tick before the server in order to compensate each own latency plus
certain buffer.
Now, let’s think what we can do to improve this algorithm:
First Enhancement: Try to send consolidated
movements once all player movements have arrived.
As we have seen, the
server waits until the end of the
tick to send back consolidated movements to the client. That’s how it should
be, you may think. Sure? Is it strictly mandatory to wait until the end of the
server tick if we already have all movements?
Let’ see... We have nothing to do once all movements
are received, at server side, except waiting to the conclusion of the tick. So,
if we advance the message containing
the consolidated movements, remote clients will receive sooner that movements
and they will paint non own movements before.
As a result, remote clients will see a more fluid and
dynamically game and that’s one of our main goals.
Risks?
If the end client tick time is so
close to the server end tick , we will
be in risk of arriving late for the
following movements. That’s where our algorithm, described in our last post,
works. The following client tick will be shorter than usual in order to keep
the same gap related to the server. If it happens very often we will only feel a faster game and, in fact,
this is what we want. Anyway, we always can wait a minimum of time before
sending back consolidated movements. In our case, Snake the Net, we have not
implemented this guarantee, except at LAN modes, because we want a fast game and
unfortunately, the latency itself introduces more time that we wanted at
minimum.
We implemented this approach with sensible results and I recommend to apply similar strategies
wherever is possible.
Second Enhancements: Extra time for the lazy clients
Let’s think in
the opposite way. What if we have lazy clients? What if we add some extra milliseconds to the server tick and then we tell him
to recover that time by reducing his tick client? By doing this, we avoid to correct our clients despite he
is a really lazy client. We make him to do a fast tick but, in the other hand,
we avoid to correct him. In our case this enhancement help
us by reducing corrections and improving stability. Of course, you have
to control if a lazy client is actually a disconnected client. Again, sense common. If he is late several
consecutive times, we will disconnect him and replaced by a bot, that they
always arrive at time ;-)