Automation of Formula Calculation: Part II

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.

    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.

    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?

FormulaEnhanced.cs

        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.

FormulaCalculator.cs

    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.

PlayerStatus.cs

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#.

FormulaVariableAttribute.cs

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.

FormulaCalculator.cs

    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.

FormulaCalculator.cs

     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)))

PlayerStatus.cs

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.

FormulaCalculator.cs

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();
    }
}

results matching ""

    No results matching ""