0
\$\begingroup\$

I've been trying to code a multiplayer client/server prototype game using SFML's networking. My server and client use TcpSelectors to wait for a certain amount of time for incoming packets without blocking the main thread; however, waiting for packets also means that the main thread won't render anything as long as the function hasn't returned yet. As a temporary solution, I've just made so the amount of time the TcpSelector waits for is 1/144th of a second; this way rendering will happen at a max of 144FPS. However, I think this is a very bad option: this effectively caps the game's rendering fps to 144, since trying to render any more than that won't be possible: just packet polling takes 1/144th of a second.

Because of this I had thought of moving my networking logic to another thread and then use a condition variable to signal whenever a packet has been received; my main problem however is that I have no idea how to make so my main thread doesn't wait indefinitely for a packet to arrive.

A solution would be to make my thread check the condition variable and then immediately keep rendering if no packet has been received.

How could I implement this? Is this even a good approach? Thank you.

\$\endgroup\$
3
  • \$\begingroup\$ If your game is not turn based, I am pretty sure you should not be using TCP. UDP is the preferred method. \$\endgroup\$
    – Almo
    Sep 20, 2023 at 18:45
  • 1
    \$\begingroup\$ I've seen the solution you propose suggested in the past: the network thread does just that, and it queues whatever it receives, and the main thread will process the queue once per frame and do whatever in needs to be done. I don't know how it's implemented, though, as I've never done it myself. (The command pattern comes to mind, though.) \$\endgroup\$
    – Vaillancourt
    Sep 20, 2023 at 19:51
  • \$\begingroup\$ @Almo I read somewhere that going with UPD, one had to re-implement a bunch of features TCP had (e.g. correct packet ordering, re-sending missing information) so there is an added layer of complexity there. Plus that some network hubs will tend to drop UDP packets more often than TCP packets because they know that the TCP packets will have to go through anyway and that the sender will resend it until it's received (so it's more work dropping a TCP packet than a UDP packet). At this point it seems like balancing ease-of-use over raw speed. \$\endgroup\$
    – Vaillancourt
    Sep 20, 2023 at 19:57

1 Answer 1

2
\$\begingroup\$

Quick summary:

  • You should be using non-blocking socket reads
  • It's a reasonable but probably unnecessary optimization if your sockets are non-blocking
  • You should probably be using UDP

Processing the network traffic in its own thread is a perfectly reasonable approach, but definitely not essential. Your instincts that you should not be blocking while waiting for network data are correct, but you can do that without multithreading.

First, as pointed out in a comment, you most likely do not want to be using TCP for your communication if this game is meant to take place in anything near real time. This does mean you need to account for some aspects that TCP handles for you, but you can do so in a way that is far more efficient than TCP's generalized implementation. You do also get one benefit from UDP that you have to account for manually with TCP, which is that when you read a packet it is always a complete message.

The standard way to account for the possible gaps in UDP is to give every update a sequential ID and include in the update an acknowledgement of the most recent update you have received from the other party. Each side then sends updates basically assuming that any updates that have been sent since the last acknowledged ID were lost. That may sound kind of wasteful, but in practice the amount of redundant data will tend to be very small and the time saved from negotiating re-sends will be huge.

Because every update includes all the data needed to match the most current state of the sender, lost updates don't have to be accounted for at all. If one does show up late and out of order, you will know this by the ID and just ignore it.

To be slightly more specific as to what this looks like on each side:

The Server: The simplest approach is to send the current state of all objects in the game. For a simple game, that could be all you need to do. The more efficient version is to send all of the values that have changed since the last acknowledged update. This means keeping a buffer of recent full updates on the server and generating a diff for the client update.

The Client: Send all input since the last acknowledged update. This can look like a couple of different things depending on your game. Common things are a queue of commands or timestamped changes in button state. The important thing is that inputs are re-sent until they are acknowledged.

If you do stick with TCP, you should set the sockets to be non-blocking so that recv() will return immediately if there is no data waiting. Also be sure to disable Nagle's Algorithm using the TCP_NODELAY flag, otherwise you will likely experience significant lag.

Either way, what this looks like in a multithreaded setup is not much different than what it would look like to do it in a single thread. The network thread would continually attempt to recv() data and as soon as it does process it into whatever format the game expects. When a complete update is received and processed, it is put into a queue that is visible to the main thread. In the main thread, you will check that queue and process any waiting updates, rather than checking the socket directly. For absolute maximum efficiency, you would make it a lock-free queue.

While this will possibly by you some time, in most cases this won't be a significant optimization. The only way to know for sure is profile your game, but if you are already using the sockets in a non-blocking way, there is almost certainly something else to optimize that will be a much bigger win.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you, this was very helpful! \$\endgroup\$
    – xGolden
    Sep 22, 2023 at 22:31

You must log in to answer this question.

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