Should I be processing data from all the clients in the same thread like this?
Yes, a single thread for all clients. Although there is an argument for multiple server threads and even they talking among them, do not start there. That is what you do in preparation to moving one server to another machine. There is plenty to do before you worry about that.
On the other hand, that does not mean you can't work with asynchronous operations in that single thread.
If I understand correctly, you have a connection
per client and then you do connection.ReceiveCommand
on each one. That looks like it would block if the client stops sending data.
I wonder why you do not have a single server socket. Anyway, if that makes no sense in your design, then making a Task
to recieve from each connection
does. Then you can use WaitAny
.
What should the ticks per second be determined by?
That is opinion based.
Am I on the right track with this loop?
If it is working, then arguebly yes.
I would worry about that lock
, who else is messing with connections
?
P.S: In the future I would process the game after I handle connections and before I do ticks++ (after I receive client data and before I count the ticks per second).
You probably do not want to use Thread.Sleep(20);
then. If you are going to put the thread to sleep, first compute the time to the next tick and use that.
And avoid to busy loop:
while (CurrentTimeMillis - lastTick <= tickDelay) { } // <- do not do this
What is that CurrentTimeMillis
? Please use an Stopwatch
. That is how to measure time in C#.
There's not much resources on the internet about the internal workings of a server game loop.
Try Awesome-Game-Networking.
If you just want something for reference... have some pseudo code:
while(Running())
{
var begin = current_time();
receive_from_clients(); // poll, accept, receive, decode, validate
update(); // AI, simulate
send_updates_clients();
var elapsed = current_time() - begin;
if(elapsed < tick)
{
sleep(tick - elapsed);
}
}
Note: if you need to deal with other systems, you might put them in other threads. For example, if you are persisting the state of the world, that does not have to happen in the same thread... then in whatever other thread you do that, you can throttle it. And of course watch for thread safety. However, do better than lock
. In fact, you can run simulation in another thread... then update here is about reading and sending data to that other thread. Consider 0MQ.
On separating threads and the slow client
First of all. Yes, there is a way to do it all in a single thread, and it will work. Will come back to the problem of the slow client.
Simulation Thread and Network Thread
If the simulation is running on a separate thread it should not be dealing with connections. Instead it needs a way to get all the network input since last iteration.
One way to do that is to have the network thread write to an network update object that holds a datastructure where all the network events are stored (commands from clients, disconnections, and so on). Then, when the simulation thread wants to get them, it can swap the object for a new one (which can be done atomically).
That also means that the network thread cannot be allowed to cache the reference to the network update object. Thus, all access to the network update object will have to be volatile.
Another way to do that is to have "channel" objects. Either a ConcurrentBag<T>
or a ConcurrentQueue<T>
will work (they are not so different). I would avoid using the BlockingCollection<T>
for this purpose (because it is blocking). Then the Network thread will push/add into the channel the network events, and the simulation thread can pick them up when it loops.
Note: just because you used one way to get input from the network, does not mean you have to use the same way to send updates. In fact, sending updates has extra considerations. However, I would recommend to start by making the communication between simulation and network symmetric (I mean, use the same approach).
On the slow client problem
Please note that the slow client problem can still have an impact even if you separate simulation and network, because if the network is waiting on a client, it will not take input from the other clients. Errata: This is only a problem if you are trying to listen from each client in a loop. However, the way it is done is to just listen on a port, not from any particular source.
Also note that you will inevitably open a socket to receive. Unless you are using a library designed for this, I recommend to stay in control of the socket.
With that in mind, you do not have to use a blocking API to receive. Regardless of how many threads the server is using. Ideally you use something like ReceiveAsync
that gives you a Task
.
Once you have a Task
that will complete when you receives data from a client, you have a few options...
First option:
You can Wait
(not await
) with a timeout (and even a CancellationToken
, useful to teardown the server). That way you know that regardless how slow the client is, the network thread will not be stuck for long.
You can then, each loop, Wait
, check if the task completed. If it did, take the result, and recreate the task. That is how you do it in a single thread.
Notes:
- When you are not receiving, the socket is buffering.
- If you have multiple tasks, you can use
WaitAny
.
Second Option:
You can add a continuation, or better yet, wrap it all in an async
method. Yes, I said to not await
the task, that is so you can pass parameters to Wait
, but that does not mean you cannot wrap it in an async
method. - then that continuation will take the input from the network, post it to whatever object you need it into (for example a channel object).
Each loop, you will take the posted data (yes, that could be in another thread), and check if you need to recreate the task.
Third option:
Following the same idea, you can recreate the task inside of the continuation. So that you do not have to worry about it each loop.
In fact, if you started with a single threaded solution and moved down this path. From this point, it just a matter of using a well placed Task.Run
(if you did not do it already) and you have a separated thread for the network (if you are using Task.Run
mark it LongRunning
, so it is a dedicated thread and not one from the ThreadPool
).