{"id":176,"date":"2022-12-16T09:56:10","date_gmt":"2022-12-16T09:56:10","guid":{"rendered":"https:\/\/noirccc.net\/blog\/?p=176"},"modified":"2024-09-27T11:38:32","modified_gmt":"2024-09-27T11:38:32","slug":"bindable-properties-and-modifiable-attributes-system","status":"publish","type":"post","link":"https:\/\/noirccc.net\/blog\/posts\/176","title":{"rendered":"Reactive Stats System in Unity"},"content":{"rendered":"\n<h1>Broadcasting Value Change<\/h1>\n\n\n\n<h2>Why?<\/h2>\n\n\n\n<ul>\n<li>Simplify UI code by eliminating the need to poll for changes and set view every frame<\/li>\n\n\n\n<li>Support gameplay logic such as conditional buffs (e.g. whenever health decreases, get a temporary attack boost)<\/li>\n<\/ul>\n\n\n\n<p>Suppose we have UI code that takes dynamic data from a referenced object and presents it on a text label. Rather than polling for changes in the object and setting the text label every frame, we can listen to an event that will be invoked when the value we&#8217;re interested in changes.<\/p>\n\n\n\n<p>For example, a UI element that cares about player health will listen to a <code>HealthChanged<\/code> event exposed by the <code>PlayerData<\/code> object:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public class PlayerData\n{\n    &#91;SerializeField] private float _health;\n    public float Health {\n        get =&gt; _health;\n        set {\n            if (_health != value)\n            {\n                _health = value;\n                HealthChanged?.Invoke(_health);\n            }\n        }\n    }\n    public event Action&lt;float&gt; HealthChanged;\n}<\/code><\/pre>\n\n\n\n<p>In stat-heavy games like RPGs or roguelikes, a player would typically have multiple attributes (aka. player stats) such as health, attack, defence, mana, etc. This type of code will seem a bit verbose if we need to repeat it for all player stats. We can encapsulate this functionality in a wrapper class.<\/p>\n\n\n\n<p>On a side note, the <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/System.ComponentModel.INotifyPropertyChanged.aspx\">INotifyPropertyChanged<\/a> interface provided by .NET <code>System.ComponentModel<\/code> supports a similar pattern. However, the event it uses to notify property change passes a <code>PropertyChangedEventArgs<\/code> which contains a property name. It&#8217;s intended to be consumed by code which use reflection to get the property&#8217;s value. In the context of game dev this would introduce unecessary performance impact so it&#8217;s not exactly what we want.<\/p>\n\n\n\n<h2>Solution: Bindable Property<\/h2>\n\n\n\n<p><code>BindableProperty&lt;T&gt;<\/code> is a simple class that maintains a value and exposes an event that will be invoked when the value changes.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>using System;\n\nnamespace NekoLib\n{\n    \/\/\/ &lt;summary&gt;\n    \/\/\/ Interface for property that holds a value and exposes an event for value change.\n    \/\/\/ &lt;\/summary&gt;\n    \/\/\/ &lt;typeparam name=\"T\"&gt;&lt;\/typeparam&gt;\n    public interface IBindableProperty&lt;T&gt;\n    {\n        public T Value { get; set; }\n        public event Action&lt;T&gt; ValueChanged;\n    }\n}<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>using System;\nusing UnityEngine;\n\nnamespace NekoLib\n{\n    \/\/\/ &lt;summary&gt;\n    \/\/\/ Data class that holds a value and exposes an event for value change.\n    \/\/\/ &lt;\/summary&gt;\n    \/\/\/ &lt;typeparam name=\"T\"&gt;&lt;\/typeparam&gt;\n    &#91;System.Serializable]\n    public class BindableProperty&lt;T&gt; : IBindableProperty&lt;T&gt; where T : struct\n    {\n        public virtual T Value {\n            get =&gt; _value;\n            set {\n                if (!_value.Equals(value))\n                {\n                    _value = value;\n                    OnValueChange();\n                }\n            }\n        }\n\n        public event Action&lt;T&gt; ValueChanged;\n\n        &#91;SerializeField] protected T _value;\n\n        public BindableProperty(T value)\n        {\n            Value = value;\n        }\n\n        protected virtual void OnValueChange()\n        {\n            ValueChanged?.Invoke(Value);\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<h2>Usage<\/h2>\n\n\n\n<p>This is similar to <a href=\"https:\/\/github.com\/neuecc\/UniRx\" target=\"_blank\" rel=\"noopener\" title=\"\">UniRX<\/a>&#8216;s reactive property. Just declare your value with the BindableProperty type, which already encapsulates a <code>ValueChanged<\/code> event. We can now use a simple one-liner to declare an observable property.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public class Player\n{\n    &#91;field: SerializeField] public BindableProperty&lt;float&gt; Health { get; private set; }\n}<\/code><\/pre>\n\n\n\n<p>Then our UI code can listen to the value change event: <\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public class UIPlayerHealthController : MonoBehaviour\n{\n    public Player Player {get; set;}\n\n    private void OnEnable()\n    {\n        Player.Health.ValueChanged += HandlePlayerHealthChanged;\n    }\n\n    private void OnDisable()\n    {\n        Player.Health.ValueChanged -= HandlePlayerHealthChanged;\n    }\n\n    public void HandlePlayerHealthChanged(float value)\n    {\n        \/\/ Update the view here.\n    }\n}<\/code><\/pre>\n\n\n\n<h1>Reactive Stats<\/h1>\n\n\n\n<h2>Implementation<\/h2>\n\n\n\n<p>Building upon our implementation of <code>BindableProperty<\/code>, we can achieve some interesting functionalities. The modifiable attributes system &#8220;Reactive Stats&#8221; is dependant on base interfaces <code>IBindableProperty<\/code>, <code>IReadonlyProperty<\/code>. The core elements include <code>Stat<\/code>, <code>StatModifier<\/code>, and corresponding interfaces.<\/p>\n\n\n\n<p>The <code>Stat<\/code> class implements the <code>IBindableProperty<\/code> interface, meaning UI code can easily listen to its changes. A modifiable attribute (i.e. stat) is represented by an instance of the Stat class. Each stat instance is declared with a base value and exposes a final value. Each stat instance maintains its own collection of value modifiers, which are used to calculate the final value. <\/p>\n\n\n\n<p>The final value is calculated lazily, so adding and removing multiple modifiers within a frame will have limited impact on performance. The final value will only be re-calculated when its getter is called.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>using System;\nusing NekoLib;\n\nnamespace NekoLib.Stats\n{\n    \/\/\/ &lt;summary&gt;\n    \/\/\/ Interface for property that represents a modifiable stat.\n    \/\/\/ &lt;\/summary&gt;\n    public interface IStat : IReadonlyProperty&lt;float&gt;\n    {\n        public event Action&lt;float&gt; ValueChanged;\n        public event Action&lt;IStatModifier&gt; ModifierAdded;\n        public event Action&lt;IStatModifier&gt; ModifierRemoved;\n\n        public void AddModifier(IStatModifier modifier);\n        public bool RemoveModifier(IStatModifier modifier);\n        public bool RemoveModifiersBySource(object source);\n    }\n}<\/code><\/pre>\n\n\n\n<p><code>IStat<\/code> extends <code>IReadonlyProperty<\/code>, which is simply an interface for a wrapper class that holds a readonly property. Its use case will be demonstrated later when <a href=\"#bounded-stat\" title=\"\">extending the system<\/a>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>namespace NekoLib\n{\n    \/\/\/ &lt;summary&gt;\n    \/\/\/ Interface for a readonly property wrapper.\n    \/\/\/ &lt;\/summary&gt;\n    \/\/\/ &lt;typeparam name=\"T\"&gt;&lt;\/typeparam&gt;\n    public interface IReadonlyProperty&lt;T&gt;\n    {\n         public T Value { get; }\n    }\n}<\/code><\/pre>\n\n\n\n<p>Rather than letting <code>Stat<\/code> directly extend BindableProperty, here I implement the IBindableProperty interface. This gives more freedom in inspector serialization (e.g. hiding <code>_value<\/code>). The <code>Value<\/code> getter and setter are going to be completely rewritten anyway. <code>Stat.BaseValue<\/code> is the equivalent of <code>BindableProperty.Value<\/code> in BindableProperty, but users from the outside are often only interested in <code>Stat.Value<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>using System;\nusing System.Collections.Generic;\nusing UnityEngine;\nusing NekoLib;\n\nnamespace NekoLib.Stats\n{\n    \/\/\/ &lt;summary&gt;\n    \/\/\/ Data class that represents a modifiable stat.\n    \/\/\/ Manages a collection of value modifiers, and provides relevant events.\n    \/\/\/ &lt;\/summary&gt;\n    &#91;System.Serializable]\n    public class Stat : IStat, IBindableProperty&lt;float&gt;\n    {\n        \/\/\/ &lt;summary&gt;\n        \/\/\/ Final value of the stat.\n        \/\/\/ &lt;\/summary&gt;\n        public virtual float Value {\n            get {\n                if (_isDirty)\n                {\n                    _value = CalculateValue();\n                    _isDirty = false;\n                }\n                return _value;\n            }\n            set { }\n        }\n        \/\/\/ &lt;summary&gt;\n        \/\/\/ Base value of the stat.\n        \/\/\/ &lt;\/summary&gt;\n        public virtual float BaseValue {\n            get =&gt; _baseValue;\n            set {\n                if (_baseValue != value)\n                {\n                    _baseValue = value;\n                    _isDirty = true;\n                    OnValueChange();\n                }\n            }\n        }\n\n        public event Action&lt;float&gt; ValueChanged;\n        public event Action&lt;IStatModifier&gt; ModifierAdded;\n        public event Action&lt;IStatModifier&gt; ModifierRemoved;\n\n        protected float _value;\n        &#91;SerializeField] protected float _baseValue;\n        protected bool _isDirty = true;\n        protected readonly List&lt;IStatModifier&gt; _modifiers = new List&lt;IStatModifier&gt;();\n\n        public Stat(float baseValue = 0f)\n        {\n            BaseValue = baseValue;\n        }\n\n        \/\/ ...\n    }\n}<\/code><\/pre>\n\n\n\n<p>A <code>StatModifier<\/code> instance holds a Stat which defines the value of the modifier. This means a stat modifier&#8217;s value can also receive modifiers just like a stat. <code>Source<\/code> records the source of the modifier. <code>Type<\/code> marks how its value should contribute to stat value calculation. <code>Order<\/code> marks the modifier&#8217;s calculation order, which provides flexibility in handling percentage-based modifiers.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>namespace NekoLib.Stats\n{\n    \/\/\/ &lt;summary&gt;\n    \/\/\/ Data class that represents a value modifier for stats.\n    \/\/\/ &lt;\/summary&gt;\n    public class StatModifier : IStatModifier\n    {\n\n        public Stat Stat { get; }\n        public object Source { get; }\n        public StatModifierType Type { get; }\n        public int Order { get; }\n\n        public StatModifier(float value, object source, StatModifierType type = StatModifierType.Flat, int order = 0)\n        {\n            Stat = new Stat(value);\n            Source = source;\n            Type = type;\n            Order = order;\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>namespace NekoLib.Stats\n{\n    public enum StatModifierType\n    {\n        Flat,\n        PercentAdd,\n        PercentMult,\n    }\n}<\/code><\/pre>\n\n\n\n<h2>Adding and Removing Modifiers<\/h2>\n\n\n\n<p>Stat provides methods to add and remove stat modifiers. It also provides a method to remove all stat modifiers from a specified source. This can be useful for use cases like buffs and weapons. After adding or removing modifiers, set <code>_isDirty = true<\/code> to mark the final value for re-calculation next time it&#8217;s accessed.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>        \/\/\/ &lt;summary&gt;\n        \/\/\/ Add a modifier.\n        \/\/\/ &lt;\/summary&gt;\n        \/\/\/ &lt;param name=\"modifier\"&gt;&lt;\/param&gt;\n        public void AddModifier(IStatModifier modifier)\n        {\n            _modifiers.Add(modifier);\n            _modifiers.Sort(CompareModifierOrder);\n            modifier.Stat.ValueChanged += OnModifierValueChanged;\n            _isDirty = true;\n            OnValueChange();\n            ModifierAdded?.Invoke(modifier);\n        }\n\n        \/\/\/ &lt;summary&gt;\n        \/\/\/ Remove a modifier.\n        \/\/\/ &lt;\/summary&gt;\n        \/\/\/ &lt;param name=\"modifier\"&gt;&lt;\/param&gt;\n        \/\/\/ &lt;returns&gt;&lt;\/returns&gt;\n        public bool RemoveModifier(IStatModifier modifier)\n        {\n            bool isRemoved = _modifiers.Remove(modifier);\n            if (isRemoved)\n            {\n                modifier.Stat.ValueChanged -= OnModifierValueChanged;\n                _isDirty = true;\n                OnValueChange();\n                ModifierRemoved?.Invoke(modifier);\n            }\n            return isRemoved;\n        }\n\n        \/\/\/ &lt;summary&gt;\n        \/\/\/ Remove all modifiers from the specified source.\n        \/\/\/ &lt;\/summary&gt;\n        \/\/\/ &lt;param name=\"source\"&gt;&lt;\/param&gt;\n        \/\/\/ &lt;returns&gt;&lt;\/returns&gt;\n        public bool RemoveModifiersBySource(object source)\n        {\n            bool isRemoved = false;\n            for (int i = _modifiers.Count - 1; i &gt;= 0; i--)\n            {\n                var modifier = _modifiers&#91;i];\n                if (modifier.Source == source)\n                {\n                    _modifiers.RemoveAt(i);\n                    isRemoved = true;\n                    modifier.Stat.ValueChanged -= OnModifierValueChanged;\n                    _isDirty = true;\n                    OnValueChange();\n                    ModifierRemoved?.Invoke(modifier);\n                }\n            }\n            return isRemoved;\n        }<\/code><\/pre>\n\n\n\n<p>ValueChanged carries the Value property. The final value will re-calculated when this property is accessed.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>        private void OnModifierValueChanged(float value)\n        {\n            OnValueChange();\n        }\n\n        protected virtual void OnValueChange()\n        {\n            ValueChanged?.Invoke(Value);\n        }<\/code><\/pre>\n\n\n\n<p>When adding a modifier, it will be ordered based on the defined calculation order.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>        private int CompareModifierOrder(IStatModifier a, IStatModifier b)\n        {\n            if (a.Order &lt; b.Order) return -1;\n            else if (a.Order &gt; b.Order) return 1;\n            return 0;\n        }<\/code><\/pre>\n\n\n\n<h2>Calculating The Final Value<\/h2>\n\n\n\n<p>How to calculate the final stat value depends on project requirements. The method shown in this section re-calculates the final value from scratch using all of the modifier values. If you&#8217;re certain your project won&#8217;t involve lots of orthogonal percentage-based modifiers to be inserted at different steps of the calculation, it&#8217;s easier to just ignore this section and calculate the final value progressively every time you add\/remove\/change a modifier.<\/p>\n\n\n\n<p>Start with the base value, apply stat modifiers in order, and iteratively calculate the final value based on the modifier type. <\/p>\n\n\n\n<ul>\n<li><code>Flat<\/code> modifier values are directly added. <\/li>\n<\/ul>\n\n\n\n<ul>\n<li><code>PercentMult<\/code> modifier values will be applied as a percentage change based on the currently calculated final value up to this step. <\/li>\n<\/ul>\n\n\n\n<p>For <code>PercentAdd<\/code> modifiers, accumulate modifier values for consecutive modifiers of the same type, then apply the accumulated percentage value when we encounter a different type or reach the end. This is expected to be the most-used type of modifiers for ARPG games like Genshin Impact, where we have distinct modifier zones where percentage-based buffs are aggregated. This type of modifier is best combined with explicitly defined <code>Order<\/code> properties, so that they can be correctly grouped as intended.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>        \/\/\/ &lt;summary&gt;\n        \/\/\/ Calculate the final value of the stat by applying the existing modifiers.\n        \/\/\/ &lt;\/summary&gt;\n        \/\/\/ &lt;returns&gt;&lt;\/returns&gt;\n        protected virtual float CalculateValue()\n        {\n            float value = BaseValue;\n            float additivePercent = 0f;\n            \/\/ Apply modifiers.\n            for (int i = 0; i &lt; _modifiers.Count; i++)\n            {\n                StatModifier modifier = _modifiers&#91;i];\n                switch (modifier.Type)\n                {\n                    case StatModifier.StatModifierType.Flat:\n                        \/\/ Apply flat value modifier.\n                        value += modifier.Stat.Value;\n                        break;\n                    case StatModifier.StatModifierType.PercentAdd:\n                        \/\/ Accumulate consequtive additive percentage modifiers to apply together.\n                        additivePercent += modifier.Stat.Value;\n                        if (i + 1 &gt;= _modifiers.Count || _modifiers&#91;i + 1].Type != StatModifier.StatModifierType.PercentAdd)\n                        {\n                            value *= (1 + additivePercent);\n                            additivePercent = 0f;\n                        }\n                        break;\n                    case StatModifier.StatModifierType.PercentMult:\n                        \/\/ Apply percentage modifier.\n                        value *= (1 + modifier.Stat.Value);\n                        break;\n                }\n                value = (float)Math.Round(value, 4);\n            }\n            return value;\n        }<\/code><\/pre>\n\n\n\n<h2 id=\"bounded-stat\">Extension: Bounded Stat<\/h2>\n\n\n\n<p>As an example, <code>BoundedStat<\/code> extends <code>Stat<\/code> to add upper and lower bounds. The bounds can be <code>Stat<\/code> instances. The final value is also lazily calculated, and always clamped. This is useful for gameplay resource stats like health and mana.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>using UnityEngine;\nusing NekoLib;\n\nnamespace NekoLib.Stats\n{\n    \/\/\/ &lt;summary&gt;\n    \/\/\/ Data class that represents a modifiable stat.\n    \/\/\/ The value is bounded inclusively by lower and higher bounds.\n    \/\/\/ &lt;\/summary&gt;\n    public class BoundedStat : Stat\n    {\n        public override float BaseValue { set =&gt; base.BaseValue = Mathf.Clamp(value, MinValue, MaxValue); }\n        public IReadonlyProperty&lt;float&gt; MinProperty { get; }\n        public IReadonlyProperty&lt;float&gt; MaxProperty { get; }\n        public float MinValue =&gt; MinProperty.Value;\n        public float MaxValue =&gt; MaxProperty.Value;\n\n        public BoundedStat(float baseValue, float minValue, float maxValue) : base(baseValue)\n        {\n            MinProperty = new ReadonlyProperty&lt;float&gt;(minValue);\n            MaxProperty = new ReadonlyProperty&lt;float&gt;(maxValue);\n        }\n\n        public BoundedStat(float baseValue, IReadonlyProperty&lt;float&gt; minProperty, IReadonlyProperty&lt;float&gt; maxProperty) : base(baseValue)\n        {\n            MinProperty = minProperty;\n            MaxProperty = maxProperty;\n        }\n\n        public BoundedStat(float baseValue, float minValue, IReadonlyProperty&lt;float&gt; maxProperty) : base(baseValue)\n        {\n            MinProperty = new ReadonlyProperty&lt;float&gt;(minValue);\n            MaxProperty = maxProperty;\n        } \n\n        public BoundedStat(float baseValue, IReadonlyProperty&lt;float&gt; minProperty, float maxValue) : base(baseValue)\n        {\n            MinProperty = MinProperty;\n            MaxProperty = new ReadonlyProperty&lt;float&gt;(maxValue);\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<p>Here <code>ReadonlyProperty<\/code> is a dummy class that implements <code>IReadonlyProperty<\/code>. <\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>namespace NekoLib\n{\n    \/\/\/ &lt;summary&gt;\n    \/\/\/ A readonly property wrapper.\n    \/\/\/ Intended to be a dummy concrete implementation of &lt;see cref=\"IReadonlyProperty{T}\"\/&gt;.\n    \/\/\/ &lt;\/summary&gt;\n    \/\/\/ &lt;typeparam name=\"T\"&gt;&lt;\/typeparam&gt;\n    public struct ReadonlyProperty&lt;T&gt; : IReadonlyProperty&lt;T&gt;\n    {\n        public T Value { get; }\n\n        public ReadonlyProperty(T value)\n        {\n            Value = value;\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<p>Why is this used? <\/p>\n\n\n\n<p>Normally we expect the upper bound of BoundedStat to be a Stat, for example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Stat MaxHealth = new Stat(100f);\nBoundedStat CurrentHealth = new BoundedStat(100f, 0f, MaxHealth);<\/code><\/pre>\n\n\n\n<p>(Recall that <code>Stat<\/code> also inherently implements <code>IReadonlyProperty<\/code> through <code>IStat<\/code>, hence why this works). When an upper bound or lower bound <code>Stat<\/code> is not specified in the constructor, we can directly create dummy <code>ReadonlyProperty<\/code> instances to initialize the BoundedStat and perform clamping in the same way. Without this, we would either have to create actual Stat instances which would take more memory, or write verbose case checks for obtaining the clamp bounds.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Using utility data structures to build a flexible attributes system<\/p>\n","protected":false},"author":1,"featured_media":440,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[54,43],"tags":[53],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/noirccc.net\/blog\/wp-json\/wp\/v2\/posts\/176"}],"collection":[{"href":"https:\/\/noirccc.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/noirccc.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/noirccc.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/noirccc.net\/blog\/wp-json\/wp\/v2\/comments?post=176"}],"version-history":[{"count":192,"href":"https:\/\/noirccc.net\/blog\/wp-json\/wp\/v2\/posts\/176\/revisions"}],"predecessor-version":[{"id":1228,"href":"https:\/\/noirccc.net\/blog\/wp-json\/wp\/v2\/posts\/176\/revisions\/1228"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/noirccc.net\/blog\/wp-json\/wp\/v2\/media\/440"}],"wp:attachment":[{"href":"https:\/\/noirccc.net\/blog\/wp-json\/wp\/v2\/media?parent=176"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/noirccc.net\/blog\/wp-json\/wp\/v2\/categories?post=176"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/noirccc.net\/blog\/wp-json\/wp\/v2\/tags?post=176"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}