Search Unity

Discussion Damage Modifiers

Discussion in 'Scripting' started by BlackSabin, Feb 23, 2024.

  1. BlackSabin

    BlackSabin

    Joined:
    Feb 27, 2021
    Posts:
    75
    I'm creating an RPG which will be dealing with multiple damage types and a number of different effects that the character can gain/lose. An easy one to add is something like "bonus damage", but the question comes how to implement it.

    The most direct method I can think of is to have whatever it is that is giving the bonus damage adds it's bonus to a centralized place where the numbers are then kept track of. Say you deal 10 fire damage and you get a skill "Hotter Flames" that gives an extra 20% to fire damage. The skill would tell a central script to multiply all fire damage by 1.2, and that is that. But what if these bonuses become more nuanced?

    I recognize this becomes a bit more niche, but I like the idea of a wide variety of interesting effects to be stacked and added together to create fun builds; so in creating that system, what if the extra 20% fire damage was just for magic attacks? Well now that's a whole other variable to track, and now we might be tracking a lot of extra things. Maybe a skill gives extra damage to specific archetypes of enemies; say goblin-kin take the extra damage. Now the effect has to figure out who is receiving the damage, what source of the damage is from, and then calculate the changes.

    Again, the most direct method I could imagine is to save all this to a number of "bonusDamagePercentFor___" variables, which could get extravagantly long depending on how many different things could be affected. I'm only making a small game currently, but at some point the "dream game" I wish to make will have many such concerns. My current solution is using a list of delegates that take all the information and spit back out a modified damage value, but this turns in to a lot of calculation and overhead for an attack so I'm also figuring out a way of caching the damage whenever a change is made.

    How would you tackle the issue?
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,666
    It feels like some kind of "pipeline" or "bus" system is good for these things, something that starts with a base amount of damage (from the weapon or spell, for instance), and creates some kind of transient object that is passed along through all the systems to be modified before finally arriving at the thing taking the damage.

    sword -> base damage
    player level -> adjust damage
    temporary player buff -> adjust damage
    hit location -> glancing blow off armor reduces damage
    target alignment -> evil, and you are a paladin, so damage goes up
    target buffs to defense -> reduce damage

    etc. etc.

    Here's an interesting survey of the space:

     
    BlackSabin and Ryiah like this.
  3. BlackSabin

    BlackSabin

    Joined:
    Feb 27, 2021
    Posts:
    75
    That is a remarkably good resource of a video. I hadn't even considered keeping all those systems separate in such a fashion. Certainly something to think about going forward. I don't have any "real-world" experience with programming, its all been self-taught through tutorials/videos/experimentation, but maybe a little bit more study on patterns like "bus" systems would be particularly valuable. Great add!
     
  4. R1PFake

    R1PFake

    Joined:
    Aug 7, 2015
    Posts:
    540
    I made a prototype for something like this.

    I added a interface called IValueModifier<TContext> with:
    - int SortPriority
    - float Modify(TContext context, float currentValue) method.

    The method returns the new, modified value which is passes as "currentValue" to the next modifier.
    The TContext class contains all kind of properties that the value modifier could use, for example the user, ability, target, base value etc. (this depends heavily on your combat design, you can add everything you need to this)

    The game had different events for example a event for prepare outgoing damage (offensive effects, like bonus damage), prepare incoming damage (defensive effects, like shields), the event args would have a List<IValueModifier<DamageContext>> and all event listener (equipment, passive effects, global battlefield effects, etc.) could add their modifiers to that list

    After all event listeners added their modifiers, the list was sorted by the SortPriority, I did it this way, so it wouldn't matter in which order the event listeners were registered.

    The downside is that you need somekind of global owerview which modifiers have which priority, for example should a flat bonus be added before or after percentage based modifiers etc. but you have to define this anyways, no matter how you code your system, the order has to be defined and assured somewhere, I decided to use a simple sort order to solve it.

    This made it also possible that certain event listeners could check if a modifier of the same type was already in the list and "stack" their values instead of adding their own modifier instance (I added some utility methods for this to the event args class, for example AddOrStackFlatValueModifier which would handle the logic to stack or create a new modifier instance, but only if they had the same SortOrder etc.)

    So in your case the "GoblinKillerRing" would have some kind of "OnEquip" method, where it registers to the "PrepareDamage" event, then inside the event listener it checks the context to see if the current target is a goblin, if the moon is half full, if the attacker ate melons, or whatever else you want to check and only then it will add a "+20% dmg" IValueModifier to the list

    I uses this "pattern" for almost everything, not only damage but also to determin cooldowns, (mana) cost, modify ability range/shape (for single target to AoE), to add additional effects after a specific ability was used etc. just add a new event for every kind of "query"

    The downside is that it is very annoying to show the "current" (damage) values in the ability tooltip, because you whould have to run all these caculcations, depending on the current target and other conditions, all the time to show the correct value. You could add some "preview" events for this, like "PrepareDamagePreview" and then show this value in the tooltip, but you would have to update the value every frame, I decided against it and only showed the base value in the tooltip.


    Update:
    Based on @CodeSmile post (and linked forum thread) I will simplyfy my prototype and remove the IValueModifier interface and simply replace the EventArgs ValueModifer list property with 2 simply float properties for FlatModifiers and PercentageModifiers, because then the event listeners can modify these 2 properties instead of adding / stacking ValueModifier classes and removes the need for sort orders
     
    Last edited: Feb 24, 2024
    CodeRonnie likes this.
  5. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,824
    See my post here: https://forum.unity.com/threads/tut...-attributes-system.504095/page-2#post-9251997

    Be sure to read the start of the thread for context.

    Don't overcomplicate things. All RPG stats are just a sequence of additions, subtractions and multiplications (divisions are never needed because multiplay by 0.1 is the same as divide by 10) that do not further alter each other. If you were to do so, the game would become tremendously difficult if not impossible to balance (there will be super-OP combos, and players will find them).

    Only associativity matters, like: ((damage + bonus) * multiplier) vs ((damage * multiplier) + (bonus * multiplier)).

    Use a spreadsheet to get a feel for the numbers and formulas!
     
    CodeRonnie, R1PFake and Nad_B like this.
  6. Nad_B

    Nad_B

    Joined:
    Aug 1, 2021
    Posts:
    712
    In my current project (a company management sim), I have the same need for Employees stats (Energy, Skill, Happiness), which can have modifiers of different origins, like Traits (Lazy: Energy -10%, Smart: Skill +15%...), their work tools quality (High end PC: Skill + 10%), or even temporary modifiers from Random events that could happen (Sickness: Skill -20%, Happiness -25%). This is a simplified version of such a system:

    Code (CSharp):
    1. public enum StatType
    2. {
    3.     Energy,
    4.     Skill,
    5.     Happiness
    6. }
    7.  
    8. // A struct that holds both the BaseValue and the final Value
    9. // (useful if we want to display the BaseValue as different color in a progress bar)
    10. public struct StatValue
    11. {
    12.     public float BaseValue;
    13.     public float Value;
    14.  
    15.     // Implicit conversion to float, so we can use StatValue directly
    16.     // as float without the need to use .Value field
    17.     public static implicit operator float(StatValue statValue) => statValue.Value;
    18. }
    19.  
    20. // StatModifer interface. Can be implemented by plain C# classes, or SOs
    21. public interface IStatModifier
    22. {
    23.     public StatType StatType { get; }
    24.  
    25.     // Could be negative for decrease
    26.     public float IncreasePercentage { get; }
    27.  
    28.     // You can add more operations (multiplication...etc)
    29. }
    30.  
    31. // "Employee" class. It has its own Stats object of type EmployeeStats
    32. // responsible for getting stats values, adding/removing modifiers,
    33. // or increasing base stats values
    34. public class Employee : MonoBehaviour
    35. {
    36.     // Initialize the Employee Stats with some initial base values...
    37.     public EmployeeStats Stats { get; } = new(new() { [StatType.Energy] = 1f, [StatType.Skill] = 0.1f, [StatType.Happiness] = 0.5f });
    38.  
    39.     // Other Employee properties...
    40. }
    41.  
    42. // The heart of the system: EmployeeStats class
    43. public class EmployeeStats
    44. {
    45.     private readonly Dictionary<StatType, float> _baseValues = new();
    46.     private readonly Dictionary<StatType, float> _finalValues = new();
    47.     private readonly List<IStatModifier> _modifiers = new();
    48.  
    49.     public EmployeeStats(Dictionary<StatType, float> baseValues)
    50.     {
    51.         if (baseValues is null)
    52.             throw new ArgumentNullException(nameof(baseValues));
    53.  
    54.         InitializeValues(baseValues);
    55.     }
    56.  
    57.     public StatValue Energy => GetStatValue(StatType.Energy);
    58.     public StatValue Skill => GetStatValue(StatType.Skill);
    59.     public StatValue Happiness => GetStatValue(StatType.Happiness);
    60.  
    61.     public void AddToBaseValue(StatType statType, float valueToAdd)
    62.     {
    63.         _baseValues[statType] += valueToAdd;
    64.  
    65.         RecalculateFinalValue(statType);
    66.     }
    67.  
    68.     public void AddModifier(IStatModifier modifier)
    69.     {
    70.         if (modifier == null)
    71.             throw new ArgumentNullException(nameof(modifier));
    72.  
    73.         _modifiers.Add(modifier);
    74.  
    75.         RecalculateFinalValue(modifier.StatType);
    76.     }
    77.  
    78.     public void RemoveModifier(IStatModifier modifier)
    79.     {
    80.         if (modifier == null)
    81.             throw new ArgumentNullException(nameof(modifier));
    82.  
    83.         _modifiers.Remove(modifier);
    84.  
    85.         RecalculateFinalValue(modifier.StatType);
    86.     }
    87.  
    88.     private StatValue GetStatValue(StatType statType)
    89.     {
    90.         return new StatValue { BaseValue = _baseValues[statType], Value = _finalValues[statType] };
    91.     }
    92.  
    93.     private void RecalculateFinalValue(StatType statType)
    94.     {
    95.         var baseValue = _baseValues[statType];
    96.  
    97.         float totalIncreasePercentage = 0;
    98.  
    99.         foreach (var modifier in _modifiers)
    100.         {
    101.             if (modifier.StatType == statType)
    102.                 totalIncreasePercentage += modifier.IncreasePercentage;
    103.         }
    104.  
    105.         _finalValues[statType] = baseValue * (1 + totalIncreasePercentage / 100);
    106.     }
    107.  
    108.     private void InitializeValues(IDictionary<StatType, float> values)
    109.     {
    110.         var allStats = Enum.GetValues<StatType>();
    111.  
    112.         foreach (var stat in allStats)
    113.         {
    114.             values.TryGetValue(stat, out var initalValue);
    115.  
    116.             _baseValues[stat] = initalValue;
    117.             _finalValues[stat] = initalValue;
    118.         }
    119.     }
    120. }
    Now we can just do:

    Code (CSharp):
    1. // Add a modifier
    2. employee.Stats.AddModifier(someModifier);
    3.  
    4. // Remove a modifier
    5. employee.Stats.RemoveModifier(someModifier);
    6.  
    7. // Get a Stat value (returns a StatValue struct)
    8. var skill = employee.Stats.Skill;
    9.  
    10. // Increase a stat base value:
    11. employee.Stats.AddToBaseValue(StatType.Skill, 0.25f);
    12.  
    13. // Decrease a stat base value:
    14. employee.Stats.AddToBaseValue(StatType.Energy, -0.1f);
    15.  
     
    Last edited: Feb 24, 2024
    CodeRonnie likes this.