열거형은 프로그래머로 하여금 가독성과 값의 안전성등의 향상 목적으로 효율적으로 사용되는 자료형 중 하나이다. 이러한 이유로 시스템, 게임과 같이 각종 이벤트를 정의하고, 효과적으로 관리되는 데 효율적으로 사용될 수 있다.
public enum Condition : short
{
// ... Some implements
OnHit = 1,
OnDead = 2,
OnAttack = 3
//
}
이렇게 정의되면 캐시하여 추후 어떤 이벤트가 발생하였는지, 또는 발생된 적이 있는지 if, switch 등으로 검사할 수 있을 것이다. 그런데 만약, 굉장히 많은 이벤트를 담고있고 그 이벤트에 대해 주기적인 확인이나 즉, "쿼리(Query)" 를 만들어야하는 상황이 오면 어떻게 해야할까?
먼저, 앞에서 정수형으로 선언된 열거형은 bit-mask가 불가능하다. OnDead | OnAttack 는 OnAttack, 10 | 11 = 11(OnAttack) 이 되기 때문에 만약 OnDead 와 OnAttack 의 두개의 값을 확인하고 싶다하면 불가능하다. 이것을 해결하기 위해서는 별도의 쿼리를 구현하는 별도 정의가 필요하다.
쿼리를 만들기 전에 해당 쿼리의 최대 개수를 고려하여 설계하여야한다. 단일 정수형으로 표현할 수 있는 최대 비트의 개수는 64개인데, 그 이상을 필요로 한다면 두개 이상의 정수형을 붙여서 설계하거나, 또는 StructLayoutAttribute으로 사용자 지정 크기를 설계할 수 있다. 그러나 StructLayoutAttribute의 경우, 안전하지 않으므로(unsafe) 추천하지 않는다.
나는 64개 이상의 이벤트를 검사할 일이 절대로 없을 것이라 생각하여 long을 사용한 쿼리를 개발한다. 그리고 실제 설계에 들어가기전 예외사항에 대해 몇가지 정의를 해본다.
- 만약, 모체가 되는 열거형의 크기가 64개와 같거나 그 이상이 되면 어떻게 할 것인가?
이것을 몇가지 추가 제한사항을 열거형내에서 명시한다.
- 같은 그룹군에 속한 이벤트는 연속된 수로 정의된다.
- 같은 그룹군에 속한 이벤트가 64개 이상이 될 경우, 그 그룹을 추가로 세분화하여 점조직화한다.
이 제한사항을 통해 위에 대한 예외사항을 해결한다.
- 만약, 64개 이상의 차이(offset)를 갖는 서로 다른 이벤트에 대해 쿼리로 변환하려하면 어떻게 할 것인가?
이것은 앞에서 제한한 사항들로 해결이 될 수 있다. 만약 이를 수행하는 코드는 설계 의도와 맞지 않는 사용이므로 확실한 처리를 위해 예외를 throw 시킨다.
64개의 비트를 최대한으로 활용하려면 모체의 열거형에서 순서를 얻어 그 값을 시작으로 쿼리를 빌드할 수 있다. 즉, 3번째에서 쿼리가 시작한다면 3번째는 0번째로 취급되고, 이후 쿼리에 추가되는 이벤트는 3번째보다 값이 크거나 작은지만 검사하면 쿼리의 시작점을 지정할 수 있다. 앞서 정의한 제한사항으로 연속된 이벤트에 대해서 쿼리로 묶어, 한번에 처리하는 것을 그 목적으로 하기에 비트를 최대한 효율적으로 사용할 수 있다.
public readonly struct ConditionQuery : IEquatable<ConditionQuery>, IEnumerable<Condition>
{
// ... Some implements
private readonly short m_Offset;
private readonly long m_Filter;
// ...
}
이제 실제로 이벤트를 이 쿼리로 변환하는 생성자를 작성한다.
private ConditionQuery(short c, long filter)
{
m_Offset = c;
m_Filter = filter;
}
public static implicit operator ConditionQuery(Condition c) => new ConditionQuery((short)c, 1);
그리고 OR, AND 연산자를 추가한다.
public static ConditionQuery operator |(ConditionQuery x, ConditionQuery y)
{
if (x.m_Filter == 0) return y;
if (y.m_Filter == 0) return x;
short o = x.m_Offset < y.m_Offset ? x.m_Offset : y.m_Offset;
if (64 <= math.abs(o - x.m_Offset) ||
64 <= math.abs(o - y.m_Offset))
throw new InvalidOperationException($"exceed query");
if (0 < y.m_Filter && x.m_Offset + 63 < (int)y.Last)
throw new InvalidOperationException($"exceed query");
long xf = x.m_Filter << math.abs(o - x.m_Offset),
yf = y.m_Filter << math.abs(o - y.m_Offset);
return new ConditionQuery(o, xf | yf);
}
앞서 이야기한 예외사항처럼 쿼리는 64개 이상의 차이값을 갖는 조건에 대해 연산할 수 없는 그 한계점이 분명함으로 해당 사항에 대해 예외처리를 수행한다. bit-mask 연산은 별도 예외처리가 되지 않으면 그 값이 우리가 원하는 값이 아니더라도 항상 성공하기 때문에 예외처리는 필수이다.
public static ConditionQuery operator &(ConditionQuery x, ConditionQuery y)
{
if (64 <= math.abs(x.m_Offset - y.m_Offset)) return default;
short o = x.m_Offset < y.m_Offset ? x.m_Offset : y.m_Offset;
if (64 <= math.abs(o - x.m_Offset) ||
64 <= math.abs(o - y.m_Offset))
throw new InvalidOperationException($"exceed query");
int yo = math.abs(o - y.m_Offset);
long xf = x.m_Filter << math.abs(o - x.m_Offset),
yf = y.m_Filter << yo;
yo -= 64;
while (0 < yo)
{
yf &= ~(1L << yo--);
}
return new ConditionQuery(o, xf & yf);
}
AND 연산도 마찬가지로 연산한다. 다른점은 쿼리를 병합하지 않기 때문에 그 중간 교집합에 대해서 처리하기 위해 유연한 코드를 갖고있다.
© 2024 - 2024 Vvidr - All Rights Reserved.
'설계 > 기술' 카테고리의 다른 글
배열을 최적화하는 방법 (0) | 2024.03.13 |
---|---|
Jetbrains AI Assistant를 활용한 프로그램 (0) | 2024.02.12 |
ECS(Entity Component System)로 설계하는 프로그램 part.1 (0) | 2024.02.08 |