10

Possible Duplicate:
What’s a way to implement a flexible buff/debuff system?

In the context of creating an engine for a RPG I want to implement a generic way to give effects / buffs / debuffs to the player, for instance:

strength boost : +15 to strength last for 10 mins
sickness : all attributes are divided by 2. Permanent until removed

Now the problem is that effects can stack upon each others but they can be removed independently. For instance you could have both above-mentioned effects and only remove the "sickness" one leaving the other one.

Are there any existing solutions to this problem? My idea is to always recompute the "final" statistics by applying each effect in chronological order to the "initial" stat, but I wonder: is there any downside?

Vaillancourt
  • 16,325
  • 17
  • 55
  • 61
lezebulon
  • 1,402
  • 1
  • 13
  • 18

3 Answers3

12

Given the fact that you have this sickness divisor, you'll get in trouble if you try to stack and destack your buffs chronologically. Arithmetic will trick you, e.g. this is what could happen:

        player base strength: 5
strength boost applied (+15): 20
       sickness applied (/2): 10
strength boost removed (-15): -5 (--> oops?!)

To avoid this problem I would maintain a simple std::list of all the active buffs, update them and recompute the stats each frame. This is what a single buff/debuff could look like, with stats an array of size STAT_COUNT:

struct Buff
{
    enum Type
    {
        Type_BonusMalus,
        Type_Multiplier,
    };

    Type type;
    int value[STAT_COUNT];
    float timeLeft; // FLT_INFINITY = permanent

    void updateAndApply(float dt, int* stats)
    {
        for (int i=0; i<STAT_COUNT; ++i)
        {
            switch (type)
            {
                case Type_BonusMalus: stats[i] += value[i]; break;
                case Type_Multiplier: stats[i] *= value[i]; break;
            }
        }
        if (timeLeft != FLT_INFINITY)
        {
            timeLeft -= dt;
        }
    }

    bool hasTimedOut()
    {
        return (timeLeft <= 0.0f);
    }
};

Let's assume that buffList is the current std::list<Buff>. Your update function would look something like this:

void updateAndApplyBuffs(float dt, int* stats)
{
    // set base stats
    setToBase(stats);

    // update & apply buffs
    std::list<Buff>::iterator it;
    for (it = buffList.begin(); it != buffList.end(); ++it)
    {
        it->updateAndApply(dt, stats);
    }

    // remove the ones that timed out
    // SuperCool® STL stolen from http://stackoverflow.com/a/596708/1005455
    buffList.remove_if(std::mem_fun(&Buff::hasTimedOut));
}

When your player picks up a new buff, you simply create it and push it to the back of the list. When you need to cancel a buff, remove it from the list. The buffs with a duration will time out by themselves after a while.

This will apply the buffs "chronologically", but I personally don't recommend this gameplay-wise. For instance the sickness divisor would be more or less penalizing depending on if you get it before or after a boost. To circumvent this issue, I would rather always apply the divisors/multipliers first, and the bonuses/maluses second:

void updateAndApplyBuffs(float dt, int* stats)
{
    // set base stats
    setToBase(stats);

    std::list<Buff>::iterator it;

    // apply multipliers first
    for (it = buffList.begin(); it != buffList.end(); ++it)
    {
        if (it->type == Buff::Type_Multiplier)
            it->updateAndApply(dt, stats);
    }

    // then apply bonuses/maluses
    for (it = buffList.begin(); it != buffList.end(); ++it)
    {
        if (it->type == Buff::Type_BonusMalus)
            it->updateAndApply(dt, stats);
    }

    // remove buffs that timed out
    // SuperCool® STL stolen from http://stackoverflow.com/a/596708/1005455
    buffList.remove_if(std::mem_fun(&Buff::hasTimedOut));
}

This should give you a solid basis, not over-engineered, not too hacky.

Laurent Couvidou
  • 9,171
  • 2
  • 41
  • 57
  • Ok thanks. Your first paragraph is something that I was aware of but I didn't really mention it clearly in my question. I don't understand why I'd need to split the addition / multiplication though, from a game POV doesn't it make more sense to have effects apply in their order of arrival? – lezebulon Jan 04 '13 at 14:40
  • How so? For instance you start at 10. Boost --> Sickness gives you (10+15)/2 = 12 Sickness --> Boost gives you 10/2+15 = 20 Wouldn't it be more logical? – lezebulon Jan 04 '13 at 14:47
  • @lezebulon I don't think so. Sickness becomes more or less penalizing depending on when you get it. It doesn't seem fair to me. But if that's what you want, you don't need that split between multipliers and bonuses/maluses. – Laurent Couvidou Jan 04 '13 at 14:52
  • So, just one for loop instead of 2, without the conditional on the buff type. – Laurent Couvidou Jan 04 '13 at 14:52
  • @lezebulon Edited my answer to try to clarify this point. – Laurent Couvidou Jan 04 '13 at 15:12
  • The whole problem can be avoided and allow you to remove arbitrary buffs quickly if all multiplications are based on the base rather than current value. This also simplifies various other balancing concerns. – Mooing Duck Jan 04 '13 at 19:14
  • @MooingDuck Indeed, that's my second option and that's what I recommend. – Laurent Couvidou Jan 05 '13 at 12:02
6

Typically, when you're talking about stat-management, there are a million ways to accomplish what you're looking to do, but also probably a few established techniques which are widespread.

Consider two players in PvP:

Their stats/levels are the same - they each have 20STR, and HP is based on STR * 10, Damage is STR * 2.

Also assume we're using an old school system, where current max_health and max_damage are caps which can be lowered when poisoned/cold, et cetera.

Lastly, consider division by X to be multiplication by 1/X.

Olag_the_Serious picked up a +15 strength amulet and then got sick. Jimmy_the_Geek got sick, but then picked up a +15 strength amulet.

Then they fight.

Olag's stats:

STR = (20 + 15) * 0.5 // 17.5
Health = STR * 10     // 175
Damage = STR * 2      // 35

Jimmy's stats:

STR = (20 * 0.5) + 15 // 25
Health = STR * 10     // 250
Damage = STR * 2      // 50

So now we've got this really unbalanced fight between these two characters who have done the exact same things, other than the order of picking up buffs.

How do we fix this?

Break your integer and fractional stats into two groups. Add the integers. Then take the final integer and use that as the basis for calculating your fractions.

var finalStat = 0,
    fractionalBaseline = 0,

    ints = [2, 15, -7],
    fractions = [0.2, -0.1, 4];

for each ints as int 
    fractionalBaseline += int;
end

finalStat = fractionalBaseline;

for each fractions as multiplier
    finalStat += Math.floor(frationalBaseline * multiplier);
end

Okay, so the PseudoScript is kinda ugly, but that's the idea.
Now, it doesn't matter what order anything happens in.
We've calculated the baseline with the ints, and then we use the baseline (rather than a running tally) to calculate fractions, which we then convert to ints (totally optional - some games only do that for the user-visible stats), and add them to the final stat.

You might think this is cheating the player, by not adding the fraction to the final tally. What it's really doing is giving everyone a fair experience, because now:

Olag:

STR = (20 + 15) * 0.5 // 17.5
Health = STR * 10     // 175
Damage = STR * 2      // 35

Jimmy:

STR = (20 + 15) * 0.5 // 17.5
Health = STR * 10     // 175
Damage = STR * 2      // 35

It's a fair fight.

Even better, when they get rid of their sicknesses, they'll go back to the same health -- removing an int is simple, but removing a fraction, you convert it to an int based on the current baseline (or float, if you're doing that) and subtract that number.

Just remember that whenever you add an int-bonus, that changes the baseline for the stat, so recompute the fractions, as well.
When you add a fraction, the baseline stays the same.

There's also nothing saying this can't be 100% OOP or Entity/Component-based. This is just the basic concept for arriving at how to have both Boots of Buttkicking (+3 against Butts) and Almost-Quad Damage (397%) in the same game, and have it be fair.

Norguard
  • 1,109
  • 7
  • 8
  • So basically your idea is to make the effects not really stack right? My initial idea was that if you get 2 consecutives effects that happen to you, then their effects gets computed according to their chronolical order. I actual had not thought about inventory effects, I was thinking only about spells effects that you receive – lezebulon Jan 04 '13 at 14:30
  • You can stack from there, but remember that BEDMAS still applies. – Norguard Jan 04 '13 at 14:40
  • @lezebulon Rules for stacking, and what does and doesn't stack get hairy, usually based on tables of what can and can't be included in that stack. In Blizzard games, you'll see "stacks with" or "will not stack with", etc. But for the basic functionality they're doing what I suggest. Otherwise, imagine WoW and a character in WoW - now in your number system, the order you put gear on a character matters. In my system, it does not. That's why this is baseline for any OO/Component/Entity system dealing with stats. Stack after this is right. – Norguard Jan 04 '13 at 14:50
  • If you want to go with order of arrival as being imperative (eg: sick people getting amulets being more powerful than people with amulets who get sick, despite all other stats being 100% the same), then a downside is going to be maintaining balance in multiplayer. If it's single-player, it'll mean a lot more play testing as doing side quests in a different order might now give you totally different stats than expected, and in a JRPG, that could mean a lot. It also means that difficulty will be more arbitrary. Stacking things like elemental damage, however, makes total sense. – Norguard Jan 04 '13 at 15:02
  • As I said I had not thought about inventory effects (ie amulet), I think they should be computed first without stacking indeed. For the rest (ie spell effects) I think they should stack on top of these – lezebulon Jan 04 '13 at 15:04
  • Next problem: Gaming the system. Get sick, take your clothes off, unbuff yourself, put your clothes on, buff yourself. You're now at a different number. This is why it's usually skills and abilities which stack. Things which happen, are computed once, and then go away. And why there are usually tables dictating what gets included in the stack calculations to maintain balance. – Norguard Jan 04 '13 at 15:09
  • So yes, stacking is totally doable, and for skills/spells it's fine. For inventory/stats/attributes/lasting-effects, the more permanent the effect, the more difficult it is to balance a stack. – Norguard Jan 04 '13 at 15:13
  • cf "Next problem: Gaming the system" . sorry I don't understand the example. With what I described you'd get the same stat regardless of the order you put your inventory on, but indeed different buff / debuff from your spells when you change inventory, which is ok for me – lezebulon Jan 04 '13 at 15:18
  • If you decided that you wanted a particular buff/bonus to stack, and you built reusable bonus components in a component or entity system (or even classes), it would take a lot more structuring to ensure that weapons with specific spell/status enchantments/curses (rather than simple stat-modifiers) wouldn't stack. Regardless of whether that's a non-issue, the game would still become a push to uber-buff, and the gameplay would develop into a group who write tutorials on the best orders to buff in, which would then become the de facto play style. – Norguard Jan 04 '13 at 17:55
2

I'm doing something similar at the moment; I've split effects into single-shot (your strength boost or sickness) and multi-shot (poison - takes X hitpoints each Y seconds). Build a class (if you're writing OO code - if not, it should not be too hard to get the idea) to manage the effects, and make it a property of each character. You can define IDs for each type of effect. For example

#define EFFECT_POISON 0
#define EFFECT_STRENGTH 1

and store them inside the Effects class inside a dictionary (keys = effect ids) of some sort. But really, any kind of iterable collection works - you can build a struct like

struct Effect
{
int EffectID;
int Duration;
int MultiShotFrequency;
}

and store one for each effect in a linked list for example). Every second of game time (I add up the time between frames until it's a full second) just iterate through the effects and see which one has to be applied.

Also expose public methods such as

//                      -1 for infinity?   0 for single shot?
ApplyEffect(int EffectID, int Duration, int MultiShotFrequency)
RemoveEffect(int EffectID)
//then you would go like:
ApplyEffect(EFFECT_POISON, 10, 1); //Poison each second for 10 seconds.
ApplyEffect(EFFECT_STRENGTH, -1, 0); //Strength boost of infinite duration (permanent), singleshot
RemoveEffect(EFFECT_POISON);

You can use floating points instead of integers if you want to apply effect each 0.5 seconds for example.