제가 인디게임을 만들고, 기획한 시스템을 점점 구현하면서 가장 난감했던 것은 유효성 검사기를 구현하는 것이었습니다. 게임의 복잡다단한 시스템을 제어할 때에 데이터를 하드코딩하지 않고 그때그때 DB에 적어놓은 조건에 맞춰 거르고 싶었는 데... 예시 없이 설명하려니 굉장히 복잡하네요. 간단한 예시를 들어보도록 하죠.
"마무리 일격 능력은 체력 30% 이하의 적에게만 사용할 수 있다."
뭐 이런 식으로 시스템 속의 컨텐츠를 하나 구현했다고 칩시다. 능력 하나를 하드 코딩하는 것은 어렵지 않습니다. if문 몇 줄 넣어놓으면 되겠죠. 하지만 컨텐츠가 늘어나고 능력이 20개가 되었을 때 이런 조건을 모두 검사하게 되면 분명히 OOP가 무너지고 캡슐화가 황폐화되는 그런 상황에 놓이게 될 겁니다. 거기다 능력 하나 늘어날 때마다 새로 코딩을 하고 컴파일을 해줘야 하니 시간도 낭비하게 되겠죠.
그래서 괜찮은 방법을 찾다가 발견한 것이입니다. NCalc는 문자열로 이루어진 표현식의 값을 평가할 수 있게 만들어진 .Net 라이브러리입니다.
즉, DB에 "2 + 5"를 적어놓고 NCalc로 평가하여 7의 값을 얻을 수 있습니다. 단순한 계산식 뿐만 아니라, NCalc의 표현식에는 다음을 평가할 수 있습니다.
- .Net 기본 자료형: int, double, decimal, boolean, 문자열
- 위 자료형들의 배열(배열을 순회할 수도 있습니다.)
- .Net의 System.Math에 내장된 함수
- 사용자가 만든 함수
또 NCalc의 표현식 매개변수에는 동적 변수도 사용할 수 있습니다. 이를 통해 위와 같은 "30% 이하의 체력을 가진 적"을 판단할 수 있습니다. 간단히 아래와 같이 하면 됩니다.
var Check = new Expression("if ([Target.HP]/[Target.MaxHP] <= 0.3, true, false)");
Check.Parameters["Target.HP"] = Target.HP;
Check.Parameters["Target.MaxHP"] = Target.MaxHP;
bool HasPassedCheck = Check.Evaluate();
편리하죠! 고통스러운 리플렉션에 시달릴 필요가 없습니다. 모듈 자체가 좀 무거운 편이지만, 바이너리를 참조하여 사용하면 속도도 빠릅니다. 그 외에 추가로 라이브러리의 기능들을 아래의 간단한 예시 코드들로 정리하였습니다.
using System;
using NCalc;
using System.Reflection;
using static System.Console;
namespace Test
{
class Program
{
static void Main(string[] args)
{
var e1 = new Expression("2 + 3 * 5");
WriteLine(e1.Evaluate()); // 출력값: 17
WriteLine(e1.ParsedExpression); // 출력값: 2 + 3 * 5. Evaluate()를 사용하기 전에는 파싱하지 않습니다.
var e2 = new Expression("1.5 + 1.2");
WriteLine(e2.Evaluate().GetType()); // 출력값: System.Double.
var e3 = new Expression("true");
WriteLine(e3.Evaluate().GetType()); // 출력값: System.Boolean
var e4 = new Expression("#01/23/2018 14:20:52#");
WriteLine(e4.Evaluate()); // 출력값: 2018-01-23 오후 2:20:52
var e5 = new Expression("'No \"x\" in Nixon.'");
WriteLine(e5.Evaluate()); // 출력값: No "x" in Nixon. '는 이스케이프 불가합니다.
var e6 = new Expression("Cos(0)"); // System.Math의 정적 메소드를 적용할 수 있습니다.
var e7 = new Expression("Sign(-23)");
var e8 = new Expression("Exp(22.53)");
// 아래의 모든 출력값은 True를 반환합니다.
WriteLine(Math.Cos(0).Equals((e6.Evaluate())));
WriteLine(Math.Sign(-23).Equals(e7.Evaluate()));
WriteLine(Math.Exp(22.53).Equals(e8.Evaluate()));
var e9 = new Expression("Example(5, 5)"); // 람다 대수로 직접 만든 간단한 함수를 이용해 표현문을 평가할 수 있습니다.
e9.EvaluateFunction += (name, arg) =>
{
if (name == "Example")
{
arg.Result = (int)arg.Parameters[0].Evaluate() + (int)arg.Parameters[1].Evaluate();
}
};
WriteLine(e9.Evaluate()); // 출력값: 10
int temp = 3;
var e10 = new Expression("Pow([temp], 2)"); // 동적 변수로 표현식을 평가할 수 있습니다.
e10.Parameters["temp"] = temp;
temp = Convert.ToInt32(e10.Evaluate()); // 숫자는 double로 반환하므로, 정수로 쓰기 위해 Convert로 변환합니다. 리플렉션을 사용한 기능이므로 (int)를 쓰면 에러!
WriteLine(temp); // 출력값: 9
var e11 = new Expression("in([temp], 1, 2, 3)");
e11.Parameters["temp"] = temp;
WriteLine(e11.Evaluate()); // 출력값: False
var e12 = new Expression("if([temp] == 9, true, false)");
e12.Parameters["temp"] = temp;
WriteLine(e12.Evaluate()); // 출력값: True
WriteLine(e12.Evaluate().GetType()); // 출력값: System.Boolean
var e13 = new Expression("if([Dynamic], true, false)"); // 람다를 이용해 동적인 값을 표현식에 넣을 수 있습니다.
int dynamic1 = int.Parse(ReadLine());
e13.EvaluateParameter += (name, arg) =>
{
if (name == "Dynamic")
{
arg.Result = (dynamic1 < 10);
}
};
WriteLine(e13.Evaluate()); // 입력한 값이 10 미만이면 True, 아니면 False 반환
var e14 = new Expression("[e15] * 5");
var e15 = new Expression("2 + 3");
e14.Parameters["e15"] = e15; // Nested Expression도 가능
e14.Parameters["dfdawf"] = 123; // 없는 파라미터에 값을 대입해도 예외가 발생하지 않습니다.
WriteLine(e14.Evaluate()); // 출력값: 25. 즉, expression이 가장 먼저 평가됩니다.
}
}
}
Congratulations @polarmagpie! You received a personal award!
Click here to view your Board
Congratulations @polarmagpie! You received a personal award!
You can view your badges on your Steem Board and compare to others on the Steem Ranking
Vote for @Steemitboard as a witness to get one more award and increased upvotes!