6
\$\begingroup\$

I'm planning to develop a game in C# (but I don't think that language matters) and I'll be using Entity Component System since I know by design that my game will have a lot of different game items with different twisted behaviours and logics. I have some experience using Unity ECS which I find very easy and intuitive. I've also programmed games in the past using the old "inheritance approach" and I found that pattern very not scalable.

As far as I know, a pure ECS (which is not what Unity implements) works like this:

A GameObject is just a collection on Components, it may be seen as a wrapper class or as a shared index in some parallel arrays of Components.

A Component is a collection of data (basically a record) which holds the data needed by its relative System to handle it. It does not implements game logic itself.

A System is a world object which loop through each GameObject which has a component it can process and update game logic of that component.

My point is: if the Systems are the ones responsible for the actual game logic update (practically speaking, the game logic code is written inside the system), is there a 1:1 relation between every component and every system ? Let me explain with an example: let's say I have 2 components:

  • RenderSpriteComponent
  • RenderLightSourceComponent

The first one does a simple 2D sprite rendering (using OpenGL or whatsoever); the second one send a light source to the rendering shaders. They do similar operations but with some actual differences. My question is: since they behaviour is different, should I implement two different Systems which will handle them separatly? If not, should I add a bunch of IF statements or overloads to handle those two components inside the same System?

From my point of view both ways seem unnecessary, especially if the ECS should improve code scalability. Using one of those approach would force me either to implement a different system for each component I have (implying a lot of GameObjects loops) or either to fill my Systems with if and overloads making them impossibile to understand after a certain point.

Furthermore, from what I can see and understand, Unity implements a different version of this pattern in which every component implements its own logic (in the Update method) and the GameObject is a wrapper class which has an internal list of components to update. Systems don't exist at all (or if they do they do, they do so for other reasons). This implementation looks easier and more "elegant" from my point of view, but I can't understand why it's not so spread and everyone sticks to the "classic" ECS implementation which has those "problems" I've described above. At the same time, I do not have control over the priority of the components updates, which can be useful in some cases.

I understand there isn't an "absolute" better way to design your game structure, but I'd like to know how is generaly implemented. Or which factors would make a way better than the other.

\$\endgroup\$

2 Answers 2

8
\$\begingroup\$

One of the big reasons some ECS proponents favour a strict separation of components and systems is that it helps architect game functionality in a data-oriented style.

In the ideal data-oriented flow, all your major systems work like particle systems: churning through big batches of data at a time that all need the same kind of operation performed on them. Each of these batch operations can be scheduled to run in a clear dependency order, to minimize doubling-back over the same content when something changes.

When done right, this can make excellent use of the processor's cache, for both instructions (the same few functions are called many times in a row, so you don't have to stall to look up the next step in the process), and data (working through a big batch of contiguous memory in-order means you're fully using the cache lines you pull in instead of taking one item from them and discarding the rest, and the access pattern is easy for the caching systems to predict to keep ahead of the processor).

This is important because one of the biggest bottlenecks in modern PCs is waiting on data that's not in the cache - main memory, fast as it is, just hasn't kept up to increases in CPU speeds & data bandwidth. See this excellent article on data locality for a more in-depth discussion of this topic.

This is in contrast to something like the Unity component system, where your component updates might be interleaved in an almost random order, so lots of memory gets pulled into the cache to be used once, then discarded because a series of unrelated components needed something different, then pulled back in when you get to another component with similar dependencies. In theory, we'd expect this to lead to a lot of CPU time burned waiting for needed memory to come in, though I'm not aware of any specific profiling that shows how much this manifests in typical Unity games.

The advantage of Unity's component-centric approach is that it's easy for even a novice to jump into without thinking deeply about architecture & dependencies, so it's fast to prototype and iterate within. And fortunately, modern hardware makes it "fast enough" for many kinds of games we want to make, on an impressive range of platforms. So I don't want to imply that this is a "wrong" way to do ECS - it's just a style that favours flexibility & expressiveness over absolutely maximizing throughput.

You can also selectively implement a more systems-focused style inside a Unity-style ECS for some subset of behaviour you've deemed to be a bottleneck - assigning one "master" component the role of updating a big batch of components of the same type. Or you could implement a Unity-style component-focused update loop inside a more strict ECS system, using it for gameplay scripts that need more flexibility than your main physics & rendering systems. So it's not an all-or-nothing decision, and many games hybridize these and other ECS strategies to make a style that works best for their developers.


Circling back to your earlier question about 1:1 relationships between systems and components: often we'll have a many-to-many relationship instead.

Take for example a Transform component that holds the location/orientation of an object in space.

  • Your physics system will want to act on collision representation & rigid body components to compute an updated Transform for each dynamic object

  • Your AI & game logic systems will want to query the Transform components of various objects to react to & interact with nearby objects and their gameplay components like health, collectable items, switches, etc.

  • Your rendering systems will want to read the final Transform of each object for the frame in order to draw it in the correct place, along with mesh and sprite components to be able to draw the right content for it, and lighting components to shade it correctly.

So here's a single component, shared by many systems, each of which also uses other components too.

One system using multiple component types does not necessarily imply branch-heavy code though: just because the system uses multiple component types does not mean they need to be delivered to it in one big unstructured blob. Your sprite rendering system might first...

  • ask for a list of all active sprites to frustum cull,
  • then ask for a list of all point lights that might affect them to subdivide them into batches by which lights are in play,
  • then ask for a list of all directional lights...

At each stage it's working through a collection of similarly-typed objects, performing similar actions, rather than trying to interleave all of these behaviours based on each component it encounters in some random order.

Some of these stages could be delegated to a distinct system, but you can choose to do that only to the degree that it helps make your code clear and enjoyable to work with - you're not under any strict obligation to define a new system for absolutely every sub-process. As long as the dependency order of each system's tasks are clear, you should be in good shape to get a smooth data-oriented flow, if that's what you're aiming for.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ Thank you for your detailed explanation. I think I'll go for an hybrid solution like suggestested. My main concern was to make sure I was using something "standard". In other words I wanted to be sure I'm not exploring new design patterns but rather using some consolidated ones. \$\endgroup\$
    – Andrew
    Commented Sep 5, 2017 at 11:14
  • \$\begingroup\$ How do you ensure that a system encounters a "valid" entity. i.e. a RenderingSystem may need a MeshComponent and TransformComponent etc. So how does the system combine relevant N components into a single view? An entity might have a MeshComponent but no TransformComponent? \$\endgroup\$ Commented Jan 7, 2020 at 21:49
  • \$\begingroup\$ One way this is sometimes done is to define a set of "archetypes" defined by which subset of component types they use. [Components of] entities of each archetype are stored in contiguous arrays. The process of looping over all entities with a specific combination of components is then a process of walking through each archetype that includes all of those components and processing every entity of that archetype. \$\endgroup\$
    – DMGregory
    Commented Jan 7, 2020 at 22:34
2
\$\begingroup\$

My point is: if the Systems are the ones responsible for the actual game logic update (practically speaking, the game logic code is written inside the system), is there a 1:1 relation between every component and every system ?

There are few ECS frameworks, like Svelto.ECS (which I wrote), where there isn't a direct correlation between Systems and Components. I first saw this concept in the Ash ECS and I extended it in Svelto.ECS. In Svelto.ECS Entities group components, but Engines know the entities components only through a node mapping. So it would be absolutely wrong to write a System for each component. Usually Systems query entities (and you can get the components from the entities), but with nodes you can do even better.

A similar question was asked before and someone actually gave a very good answer that I now use to save time:

It is common for "systems" (or "engines" as they are known in svelto) to need to access multiple components per entity. For example an EnemyEngine might need to access a Health component, an Ammo component and a Positioning component. This creates at least a 1-to-many relationship between Engines and Components. (In fact once you share components among several engines that turns into a many-to-many relationship - but only the 1-to-many side of it need be modeled).
Their solution to this problem is the Nodes concept. They represent that 1-to-many relationship as a Node object and that object might hold several components within it. Then they simply have a 1-to-1 relationship between Engines and Nodes. It also means an Engine need only manage a single object which fully embodies the relevant aspects of an entity.

I wrote a lot of articles about Inversion of Control and how ECS is the definitive pattern to exploit the principle. If you have the time, you may want to have a look a these articles. If you have questions, I will be happy to answer:

//Dissertation on how to exploit the ECS pattern to achieve Inversion of Control

http://www.sebaslab.com/the-truth-behind-inversion-of-control-part-i-dependency-injection/

http://www.sebaslab.com/the-truth-behind-inversion-of-control-part-ii-inversion-of-control/

http://www.sebaslab.com/the-truth-behind-inversion-of-control-part-iii-entity-component-systems/

http://www.sebaslab.com/the-truth-behind-inversion-of-control-part-iv-dependency-inversion-principle/

http://www.sebaslab.com/the-truth-behind-inversion-of-control-part-v-drifting-away-from-ioc-containers/

//how my ECS framework works in practice

http://www.sebaslab.com/ecs-1-0/

http://www.sebaslab.com/svelto-ecs-svelto-tasks-new-example-plus-whats-coming-and-optimization-related-thoughts/

\$\endgroup\$
4
  • \$\begingroup\$ This is a very link only answer. I suggest you write a comment to the question and invite the OP to visit your site for more details. \$\endgroup\$
    – Vaillancourt
    Commented Oct 27, 2017 at 17:19
  • \$\begingroup\$ @AlexandreVaillancourt done, maybe you want to remove the -1? \$\endgroup\$
    – sebas
    Commented Oct 28, 2017 at 13:06
  • 1
    \$\begingroup\$ I can't as I'm not the one who downvoted, sorry. \$\endgroup\$
    – Vaillancourt
    Commented Oct 28, 2017 at 15:58
  • 1
    \$\begingroup\$ Hey I just put a bit more forward that you're the author of the ECS. \$\endgroup\$
    – Vaillancourt
    Commented Oct 29, 2017 at 0:25

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .