Component Based Entities - How interconnected is too interconnected?

Started by
7 comments, last by theOcelot 13 years, 11 months ago
Hey all, I'm working out a component based entity system for the Flash engine that I constantly rework after every successful project to address the shortfalls that came up during development. In the interests of re-usability I plan to make the switch. However, I'm wondering just how re-usable / interconnected components can / should be? It seems like in theory I should be able to take any component from any project, add it to a new project and it should "just work". I must be missing something though. Example: HealthComponent seems like something that would easily be re-usable. It has two properties, MaxHealth and CurrentHealth. It exposes a TakeDamage() method to modify those properties. Now I have a Fireball entity and when it hits something, it checks to see if that Entity has a HealthComponent to deal damage to. If it doesn't, it doesn't deal damage. Seems very straight forward. The problem is... Who is responsible for modifying that damage if need be? Say characters have a Fire Resistance attribute. A Fireball's damage should scale depending on that attribute. The Fireball could do it like such: EntityThatWasHit->HealthComponent->TakeDamage(25 * CollidedEntity.GetProperty("FireResistance")); But I don't think the Fireball should (or could) be responsible to know every single property that can modify damage. What if you're playing on Hard and all damage is doubled? What if there's a special boss monster that takes twice as much damage? All of those seem like the responsibility of the HealthComponent to manage. But if the next game doesn't have a Hard mode, or it doesn't have Fire Resistance, or it doesn't have bosses... it's no longer re-usable. Any insights? Apologies for the long ramble.
Advertisement
"Functional" is always better than "Reusable" - And reusability isn't as important as you think.

If you aren't sure which option is best, just pick one.
Instead of using Fireball, abstract the notion of health damagers into something like HealthDamager. Give each HealthDamager a map of damage types to potencies - so a fireball, for example, is simply a HealthDamager with a high fire potency. Then in TakeDamage, do whatever algebra you like to combine the entity's resistance attributes and the damager's potencies into a final health reduction value. This way, HealthComponents only need to know about HealthDamagers and their associated GetPotency(ResistanceType type) function, and HealthDamagers only need to know about their specific damage attributes.

To double fire damage, either halve the entity's fire resistance (for a debuff, for example) or double the damager's fire potency (for spell amplification and such).
I go even farther than Ariste. I just tell entities, "You came in contact with fire. Deal with it." And they work out their own reactions to it, with the help of a couple extra tools for doing the "algebra".
Quote:Original post by theOcelot
I go even farther than Ariste. I just tell entities, "You came in contact with fire. Deal with it." And they work out their own reactions to it, with the help of a couple extra tools for doing the "algebra".


I basically use the same system. No entity directly interacts with any other, they can only send messages which contain the relevant data. It's the entity's responsibility to respond correctly to its message queue.
Quote:Original post by theOcelot
I go even farther than Ariste. I just tell entities, "You came in contact with fire. Deal with it." And they work out their own reactions to it, with the help of a couple extra tools for doing the "algebra".


The only way I can think of to do this is to have custom Entity types each with a huge list of switch statements to parse the message. Would you mind sharing your approach? (Sorry, bit of a noob here ><)

Perhaps some kind of path evaluator or something...?
Reusability often means some sort of generalization. The less special an approach is, the more reusable it is. But at the same time often the overhead increases, at least for its management.

An example (code snippets are below):

Think on all the possible kinds of usages of things that may introduce damage. A sword may be used for striking or pricking (not sure whether these terms a correct w.r.t. weapons). A sabre is useable for striking. A club is useable for ramming/bumping (?). A fireball is blazing, and perhaps ramming (e.g. if it has a solid core). And so on, and so on. You don't want to hardcode them all due to reusability reasons, so you need to abstract them. Lets say there is a basic WeaponUsageKind class. It allows 2 things when being sub-classed and made effective: Being a tag and holding a catalog of (perhaps localized) textual descriptions.

If such an action was picked and executed, the belonging animation played, and no counter-action has taken place, then WeaponUsage instances are created. They allow to transport "damage stimuli".

An entity may have an SensitivityComponent, holding a list of Sensitivity class instances. A Sensitivity has to refer to the WeaponUsageKind instance for which it denotes a sensitivity. When a list of WeaponUsage instances is arrived at a Sensitivity, it is iterated to find all contained instances that refer to the same WeaponUsageKind as self does. If one is found, then its WeaponUsage::volume (if available) is used to determine whether the entity is in the vicinity range of the usage. If so (or none volume is given), then the usage "passes".

A Sensitivity can further be parametrized with a (chain of) Sensitivity::Filter instance(s). The WeaponUsage::potency is feeded into the chain, and a probably changed float comes out; this is finally used as damage and hence passed to the HealthComponent. Wearing an armor, for example, means to add a filter to the Sensitivities tagged with the usages for striking and pricking. Several sub-classes of Sensitivity::Filter can show different influences, of course.


As code it may look like:
class WeaponUsageKind {public: // informations   string const* designation() const;   string const* description() const;public: // registry   static set< WeaponUsageKind* > registry;};class WeaponUsage {public:   // tags the usage with the kind, allowing to identify its potential application   //   WeaponUsageKind const& kind;   // useable to detect whether sensible parts are in vicinity   //   Volume const* volume;   // a measure for the "violence" of the usage   //   float potency;};class Sensitivity {public:   void checkFor( list< WeaponUsage > const* usages );public: // filters   class Filter {   public:      virtual float filter( float value ) const =0;   };protected: // filters   list< Filter * > filters;};


Such a system doesn't prescribe kinds of damages. It allows different and even several damage stimuli to occur in dependence of the applied action. It allows to restrict the stimuli to a space volume, e.g. a "circle of impact" or something. It allows to have protection against damage.

However, it obviously introduces some overhead to manage all those instances. It further requires a couple of (virtual) routine invocations for each damage stimulus.

[Edited by - haegarr on April 29, 2010 4:50:33 AM]
Quote:Original post by GroZZleR
But I don't think the Fireball should (or could) be responsible to know every single property that can modify damage. What if you're playing on Hard and all damage is doubled? What if there's a special boss monster that takes twice as much damage?

All of those seem like the responsibility of the HealthComponent to manage. But if the next game doesn't have a Hard mode, or it doesn't have Fire Resistance, or it doesn't have bosses... it's no longer re-usable.


Turns out you need a generalized means to handle inputs and outputs (I'm having a hard time with the v2 of my entity system because of this).

Imagine the following cases:

  1. Player is silenced, thus he / she cannot cast spells (but can beat people).

  2. Player is pacified, thus he / she may not attack at all

  3. Player has a "fire damage weakness" debuff and receives 100 fire damage

  4. Player has a "shield" buff and receives 100 damage



1. Outbound messages. Player is trying to send a message categorized as "Spell" (or variations thereof). There must be some custom behaviour telling the system to discard the message before sending it, as the player is silenced. Ideally, you have a "Send Message Request" Message handler attached to the Player entity, and if the "request" is not cancelled, normal message dispatching happens. Other components, such as the "Silenced" component, make sure that whenever a "Send Message Request" message is sent to the entity, it gets cancelled. Basically, you let components alter outbound messages, in addition to inbound messages. However, this takes additional thinking and, as far as I'm concerned, it's not easy thinking.

2. See 1. Only this time you add handlers for a more generic category of messages (i.e. "Attack"). Still, it's easier to change criteria.

3. Inbound messages. Damage, Type = DamageType.Fire, Value = 100. Player has a FireWeaknessComponent, and a HealthComponent.

The following might be a message handler added by the FireWeaknessComponent:
function onDamage(globals, self, component, msg) {  if(msg.Type != DamageType.Fire)    return;  msg.Value *= 100;}


Question: how do we know we must increment the damage taken because of the debuff before actually making the player lose HP because of it?
One solution is to give each message handler a priority. Normal behaviour is usually priority = 100 (for example). Additional behaviour should be prioritized accordingly. onDamage, as seen above, should have a priority < 100, so we're sure that when its parent component is added, it is chained before the HealthComponent Damage message handler.

4. See 3. This time, ShieldComponent makes sure that any inbound Damage message has its Value field halved. You may also specify if buffs should be chained before debuffs etc. via handler priorities.

This is tough to model and even worse to code... hope this helped a bit.
Rainweaver Framework (working title)

IronLua (looking for a DLR expert)



Quote:Original post by faultymoose
Quote:Original post by theOcelot
I go even farther than Ariste. I just tell entities, "You came in contact with fire. Deal with it." And they work out their own reactions to it, with the help of a couple extra tools for doing the "algebra".


The only way I can think of to do this is to have custom Entity types each with a huge list of switch statements to parse the message. Would you mind sharing your approach? (Sorry, bit of a noob here ><)

Perhaps some kind of path evaluator or something...?


I call them Elements, but they're really just a list of string properties. When an entity receives an Elements from another entity, it just checks to see if the ones it knows how to deal with are there, and ignores any others. To simplify the checking, there are a couple extra classes named ElementalPredicate and ElementalFunction, each with an Eval function that takes a set of elements. A Predicate's Eval returns true or false based on whether the elements satisfy a specific set of properties, set in its constructor. Function's Eval returns a number based on a formula specified in its constructor.

Basically, it all comes down to, "Is this element that I know what to do with present?" If not, oh well. Nothing happens.

I wrote a documentation page for my project about it, here. You can just ignore the LayerOccupancy bits. Please ask questions about anything that's not clear.

This topic is closed to new replies.

Advertisement