10
\$\begingroup\$

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?

\$\endgroup\$
0

3 Answers 3

12
\$\begingroup\$

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.

\$\endgroup\$
7
  • \$\begingroup\$ 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? \$\endgroup\$
    – lezebulon
    Commented Jan 4, 2013 at 14:40
  • \$\begingroup\$ 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? \$\endgroup\$
    – lezebulon
    Commented Jan 4, 2013 at 14:47
  • \$\begingroup\$ @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. \$\endgroup\$ Commented Jan 4, 2013 at 14:52
  • \$\begingroup\$ So, just one for loop instead of 2, without the conditional on the buff type. \$\endgroup\$ Commented Jan 4, 2013 at 14:52
  • \$\begingroup\$ @lezebulon Edited my answer to try to clarify this point. \$\endgroup\$ Commented Jan 4, 2013 at 15:12
6
\$\begingroup\$

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.

\$\endgroup\$
9
  • \$\begingroup\$ 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 \$\endgroup\$
    – lezebulon
    Commented Jan 4, 2013 at 14:30
  • \$\begingroup\$ You can stack from there, but remember that BEDMAS still applies. \$\endgroup\$
    – LetterEh
    Commented Jan 4, 2013 at 14:40
  • \$\begingroup\$ @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. \$\endgroup\$
    – LetterEh
    Commented Jan 4, 2013 at 14:50
  • \$\begingroup\$ 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. \$\endgroup\$
    – LetterEh
    Commented Jan 4, 2013 at 15:02
  • \$\begingroup\$ 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 \$\endgroup\$
    – lezebulon
    Commented Jan 4, 2013 at 15:04
2
\$\begingroup\$

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.

\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged .