#Add stats that decay over time

1 messages · Page 1 of 1 (latest)

safe laurel
#

I've made a stats system for my game that allows me to modifiy a player's stats (like speed, health etc) with both flat and multiplicative values, which also allows you to specify a lifetime before the bonus disappears. However, I'd like to be able to create decaying bonuses as well, i.e. lasts for a lifetime then falls off linearly from 100% effect to 0% effect over a decay time. I've been stuck on this for a little while, and I can't think of a way to make it work with my current setup. Is anyone able to let me know if this is possible with how it's set up now, and how I might go about doing it?

This is how I've implemented the stats system, using classes for StatModifier and Stat, in which Stat contains a base value and a list of StatModifiers: https://paste.mod.gg/tkulirmvmyzz/0

Also worth noting, everything I've got so far has been tested and works

wind dagger
#

So here's what I'm seeing. You've these stat modifiers which seem unique enough that each one has its own value inside of your list of modifiers. The problem with decaying values is now you've this modifier that is ever changing which now requires you to Add/Remove each time it decays

hasty ruin
#

first thing i thought of would be something using an AnimationCurve with the same "lifetime" value you have here
but when applying the value, you'd use the initial value multiplied by the evaluated AnimationCurve at that time

wind dagger
#

Kinda loses that unique reference binding too if you don't want stacking decay values

hasty ruin
#

that would allow a lot of flexibility

wind dagger
#
public enum StatModifierType
{
    Additive,
    Multiplicative,
    DecayAdditive,
    DecayMultiplicative
}```
Actually that's a good idea. Just use the elapsed time to figure out the decay so you still only have that single modifier type
#

Well, I don't think you need specific Decay types here, but rather here

public float Value
    {
        get
        {
            float finalValue = BaseValue;
            foreach (StatModifier modifier in modifiers)
            {
                if (modifier.Type == StatModifierType.Additive)
                {
                    Debug.Log("Read Additive Modifier");
                    finalValue += modifier.Value;
                }
                else if (modifier.Type == StatModifierType.Multiplicative)
                {
                    finalValue *= modifier.Value;
                }
            }
            return finalValue;
        }
    }

Check if it has any decay multiplier and if it does then modify the stat value further

#

if decay multiplier == 0 -1, it doesn't decay

#
public class StatModifier
{
    public StatModifierType Type;
    public int Value;
    public float Lifetime;
    public float DecayMultiplier;


    public StatModifier(StatModifierType type, int value, int lifetime = -1, int decayMultiplier = -1 )
    {
        Type = type;
        Value = value;
        Lifetime = lifetime; //-1 means infinite time
        DecayMultiplier = decayMultiplier; //-1 means it doesn't decay
    }
}

some ideas

#

Well, but again there's a problem here:
#1446020553487024129 message
You only apply the stat updates when you call the Value property, so I'm not seeing where it's being modified between accessing it

safe laurel
#

I'm thinking I could theoretically make some kind of function to give the time since a certain point and when returning the value, use the time since the value was added, but I'm not sure how

wind dagger
#

It's a little tricky cause if you end up having a ton of modifiers like some diablo game then you're having to do full recalcs every frame or whenever

#

like your value property here I assume you only call once and cache it cause there's no reason to calculate fully per hit

safe laurel
#

The way I've been doing it I've been calling it every time I use it, is there a significant performance cost to doing that every frame?

#

If potentially there's a max of like 20 modifiers on any given stat

wind dagger
#

I would suggest keep your perma stat modifiers always cached, but time limited calcs / decays can be calculated much quicker as you're less likely to have a ton of those.

#

only problem with that idea is that if you do have additive time buffs then you wouldnt apply them to the base calc making them less powerful I guess

safe laurel
safe laurel
wind dagger
#

I think a lot of games I played much time stuff were multipliers probably to get around this problem lol

wind dagger
safe laurel
#

Oh I think if I were doing that I'd just make a temp value that calculates the total damage once first and then send that fixed value to each enemy

wind dagger
#

I'd probably just try updating it in fixed update for now and see how that goes

safe laurel
#

I'm still not sure how to actually go about the decaying part though

wind dagger
#

Well, since Value is constantly being updated in some loop now, you just need to decrement it by the delta

#

probably can remove the coroutine too for the time variables and do it all in update/fixed

safe laurel
#

What do you mean by "do it in fixed"? I'm not sure how that's possible with the current way I'm doing it

#

The goal of the method I'm using is to avoid modifying the actual value directly so that there's no conflict between order of applications and whatnot

wind dagger
#

Like I said, I assume you only call Value once after you add/remove a modifier to prevent needlessly updating your stats. However, your idea to decay these stats now breaks this optimization, requiring you to constantly update them each time they decay.

safe laurel
#

Currently I'm calling Value every time I use the value, I'm not cacheing it when it updates

hasty ruin
#

also btw, having a decay multiplier means it asymptotes at 0, it doesn't reach 0

safe laurel
wind dagger
#

Oh if you're calling it each time you attack then in that case you might as well just update them in a loop

hasty ruin
#

hm, do multipliers even work with deltaTime

wind dagger
#

I don't know too much of your game so I can't say the best way to go about it, but in my experience I've crashed many games by doing calcs per enemy hit ;p

safe laurel
#

It's only ever read from

wind dagger
#

Update the decaying values and everything else in a loop instead of having a value property to calculate your stats

safe laurel
wind dagger
#

that's the best solution you're going to get unless you do want to rid of the additive calc and make decaying values as a secondary calculation (multiplier)

hasty ruin
#

wouldn't it just have the same results as wronglerp

#

you'd need an Exp function to calculate it accurately

#

well, wronglerp is exactly a multiplier with deltatime.

#

oh, to be clear, i'm referring to the decayMultiplier mao had, not the stat multiplier

safe laurel
#

I'm not so much worried about getting it accurate right now, I'm still stuck on getting any kind of decaying value at all

hasty ruin
#

you should

#

if you pick an approach that makes it hard to get consistent results, you'll be adding more work for yourself later

safe laurel
hasty ruin
#

i don't really think it is tbh?

safe laurel
safe laurel
hasty ruin
#

you have pretty easy access

#

though, i see it's a class now, so i guess you can't update it

safe laurel
hasty ruin
#

yeah? if this were a struct, you could easily modify it in the coroutine (though it'd be harder to remove as a struct)

safe laurel
#

wait, which coroutine is this? i'm not sure I understand that message

hasty ruin
#

you only have one coroutine in the code you sent

safe laurel
#

the TempModifier

hasty ruin
#

yeah

#

ah shoot, indices will move around as you remove stuff, nvm

safe laurel
#

I don't think I can access the specific stat after it's been added to the list (since I can't use indices) though, right?
Like for a more simple example, if I had

testfunc(int number)
{
listOfNumbers.Add(number);
number += 5;
}

That obviously wouldn't actually change the value in the list, and I'm not sure how I could

wind dagger
#
public float Value { get; private set; }

public void FixedUpdate()
{
    float finalValue = BaseValue;
    foreach (StatModifier modifier in modifiers)
    {
        if (modifier.Type == StatModifierType.Additive)
        {
            Debug.Log("Read Additive Modifier");
            finalValue += modifier.Value;
        }
        else if (modifier.Type == StatModifierType.Multiplicative)
        {
            finalValue *= modifier.Value;
        }
    }
    Value = finalValue;
}

This would be my idea. Crude but that's what you'll need to sacrifice for the requirements. You'll want to also check the duration and change values depending on the decay cost so yeah youll need some more logic here.

hasty ruin
#

what i was thinking would be something like this

struct Modifier {
  public StatModifierType Type;
  public float Value;
  public float Lifetime;
  public AnimationCurve decay;
  internal float startTime;
  
  public float Evaluate() => decay.Evaluate((Time.time - startTime) / Lifetime) * Value;
}
wind dagger
#

I would probably make stat updates it's own update like maybe every 0.1 seconds

safe laurel
hasty ruin
#

you can't modify that because it's a value type

#

classes aren't the same

#

but yeah you wouldn't want to change the single instance of the modifier anyways

safe laurel
#

There's a seperate list of modifiers for each stat, of which there are multiple

#

I don't think that would actually work

wind dagger
safe laurel
wind dagger
#

But again I don't know the absolute requirements and only can think of how games have done it

hasty ruin
#

im not using a decay multiplier

#

-# holy shit why am i incapable of typing multiplier

wind dagger
#

right but what I'm saying. You still need to call evaluate each time it's used, but imagine something like a damage over time effect. You're constantly updating it on a frame basis.

safe laurel
#

@wind dagger I think you've got a different idea of what this is meant to be, there's meant to be multiple different stats that keep track of their own modifiers

wind dagger
#

I'd imagine here you'd have some HP degen and you want to be calling it constantly

hasty ruin
safe laurel
hasty ruin
#

you could have a wrapper struct to hold a reference to the class and a startTime separately though, i guess.

wind dagger
safe laurel
hasty ruin
hasty ruin
wind dagger
#

Any case, there's a bunch of ways to do this, but reducing how much you're calculating it is the real challenge and if you are making something intensive you'll see the issues yourself

safe laurel
hasty ruin
#

you could have a list of stats and then loop over that

#

you could have the logic be in Stat still, just that it'd be updated every tick

#

there's a ton of ways to achieve it

safe laurel
wind dagger
#

worst case scenario you do have 1000 modifiers, and you just shot a bomb into 1000 enemies

#

do you really want to calculate your stats 1000 times in that frame

safe laurel
#

I would just calculate the stats once, in the player's script, and send that calculated value to each enemy? I don't see why I'd need each enemy to run the calculation

wind dagger
#

That's some information I was missing but yes that's a good way to handle it

safe laurel
#

And if I had tick damage (as you mentioned earlier) I'd just calculate the damage to apply once and not worry about if the player stats changed during

#

Anyway I do have a solution for now so I'll mark as solved I think and if I run into anymore problems I'll come ask another question

#

Thank you both very much for helping!

hasty ruin
#

well, naively you might have something like this

Attack() {
  enemies = Boxcast(...);
  foreach (enemy in enemies) {
    enemy.Damage(GetDamage())
  }
}

GetDamage() {
  damage = baseDamage
  foreach (modifier in modifiers) {
    damage += modifier.value
  }
  return damage
}
```caching the value has to be an explicit choice
wind dagger
#

Yeah I'd probably have some bool in Value to check if it was calculated this frame and exit early if so

hasty ruin
#

or just...

safe laurel
hasty ruin
#
Attack() {
  enemies = Boxcast(...);
  damage = GetDamage();
  foreach (enemy in enemies) {
    enemy.Damage(damage)
  }
}

GetDamage() {
  damage = baseDamage
  foreach (modifier in modifiers) {
    damage += modifier.value
  }
  return damage
}
#

yeah

wind dagger
#

really if you want to be optimal. Just calculate your stats when you add/remove

#

but the again, the decay makes that hard

safe laurel
hasty ruin
#

i was thinking about supporting multiple instances of the same modifier

#

i don't know if that's necessary for your case

safe laurel
#

Wait that wouldn't work with a class?

hasty ruin
#

given that there's now a state, no

#

to be clear, the modifier itself can be a class, but the thing holding startTime should be a struct
those could be separate

safe laurel
#

Ok, I'll stick with using struct then for now