RPGSystems: Stat System 08: Updating Stat Modifiers

In this section we’re going back to our previous implementation of our stat modifiers. The method we used with our last go at the stat modifiers used a Enum to list all the types of stat modifiers available while hard coding how each modifier would affect the stat’s value in the RPGStatModifiable class. This method limits the flexibility of the stat modifier system by making it more difficult to add additional modifiers. Now we’re going to recreate our stat modifiers to use a more friendly method to allow for an easier time to create new stat modifiers while allowing us to add some additional functionality

Reworking the Stat Modifier

The first major change is to modify our RPGStatModifier class. This class will be changed into an abstract class with all the functionality that modifiers are expected to contain. Any new stat modifier will inherit from this class. (Below we’ll inherit from the RPGStatModifier to recreate our previous four stat modifier types)

public abstract class RPGStatModifier {
    private float _value = 0f;

    public event EventHandler OnValueChange;

    public abstract int Order { get; }

    public float Value {
        get {
            return _value;
        }
        set {
            if (_value != value) {
                _value = value;
                if (OnValueChange != null) {
                    OnValueChange(this, null);
                }
            }
        }
    }

    public bool Stacks { get; set; }

    public RPGStatModifier() {
        Value = 0;
        Stacks = true;
    }

    public RPGStatModifier(float value) {
        Value = value;
        Stacks = true;
    }

    public RPGStatModifier(float value, bool stacks) {
        Value = value;
        Stacks = stacks;
    }
    
    public abstract int ApplyModifier(int statValue, float modValue);
}

In this code we’re adding a lot of functionality that you should take notice of, along with a few things that have been removed from the class. The two things that have been removed are the StatType property and the enum that listed all the stat modifiers. These two elements will no longer be needed.

The first major change is that the RPGStatModifier now contains a OnValueChange event and it is triggered from the Value property. This change will allow later on for external scripts to modify the stat modifier’s value while it’s attached to a stat and the stat will automatically update its value to reflect the changes. (This will add some additional functionality later on when we create spell buffs)

Another major change is the abstract Order property, this will effect in what order the extended classes will be applied to the stat. When creating new stat modifier you will need to take care to not use the same order value on different modifiers as when we calculate the stat’s value it can make mistakes. (Use of the order property will be shown below)

The next change is the Stacks property, this will determine if similar modifiers (Modifiers with the same order value) will have their value combined together or if the single modifier will be used. A quick example would be: if there was 4 total modifiers of the same type. Three modifiers have a value of 3 and Stacks is set to true. The other modifier has a value of 10 and Stacks to false. The value used while calculating the total value given from the modifiers would be the single modifier with the value of 10. This is cause the 3 stacking modifier total value would be equal to 9. This small additional feature will allow for items/buffs to have non stacking values.
Finally the ApplyModifiers method will be used to calculate the value that the modifier will add onto the stat’s value. Below you will see how this method is used, but for now , the mod value parameter is the sum of all stacking modifiers of the same type or the highest non stacking modifier. The statValue parameter is the stat’s base value with all previous modifiers added into to the stat.

Extending The Stat Modifier

Next we’ll cover the creation of the four previous stat modifiers that we created in out last version. We’ll be creating four new c# classes that all inherit from RPGStatModifier.

Create a new C# script called RPGStatModBasePercent

public class RPGStatModBasePercent : RPGStatModifier {
    public override int Order {
        get { return 1; }
    }

    public override int ApplyModifier(int statValue, float modValue) {
        return (int)(statValue * modValue);
    }

    public RPGStatModBasePercent(float value) : base (value) { }
    public RPGStatModBasePercent(float value, bool stacks) : base(value, stacks) { }
}

Create a new C# script called RPGStatModBaseAdd

public class RPGStatModBaseAdd : RPGStatModifier {
    public override int Order { get { return 2; } }

    public override int ApplyModifier(int statValue, float modValue) {
        return (int)(modValue);
    }

    public RPGStatModBaseAdd(float value) : base (value) { }
    public RPGStatModBaseAdd(float value, bool stacks) : base(value, stacks) { }
}

Create a new C# script called RPGStatModTotalPercent

public class RPGStatModTotalPercent : RPGStatModifier {
    public override int Order {
        get { return 3; }
    }

    public override int ApplyModifier(int statValue, float modValue) {
        return (int)(statValue * modValue);
    }

    public RPGStatModTotalPercent(float value) : base (value) { }
    public RPGStatModTotalPercent(float value, bool stacks) : base(value, stacks) { }
}

Create a new C# script called RPGStatModTotalAdd

public class RPGStatModTotalAdd : RPGStatModifier {
    public override int Order { get { return 4; } }

    public override int ApplyModifier(int statValue, float modValue) {
        return (int)(modValue);
    }

    public RPGStatModTotalAdd(float value) : base (value) { }
    public RPGStatModTotalAdd(float value, bool stacks) : base(value, stacks) { }
}

The main sections of these scripts to take notice of are the implementations of the Order properties and the ApplyModifier properties. Next we’ll updated the remaining code to make use of our new Stat Modifiers.

Updating RPGStatModifiable

Before we dive into our changes in the stat modifiable class we need to make a small change to the IStatModifiable interface. Add the following line to the interface:

    void RemoveModifier(RPGStatModifier mod);

Below we will be also adding the functionality to remove individual stat modifiers from the stat without having to clear out all modifiers and re add the ones we want.

Next to change the RPGStatModifiable class. (Below is the complete class for the RPGStatModifiable, some code will not change from the previous verson.)

public class RPGStatModifiable : RPGStat, IStatModifiable, IStatValueChange {
    private List<RPGStatModifier> _statMods;

    private int _statModValue;

    public event System.EventHandler OnValueChange;

    public override int StatValue {
        get { return base.StatValue + StatModifierValue;}
    }

    public int StatModifierValue {
        get { return _statModValue; }
    }

    public RPGStatModifiable() {
        _statModValue = 0;
        _statMods = new List<RPGStatModifier>();
    }

    protected void TriggerValueChange() {
        if (OnValueChange != null) {
            OnValueChange(this, null);
        }
    }

    public void AddModifier(RPGStatModifier mod) {
        _statMods.Add(mod);
        mod.OnValueChange += OnModValueChange;
    }

    public void RemoveModifier(RPGStatModifier mod) {
        _statMods.Add(mod);
        mod.OnValueChange -= OnModValueChange;
    }

    public void ClearModifiers() {
        foreach (var mod in _statMods) {
            mod.OnValueChange -= OnModValueChange;
        }
        _statMods.Clear();
    }

    public void UpdateModifiers() {
        _statModValue = 0;

        var orderGroups = _statMods.OrderBy(m => m.Order).GroupBy(m => m.Order);
        foreach(var group in orderGroups) {
            float sum = 0, max = 0;
            foreach(var mod in group) {
                if(mod.Stacks == false) {
                    if(mod.Value > max) {
                        max = mod.Value;
                    }
                } else {
                    sum += mod.Value;
                }
            }

            _statModValue += group.First().ApplyModifier(
                StatBaseValue + _statModValue, 
                sum > max ? sum : max);
        }
        TriggerValueChange();
    }

    public void OnModValueChange(object modifier, System.EventArgs args) {
        UpdateModifiers();
    }
}

Two major changes were made within the class. The first change is the UpdateModifiers method, we updated the class to use Linq to order the modifiers their Order property, then group them also by their Order Property. Then we simply loop through all the grouped modifiers and find the sum of all stackable modifiers and the max value of all not stacking modifiers. Finally we determine which value is greater of the sum and max values then pass that value along with the current stat value to the ApplyModifier of the modifier of the current group.

The other major change is we add code to listen to the stat modifier’s new OnValueChange event in the AddModifier, RemoveModifier and the ClearModifiers methods. Then in our new OnModValueChange method, whenever a modifier’s value changes we will update the value of the stat.

Adding Modifiers

After our changes, how we add modifiers to a stat will slightly change, but mostly by making them easier to add. All you have to do is add the modifier of the type you want and pass the value and if it stacks.

var health = stats.GetStat<RPGStatModifiable>(RPGStatType.Health);
health.AddModifier(new RPGStatModBasePercent(1.0f));
health.AddModifier(new RPGStatModBaseAdd(50f));
health.AddModifier(new RPGStatModTotalPercent(1.0f, false));
health.UpdateModifiers();

Helpful Reads


Microsoft Documentation: EventHandler Delegate
Microsoft Documentation: Events Tutorial
Event Implementation Fundamentals
Getting Started with LINQ in C#

Github Repository

https://github.com/jkpenner/RPGSystemTutorial

Leave a comment