[Unity3D] RPG キャラクター ステータスの 公式計算を 自動化 - PART II

# Automation of Formula Calculation: Part II

*This post is the second part of the series. If you like this post, leave a star on the [github project page](https://github.com/kimsama/Unity-QuickSheet) and any feedbacks are welcome!*

> **NOTE** The github project page of *'Unity-Quicksheet'* used for this article  can be found on [here](https://github.com/kimsama/Unity-QuickSheet). Also you can find any other document on there.

On the previous document, each of stat is separately calculated. It is tedious work. Even worse, we should calculate and set over again whenever a new stat is added to the *'PlayerStatus'* class.


``` csharp
    playerStatus.SkillLevel = 4;

    calculator = new CalcEngine.CalcEngine();
    calculator.DataContext = playerStatus;

    playerStatus.STR = Convert.ToSingle(calculator.Evaluate(GetFormula("STR")));
    playerStatus.DEX = Convert.ToSingle(calculator.Evaluate(GetFormula("DEX")));
    playerStatus.ITL = Convert.ToSingle(calculator.Evaluate(GetFormula("ITL")));
    playerStatus.HP = Convert.ToSingle(calculator.Evaluate(GetFormula("HP")));
    playerStatus.MP = Convert.ToSingle(calculator.Evaluate(GetFormula("MP")));
```

For an example, consider that if we add a new stat, *'LUK'* which is used for lucky stat for a character we should write its calculation code again like the following code.

``` csharp
    playerStatus.MP = Convert.ToSingle(calculator.Evaluate(GetFormula("LUK")));
```

It may not a big deal but, yes, tedious.

If so, how does it look like if we can done all calculations at once like the following code?


``` chsarp
        playerStatus = new PlayerStatus();
        playerStatus.SkillLevel = 4;

        // BOOM!
        formulaCalculator = new FormulaCalculator();
        formulaCalculator.Calculate<PlayerStatus>(playerStatus, fighterFormula.dataArray);
```

Doesn't it look charm? It is not only simple but also even flexible. Ok, let's see how it can be possible.

The first thing to do are extracting formulas and resolving its correspond stat. It can be processed by calling `FormulaCalculator.GetFormulaTable` method. Consider that it creates a dictionary with the given *'FighterFormulaData'* array which is imported from the spreadsheet and being in the *'FighterFormula'* class, a *ScriptableObject* derived class for its first parameter. The second parameter is  'stat' name and the last is 'formula' itself as a string type.


``` csharp
    public void Calculate<T>(System.Object data, System.Object[] obj, string key, string value)
    {
        // obj: FighterFormulaData array
        // key: Stat property of FighterFormulaData class
        // value: Formula property of FighterFormulaData class
        Dictionary<string, string> formulaTable = GetFormulaTable(obj, key, value);
        Calculate<T>(data, formulaTable);
    }
```

By the way, there is a problem that we should calculate only the properties which are needed for the formulas.  See *'PlayerStatus'* class. Not all of the proeprites are used as variables for the formula calculation. The `SkillLevel` property is not needed one for that purpose.  So we need a way to distinguish the properties which are used for the variables and which are not. But how?

It can be solved by setting a custom attribute on any property which is used formula variable. The `FormulaVariable` attribute which is shown at the following code is used for that purpose.


``` csharp
public class PlayerStatus
{
    public int SkillLevel { get; set; }

    // A stat member field which has 'FormulaVariable' is automatically recognized
    // as formula variable.
    [FormulaVariable]
    public float STR { get; set; }
    [FormulaVariable]
    public float DEX { get; set; }
    [FormulaVariable]
    public float ITL { get; set; }
    [FormulaVariable]
    public float HP { get; set; }
    [FormulaVariable]
    public float MP { get; set; }
}
```

*'FormulaVariableAttribute'* is a simple attribute of C#.


``` csharp
using System;

[AttributeUsage(AttributeTargets.Property)]
public class FormulaVariableAttribute : Attribute
{
}
```

Before doing calculate the given formula, *GetFormulaProperties* method gathers all properties which have the *'FormulaVariable'* attribute from *'PlayerStatus'* class. With reflection mechanism, we can easily do that.


``` csharp
    private PropertyInfo[] GetFormulaProperties<T>()
    {
        var _type = typeof(T);

        List<PropertyInfo> formulaPropList = new List<PropertyInfo>();

        // Reflection. Get all properties of the given class T.
        PropertyInfo[] properties = _type.GetProperties(BindingFlags.Public | BindingFlags.Instance);

        foreach (PropertyInfo p in properties)
        {
            if (!(p.CanRead && p.CanWrite))
                continue;

            object[] attributes = p.GetCustomAttributes(true);
            foreach (object o in attributes)
            {
                // We get a property whcih has 'FormulaVariable' custom attribute.
                if (o.GetType() == typeof(FormulaVariableAttribute))
                    formulaPropList.Add(p);
            }
        }
        return formulaPropList.ToArray();
    }
```

Now, it is time to do actual calculation. We do it by callling *'Calculate'* a generic method, so it can do calculation on any type of class, not just only for *'PlayerStatus'* class.


``` csharp
     public void Calculate<T>(System.Object dataInstance, Dictionary<string, string> formulaTable)
    {
        var _type = typeof(T);

        // Tell CalcEngine the value of variables.
        calcEngine.DataContext = dataInstance;

        // Evaluate each of properties which has 'FormulaVariable' attribute.
        PropertyInfo[] properties = GetFormulaProperties<T>();
        foreach (PropertyInfo p in properties)
        {
            string formula = null;
            if (formulaTable.TryGetValue(p.Name, out formula))
            {
                if (!string.IsNullOrEmpty(formula))
                {
                    var value = calcEngine.Evaluate(formula);
                    p.SetValue(dataInstance, Convert.ChangeType(value, p.PropertyType), null);
                }
            }
        }
    }
```

Note that the order of declaration of the properties which should be calculated are important.

See the *'PlayerStatus'* class again. To evaluate the variable hold on `HP` property,  firstly it should evaluate and set correct value of STR, DEX and ITL.

> HP = (5 * ((STR * 0.5) + (DEX * 0.39) + ( ITL * 0.23)))


``` csharp
public class PlayerStatus
{
    ...
    [FormulaVariable]
    public float STR { get; set; }
    [FormulaVariable]
    public float DEX { get; set; }
    [FormulaVariable]
    public float ITL { get; set; }
    [FormulaVariable]
    public float HP { get; set; }
    ...
}
```

That is all.

One thing to keep in mind is that we heavily used reflection to simplify our formula calcuations. As you already know, reflection is not a fast way even on runtime. So, when you work with this approach, it should be carefully done where the speed of game loop is important.  e.g. Avoid to do calculation in MonoBehaviour's Update.

The following shows whole line of FormulaCalculator class code.


``` csharp
public class FormulaCalculator
{
    CalcEngine.CalcEngine calcEngine = new CalcEngine.CalcEngine();

    public void Calculate<T>(System.Object data, System.Object[] obj)
    {
        PropertyInfo[] infos = obj[0].GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
        string key = infos[0].Name;
        string value = infos[1].Name;

        Calculate<T>(data, obj, key, value);
    }

    public void Calculate<T>(System.Object data, System.Object[] obj, string key, string value)
    {
        Dictionary<string, string> formulaTable = GetFormulaTable(obj, key, value);
        Calculate<T>(data, formulaTable);
    }

    public void Calculate<T>(System.Object dataInstance, Dictionary<string, string> formulaTable)
    {
        var _type = typeof(T);

        calcEngine.DataContext = dataInstance;

        PropertyInfo[] properties = GetFormulaProperties<T>();
        foreach (PropertyInfo p in properties)
        {
            string formula = null;
            if (formulaTable.TryGetValue(p.Name, out formula))
            {
                if (!string.IsNullOrEmpty(formula))
                {
                    var value = calcEngine.Evaluate(formula);
                    p.SetValue(dataInstance, Convert.ChangeType(value, p.PropertyType), null);
                }
            }
        }
    }

    public Dictionary<string, string> GetFormulaTable(System.Object[] obj, string _key, string _value)
    {
        Dictionary<string, string> result = new Dictionary<string, string>();

        foreach (System.Object o in obj)
        {
            PropertyInfo[] infos = o.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);

            // key of the dictionary
            string statName = null;
            var found = infos.Where(e => e.Name == _key).FirstOrDefault();
            if (found != null)
            {
                object value = found.GetValue(o, null);
                statName = value.ToString();
            }

            // value of the dictionary
            string formula = null;
            found = infos.Where(e => e.Name == _value).FirstOrDefault();
            if (found != null)
            {
                object value = found.GetValue(o, null);
                formula = value.ToString();
            }

            if (string.IsNullOrEmpty(statName) == false && string.IsNullOrEmpty(formula) == false)
                result.Add(statName, formula);
            else
            {
                // error
            }
        }

        return result;
    }

    private PropertyInfo[] GetFormulaProperties<T>()
    {
        var _type = typeof(T);

        List<PropertyInfo> formulaPropList = new List<PropertyInfo>();

        PropertyInfo[] properties = _type.GetProperties(BindingFlags.Public | BindingFlags.Instance);

        foreach (PropertyInfo p in properties)
        {
            if (!(p.CanRead && p.CanWrite))
                continue;

            object[] attributes = p.GetCustomAttributes(true);
            foreach (object o in attributes)
            {
                if (o.GetType() == typeof(FormulaVariableAttribute))
                    formulaPropList.Add(p);
            }
        }

        return formulaPropList.ToArray();
    }
}
```

Thank you for reading!