Unity 公式编辑器
青思游戏
编辑于 2023年06月04日 18:07

问题:

        游戏中用到的各种公式一般写死在程序里,虽然简单快捷,但会有些问题

  1. 不具备通用性,公式越多越复杂,向其他项目转移就越容易出问题。

  2. 不利于工种协作,策划要反复调整公式看数值平衡性,就需要依赖程序,增加沟通成本。

  3. 不利于调试,只能在游戏运行时看到结果,内部细节被忽略。

  4. 批量测试验证数值不方便。

需求:

  1. 运算操作可视化编辑。

  2. 通过输入设定的值,可以自动计算得到结果。

  3. 可以查看每一步操作的结果和运算细节。

  4. 可以反复操作验证数值。

思路:

  1. 将公式运算拆解为一步多值的简单运算,最后依次计算单步运算,得到每一步的值并展示。

  2. 学习shder的编写思路,将if语句通过step从逻辑判断降解为纯运算,以实现对简单逻辑的支持。

效果图:

设定公式输入,程序需输入这些变量值

每一步运算配置,这是一个考虑暴击率的伤害计算公式,每一步的运算结果按name存储在cache里

测试界面,通过在这里输入变量值,计算后查看每一步的计算结果和计算细节,可反复运算

代码:

代码块
C#
自动换行
复制代码
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif

namespace QSGame.Base
{
    [Serializable]
    public enum QS_FormulaNumType
    {
        Variable,
        Constant,
    }
    [Serializable]
    public enum QS_FormulaOperateType
    {
        Add,
        Sub,
        Mul,
        Div,
        Step,
        Random,
    }
    [Serializable]
    public class QS_FormulaNum
    {
        public QS_FormulaNumType _NumType = QS_FormulaNumType.Constant;
        public string _NumStrValue;
        public float _NumFloatValue;
    }
    [Serializable]
    public class QS_FormulaOperate 
    {
        public string _Name;
        public QS_FormulaOperateType _Type = QS_FormulaOperateType.Add;
        public List<QS_FormulaNum> _Nums = new List<QS_FormulaNum>();
    }
    [CreateAssetMenu(menuName = "QSGame/Base/Config/FormulaConfig", fileName = "New FormulaConfig", order = 0)]
    public class QS_FormulaConfig : QS_Config
    {
        public List<string> _InputVarName = new List<string>();
        public List<QS_FormulaOperate> _Operatea = new List<QS_FormulaOperate>();
        public QS_Dictionary<string, float> _Cache = new QS_Dictionary<string, float>();
        public void ClearCache()
        {
            _Cache.Clear();
        }
        public void SetInputVar(string varName,float varNum)
        {
            if (!_InputVarName.Contains(varName))
            {
                Debug.LogError("QS_FormulaConfig SetInputVar err,varName not exsit:" + varName);
                return;
            }
            _Cache[varName] = varNum;
        }
        public float GetResult(string varName)
        {
            if (!_Cache.ContainsKey(varName))
            {
                Debug.LogError("QS_FormulaConfig GetResult err,varName not exsit:" + varName);
                return 0;
            }
            return _Cache[varName];
        }
        private float getOneNum(QS_FormulaNum formulaNum)
        {
            float num = 0;
            switch (formulaNum._NumType)
            {
                case QS_FormulaNumType.Variable:
                    if (!_Cache.ContainsKey(formulaNum._NumStrValue))
                    {
                        Debug.LogError("QS_FormulaConfig getOneNum err,numStrValue not exsit in cache:" + formulaNum._NumStrValue);
                        return num;
                    }
                    num = _Cache[formulaNum._NumStrValue];
                    break;
                case QS_FormulaNumType.Constant:
                    num = formulaNum._NumFloatValue;
                    break;
                default:
                    Debug.LogError("QS_FormulaConfig getOneNum err,numType not handled:" + formulaNum._NumType.ToString());
                    break;
            }
            return num;
        }
        public void Calculate()
        {
            float result = 0;
            foreach (QS_FormulaOperate formulaOperate in _Operatea)
            {
                switch (formulaOperate._Type)
                {
                    case QS_FormulaOperateType.Add:
                        if (formulaOperate._Nums.Count < 2)
                        {
                            Debug.LogError("QS_FormulaConfig Calculate err,num count err, [Add]");
                            return;
                        }
                        result = getOneNum(formulaOperate._Nums[0]) + getOneNum(formulaOperate._Nums[1]);
                        break;
                    case QS_FormulaOperateType.Sub:
                        if (formulaOperate._Nums.Count < 2)
                        {
                            Debug.LogError("QS_FormulaConfig Calculate err,num count err, [Sub]");
                            return;
                        }
                        result = getOneNum(formulaOperate._Nums[0]) - getOneNum(formulaOperate._Nums[1]);
                        break;
                    case QS_FormulaOperateType.Mul:
                        if (formulaOperate._Nums.Count < 2)
                        {
                            Debug.LogError("QS_FormulaConfig Calculate err,num count err, [Mul]");
                            return;
                        }
                        result = getOneNum(formulaOperate._Nums[0]) * getOneNum(formulaOperate._Nums[1]);
                        break;
                    case QS_FormulaOperateType.Div:
                        if (formulaOperate._Nums.Count < 2)
                        {
                            Debug.LogError("QS_FormulaConfig Calculate err,num count err, [Div]");
                            return;
                        }
                        result = getOneNum(formulaOperate._Nums[0]) / getOneNum(formulaOperate._Nums[1]);
                        break;
                    case QS_FormulaOperateType.Step:
                        if (formulaOperate._Nums.Count < 2)
                        {
                            Debug.LogError("QS_FormulaConfig Calculate err,num count err, [Step]");
                            return;
                        }
                        result = getOneNum(formulaOperate._Nums[0]) <= getOneNum(formulaOperate._Nums[1]) ? 1f : 0f;
                        break;
                    case QS_FormulaOperateType.Random:
                        result = UnityEngine.Random.Range(1,101);
                        break;
                }
                _Cache[formulaOperate._Name] = result;
            }
        }
    }

#if UNITY_EDITOR
    [CustomEditor(typeof(QS_FormulaConfig), true)]
    public class QS_FormulaConfigEditor : Editor
    {
        private QS_FormulaConfig _target = null;
        void OnEnable()
        {
            _target = target as QS_FormulaConfig;
        }

        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();
            if (GUILayout.Button("Clear Cache"))
            {
                _target._Cache.Clear();
                foreach (string needVarName in _target._InputVarName)
                {
                    _target._Cache[needVarName] = 0;
                }
            }
            if (GUILayout.Button("Calculate"))
            {
                _target.Calculate();
            }
            if (GUI.changed)
                EditorUtility.SetDirty(_target);
        }
    }
    [CustomPropertyDrawer(typeof(QS_FormulaNum), true)]
    public class QS_FormulaNumDrawer : PropertyDrawer
    {
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            SerializedProperty numTypeProperty = property.FindPropertyRelative("_NumType");
            SerializedProperty numStrValueProperty = property.FindPropertyRelative("_NumStrValue");
            SerializedProperty numFloatValueProperty = property.FindPropertyRelative("_NumFloatValue");

            Rect numTypePropertyRect = new Rect(position.x, position.y, position.width * 0.5f
                , EditorGUIUtility.singleLineHeight);
            EditorGUI.PropertyField(numTypePropertyRect, numTypeProperty, new GUIContent(""));
            Rect numValuePropertyRect = new Rect(numTypePropertyRect.x + numTypePropertyRect.width, position.y, position.width * 0.5f
                , EditorGUIUtility.singleLineHeight);
            switch (numTypeProperty.enumValueIndex)
            {
                case (int)QS_FormulaNumType.Variable:
                    EditorGUI.PropertyField(numValuePropertyRect, numStrValueProperty, new GUIContent(""));
                    break;
                case (int)QS_FormulaNumType.Constant:
                    EditorGUI.PropertyField(numValuePropertyRect, numFloatValueProperty, new GUIContent(""));
                    break;
            }
        }
        public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
        {
            return EditorGUIUtility.singleLineHeight;
        }
    }
    [CustomPropertyDrawer(typeof(QS_FormulaOperate), true)]
    public class QS_FormulaOperateDrawer : PropertyDrawer
    {
        private const int TOP_SPACE = 4;
        private const int BOTTOM_SPACE = 4;
        private const int BETWEEN_SPACE = 3;
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            SerializedProperty nameProperty = property.FindPropertyRelative("_Name");
            SerializedProperty typeProperty = property.FindPropertyRelative("_Type");
            SerializedProperty numsProperty = property.FindPropertyRelative("_Nums");
            GUI.Box(position, GUIContent.none, EditorStyles.helpBox);

            float posY = position.y;
            posY += TOP_SPACE;
            Rect namePropertyRect = new Rect(position.x + 0.01f * position.width / 2f, posY, position.width * 0.99f
                , EditorGUIUtility.singleLineHeight);
            EditorGUI.PropertyField(namePropertyRect, nameProperty,new GUIContent("Name"));

            posY += namePropertyRect.height + BETWEEN_SPACE;
            Rect typePropertyRect = new Rect(position.x + 0.01f * position.width / 2f, posY, position.width * 0.99f
                , EditorGUIUtility.singleLineHeight);
            EditorGUI.PropertyField(typePropertyRect, typeProperty, new GUIContent("Type"));

            posY += typePropertyRect.height + BETWEEN_SPACE;
            Rect tipRect = new Rect(position.x + 0.01f * position.width / 2f, posY, position.width * 0.99f
                , EditorGUIUtility.singleLineHeight);
            string tip = "";
            switch (typeProperty.enumValueIndex)
            {
                case (int)QS_FormulaOperateType.Add:
                    tip = "Num1 + Num2";
                    break;
                case (int)QS_FormulaOperateType.Sub:
                    tip = "Num1 - Num2";
                    break;
                case (int)QS_FormulaOperateType.Mul:
                    tip = "Num1 * Num2";
                    break;
                case (int)QS_FormulaOperateType.Div:
                    tip = "Num1 / Num2";
                    break;
                case (int)QS_FormulaOperateType.Step:
                    tip = "if Num1 <= Num2 get 1 ,else get 0";
                    break;
                case (int)QS_FormulaOperateType.Random:
                    tip = "get random num in [1,100]";
                    break;
            }
            EditorGUI.LabelField(tipRect, new GUIContent(tip));

            posY += tipRect.height + BETWEEN_SPACE;
            float NumsHeight = EditorGUI.GetPropertyHeight(numsProperty, numsProperty.isExpanded);
            Rect numsRect = new Rect(position.x + 0.01f * position.width / 2f, posY, position.width * 0.99f
                , NumsHeight);
            EditorGUI.PropertyField(numsRect, numsProperty, new GUIContent("Nums"));
        }
        public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
        {
            SerializedProperty numsProperty = property.FindPropertyRelative("_Nums");
            float NumsHeight = EditorGUI.GetPropertyHeight(numsProperty, numsProperty.isExpanded);
            float height = 0;
            height += EditorGUIUtility.singleLineHeight * 3 + NumsHeight + BETWEEN_SPACE * 3;
            height += TOP_SPACE + BOTTOM_SPACE;
            return height;
        }
    }
#endif
}
复制成功

        这里只支持了加减乘除,随机,step运算,如果需要其他运算可自行扩充。

在程序中使用:

        

代码块
C#
自动换行
复制代码
QS_FormulaConfig formula;
//清空cache
formula.ClearCache();
//设置需输入的变量
formula.SetInputVar("攻击者的攻击",10);
//计算
formula.Calculate();
//得到计算结果
formula.GetResult("最终伤害");
formula.GetResult("是否暴击");
复制成功