ECS makes developing a game (or other any complicated system of systems) easier. Each system operates on a small number of components and components provide a clear data oriented interface to the system.

For example, I have a 'ScreenElement' component which describes a location on the screen. I also have a 'Position' component that describes a world location in 3D space. Then there are multiple systems that either consume or modify elements that have ScreenElements or Positions.

ScreenElements are relative locations on the screen, e.g. this text should be 10 pixels from the top border of that sprite. So, I wrote a ScreenElementProcessor system that looks at every entity with a ScreenElement. It doesn't care what other components exist on that entity. It doesn't matter if there is a Sprite, Text, Dropdown, Clickable, etc. component as well. The ECS architecture makes it easy to only load the ScreenElement data and act on it. Then ScreenElementProcessor iterates through every ScreenElement in hierarchical order (everything in depth 0, then everything in depth 1, etc.) and calculates the correct the screen position extents.

My sprite renderer system can render the UI by iterating over every entity with a Sprite and a ScreenElement then render the Sprite at the extent position in the ScreenElement.

Each system is relatively simple, which makes it easy to be robust and reliable. Similarly, it's easy to add new systems. The new system can consume data from any existing component, and if necessary, add a new component with full confidence that no other existing system already uses this component.

The biggest danger is if there are multiple separate systems that modify the same component. For example, I have a RenderLayer system that checks the current game screen being displayed and hides anything with a ScreenElement that shouldn't be displayed. However, it does so by setting the ScreenElement component .isVisible variable to false. But sometimes I also want to show/hide UI elements based on user interaction, such as a dropdown menu. If I create a dropdown menu and add a RenderLayer component, then either the dropdown system or the render layer system will clobber the changes of the other one depending on the order of systems in the main loop.

There are several options for working around this system conflict. One is to make sure only one system has modification authority over a given component, every other system should be read-only. However, this can limit the capabilities of other systems, like the render layer system.

Two would be to include warnings to avoid using two incompatible components together, e.g. the simplest version would be a comment on both render layer and dropdown that they modify the screen element visibility component, so an entity shouldn't have a RenderLayer and Dropdown. However, unless you go to extra effort to include compile-time checks, it would be possible to create bugs by accidentally using both without double checking the restrictions.

Third is increasing the complexity of the separate systems by making them check for the components of each other. For example, the render layer system would have to check the dropdown component and use it if it exists to avoid overwriting the dropdown preferences. This is bad because it can create an exponential level of complexity where each new system and component increases the complexity of every interacting system before it.

The fourth option is to create event entities. They are entities with a one-time use component that informs a separate system to make a change. For example, I have multiple systems that affect the Health of an entity. An entity could heal over time, be healed or hurt by a skill, be taking environmental damage, etc. However, I don't want all those systems making separate modifications to the Health component. So, instead, they create a new entity with just a HealthChangeEvent component which contains the target entity and the health to be changed. Then the HealthSystem consumes all the HealthChangeEvents, applies the necessary modifications, updates the appropriate UI elements, and deletes the event entity. This approach adds some complexity of managing the events, but it is a great option for a system that needs many small changes from many different systems.

I use most of these approaches in different places depending on the system. There's no one ideal solution, but in my experience all of these approaches are better than trying to create systems using class inheritance.

Back to Home