The game loop
Your game's server is one class implementing IGameServer. The host calls its
Start once, its Update on a steady tick, and its Stop once at shutdown. Packet handlers run
beside the loop, not inside it, so Packet handlers and the loop
below matters for any state they share.
using BSGameLib.Server;
public class MyGameServer : IGameServer
{
public void Start() { }
public void Stop() { }
public void Update(TimeSpan elapsed) { }
}
Starting
The host starts the server in this order:
- Each IInitializer runs its
Initialize(), in an order you shouldn't rely on. - The server starts listening, so clients can connect.
IGameServer.Start()runs.- The loop starts, and
Updatebegins to run.
Step 2 comes before step 3, so a client can connect and send packets before Start returns, and
before the first Update. Do the setup that packet handlers depend on in an IInitializer, which
runs before anything listens.
The loop
Update runs on a thread of its own, not on the thread pool, and it runs sixty times a second. Each
call receives elapsed of a sixtieth of a second, whatever the real time between calls was, so the
argument is always the same. If your game needs the real time, measure it yourself.
Add up elapsed as a TimeSpan, not as whole milliseconds: a sixtieth of a second is 16.67 ms,
and timers that count 16 ms a tick run about 4% slow.
After a stall, the loop makes up for the ticks it missed, but only up to 100 ms of them at once: about six ticks in a row. The rest of the lost time is dropped, so the game runs slow for that stall rather than jumping ahead.
Failing ticks
When Update throws, that tick is skipped and the loop carries on with the next one. The first
failure is logged as an error with the exception. Failures after that are logged at most once every
five seconds, with the number of failed ticks so far, so a fault that repeats every tick doesn't
fill the log.
Packet handlers and the loop
Packets are handled on the transport's threads. Their order against Update is not defined.
- Reliable packets from one client are handled one at a time, in the order the client sent them. Handlers for different clients can run at the same time.
- Unreliable packets (datagrams) from all clients go through one receive loop, so their handlers run one at a time, across every client. A slow unreliable handler holds up the others.
- A reliable and an unreliable packet from the same client can be handled at the same time.
An exception from a handler is logged as an error, and the connection stays up.
So a handler and Update can touch the same state at the same time. There are two ways to stop
that. Either take a lock in the handler and in Update, or set
HandlerRunner in an IInitializer. The runner receives
each packet's handling as a function, and the type of the packet (or null for one the server
doesn't know). It can take the lock and run the handler, or queue it for Update:
using BSGameLib.Networking;
using BSGameLib.Server;
public class World : IGameServer, IInitializer
{
private readonly IServerAdapter _adapter;
private readonly object _gate = new();
public World(IServerAdapter adapter) => _adapter = adapter;
public void Initialize() =>
_adapter.HandlerRunner = (packetType, handle) =>
{
lock (_gate)
handle();
};
public void Start() { }
public void Stop() { }
public void Update(TimeSpan elapsed)
{
lock (_gate)
{
// Advance the world.
}
}
}
Shutdown
When the host is asked to stop (Ctrl+C, SIGTERM from systemd, or the service manager):
- The loop finishes the tick it is in, and ends. The host waits for this for its shutdown timeout, which is 30 seconds by default.
IGameServer.Stop()runs. The sockets are still open, so packet handlers can still run.- The server stops listening. Every connected client is disconnected with the reason
Server shutting down. - The host finishes shutting down.
If the shutdown timeout runs out while a tick is still running, Stop runs anyway, while that tick
is still going. If Stop saves the world, take the same lock the handlers and Update take.