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.
Introduction
Entity–component systems are an object-oriented architectural technique.
There is no universal consensus of what the term means, same as object-oriented programming. However, it is clear that entity–component systems are specifically intended as an architectural alternative to inheritance. Inheritance hierarchies are natural for expressing what an object is, but in certain kinds of software (such as games), you would rather express what an object does.
It is a different object model than the “classes and inheritance” one to which you’re most likely accustomed from working in C++ or Java. Entities are as expressive as classes, just like prototypes as in JavaScript or Self—all of these systems can be implemented in terms of one another.
Examples
Let’s say that Player
is an entity with Position
, Velocity
, and KeyboardControlled
components, which do the obvious things.
entity Player:
Position
Velocity
KeyboardControlled
We know Position
must be affected by Velocity
, and Velocity
by KeyboardControlled
. The question is how we would like to model those effects.
Entities, Components, and Systems
Suppose that components have no references to one another; an external Physics
system traverses all Velocity
components and updates the Position
of the corresponding entity; an Input
system traverses all KeyboardControlled
components and updates the Velocity
.
Player
+--------------------+
| Position | \
| | Physics
/ | Velocity | /
Input | |
\ | KeyboardControlled |
+--------------------+
This satisfies the criteria:
The systems are now responsible for handling events and enacting the behaviour described by the components. They are also responsible for handling interactions between entities, such as collisions.
Entities and Components
However, suppose that components do have references to one another. Now the entity is simply a constructor which creates some components, binds them together, and manages their lifetimes:
class Player:
construct():
this.p = Position()
this.v = Velocity(this.p)
this.c = KeyboardControlled(this.v)
The entity might now dispatch input and update events directly to its components. Velocity
would respond to updates, and KeyboardControlled
would respond to input. This still satisfies our criteria:
Here component interactions are explicit, not imposed from outside by a system. The data describing a behaviour (what is the amount of velocity?) and the code that enacts it (what is velocity?) are coupled, but in a natural fashion. The data can be viewed as parameters to the behaviour. And some components don’t act at all—a Position
is the behaviour of being in a place.
Interactions can be handled at the level of the entity (“when a Player
collides with an Enemy
…”) or at the level of individual components (“when an entity with Life
collides with an entity with Strength
…”).
Components
What is the reason for the entity to exist? If it is merely a constructor, then we can replace it with a function returning a set of components. If we later want to query entities by their type, we can just as well have a Tag
component which lets us do just that:
function Player():
t = Tag("Player")
p = Position()
v = Velocity(p)
c = KeyboardControlled(v)
return {t, p, v, c}
Interactions must now be handled by abstract queries, completely decoupling events from entity types. There are no more entity types to query—arbitrary Tag
data is probably better used for debugging than game logic.
Conclusion
Entities are not functions, rules, actors, or dataflow combinators. They are nouns which model concrete phenomena—in other words, they are objects. It is as Wikipedia says—entity–component systems are a software architecture pattern for modeling general objects.
Best Answer
To begin with, a caveat: even more than other domains of programming, every design decision in game development is a tradeoff. The "right" answer is "whatever works for the game you are making."
Okay. There are a few approaches we can take:
If there are a small, known set of general behaviors, write them inline and allow the user to tweak parameters. (i.e., which enemies does this tower avoid, how far away does it need to be, etc.) This is the most straightforward and maintainable approach, but it scales poorly the more different behaviors you have.
Functions can be data! In C, this is a function pointer, other languages have equivalents. The code that creates the entity is responsible for giving it a function telling it how to act.
In C it'd look something like this:
We actually have other options here as well. If you need to allow non-programmers to define behaviors, you could replace the function pointer with a behavior tree, compiled script code, etc. All of these are much more complicated, though, so don't bother unless you actually need the extra flexibility. Keep in mind that the more general you make the system, the harder it is to implement (and optimize), for both the engine and client code.