1
\$\begingroup\$

I'm making a multiplayer game where players fight in vehicles, and I am doubting my server architecture. As it will be physics, network and AI intensive I would like to split these 3 topics into their own thread. Perhaps even one thread per AI.

The physics thread will run at a fixed time-step and do the same loop over and over.

  1. Integrate gamestate at fixed timestep of 0,005s (200Hz)
  2. Solve gamestate for constraints and collisions
    One such constraint is a vehicle controller, which is a struct consisting of throttle, steer, brake etc. is applied to a single vehicle.
  3. Callback
    1. The network is called every 6 physics frames, thus the networking runs at ~33Hz
    2. The AI is also called every 6 frames.
      Note: network and AI can be called in lockstep

The AI threads will calculate the next move on a need-by-need basis.

  1. Copy gamestate into private memory
  2. Calculate next move
  3. Update their vehicle controller in the game state

The network thread will

  1. Listen for incoming UDP packets, handle them, apply changes to gamestate such as player's vehicle controller, bullet spawn
  2. When called for, broadcast the new gamestate to clients
    1. Copy gamestate into private buffer
    2. Create array of differences from last state
      1. Vehicle parts removed/added
      2. Vehicle parts moved/rotated
      3. Players joined/left
      4. Projectile impacts and damages
    3. Pack the differences together and broadcast.
  3. When a new player joins:
    1. grab the last private game state buffer and send it whole
    2. Change gamestate by spawning a vehicle and allocating a controller

What I'm having trouble with:

  1. Creating threads on the fly has overhead, so I would like to avoid it by pre-creating and "parking" the threads, and calling them when needed. Once called, they do their duty, such as calculate the next move for the AI to make. Then the threads "hibernate". Note that I don't want the threads to wait in a busy loop and read some shared memory for "activation".
  2. The AI and Network threads need to make private copies of the game state, because the physics run so fast the game state might be changed mid-read.
    1. Mitigation 1: swapping game state buffers. Still no guarantee.
    2. Mitigation 2: locking the gamestate. This is a bad idea because the physics should not be held up.

How can I achieve this in C++? Does x86 offer actual interrupts for this? How about other architectures, or are there libraries that do this for me?

Or should I do it another way?

\$\endgroup\$
4
  • \$\begingroup\$ While the idea of putting different systems on their own threads is intuitively appealing, I've never seen a successful implementation in a game or engine. In practice in games, too many things care about the state "now", leading to lots of synchronization or state-copying. Have you considered a "job-ified" or ECS approach instead, where you process your physics ticks, AI, and networking systems sequentially, but within each of these system steps you "go wide", spreading the work between multiple workers acting in parallel? This can often get you better scaling with less coordination overhead. \$\endgroup\$ – DMGregory Jan 6 at 18:24
  • \$\begingroup\$ @DMGregory That actually sounds like an awesome alternative, I can already picture it. Still, how would I go about efficiently dispatching the jobs? \$\endgroup\$ – AnnoyinC Jan 6 at 19:45
  • \$\begingroup\$ Sounds like you might want to do a bit of reading into Job Systems, as this is a whole paradigm of its own, with published material from folks far more knowledgeable than myself. ;) Unity's Data Oriented Technology Stack (DOTS) is one implementation of this idea that's been a focus of some attention in the past few years, and might have some useful inspirations. \$\endgroup\$ – DMGregory Jan 6 at 19:48
  • \$\begingroup\$ @DMGregory some cursory searches turned up little of interest to me... Can you recommend any clear-cut literature, preferably with code examples/snippets? Or perhaps a book? \$\endgroup\$ – AnnoyinC Jan 6 at 19:50
1
\$\begingroup\$
  1. a std::condition_variable will let you wait_for the event_queue.has_event() when pushing an event you can notify_one to wake up the waiting thread. However the downside of this is that you are depending on the scheduler to actually wake up the thread. However scheduler resolution can be larger than you'd expect.

  2. it's better if only 1 thread is doing the manipulation of the game state. So instead of your AI thread doing updates directly it instead sends message tot he physics thread of how the AI struct should be updated. Similarly with networking.

You can take read-only snapshots of the gamestate to make AI decisions. This can be made easier with double buffering.

The AI thread has 2 buffers it uses: reading from the last state and writing to the new state. Then when the update is done it sets the newly written state as the latest. And the other 2 threads can read-lock the latest buffer to make their decisions. Make sure you have enough that there is always a unused buffer to allocate for the next written to. In your case you'll need 5 gamestate buffers, 2 for the networking thread doing a diff, 1 for the AI, and 2 for the updater thread.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the answer. I think I'll have the physics be a main loop, dispatching the AI and network_broadcast via this job library: github.com/Joshua-Ashton/libjobs. Perhaps the physics loop will keep the old gamestate around and pass it through the dispatch, who will then free it. \$\endgroup\$ – AnnoyinC Jan 7 at 21:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.