Status Effects (Buffs Debuffs) in an ECS Architecture

Recommended Posts

I'd like to implement status effects in an ECS architecture but I'm unsure of exactly how to do that. I seem to recall seeing an example where each status effect had an enum representing its effect, and an update function where the enum was switched and the effect was applied.

Share this post


Link to post
Share on other sites

The enum way seems to me pretty hand-job and not really OOP solution.

I think I would go for some basic base classes (maybe one or two) like "buff" and "debuff" and then use heritage and polymorfism.

With that you would have almost every case managed, and your player entity just will need to manage all the buffs/debuffs status in its Update.

Share this post


Link to post
Share on other sites

There is no standard "ECS" architecture - they vary wildly, and some are decent, others are terrible.

There's a pitfall in thinking that everything needs to fit into that one ECS system - for example, GUIs are usually not a good choice to fit into the ECS, but should exist outside of it. Even within an ECS, there are multiple sub-systems, and those systems usually aren't ECS's themselves - they aren't an ECS within an ECS.

So back to your question; what kind of buffs/debuffs are you talking about? How are you currently storing the variables that you want those buffs to interact with?

Share this post


Link to post
Share on other sites

The example you mentioned it not ECS-like in the slightest and IMO is the wrong way to go about it even if you aren't working with an ECS-like architecture. The way I would do it is to have a component per status effect/buff. This component would contain any state the status effect needs, eg. a countdown until the effect expires or how much damage something like a "damage reduction" buff has absorbed. When a status effect is applied to an entity, that entity gets a component. The component is removed when the status effect expires or is otherwise removed. Any update logic specific to the status effect is applied to all entities with the status effect component.

This is exactly the kind of thing that ECS is good at, provided your ECS allows you to think of "components" as small pieces of mostly self-contained state that can be added to and removed from entities at runtime. I don't think you can really get the benefits of ECS without supporting that kind of dynamic composition.

Edited by Oberon_Command

Share this post


Link to post
Share on other sites

Hm... I think I'd do the following.

Have a BaseStats component that has, I dunno, fields like meleeAttack, magicAttack, fireDefense, meleeDefense, that kind of thing.

A Modifiers component, with the same fields, except these are cumulative, and it has a modifiers list inside.

The list of modifiers is a list of functions that receive the original stats (or the entity altogether), and the Modifiers component itself.

Each function computes the modifier (using the BaseStats component as reference, or the entire entity and fetching other components as needed) and stores it in the destination component, which is our Modifiers component.

class Modifiers {
 float meleeAttack;
 float magicAttack;
 int luck;
 // etc
 List<Consumer<BaseStats, Modifiers>> modifiers;
}

class BaseStats {
 float meleeAttack;
 float magicAttack;
 int luck;
}

var cmp = new Modifiers();
// Say, artifact that absorbs 20% melee attack and turns it into magic attack 
cmp.modifiers.add((base, dst) -> {
  dst.meleeAttack += base.meleeAttack * -0.2;
  dst.magicAttack += base.meleeAttack * 0.2;
});
// And say we also have a charm that increases our luck.
cmp.modifiersList.add((base, dst) -> {
  dst.luck += 1;
});
// Adding the component to the entity.
modifiers.add(entityId, cmp);

// In the modifiers system.
for(var e : entities) {
 var mods = modifiers.get(e);
 var base = baseStats.get(e);
 // Clear last tick's computed modifiers.
 mods.reset();
 // Compute new ones.
 for(var mod : mods.modifiers) {
  mod(base, mods);
 }
}

/* 
* Now either have a FinalStats component that has the final values pre computed, 
* or do baseStats + modifiers each time you want to use any stat. 
*/

 

Edited by TheChubu

Share this post


Link to post
Share on other sites
11 hours ago, Tristan Richter said:

Oberon, what do you do, then, for stacking multiple component status effects of the same type?

 

8 hours ago, Hodgman said:

Instantiate multiple components of the same type? 

This.

Instead of making your status effects "components", per se, just have a flat array for each type of active status effects and give each status effect a handle to an entity. Work otherwise as I described.

Or, each status effect component could track a list of active status effects of that type:

struct DamageReductionComponent {
  struct Record {
    float timeExpiration;
    int maxReductionPerHit;
    int totalRemainingReduction;
  }
  std::vector<Record> records;
};

void update_damage_reduction(float currentGameTime, ComponentArray<DamageReductionComponent>& damageReduction) {
  damageReduction.erase_remove_if([currentGameTime](const DamageReductionComponent& component) {
    return std::all_of(begin(component.records), 
                       end(component.records), 
                       [currentGameTime](const DamageReductionComponent::Record& record) {
                         return currentGameTime >= record.timeExpiration || record.totalRemainingReduction == 0;
                       });
  });
}

void apply_damage_reduction(ComponentArray<DamageReductionComponent>& damageReduction, 
                             ComponentArray<SuccessfulHitComponent>& hits) {
  for (auto&& hitComponent : hits) {
    if (auto damageReductionComponent = damageReduction.try_and_get_mutable(hits.entity_of(hitComponent)) {
      for (auto&& reductionRecord : damageReductionComponent->records) {
        int damageToRemove = std::min(reductionRecord.maxReductionPerhit, hitComponent.damage);
        hitComponent.damage -= damageToRemove;
        reductionRecord.totalRemainingReduction -= damageToRemove;
      }
    }
  }
}

(note: I just woke up and wrote that from scratch, so it almost certainly has bugs, but it should get the idea across, the important thing is the way the data is structured)

Edited by Oberon_Command

Share this post


Link to post
Share on other sites
On 16/09/2017 at 4:08 PM, Tristan Richter said:

So then you map each entity (an integer) to a set of integers (indices in an array of components)?

I would probably use the entity ID as a foreign key inside the component structure. That's also what most of the "ECS" dogmatic blogs like T-machine seem to talk about doing.

e.g.

struct DamageOverTime
{
   int entity;//foreign key
   float damagePerSecond;
   float timeLeft;
};

struct DamageMessage { int entity; float damage; };

struct HealthSystem { void Process( const std::vector<DamageMessage>& ); };

struct DamageOverTimeSystem
{
  std::vector<DamageOverTime> components;

  void Process( float deltaTime, HealthSystem& health )
  {
    std::vector<DamageMessage > results;
    results.reserve(components.size());
    for(auto c = components.begin(); c != components.end(); )
    {
      results.push_back({c->entity, c->damagePerSecond * deltaTime});
      c->timeLeft -= deltaTime;
      if( c->timeLeft > 0 )
        ++c;
      else // fast erase
      {
        std::swap(*c, *(components.end() - 1));
        components.pop_back();
      }
    }
    health.Process(results);
  }
};

You can put as many components in there as you like for the same entity, and they will stack additively (two 3-damage-per-second components will do 6 damage per second).

If you wanted to change the game rules so that they stack multiplicatively (two 3dps components results in 9dps), that's a simple tweak:

  void DamageOverTimeSystem::Process( float deltaTime, HealthSystem& health )
  {
    //group components by entity
    std::sort(components.begin(), components.end(), [](const DamageOverTime& a, const DamageOverTime& b) {
        return a.entity < b.entity;   
    });

    std::vector<DamageMessage > results;
    results.reserve(components.size());
    //multiply together all the damage values for each entity
    int groupedDamage = 1;
    for(auto c = components.begin(); c != components.end() )
    {
      int entity = c->entity;
      c->timeLeft -= deltaTime;
      groupedDamage *= c->damagePerSecond;
      if( c->timeLeft > 0 )
        ++c;
      else
        c = components.erase(c);
      //end of a group of entities
      if( c == components.end() || entity != c->entity )
      {
        results.push_back({entity, groupedDamage * deltaTime});
        groupedDamage = 1;
      }
    }
    health.Process(results);
  }

In this example I've not given the DamageOverTime components their own component-ID. If you can somehow cancel these effects (e.g. killing a vampire removes every life-drain spell that they've cast) then you'd probably need component ID's too so that you can keep track of them.

Also note that you don't need a big framework to do ECS. I just did it in plain C++ in a few minutes and IMHO, writing ECS style code manually (without a framework) results in cleaner, more maintainable code and a better understanding of the data flow in your program :D

Share this post


Link to post
Share on other sites
On 10/11/2017 at 11:07 PM, Tristan Richter said:

Can someone please explain to me what the problem is with having a multidictionary that maps things to indexes of components in an array, instead of having a normal dictionary which maps each thing to an index of a component in an array?

A set of indices instead of a single index would imply a significantly greater complexity, e.g. because values have a variable size, with performance ranging between slightly worse and much worse.

But all suggestions so far simply don't need this kind of multidictionary (linking an entity to the set of its buffs) because they are about navigating the association only in the opposite direction (from a buff to the entity it affects), which is simpler, "more ECS", more flexible, etc.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now