Isn't an Entity-Component System terrible for decoupling/information hiding?

by PawkyPenguin   Last Updated June 14, 2018 06:05 AM

The title is intentionally hyperbolic and it may just be my inexperience with the pattern but here's my reasoning:

The "usual" or arguably straightforward way of implementing entities is by implementing them as objects and subclassing common behaviour. This leads to the classic problem of "is an EvilTree a subclass of Tree or Enemy?". If we allow multiple inheritance, the diamond problem arises. We could instead pull the combined functionality of Tree and Enemy further up the hierarchy which leads to God classes, or we can intentionally leave out behaviour in our Tree and Entity classes (making them interfaces in the extreme case) so that the EvilTree can implement that itself - which leads to code duplication if we ever have a SomewhatEvilTree.

Entity-Component Systems try to solve this problem by dividing the Tree and Enemy object into different components - say Position, Health and AI - and implement systems, such as an AISystem that changes an Entitiy's position according to AI decisions. So far so good but what if EvilTree can pick up a powerup and deal damage? First we need a CollisionSystem and a DamageSystem (we probably already have these). The CollisionSystem needs to communicate with the DamageSystem: Every time two things collide the CollisionSystem sends a message to the DamageSystem so it can subtract health. Damage is also influenced by powerups so we need to store that somewhere. Do we create a new PowerupComponent that we attach to entities? But then the DamageSystem needs to know about something it would rather know nothing about - after all, there are also things that deal damage that can't pick up powerups (e.g. a Spike). Do we allow the PowerupSystem to modify a StatComponent that is also used for damage calculations similar to this answer? But now two systems access the same data. As our game becomes more complex it would become an intangible dependency graph where components are shared among many systems. At that point we can just use global static variables and get rid of all the boilerplate.

Is there an effective way to solve this? One idea I had was to let components have certain functions, e.g. give the StatComponent attack() which just returns an integer by default but can be composed when a powerup happens:

attack = getAttack compose powerupBy(20) compose powerdownBy(40)

This doesn't solve the problem that attack must be saved in a component accessed by multiple systems but at least I could type the functions properly if I have a language that supports it sufficiently:

// In StatComponent
type Strength = PrePowerup | PostPowerup
type Damage = Int
type PrePowerup = Int
type PostPowerup = Int
attack: Strength = getAttack //default value, can be changed by systems
getAttack: PrePowerup

// these functions can be defined in other components or in PowerupSystems
powerupBy: Strength -> PostPowerup
powerdownBy: Strength -> PostPowerup
subtractArmor: Strength -> Damage

// in DamageSystem
dealDamage: Damage -> () = attack compose subtractArmor compose hurtSomeEntity

This way I at least guarantee correct ordering of the various functions added by systems. Either way, it seems I'm rapidly approaching functional reactive programming here so I ask myself whether I shouldn't have used that from the beginning instead (I've only just looked into FRP, so I may be wrong here). I see that ECS is an improvement over complex class hierarchies but I'm not convinced it's ideal.

Is there a solution around this? Is there a functionality/pattern I'm missing to decouple ECS more cleanly? Is FRP just strictly better suited for this problem? Are these problems just arising out of inherent complexity of what I'm trying to program; i.e. would FRP have similar issues?

Answers 1

There is almost no way get around the fact that a system needs to access multiple components. In order for something like a VelocitySystem to work, it would probably need access to a VelocityComponent and PositionComponent. Meanwhile the RenderingSystem also needs to access this data. No matter what you do, at some point the rendering system needs to know where to render the object and the VelocitySystem needs to know where to move the object to.

What you require for this is the explicitness of dependencies. Each system needs to be explicit about what data it will read and what data it will write to. When a system wants to fetch a particular component, it needs to be able to do this explicitly only. In its simplest form, it simply has the components for each type it requires (e.g. the RenderSystem needs the RenderComponents and PositionComponents) and returns whatever it has changed (e.g. the RenderComponents only).

This way I at least guarantee correct ordering of the various functions added by systems

You can have ordering in such a design. Nothing is saying that for ECS your systems must be independent of order or any such thing.

Is FRP just strictly better suited for this problem? Are these problems just arising out of inherent complexity of what I'm trying to program; i.e. would FRP have similar issues?

Using this Entity-component-system design and FRP isn't mutually exclusive. In fact, the systems can be seen as nothing else as having no state, simply performing data transformations (the components).

FRP would not solve the problem of having to use the information you require in order to perform some operation.

Athos vk
Athos vk
June 13, 2018 14:16 PM

Related Questions

Entity Component System Coupling

Updated May 26, 2018 15:05 PM


Updated April 09, 2018 13:05 PM