본문 바로가기
Programming/Unity

수식 문자를 계산 하기

by subutie 2021. 2. 16.
728x90

ScriptAbleMath.cs
0.01MB

프로그래머 혼자 프로젝트를 온전히 담당 하면 생기지 않을 문제중에 하나는

기획이나 운영팀에서 원하는 데이터를 넣거나 이전 수식을 변경하는 경우

매번 수정해야 하는 코드와 연결된 부분을 다 봐야 합니다.

특히나 해당 코드가 여러 해에 걸쳐 여러명이 건드린 코드인 경우 같은 수식을 다르게 구현한

여러 군데를 수정 해야 하는 경우가 생깁니다.

이런 부분을 최소화 하기 위해 수식을 쓰고 수식대로 계산 값이 나오는 코드를 예전에 만든 걸 공유합니다.

(넋두리를 잠깐 하자면... 아주 오래전에 만들고 건들지 않은 코드입니다.

특히나 개인적으로 만들고 가지고 있다가 회사에 적용하고

이후에 개인적으로 가지고 있던 코드는 더 확장해서 좋게 만들었었는데

얼마전 백업 하드디스크가 완전이 맛이가고 그걸 개인적으로 복구 해보겠다고

하드디스크를 분해 조립을 반복 하다 결국 완전히 날려 먹었습니다.

결국 백업 용으로 하드를 새로 샀지만 이전 데이터는 바로 전 회사에서 만들었던 일부 문서와 일부 스크립트만 남고

프로젝트 단위에 예전 작업물들은 완전히 날라가 처음부터 다시 쌓아야 하는 상황이 왔네요

그래서 다시는 날려 먹지 않기 위해 Git과 Blog, Hard Disk 이렇게 3군데에 남기는 중입니다.

물론 공개 불가한 많은 것들은 Blog에 올리진 못하구요)

 

** 아래 코드에 해당하는 파일은 첨부 해놨습니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

namespace CustomMath
{
    public class MathObject
    {
        public virtual double VALUE { get { return 0; } }
        public virtual MathSign.SIGN Sign { get { return MathSign.SIGN.Empty; } }
        public virtual double Calc(double a, double b)
        {
            Debug.LogError("is not MathSign!!");
            return 0;
        }
    }

    public class MathValue : MathObject
    {
        public Func<string, double> GetValue;
        protected string value = "";
        protected double dValue = 0;
        public MathValue(string v, Func<string, double> getValue)
        {
            value = v;
            if (getValue == null) GetValue = (key) => { return 0; };
            else GetValue = getValue;

            if (string.IsNullOrEmpty(value)) dValue = 0;
            if (!double.TryParse(value, out dValue))
            {
                dValue = GetValue == null ? 0 : GetValue(value);
            }
        }

        public MathValue(double d)
        {
            value = "";
            dValue = d;
        }

        public override double VALUE
        {
            get
            {
                if (!string.IsNullOrEmpty(value) && !double.TryParse(value, out dValue))
                {
                    dValue = GetValue(value);
                }

                return dValue;
            }
        }

        public override string ToString() { return string.IsNullOrEmpty(value) ? dValue.ToString() : value; }
    }

    public class MathSign : MathObject
    {
        public enum SIGN
        {
            Plus,
            Sub,
            Multi,
            Divide,
            Rest,
            Empty,
        }

        public static char[] SIGNS = { '+', '-', '*', '/', ',', ' ' };

        public static SIGN CheckSign(char ch)
        {
            for (int i = 0; i < SIGNS.Length; ++i)
            {
                if (SIGNS[i] == ch) return (SIGN)i;
            }

            return SIGN.Empty;
        }

        public SIGN sign = SIGN.Empty;
        public MathSign(SIGN s) { sign = s; }

        public override string ToString() { return new string(new char[] { SIGNS[(int)sign] }); }

        public override MathSign.SIGN Sign { get { return sign; } }

        public override double Calc(double a, double b)
        {
            switch (sign)
            {
                case SIGN.Plus: return a + b;
                case SIGN.Sub: return a - b;
                case SIGN.Multi: return a * b;
                case SIGN.Divide: return a / b;
            }

            Debug.LogError("Calc Empty sign");
            return 0;
        }
    }

    public class BlockTransform : MathObject
    {
        public enum MATH_TYPE
        {
            None,
            Pow,
            Sqrt,
            Ceil,
            Floor,
            Clamp,
            Sin,
            Cos,
            Tan
        }

        MATH_TYPE mType = MATH_TYPE.None;
        List<MathObject> expresssions = new List<MathObject>();

        public BlockTransform(string math = "")
        {
            if (string.IsNullOrEmpty(math))
            {
                mType = MATH_TYPE.None;
                return;
            }

            string tMath = math.ToLower();

            switch (tMath)
            {
                case "pow": mType = MATH_TYPE.Pow; break;
                case "sqrt": mType = MATH_TYPE.Sqrt; break;
                case "ceil": mType = MATH_TYPE.Ceil; break;
                case "floor": mType = MATH_TYPE.Floor; break;
                case "clamp": mType = MATH_TYPE.Clamp; break;
                case "sin": mType = MATH_TYPE.Sin; break;
                case "cos": mType = MATH_TYPE.Cos; break;
                case "tan": mType = MATH_TYPE.Tan; break;
                default: mType = MATH_TYPE.None; break;
            }
        }

        public void Add(MathObject value)
        {
            if (value == null) return;
            expresssions.Add(value);
        }

        public MathObject GetLast()
        {
            if (expresssions.Count == 0) return null;
            return expresssions[expresssions.Count - 1];
        }

        public override string ToString()
        {
            if (expresssions.Count == 0) return "Empty";

            System.Text.StringBuilder builder = new System.Text.StringBuilder();
            switch (mType)
            {
                case MATH_TYPE.None: builder.Append("( "); break;
                default: builder.Append(mType.ToString() + "("); break;
            }

            for (int i = 0; i < expresssions.Count; ++i)
            {
                builder.Append(expresssions[i].ToString());
                builder.Append(" ");
            }
            builder.Append(" )");
            return builder.ToString();
        }

        double CaclValue()
        {
            List<MathObject> calc_que = new List<MathObject>();
            MathValue preValue = null;
            for (int i = 0; i < expresssions.Count; ++i)
            {
                MathSign.SIGN sign = expresssions[i].Sign;
                if (expresssions[i].Sign == MathSign.SIGN.Empty)
                {
                    preValue = new MathValue(expresssions[i].VALUE);
                }
                else if (sign == MathSign.SIGN.Plus || sign == MathSign.SIGN.Sub)
                {
                    if (preValue != null) calc_que.Add(preValue);
                    calc_que.Add(expresssions[i]);
                    preValue = null;
                }
                else
                {
                    double preDouble = preValue == null ? 1 : preValue.VALUE;
                    double nxtDouble = expresssions.Count > i + 1 ? expresssions[i + 1].VALUE : 1;
                    preValue = new MathValue(expresssions[i].Calc(preDouble, nxtDouble));
                    ++i;
                }
            }

            if (calc_que.Count == 0)
            {
                if (preValue == null)
                    return 0;
                else
                    return preValue.VALUE;
            }

            if (preValue != null) calc_que.Add(preValue);
            preValue = null;

            for (int i = 1; i < calc_que.Count; ++i)
            {
                if (calc_que[i].Sign != MathSign.SIGN.Empty)
                {
                    double preDouble = preValue == null ? calc_que[i - 1].VALUE : preValue.VALUE;
                    double nxtDouble = calc_que.Count > i + 1 ? calc_que[i + 1].VALUE : 1;
                    preValue = new MathValue(calc_que[i].Calc(preDouble, nxtDouble));
                    ++i;
                }
            }

            return preValue.VALUE;
        }

        public override double VALUE
        {
            get
            {
                switch (mType)
                {
                    case MATH_TYPE.Pow:
                        {
                            if (expresssions.Count != 3)
                            {
                                Debug.LogError("agument count is diffrent : " + mType.ToString());
                                return 0;
                            }
                            return (double)Mathf.Pow((float)expresssions[0].VALUE, (float)expresssions[2].VALUE);
                        }
                    case MATH_TYPE.Clamp:
                        {
                            if (expresssions.Count != 5)
                            {
                                Debug.LogError("agument count is diffrent : " + mType.ToString());
                                return 0;
                            }
                            return (double)Mathf.Clamp((float)expresssions[0].VALUE, (float)expresssions[2].VALUE, (float)expresssions[4].VALUE);
                        }
                    case MATH_TYPE.Sqrt: return (double)Mathf.Sqrt((float)CaclValue());
                    case MATH_TYPE.Ceil: return (double)Mathf.Ceil((float)CaclValue());
                    case MATH_TYPE.Floor: return (double)Mathf.Floor((float)CaclValue());
                    case MATH_TYPE.Sin: return (double)Mathf.Sin((float)CaclValue());
                    case MATH_TYPE.Cos: return (double)Mathf.Cos((float)CaclValue());
                    case MATH_TYPE.Tan: return (double)Mathf.Tan((float)CaclValue());
                    default:
                        return CaclValue();
                }
            }
        }
    }

    /* test sample code
        string math = "(a+b)*c+(d+e)+f*(g+(h*i)+j*(+k))+22.2";
        string math = "(10+11)*10.2+(11+20)+15.3*(2.2+(3.3*4.1)+5*(6+1))+22.2";
        ScriptAbleMath sMath = new ScriptAbleMath(math);
    */

    public class ScriptAbleMath
    {

        public enum MATH_RESULT
        {
            Fail = -1,
            None = 0,
            Success,
        }

        public Func<string, double> funcValue = null;
        BlockTransform math_block = null;
        MATH_RESULT math_result = MATH_RESULT.None;
        public MATH_RESULT RESULT { get { return math_result; } }

        public override string ToString()
        {
            return math_block.ToString();
        }

        public ScriptAbleMath(string expression)
        {
            math_result = MATH_RESULT.None;

            if (string.IsNullOrEmpty(expression)) return;

            string no_empty = expression.Replace(" ", string.Empty);
            math_block = new BlockTransform();
            int result = SerialCalc(ref no_empty, ref math_block, 0, 0);
            math_result = (result == -1) ? MATH_RESULT.Fail : MATH_RESULT.Success;
        }

        double GetChangeValue(string value)
        {
            if (string.IsNullOrEmpty(value) || funcValue == null) return 0;
            return funcValue(value);
        }

        public int SerialCalc(ref string value, ref BlockTransform bt, int start, int depth)
        {
            bool inBlock = depth > 0 && (value[start] == '(');
            if (inBlock) ++start;
            int valueStart = start;
            for (int n = start; n < value.Length; ++n)
            {
                char each = value[n];
                MathSign.SIGN sign = MathSign.CheckSign(each);

                if (each == '(')
                {
                    string math = "";
                    if (valueStart < n) math = value.Substring(valueStart, n - valueStart);
                    BlockTransform child = new BlockTransform(math);
                    bt.Add(child);
                    int jumpPos = SerialCalc(ref value, ref child, n, depth + 1);
                    if (jumpPos == -1) return -1;
                    n = jumpPos;
                    valueStart = n + 1;
                    each = value[n];
                }
                else if (sign != MathSign.SIGN.Empty)
                {
                    if (valueStart < n)
                    {
                        bt.Add(new MathValue(value.Substring(valueStart, n - valueStart), GetChangeValue));
                        bt.Add(new MathSign(sign));
                        valueStart = n + 1;
                    }
                    else
                    {
                        MathObject last = bt.GetLast();
                        if (last == null || last.Sign != MathSign.SIGN.Empty)
                        {
                            if (sign == MathSign.SIGN.Plus)
                                valueStart = n + 1;
                            else if (sign == MathSign.SIGN.Multi || sign == MathSign.SIGN.Divide)
                                return -1;
                        }
                        else
                        {
                            bt.Add(new MathSign(sign));
                            valueStart = n + 1;
                        }
                    }
                }
                else if (each == ')')
                {
                    if (!inBlock)
                        return -1;

                    if (depth > 0)
                    {
                        if (valueStart < n)
                        {
                            bt.Add(new MathValue(value.Substring(valueStart, n - valueStart), GetChangeValue));
                        }
                        return n;
                    }
                }
            }

            if (valueStart < value.Length)
            {
                bt.Add(new MathValue(value.Substring(valueStart, value.Length - valueStart), GetChangeValue));
            }

            if (inBlock) return -1;
            return value.Length;
        }

        double VALUE
        {
            get
            {
                if (math_block == null) return 0;
                return math_block.VALUE;
            }
        }

        public double GetValue() { return VALUE; }

        public double GetValue(Func<string, double> func)
        {
            funcValue = func;
            return VALUE;
        }
    }
}

 

 

 

사용 방법은 아래와 같습니다.

using CustomMath;

////
~~

////
    void SomeFunction()
    {
        string expression = "(10+11)*10.2+(11+20)+15.3*(2.2+(3.3*4.1)+5*(6+1))+22.2";

        ScriptAbleMath math = new ScriptAbleMath(expression);
        Debug.Log(string.Format("Only Numberic {0} = {1}", expression,  math.GetValue()));


        //////////////////////////////////////////////////////////////////////////////////////


        expression = "(a+b)*c+(d+e)+f*(g+(h*i)+j*(k+l))+22.2";
        ScriptAbleMath math2 = new ScriptAbleMath(expression);

        // 테스트를 위해 한 함수에 다 구현 했지만 상수를 특정 상태 값이나 
        // 특정 문자에 지정된 값으로 가져 오도록 하면 구현에 큰 도움이 됩니다.
        // 예로 : Pow(LEVEL, 2) + HP + MP 
        // 뭐이 런 공식을 받아서 레벨과 체력과 MP를 받아 오도록 Func 를 만들어 넣어주면 됩니다.
        // 즉 어느정도는 수식을 짜는 사람과 사용할 상수의 이름과 값을 같이 정하고 구현 해야 합니다.
#region for test
        Dictionary<string, double> values = new Dictionary<string, double>();
        values.Add("a", 10.0);
        values.Add("b", 11.0);
        values.Add("c", 10.2);
        values.Add("d", 11.0);
        values.Add("e", 20.0);
        values.Add("f", 15.3);
        values.Add("g", 2.2);
        values.Add("h", 3.3);
        values.Add("i", 4.1);
        values.Add("j", 5.0);
        values.Add("k", 6.0);
        values.Add("l", 1.0);

        Func<string, double> func = (v) => {
            if (values.ContainsKey(v))
                return values[v];
            return 0;
        };
#endregion

        Debug.Log(string.Format("Constant Combine {0} = {1}", expression, math2.GetValue(func)));
    }

/* out put console
Only Numberic (10+11)*10.2+(11+20)+15.3*(2.2+(3.3*4.1)+5*(6+1))+22.2 = 1043.569

Constant Combine (a+b)*c+(d+e)+f*(g+(h*i)+j*(k+l))+22.2 = 1043.569
*/

 

 

또 여러개의 UnityEngine.Mathf 함수를 사용 할 수 있는 표현식도 구현 해놨습니다.

Mathf.Pow = "pow(a,b)"

Mathf.Clamp = "clap(a,b,c)"

Mathf.Sqrt = "sqrt(a)"

Mathf.Ceil = "ceil(a)"

Mathf.Floor = "floor(a)"

Mathf.Sin = "sin(a)"

Mathf.Cos = "cos(a)"

Mathf.Tan = "tan(a)"

PS : 아마 검색을 해보면 외국이나 국내 프로그래머 분들이 만드신 더 좋은 것이 있을 겁니다.

(검색 해보지 않아 잘 모르지만 ^^;)

개인적으로 간단한건 머리에 이끼끼지 말라는 차원에서 직접 짜는 걸 좋아 해서 짰지만 .... 다른 사람과 비교 해보질 않아 어떨지 모르겠네요

'Programming > Unity' 카테고리의 다른 글

UGUI Text에 배경 사이즈 자동 맞춤  (0) 2021.03.24
Unity Scroll View Auto Focus  (0) 2021.02.16

댓글