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
Enemy?". If we allow multiple inheritance, the diamond problem arises. We could instead pull the combined functionality of
Enemy further up the hierarchy which leads to God classes, or we can intentionally leave out behaviour in our
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
Entity-Component Systems try to solve this problem by dividing the
Enemy object into different components - say
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
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?
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.