I'm just not sure if moving to multiple tasks is the right choice, on
one hand you have the multiple intervals issue, but then the overhead
of managing multiple tasks, but is that a bad thing?
By overhead if you mean programming/maintenance overhead, yes it could be depending on how difficult it is to make your system thread safe. Since you don't specify what each manager is doing, I can only compare it to common game engines like AAA game engines that use ECS, and many opt not to concurrently run their systems ("managers") because of the difficulty of the state management and the evaluation order dependencies between their systems ("managers"). Instead they parallelize what the managers individually do. The few who dared to try this often came back with battle scars and stories to tell about how difficult it was to concurrently run the physics engine efficiently at the same time other systems were tampering with motion components while the system handling player interactions was simultaneously moving things around, e.g.
My second reason was to ask if there was anything that seemed to do a better job doing these sort of operatings? Maybe some sort of thread or timer, over a task.
A very immediate and unambiguously better version over what you are doing now by polling a stopwatch in a loop with manual thread sleeps is to use System.Timers.Timer
and instead invoke manager cycles in the Elapsed
event:
System.Timers.Timer
That's a much easier answer than whether or not you should design your game so that each system ("manager") can be run in parallel (either through tasks or threads) which can be, depending on the scope of the game, a moderately to extremely ambitious undertaking.
It also doesn't make much sense to use tasks here, assuming your Game
and Managers
are persistent. They don't benefit from utilizing the thread pool as with short-lived tasks that are executed repeatedly (ex: in a parallel loop). So it's more of a question of threads vs. timers, and timers are a definite improvement over what you have now with polling... threads are "conceptually optimal" but may make your system so much more difficult to design safely and efficiently.
My main reason for asking this is I don't know if I should move from having 1 task in 'Game' to just giving the managers their individual classes, someone coming from an advanced background of C#, could you give me a clear response?
In my opinion, what you're considering so far isn't bad assuming this is not like some ambitious AAA game engine. I tripped up over the sentence where you asked about giving managers individual classes which sounds like it relates to design and organization. The idea to have these Managers
is not bad in my opinion and it at least adheres better to SRP than one giant Game object processing everything. That said, I wasn't clear if your question was about organization or about multithreading or both so I'll focus the answer on how to effectively design and organize the code in a way that will give you option of exploring the most efficient multithreading capabilities.
[...] although that could change, so I'll need something that could be
good for any kind of operation.
Your design is actually somewhat starting to resemble what you find in AAA game engines using entity-component systems so I recommend studying them and how they parallelize processing for a start. One major difference in those engines is that they call these managers Systems
, like RoomSystem
, and the systems have sophisticated central capabilities to query components in the engine (ex: RoomSystem
being able to fetch all the Room
components to process at a specific time). The components managed by the systems in an ECS are also just raw data (Room
is just data, not a complex object), and for good reason. The systems also don't store the components, they fetch them from a central place representing all the entities that have components in the game, because two or more systems might be interested in processing the same game entities.
Multithreading
What I'd suggest as a next step in your immediate case is to formalize the idea of a Manager
interface so that you don't have to hard-code what OnCycle
methods to call on which managers in your GameCycle
. The manager should be able to specify how frequently it wants to be called through the common interface that all managers share, and the game should be able to store a collection of managers and invoke these kinds on OnCycle
methods on the managers without knowing precisely what kind of manager they are. The central Game
object can be responsible for whether it uses a separate thread per manager, a single thread with a stopwatch, a task per manager, etc.
That will also make it easier to explore things besides a crude thread that just sleeps and periodically calls OnCycle
if you ever need, and will prevent you from having to hardcode things like:
if (lastOperation.ElapsedMilliseconds >= 500)
{
...
}
... into every single manager. It'll also eliminate the need to manually keep track of a stop watch in every manager. The stop watch polling approach with a single thread that sleeps a lot is probably decent enough in practice for a smaller project and shields you from thread safety challenges, but it's far from optimal in terms of efficiency, and this will at least allow you to explore alternatives much easier in the future without changing code in every single manager, only changing central code in Game
. Timer events would be an immediately superior solution to polling this way without making you deal with thread safety since the OS generally does things a lot smarter than polling a stop watch while sleeping the thread repeatedly for a fixed duration in a loop.
Process All Entities
The next thing I'd explore possibly is a way to allow managers to fetch game data they're interested in efficiently and maybe even process it in parallel, like:
// Inside a manager: call SomeDelegate for each `Room` in the game.
game.ProcessAll(SomeDelegate, Room);
game.ProcessAll
could be responsible for iterating all through game objects which have a specific type of component or adhere to a specific interface, like a room, and this gives you room to make ProcessAll
multithreaded so that it's automating the process of invoking the delegate on multiple rooms concurrently without the RoomManager
having to bother with the multithreading.
I suggest this because you're more likely to be immediately bottlenecked processing the entities in your game world instead of failing to parallelize the game systems ("managers") themselves. Having a way to conveniently process entities of interest in any given manager in parallel could net you more performance gains than even running every manager in parallel because the heaviest loops in most game engines typically are the ones that have to iterate through all the entities of interest (can't skip any and this will always be linear time) and do something with them within a given system (or "manager"). There's enough work often in just one of those entity-processing loops within a system to devote the entire hardware's resources without overkill. That could especially apply for the server side if you have many, many clients connected.
Timers vs. Threads. vs Tasks
Again this is not a direct answer to your question of whether you use tasks or threads or timers, but it offers you the design to explore all these options in the future without changing much code. For a direct answer, the ideal theoretical solution for hardware scalability would likely be to make each manager a separate thread while likewise making all the bulky operations each manager performs parallelized, but that also comes with the most gotchas and will really require you to think carefully about how to make the game state both thread safe and thread efficient. Even a lot of commercial ECS engines don't bother to parallelize the processing that goes on in every system ("manager") because even if that would theoretically yield the most optimal solution, it's too difficult to get correct to be worthwhile.
John Carmack has some good ideas on doing this with central, immutable data structures which can be partially copied (made unique) atomically as threads read and write to certain sections of a buffer with new versions of the buffer potentially being swapped with the old after every frame, but it's a very ambitious undertaking to say the least to try to make an engine that can simply run all systems concurrently ("managers") without a care in the world as they seek to read and transform overlapping game data.
So IMHO, it's generally overkill to try to run all the systems concurrently with tasks or threads. It can become a lot easier to reason about the system if, at least for most systems, you can deduce an order in which they are executed which could be important if you have a system depending on the output of a previous system. Instead it's much easier to parallelize the most expensive operations performed by specific systems than to run all the systems in parallel.
That said, something that occurred to me just now is that you are running lots of your managers against a timer. That already tends to make it so they aren't executing their operations in any deterministic order. If there are no order dependencies among managers, it will at least make it a lot easier to run those managers that lack order dependencies in a separate thread (but still requiring appropriate ways to access shared resources safely and efficiently in parallel if you do this). For breathing room, you might include a way in, say, IManager
implemented by all managers to indicate whether they should run in a separate thread or task or from within the central game logic thread. Game
can then use that to decide whether to spawn off each manager in a separate thread or not.
ObjectManager Operations
Anyway, if you organize things this way, there's something going on here:
// Inside ObjectManager:
public void Operation1()
{
...
}
public void Operation2()
{
// I only want to run this one every 30 seconds.
if (lastOperation.ElapsedMilliseconds >= 30000)
{
...
}
}
If you use this kind of manager idea, I would suggest that ObjectManager
should be split into two -- one that does whatever Operation2
does every half a minute, and the other which does Operation1
all the time. The central game state shouldn't be stored in these managers, maybe in your Game
object which can be injected into these managers or passed by parameter. That'll give you more flexibility to design managers which process the game elements when they aren't storing the game elements themselves.
Anyway, this is just a start -- one iteration to kind of improve your design a little bit and make it easier to optimize centrally without cascading changes. It's actually working a little bit towards the entity-component system route with some of its benefits, but without going towards a full-blown ECS.
ECS Example
Here's a basic simplistic example of how an ECS is organized and processed to give you ideas:
As you can see above, there's a processing order indicated in the diagram which indicates that the systems are invoked sequentially, not in parallel with a separate thread per system. However, some expensive operations that process all the components in the ECS might be invoked in parallel within a given system.
The ECS is favored a lot among game engines given that it leaves a lot of room for optimization when you have these bulky systems ("managers") doing a lot of processing instead of teeny processing scattered across a boatload of tiny objects. It also tends to actually simplify the maintainability of the code and your ability to reason about how the system works when you only have the major game logic inside a handful of systems and not scattered across, say, a hundred different subtypes which have to communicate with each other and depend on each other forming a complex graph of interdependencies (however loosely coupled). In the above diagram, the Car
entity contains no functionality, only components, and the components contain no functionality, only data. The only place that contains functionality are those 3 systems in the diagram: MovementSystem
, AudioSystem
, and RenderingSystem
.