So the question is - how should I implement the interaction between
the various systems?
Ideally they don't interact, not in any direct sense. The systems in an ECS all have access to the central ECS database where they can fetch entities and components attached to them. They don't talk to each other directly. They talk to the database and all run independently of each other.
Dependencies Flow Towards Raw Data, Not Abstractions
The dependencies in an ECS do not flow towards functions, not even abstract functionality. They all flow towards raw data which might sound like an epic violation of many accepted software engineering principles, and in my opinion it is, but yields something easier to maintain for some cases. Maybe some software engineering principles are wrong or at least not applicable for all scenarios. There are many situations where it's easier to achieve data stability than interface/design stability. As a basic example, it's much easier to reason about what data fields a raw matrix component should have once and for all and keep that stable (unchanging) for years to come. It's much harder to figure out all the functions an abstract IMatrix
interface should provide once and for all and keep that perfectly stable (unchanging) for years to come without facing temptations, if not outright needs, to add and remove and change functions.
So in appropriate cases, when your dependencies flow towards data instead of abstract functionality, your codebase will find fewer and fewer and fewer reasons to have to face central design changes with cascading effects and potentially big parts having to be rewritten. To direct dependencies towards data in that case is directing them towards stability. It's worth asking yourself as a developer whether the tendency in your system is for developers to add, change, and remove functions or to add, change, and remove data from components. If it's the former case, you might benefit greatly from an ECS engine.
If systems start to depend on each other a lot, that's directing dependencies away from data and towards functionality, and many of the maintenance benefits and the ability to reason about the correctness of your engine and easily keep it stable at the design level will be lost. Of course a pragmatic solution might sometimes call for a system calling a function in another every once in a while, but you should generally seek to keep that to a bare minimum. Instead of talking directly to each other, you can have systems modify and attach components to entities in a way such that other systems can then pick up those changes and react accordingly.
System Interaction
[...] the rendering system must know the data from the positional
component of an entity in order to draw it in a correct position. And
so on.
That it can grab from the ECS database, looping through entities with renderable and position components, just as the physics system before it might loop through entities with position components and modify their position. Generally each system fits into a basic loop model:
for each entity with the components I'm interested in:
do something with the components
... and you have to start thinking about doing things in passes, often multiple passes even if the intuitive solution is to do everything in one pass. For example, it might come more intuitively to loop through all your game entities and apply physics and respond to input and process AI and render them all in one go. That can minimize the amount of loops you have and also require less state. However, the ECS tackles this typically with multiple simpler passes and sometimes slightly more intermediary state to use from one pass to the next, but as a trade-off, it leads to a much easier system to maintain and one which is easier to change and potentially parallelize* and vectorize.
- As yuri mentioned, it could also make things harder to parallelize, at least across systems in an inter-system way, but could make things easier to parallelize in an intra-system way because it's easier to reason about the correctness of a parallel loop without locking if it's, say, making less state changes on the way and the code involved in the pass is much simpler. In my blunt opinion, it's often not worth multithreading the systems themselves so much as the loops they are performing inside for the most performance-critical systems.
Multiple, Simpler Passes
It's somewhat similar to GPU programming since GPUs aren't so good at doing complex things with each iteration, so they often excel instead at doing simple things per iteration that add up to a complex task after repeatedly running through the same data with multiple, simpler passes.
Unlike GPU programming, you can still potentially do much more complex things in a single pass, but each pass will represent like one logical thought: "for each of these components, apply physics", not both physics and rendering. The physics system performs its own pass just as the rendering system, living in its own isolated world, performs its own completely separate and detachable rendering pass. Each system lives in its own little world, seeing only the ECS database and being able to grab components and entities inside. They shouldn't have to bother with what other systems are doing.
In fact, in a well-designed ECS, you can remove any system from the engine and not have the codebase collapse horribly on itself because systems don't depend on each other to function. All they care about is the central database and the components (which are raw data) that they are interested in processing. They all live in their own isolated world. As a result you should be able to remove the physics system from your game, at which point motion components will cease to have physics applied, but everything else should keep on working just as before. It's extremely orthogonal in that respect.
Event-Driven Programming
Event-driven programming can be a bit awkward with ECS, but one straightforward way to solve that is to have event queue components. A system can push events to these queue components for another system to pop and process in a deferred fashion without the first system directly calling functions in the second. Again the bulk of your interactions should not be system->system, but system->ECS, and system->component.
DeadMG is spot on on the specifics of your code but I feel like it misses a clarification. Also I don't agree with some of his recommendation that don't hold in some specific contexts like most high performance-videogame development for example. But he's globally right that most of your code is not useful right now.
As Dunk says, Manager classes are called like this because they "manage" things. "manage" is like "data" or "do", it's an abstract word that can contain almost anything.
I was mostly in the same place than you around 7 years ago, and I started to think that there was something wrong in my way of thinking because it was so much efforts to code to do nothing yet.
What I changed to fix this is to change the vocabulary I use in code. I totally avoid generic words (unless it's generic code, but it's rare when you're not making generic libraries). I avoid to name any type"Manager" or "object".
The impact is direct in code: it forces you to find the right word corresponding to the real responsibility of your type. If you feel like the type does several things (keep an index of Books, keep them alive, create/destroy books) then you need different types that each will have one responsibility, then you combine them in the code that uses them. Sometime I need a factory, sometime not. Sometime I need a registry, so I setup one (using standard containers and smart pointers). Sometime I need a system that is composed of several different sub system so I separate everything as much as I can so that each part do something useful.
Never name a type "manager" and make sure all your type have a single unique role is my recommendation. It might be hard to find names sometime, but it's one of the hardest thing to do in programming generally
Best Answer
Indeed, in your design, the
SceneManager
acts as a container for global data. This is the inconvenience of a data centric design that sees data as a flat passive structure, and which leave to subsystems' functions the responsibility to manage the data.A first remark is that each of the subsystems has to know that your data is in an array of fixed size, and needs to know the number of active elements therein. This is a fatal dependency. If later you'd choose to use a dynamic vector, or a searchable unordered multimap (e.g. for targeting group of components corresponding to a game tag), you'd need to rewrite everything.
You therefore need a minimum of encapsulation for accessing to elements in the container (e.g. With an iterator like interface such as
first()
,next()
,previous()
,last()
). The containers would then remain private, and the access to the components would be controlled.Nevertheless this kind of architecture will be difficult to maintain. If you encapsulate data and the interface for accessing it, you are in an object oriented paradigm. So why not go for it ? Your
SceneManager
would the maintain a a container ofComponents
. The components would become more comprehensive entities, which know their own position and velocity, the other subsystems will then not know these internals, but use the object's interface in a well behaved fashion.If you want to know more about such approach, I'd recommend you Mike McShaffry's "Game Coding Complete". This book gives full insight about a game architecture, including hints about when to use object oriented features such as inheritance, and when rather avoid it.