2
\$\begingroup\$

I'm trying to setup a very simple multiplayer game that allows some users to join and walk around. The game is 2D and the players will have a position, the direction they are looking in (it's a top-down game) and their animation frame index (so they can be animated for all clients). This is my game loop that I have at the moment:

private void run()
{
    long lastTick = 0;
    long tickDelay = 33;
    int tps = 0;
    int ticks = 0;
    long lastTickCount = 0;

    List<Connection> toRemove = new List<Connection>();

    while (!shouldStop)
    {
        lock (connections)
        {
            if (connections.Count != 0)
            {
                foreach (Connection connection in connections)
                {
                    if (!connection.IsInitialized)
                    {
                        if (connection.Initialize())
                        {
                            BroadcastChatMessage(connection.Username + " has joined");
                        }
                        else
                        {
                            Console.WriteLine("Disconnected " + connection.IP + ": failed to authenticate");
                            connection.Close();
                            toRemove.Add(connection);
                        }
                    }
                    else
                    {
                        int cmd = connection.ReceiveCommand();

                        if (cmd == -1)
                        {
                            BroadcastChatMessage(connection.Username + " has left");
                            connection.Close();
                            toRemove.Add(connection);
                        }
                    }
                }
            }

            if (toRemove.Count != 0)
            {
                foreach (Connection connection in toRemove)
                {
                    connections.Remove(connection);
                }

                toRemove.Clear();
            }
        }

        ticks++;
        long now = CurrentTimeMillis;

        if (now - lastTickCount >= 1000)
        {
            lastTickCount = now;
            tps = ticks;
            ticks = 0;
        }

        int connectionsCount = 0;

        lock (connections)
        {
            connectionsCount = connections.Count;
        }

        Console.Title = "Top-Down Multiplayer Server [TPS: " + tps + ", Connected: " + connectionsCount + "]";

        lastTick = CurrentTimeMillis;
        Thread.Sleep(20);

        while (CurrentTimeMillis - lastTick <= tickDelay) { }
    }
}

I have a simple mechanism that makes the server run at 30 ticks per second with a Thread.Sleep to free up CPU. Should I be processing data from all the clients in the same thread like this? What should the ticks per second be determined by? Am I on the right track with this loop?

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).

\$\endgroup\$
2
  • \$\begingroup\$ Does this game loop work for you currently? If you just want open-ended feedback on improving already-working code, we have a Code Review StackExchange where you can ask this instead. There are definitely a lot of improvements they'd be able to suggest here. If this loop is not working for you though, then tell us where it's going wrong / what you need help changing. \$\endgroup\$
    – DMGregory
    Commented Aug 1, 2019 at 11:36
  • \$\begingroup\$ It is working, I'm asking if handling new connections and receiving data from each client in the same thread is a good idea or not and advice on what the ticks per second should be. There's not much resources on the internet about the internal workings of a server game loop. \$\endgroup\$
    – Logan
    Commented Aug 1, 2019 at 11:44

1 Answer 1

3
\$\begingroup\$

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).

\$\endgroup\$
9
  • \$\begingroup\$ I accept connections in a different thread which is why there is a lock on connections. Should I also accept connections in that same thread? Also, what if one of the clients takes a while to send data? Wouldn't that slow down the entire game loop? How would I avoid something like that? \$\endgroup\$
    – Logan
    Commented Aug 2, 2019 at 4:23
  • \$\begingroup\$ @Logan expanded answer. \$\endgroup\$
    – Theraot
    Commented Aug 2, 2019 at 5:21
  • \$\begingroup\$ Are ticks supposed to last a certain amount of time or is it just the delay between each? I'm a bit confused on that. Would I be able to use the Stopwatch to control that? \$\endgroup\$
    – Logan
    Commented Aug 2, 2019 at 5:56
  • \$\begingroup\$ @Logan servers usually have a fixed number of ticks per seconds. That is the state of the art. However, even if you are fixing the timestep, you still need to measure elapsed time, so you can have the thread wait until the next tick. I have the basic use of Stopwatch here (scroll down to C#): gamedev.stackexchange.com/questions/173757/… - addendum: see the pseudo code above in this answer for what to do with the elapsed time. There tick is the time between server ticks. \$\endgroup\$
    – Theraot
    Commented Aug 2, 2019 at 6:11
  • \$\begingroup\$ The reason I didn't just use Thread.Sleep for the entire delay was because in Java the time a thread sleeps for isn't very accurate. In C# if I enter 1 will it actually sleep for 1 millisecond nothing more nothing less? \$\endgroup\$
    – Logan
    Commented Aug 2, 2019 at 6:40

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .