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?