
게임 코드에서는 대미지, 회복량, 이동 속도처럼 계산 방식이 비슷한 수치가 계속 등장한다. 이를 모두 float로 표현하면 구현은 빠르지만, 회복량을 대미지 인수에 넘기는 실수도 컴파일을 통과한다.
인터페이스로 공통 연산을 묶을 수도 있지만, 기존 인터페이스는 Zero 같은 정적 값이나 +, * 연산자를 계약으로 선언하기 어려웠다. C# 11의 static abstract interface member를 사용하면 값 타입의 의미를 유지하면서 범용 수치 연산을 만들 수 있다.
1. 연산자를 인터페이스 계약에 포함하기
먼저 게임 수치가 제공해야 할 연산을 정의한다.
public interface IGameStat<TSelf>
where TSelf : unmanaged, IGameStat<TSelf>
{
static abstract TSelf Zero { get; }
static abstract TSelf operator +(
TSelf left,
TSelf right);
static abstract TSelf operator *(
TSelf value,
float scale);
}
public readonly struct Damage : IGameStat<Damage>
{
public float Value { get; }
public Damage(float value)
{
Value = value;
}
public static Damage Zero => new(0f);
public static Damage operator +(Damage a, Damage b)
=> new(a.Value + b.Value);
public static Damage operator *(Damage value, float scale)
=> new(value.Value * scale);
}
Damage는 참조 객체가 아니라 값 타입이다. unmanaged 제약까지 추가했으므로 참조형 필드를 가진 구현체는 들어올 수 없다. 대미지와 회복량을 서로 다른 타입으로 만들면 단순한 float 조합보다 잘못된 인수 전달을 컴파일 단계에서 잡기 쉽다.
이 문법의 기본 규칙은 Microsoft의 인터페이스 정적 추상 멤버 공식 문서에서 확인할 수 있다.
2. Span과 결합해 범용 핫패스 만들기
인터페이스를 일반 제약으로 사용하면 구체적인 값 타입을 유지한 채 연산자를 호출할 수 있다.
public static class StatMath
{
public static T SumScaled<T>(
ReadOnlySpan<T> values,
float scale)
where T : unmanaged, IGameStat<T>
{
T total = T.Zero;
foreach (ref readonly T value in values)
total += value * scale;
return total;
}
}
Damage[] hits =
{
new(12f),
new(18f),
new(25f)
};
Damage result =
StatMath.SumScaled<Damage>(hits, 0.8f);
이 메서드 안에서는 배열 복사, LINQ 열거자, 델리게이트 생성이 발생하지 않는다. ReadOnlySpan<T>로 연속 메모리를 읽고 ref readonly로 원소의 불필요한 복사도 피한다.
같은 계약을 구현하는 Heal, Shield, MoveSpeed 타입도 동일한 알고리즘에 넣을 수 있다. 호출부에는 여전히 구체 타입이 남으므로 Damage 결과를 실수로 Heal 변수에 대입할 수도 없다.
3. 인터페이스 변수로 되돌리지 않기
정적 추상 멤버의 장점은 일반 제약 안에서 구체 타입을 보존할 때 살아난다. 다음처럼 핫패스의 입력을 습관적으로 IEnumerable<T>로 받으면 열거 과정에서 인터페이스 호출과 열거자 박싱 가능성이 생긴다.
// 범용적이지만 프레임 반복 경로에는 신중해야 한다.
static Damage Sum(IEnumerable<Damage> values)
{
Damage total = Damage.Zero;
foreach (Damage value in values)
total += value;
return total;
}
업데이트 루프에서는 ReadOnlySpan<T>, 배열, 구체적인 List<T>처럼 실제 저장 구조가 드러나는 인수를 우선하는 편이 낫다. 반대로 에디터 도구나 로딩 코드처럼 호출 빈도가 낮은 곳에서는 IEnumerable<T>의 편의성이 더 중요할 수 있다.
또한 값 타입 제네릭은 사용된 타입 조합만큼 코드가 생성될 수 있다. 수십 개의 수치 타입을 무작정 만드는 대신, 서로 섞이면 실제 버그가 되는 단위에만 적용하고 IL2CPP 빌드 크기와 실행 시간을 함께 측정해야 한다.
핵심은 인터페이스를 객체 다형성에만 쓰지 않는 것이다. static abstract와 값 타입 제약을 조합하면 타입 안전성, 재사용 가능한 연산, 할당 없는 핫패스를 하나의 설계 안에서 얻을 수 있다.
'C#·C++ 모던' 카테고리의 다른 글
| C++23 std::to_underlying와 enum class로 게임 상태 플래그 제대로 쓰는 법 (0) | 2026.07.14 |
|---|---|
| C++23 std::optional 모나딕 연산으로 게임 조회 로직의 분기와 복사 줄이기 (0) | 2026.07.12 |
| C# 12 람다 기본값으로 게임 이벤트 필터를 더 작게 설계하기 (0) | 2026.07.07 |
| C++23 std::move_only_function으로 소유권 있는 게임 콜백 안전하게 넘기기 (0) | 2026.07.05 |
| C++23 std::scope_exit로 게임 상태 복구 코드를 안전하게 만들기 (0) | 2026.06.30 |