Concurrent Game Actors
Most games always have a GameObject base class which represents any object that can exist independent of the world. On a more concrete level, there also usually exists the concept of an Actor in which exists within the game world. Most game engines most likely still don’t multithread on this level but if they did, some thought would have to be given to weigh the benefits / efforts expended to bring this about.
Discussed this concept awhile back with a friend also interested in this topic and we arrived at this conclusion. Essentially, the Actor will buffer any calls that can alter runtime state. So, all incoming events will essentially be stored into a message queue. To keep the public interface functions for Setters familiar to callers, they will generate the corresponding event type. So, a setPosition() will create a “SetPosition” event and buffer that.
You can expose a lot of ‘outputs’ (aka getter functions). Such as you’d return cached position/rotation data etc. This will likely be all information computed the last known frame. Who knows, the data is probably relatively fresh depending on how well the “Actor Job Manager” keeps up with the main thread.
Call me conceited, but I really don’t see this being too hard to pull off as long as you introduce this of course early in the project.
Here’s a breakdown:
- Actors gets created by the World/Level and will be assigned to an Actor “Job” whose task is basically to tick these guys within the local thread loop
- The only public interfaces will return cached data such as Position, Rotation, Translation matrix, and so forth. Any calls that can possibly change the runtime state of this object will be funneled through a message queue. The Actor will receive Events and process these when it gets a chance.
- When the Actor gets ‘Ticked’ by the Job that owns it, it will proceed to perform its calculations inside its void doTick(float deltat) function
- The only tricky part is how to deal with variables that need to be returned to other callers. So possibly at the end of its’ Tick, you copy over the new updated position/rotation into a cached version which essentially has synchronized protected access to it. This way we reduce the time we have to stall other threads as we update our former state. At the begining of the tick, you pull off events from the message queue just incase an external source wants to update your position and so forth.
—————————————————–
I do not anticipate very many other major differences between a Concurrent Actor system and a Traditional one. The Level can still maintain a list of Actors it owns as usual. External callers can still store pointers and references to these Actors as normal as well.
The benefits should be pretty huge of course since the majority of gameplay code CPU costs will be expended within the main game loop. Often times AI, Pathfinding, and scripted actors will all be ticked along the main game loop and cause spikes here and there making it hard to perform optimization passes. Doing things this way you build for the future, constructing dynamic software that can analyze client hardware and distribute Actors to various Job Threads as needed.
The beauty of such a system is that you can handle various problems that occur dynamically. Is Script code burning too much CPU? Well distribute that to another thread. AI? Distribute all of your NPCs to a thread. Basically, the goal is to perform runtime analysis of the software and perhaps move around Actors dynamically to linearize CPU costs fully leveraging Quad core CPUs and beyond.
The average game experiences a lot of bottlenecks due to AI & Pathfinding. And it might be tricky trying to get programmers to only write big things and put that on Jobs. Instead, this approach encourages games to put everything on the Job queue. Yes, everything. This way, you can move around processes as desired to optimize runtime performance
——————————————————–
Benefits:
- All incoming events are buffered making it easier to debug how an Actor arrived at a certain state
- Since we are buffering events, we can store them for playback later. So we automatically gain the ability to replay scenes and ingame cinema
- Makes the game engine fully scalable allowing us to fully leverage the logical processors on the local machine
- Depending on the CPU Load, we can evenly distribute Actor Jobs to maintain steady, reliable performance
- Gameplay programmers do not have to worry about writing Jobs. Essentially, you are doing this for them brute force
Pitfalls
- In Actor subclasses, programmers might have to become aware of multithreaded programming paradigms. Any data they access within their game tick obviously cannot be manipulated by other threads at that time. So, outside callers can only access cached information.
- More memory is consumed. Every actor will also require cached versions of data that is protected by synchronized access
- Additional layer of complexity due to the asynchronous nature of the beast
- Collision detection code will also need to be updated to deal with Concurrent Game Actors
Recent Comments