The thing about splitting is that it should be for the right reasons. In general, DRY (don’t repeat yourself) is not a good reason on its own… though it’s one that gets software developers in trouble in lots of ways.
To me, it’s important to remember the main features of ES as your driving motivators. The main one I keep coming back to in design is: easily add new ‘systems’ without affecting other systems.
This implies a lot of things. One, your systems should be as independent as possible. If you require a lot of interaction between 3 different systems and it’s not in the form of waterfall side-effects then chances are that those aren’t really different systems. Whenever I see threads about “Some event framework to let my systems communicate” I always want to drill into that because it’s a ‘bad smell’ from an ES perspective. Likely, those are one system that has been erroneously split out of some non-ES pseudo-need.
Also, I like to think of the entity systems as managing the state (and low level rules) of the world… not necessarily the game logic. The fact that there is a fire and it’s burning everyone standing in it is part of the game world state and low level game world rules. That casting a fireball level 8 spell created this fire, deducted mana, took spell ingredients, etc… that’s game logic. That’s the part you code in more traditional ways.
So maybe let’s tease out the spell example a little bit and see
Let’s say we have a player entity and some kind of ‘tool/spell/usable thing’ entities attached to that player. What might they look like? I’m going to spitball for a second so hopefully I don’t design myself into a corner. (I’m going to use a sort of pseudo-groovy style for representing components to try to be clear without having to define them all.)
fireballSpell = [ActionFor(actor:player, enabled:true), Name(“Fireball”)]
This can be used to present a list of ‘things the player can do’ to the player… either as icons or whatever. It’s up to the game logic to decide what to do when it’s enabled and executed. That’s a UI level system and a player-actor level system depending on whether single or multiplayer.
In this case, let’s pretend that the fireball spell is an instant effect and just plops a fire entity down somewhere.
fire = [Position(spot on the ground), Range(some radius), Damage(type:Fire, power:10)]
We have a few options here. We could have a special system that watched just position + range and created linking entities between them. The the damage system could watch for these… but it’s an extra level of indirection that feeds our sense of DRY without actually feeding the needs of the ES.
Instead, we could have a AOE damage system that keeps track of two entity sets:
- entities with Position, HitPoints… no reason to apply damage to things that can’t be damaged.
- entities with Position, Range, Damage
The system does all of the work of either spatial indexing or not based on the scale of the game. Let’s pretend we’ll only ever have a few hundred AOEs at a time so we can just track them in the standard way.
Whenever something from set (1) moves we see if it’s entered or exited one of our AOEs. If it (say the player) enters, we attach a Damage entity to it:
fireDamange = [TargetTo(target:player), Damage(type:Fire, power:10)]
If any of the entities in the second set are removed from the set then we remove any damage objects we’ve attached to entities in that AOE. (Or set them to decay.) This is the same as if the entity exited the AOE.
So if an entity disappears from set (1) or leaves the range of an entity in set (2) or is in the range of something in that is removed from set(2)… we add a decay component to the damage entity we created before.
fireDamage += Decay(0.1) or whatever… or we just delete the entity.
Some other damage applier system is responsible for taking all damage entities and applying them to object hitpoints. The principle here is that we only want one thing modifying hitpoints… so all hitpoint adjustments whether positive or negative should be attached to the player as either damage or healing entities. So the HealthDamageSystem or whatever name has at least one entity set so far in this discussion (health application is left as an exercise for the reader)
- entities with TargetTo and Damage.
At an interval that the game decides (probably not once a frame but maybe) the system loops over all of its entities, grabs the entity specified in TargetTo, and applies the damage. At this time, it can keep track of any buffs the player might have, etc. against fire damage, magic damage, immunity to heat, whatever. Every interval it will keep applying damage until the TargetTo, Damage entity has disappeared from its set.
Here we have represented most of the game-level ‘rules’ for AOE spells. Lots of other systems could be added that deal with visualization and sound. All of the information is there for them. They just have to watch the proper entity sets.
Furthermore, we can damage any object in any way we choose just by attaching a damage entity to it. We don’t have to worry about how buffs are handled, whether healing is applied before damage, etc… there is some system that has already codified that set of world rules.
If we want to add a poison gas AOE then we add another damage type. Maybe poison gas also decreases visibility or something and that’s a brand new feature that can now be easily added without touching any of the existing systems. Just have a system that watches for Range and Damage of type:PoisonGas.
This approach also nicely damages any object that can be damaged. If wooden chairs and tables are in the fire AOE then they can be burned if they have hitpoints. NPCs, MOBs, whatever all handled.
“What about spell cool down?” (the original issue)
This could be done in two ways… either add another AvailableAtTime() component or create a whole other ‘entity attached to the spell’ that can be part of a system that watches those entities and enables/disables the target entity based on cool down time. The first is simpler… always go with simpler first.
“What about NPCs? Do I have to keep an active spell entity set per NPC?”
Nope. Part of AI (which will be a big system on its own) will be first setting up the world state for the AI ‘brains’ at the beginning of every AI interval. So you can run through ALL of the enabled spell entities in the whole game and simply mark them as usable in the local memory for that AI entity. This is all internal to the AI system and is part of any of the other half-dozen world state gathering entity sets you might have for the AI actors. Else for a relatively low number of actors, you could just query it when needed.
Ok… that’s enough of my rambling. I’m not going to check the spelling or coherence because I’ve already wasted one can of soda on this.