Lacti's Archive

멤버 데이터 포인터를 사용하여 연산식 묶어내기 1

October 01, 2012

게임 내 스탯 시스템을 구현한다고 해보자. 그 스탯들은 여러 상황에서 버프에 의해 값이 변경될 수 있기 때문에

  • 스탯을 저장하는 자료구조
  • 스탯을 변경하기 위한 버프 종류 enum과,
  • 각 버프 종류 enum에 따라 스탯을 어떻게 변경할지에 대한 연산식에 대해서

코딩을 해주어야 한다.

enum BuffType {
    BUFF_HP,
    BUFF_MP,
    BUFF_ATTACK,
    BUFF_DEFENCE,
};
struct Stat {
    float hp;
    float mp;
    float attack;
    float defence;
};

일단 각 스탯에 어떤 값을 어떻게 계산하면 되는지에 대해서는 다음의 3가지로 추상화를 했다고 치자. SET, ADD, RATE 즉, 지정, 더하기, 곱하기이다.

그럼 대충 다음과 같은 코드가 나온다.

switch (buffType) {
case BUFF_HP:
    switch (buffMethod) {
    case SET: stat.hp = buffValue; break;
    case ADD: stat.hp += buffValue; break;
    case RATE: stat.hp *= buffValue; break;
    }
    break;

BUFF_MP, BUFF_ATTACK, BUFF_DEFENCE 등 버프 종류가 바뀌어도 연산하는 식은 똑같다. 다만 stat 구조체 내의 어떤 변수를 접근하는지만 바뀌는 것이다.

버프의 종류가 한 100가지 된다고 치자. 그러면 100개의 동일한 case pattern을 다 코딩해주어야 하나? 그리고 SET, ADD, RATE 말고 EXPONENT라는 방법이 추가되었다고 해보자. 맙소사!

위 상황을 해결할 수 있는 가장 간단한 방법은 BuffType enum에 대응되는 Stat 자료구조의 멤버 데이터 포인터를 묶어주는 것이다.

typedef float Stat::*StatDataPtr;
StatDataPtr statDataPtr[BUFF_MAX];

statDataPtr[BUFF_HP] = &Stat::hp;
statDataPtr[BUFF_MP] = &Stat::mp;

각 Buff의 종류별로 어떤 멤버를 접근할지 정보를 구성했으니, 아까의 코드가 한결 간편해진다.

StatDataPtr dataPtr = statDataPtr[buffType];
switch (buffMethod) {
case SET: stat.*dataPtr = buffValue; break;
case ADD: stat.*dataPtr += buffValue; break;
case RATE: stat.*dataPtr *= buffValue; break;

이제 BuffType이 추가될 때마다 해당 BuffType에 대응되는 Stat의 데이터 포인터를 배열에 추가해주기만 하면 된다. 그리고 Method가 추가되어도, 그러한 연산을 수행하는 코드가 한 곳에만 존재하기 때문에 Method를 확장하기도 좋다.

하지만 아무래도 enum을 정의하고 나서 데이터 포인터와의 연결을 위해 다시 한 번 enum을 언급하는 것은 귀찮다. 이를 해결하기 위해서 X-Macro pattern를 써보자.

// buff_type.inl
BUFF_ENUM(BUFF_HP, &Stat::hp);
BUFF_ENUM(BUFF_MP, &Stat::mp);

위와 같이 BUFF_ENUM이라는 매크로를 사용하여 enum 정의와 데이터 포인터 연결을 같이 할 수 있도록 묶어주고,

// buff_type.h
#define BUFF_ENUM(name, ptr) name,
enum BuffType {
#include "buff_type.inl"
};
#undef BUFF_ENUM

header 파일과 cpp 파일에 각기 다른 macro 함수를 적용하여 적절한 코드를 생성하도록 한다.

// buff_bind.cpp
#define BUFF_ENUM(name, ptr) statDataPtr[buff] = ptr;
#include "buff_type.inl"
#undef BUFF_ENUM

이제 새로운 버프가 추가되면, bufftype.inl 파일 하나에만 `BUFFENUM`으로 추가해두면, bufftype.h와 buffbind.cpp 양 쪽에 코드가 적절히 치환되어 적절하게 추가될 것이다.

물론 코드가 모두 이렇게 깔끔하게 묶어낼 수 있는 것은 아니지만 만약 대부분의 코드를 이렇게 묶어낼 수 있다면, 이는 충분히 시도할만한 가치가 있는 방법이라고 생각된다.

편의상 Stat 구조체의 모든 변수가 float임을 가정했는데, 만약 그렇지 않다면 type erasure를 써야 하는데 이건 본 글에서 다루는 내용보다 더 큰 내용이니 다음에 다루도록 하겠다.

다음 글에서는 어제 [summerlight]님께 배운 정보를 바탕으로 Stat 구조체 내부에 배열이 있을 경우에 어떻게 처리할 수 있을지에 대해서 알아보겠다.

Loading script...