Question

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?

Was it helpful?

Solution

ECS completely ruins data hiding. This is a trade-off of the pattern.

ECS is excellent at decoupling. A good ECS lets a move system declare that it works on any entity that has a velocity and a position component, without having to care about what entity types exist, or which other systems access these components. This is at least equivalent in decoupling power to having game objects implement certain interfaces.

Two systems accessing the same components is a feature, not a problem. It is fully expected, and it doesn't couple systems in any way. It's true that systems will have an implicit dependency graph, but those dependencies are inherent in the modeled world. To say that the damage system should not have the implicit dependency on the powerup system is to claim that powerups do not affect damage, and that is probably wrong. However, while the dependency exists, the systems aren't coupled - you can remove the powerup system from the game without affecting the damage system, because the communication happened through the stat component and was completely implicit.

Resolving these dependencies and ordering systems can be done in a single central location, similar to how dependency resolution in a DI system works. Yes, a complex game will have a complex graph of systems, but this complexity is inherent, and at least it is contained.

OTHER TIPS

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) as its arguments 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.

Licensed under: CC-BY-SA with attribution
scroll top