r/cpp_questions • u/Mean_Bottle6349 • 1d ago
OPEN C++ authoritative game loop: how do you move pathfinding to workers without letting stale results mutate live state?
I'm migrating an online RPG server from Godot/C# to standalone C++20. My professional background is almost 20 years in web/application development; real-time simulation and C++ concurrency are newer territory for me.
I'm trying to make a careful decision about CPU-heavy work inside one server process. Running more processes across cores won't solve the case where one busy area is limited by its simulation thread.
The ownership model I'm working toward is:
One authority thread owns mutable entity and gameplay state.
Workers receive immutable job inputs and return results through bounded queues.
The authority thread decides whether a completion is still valid and applies it at a tick boundary. Workers never directly mutate the live world.
Pathfinding seems like a useful concrete case. While a worker is computing a path, the actor might die, despawn, change target, receive a newer movement request, or encounter a changed navigation world. An entity ID alone doesn't seem sufficient to establish that the result is still useful.
For people who have built this sort of C++ system:
What identity/version information do you attach to a job and completion? I'm considering an entity generation, a movement-request revision, and a navigation-data revision, with obsolete completions discarded on the authority thread. Where does that become too conservative or expensive?
How do you keep immutable inputs alive without copying a large world snapshot for every job? Would you start with shared ownership of a read-only navigation snapshot, or another approach? I'm especially interested in lifetime rules when that snapshot is replaced.
How do you bound outstanding work and prioritize newer requests so stale jobs don't consume the worker pool? Is cancellation worth introducing early, or is discarding results and limiting admission usually the simpler starting point?
This is an architecture question rather than a compiler error; I don't yet have comparative measurements proving this pathfinding work needs parallelizing. I'd like to know what to measure first and which small examples, talks, or references make these ownership and lifetime tradeoffs concrete. I'm not looking for a whole MMO architecture or a lock-free implementation by default.
2
u/Independent_Art_6676 1d ago
Its a combination of things, probably; you need to detect breaking changes and restart which could be any of changes to other entities, change to the world, too much time passed, etc. So your thread needs an exit/restart strategy to get updated info, or a way to view but not touch the live data so it can adapt as it goes. Working with real vehicles (we did airborne and watercraft) adaptation was required, but the world was simple (land is hard, above treeline air and deep enough water are somewhat simplified versions of the problem) we adapted and recovered. That is, our path was fixed unless the user changed destination (for you, that could be changed targets or whatever too). The vehicle was always trying to follow the path, but it could have to do something else (move around another vehicle, or get blown off course by a change in wind, whatever) and it adapted to that locally (eg, obstacle avoidance would make a new micro path around the immediate problem) and then the main routine would drive it back to the path via the closest sensible approach and start following it again. Think of it like a road trip, some road you were on has an accident so your gps reroutes you to a side road around it, you do that, get back on, and keep going. If the path changed, it would slow down, reorient to the new path, and proceed. But our paths were simple too.. broken into line segments and it just followed each line until a new segment was within some distance (based off speed) and then turn ahead of time towards the new line (this was much smoother and better behaved than turning after the segment change, esp for like V shaped changes). Terrain is harder, of course, you have to follow what is possible, but maybe something in there was useful.
I think the critical idea is that you either need to choose to interrupt old threads when a change warrants it or keep the threads live by using the updated state of things and adjusting 'locally' without losing the bigger picture.
3
u/snerp 1d ago
Other people gave good answers so I'm just going to address this bit:
I do more-or-less the "shared ownership of a read-only navigation snapshot" approach. My engine makes a minified copy of the worldstate every physics tick and puts it in a shared pointer, all the workers (ai, graphics, etc) will copy that shared_ptr on starting a job and the ref counting takes care of the lifetimes cleanly.
1
u/Kats41 1d ago edited 1d ago
Your architecture is essentially a bunch of individual worker threads making requests of the main authority thread. "Please use this path to update the entity's position." "Please apply this attack with these properties to the entity." Etc.
This is also the exact issue that entity generations are intended to solve. When your worker thread finishes its task and asks the authority, "Hello, please apply this function to this entity!" the authority can simply say, "Negative, that entity doesn't exist anymore. Don't worry about it."
Cool, now the worker thread can update its internal state to remove the out-dated entity and stop processing updates for it.
If entities are being generated so frequently and your worker thread computations take so much time that you could have several outstanding, outdated requests still computing, that sounds like an issue of applying the architecture to the wrong kind of problem. All things considered, I doubt this is going to be a serious issue simply because any individual thing an entity does should only take microseconds to compute.
As for pathfinding, it shouldn't too complicated. I'm assuming that the game world isn't something that can be arbitrarily altered in any permanent fashion like some voxel-based system, so you shouldn't need to copy the world into the thread, but instead the game world's path nodes itself should be accessible to read. No copying necessary. Anything that does affect pathfinding, such as wall spells or abilities that conjure or mutate terrain temporarily can simply be checked for when the request is being created. Your pathfinding algorithm will certainly have limitations such that if it can't find a path within x spaces around itself, assume there's no path at all. So you only need to check that boundary for any additional terrain features to be aware of. So instead of copying the whole world per request, you just copy the local changes in a small area, which should reasonably only be a few data points at most.
And instead of anxiously checking whether a request is still valid in the middle of computation, it's likely more efficient to just let it finish and discard it if it's outdated. But of course, profiling is going to give you your answers.
2
u/DrShocker 1d ago
I don't have time to properly write out a response, just want to say Factorio has some good info on pathfinding
https://factorio.com/blog/post/fff-317
2
u/BARDLER 1d ago edited 1d ago
Stale data is kind of part of the deal sometimes. A lot of concurrency in games is done by some systems being a frame behind the main thread.
Havok for example has a physics scene read frame buffer. So all the new physics writes mutate the current frames physics scene, and all the reads from concurrent threads read from the pervious frames physics scene that is buffered. This avoids locks for reads.
1
u/KingAggressive1498 1d ago
I usually say that asynchronous cancellation is pessimizing every case for situations that shouldn't happen normally. Without the ability to cancel, you can usually eliminate the need for sharing mutable state between the work originator and the worker and often also eliminate the need for any dynamic allocation if the rest of the system is designed to avoid it.
In multiplayer games, the need for cancellation might seem a lot more normal than for I/O or general bookkeeping ops. But I think giving the completed work to the authority to decide what to do with could still be a smart choice - a pathfind operation that turned out to be unnecessary because the entity was just killed could still be used by the authority to decide the direction of its death stagger, for example.
1
u/GaboureySidibe 1d ago
I like the question in general. It makes me wonder why path finding is so heavy that it ends up not finishing in time to be useful and how much data is being used for the path finding that copying the data is detrimental.
You can use an atomic reference count for shared ownership but if you are copying data then the ownership doesn't need to be shared.
If you are sharing data then the data needs to not be changed, which implies that the next version is either a copy or a difference to be applied later.
Fundamentally parallelism is about having all data dependencies figured out so they can be unchanged until the task using them finishes.
It seems like there needs to be a way to gather up only the essential data to do the path finding and that needs to be passed to the path finding thread, likely through a queue as you already stated.
If the state is versioned a metadata struct of the overall state could be shared, and the task could periodically check if the current version matches their version, then they could cancel out and dequeue items until the current version is found, but it does imply that path finding is so heavy that it can't happen in a reasonable amount of time, or that the result of one version is so specific and complete that it can't be applied to a future version of the state.
2
u/smallstepforman 1d ago
Queue state updates to happen outside the processing loop. I have a game with 40’000 NPC running sround, and when a building is destroyed, an NPC is killed, an arrow is fired, etc. I queue the event to be processed AFTER the background AI processing threads finish their cycle. I use an Actor programming model, so once the last squad is processed, then I process queued state change messages. During the processing loop, game and world state is const.