GPG4권의 C 구현은 메시지 수용 함수 내에 길다란 switch-case문을 늘어놓고 타입에 따라 해당 기능을 호출하도록 해
놓았다. 이 아이디어를 플래시 게임에 적용했을 때에는 함수를 일급 값으로 취급하는 액션스크립트의 특성상, 수용 클래스의
private 메소드를 리스트에 등록해놓고 메시지의 타입에 따라 호출하는 것으로 간단하게 구현할 수 있었다. 그러나 객체의 타입을 확실히 알지 못할 경우에도 메소드를 호출할 수 있는 액션스크립트에서는 별로 쓸모가 없는 시스템이었다.
C++에서도 메소드 호출은 포인터의 다운캐스트(좋은 것은 아니지만)로 얼마든지 호출할 수 있다. 그러나 온라인게임에서 네트워크로 전달되는 메시지를 어떻게 처리해야 하는가 하는 문제에 직면해서는 결국 메시지 기반 시스템이 필요하다는 생각이 들었다. 액션스크립트에서 했던 것과 같은 방식을 사용하기로 하고, 클래스의 메소드를 맵에 등록시킬 수 있는지 여부를 확인해 보았다.
간단하게 테스트 해본 결과, 잘 작동한다. 실제 구현을 보기로 하자.
struct Message
{
int ID;
int target;
int delay;
int type;
};
기본적으로 기반 메시지는 다음과 같은 구조를 가질 것이다. 이것만으로 전달할 수 있는 메시지도 있겠지만 대부분의 경우에는 이를 상속하는 구조체를 사용해야 할 것이다.
그럼 실제로 이 메시지를 수용할 클래스의 정의를 보기로 하자.
//header
struct Message;
class Spacecraft;
typedef void (__thiscall Spacecraft::*MessageReceiver) (Message*);
class Spacecraft
: public Entity
{
public:
Spacecraft(void);
virtual ~Spacecraft(void);
virtual void receive(Message* msg);
protected:
std::map<int, MessageReceiver> receiver;
private:
void onAdvance(Message* msg);
};
//source
...
Spacecraft::Spacecraft(void)
{
receiver[0] = &Spacecraft::onAdvance;
}
void Spacecraft::receive(Message* msg)
{
if (receiver.find(msg->type) == receiver.end())
return;
( this->*(receiver[msg->type]) ) (msg);
}
void Spacecraft::onAdvance(Message* msg)
{
// 우주선을 전진시킨다. 직접 위치값을 조작하든, 물리엔진을 쓰든.
}
...
현재 수용자 목록에 해당 메시지의 타입을 받는 수용자가 없다면 메소드는 그냥 리턴한다. 존재한다면, 수용자에 메시지를 넘기고 호출한다. 멤버 포인터를 호출할 때의 연산자에 주목하자. 메소드의 호출규약 __thiscall의 작동방식을 이해할 필요가 있다. C++ 클래스의 메소드는 묵시적으로 첫번째 인자에 클래스 자신, this를 받는다. 그러므로 메소드를 포인터로 호출하는데 this를 명시해주지 않으면 C2064 오류가 발생할 것이다. 오류에 대한 상세한 설명은 MSDN을 참고.
이제 클래스의 생성자나 초기화 함수에서 메시지의 타입을 지정해 두면, 다음과 같은 방식으로 메시지를 전달할 수 있다.
ControlMessage m; // Message에서 파생된 구조체
Spacecraft s;
/*초기화 작업 및 인자설정 등등*/
C++에서도 메소드 호출은 포인터의 다운캐스트(좋은 것은 아니지만)로 얼마든지 호출할 수 있다. 그러나 온라인게임에서 네트워크로 전달되는 메시지를 어떻게 처리해야 하는가 하는 문제에 직면해서는 결국 메시지 기반 시스템이 필요하다는 생각이 들었다. 액션스크립트에서 했던 것과 같은 방식을 사용하기로 하고, 클래스의 메소드를 맵에 등록시킬 수 있는지 여부를 확인해 보았다.
간단하게 테스트 해본 결과, 잘 작동한다. 실제 구현을 보기로 하자.
struct Message
{
int ID;
int target;
int delay;
int type;
};
기본적으로 기반 메시지는 다음과 같은 구조를 가질 것이다. 이것만으로 전달할 수 있는 메시지도 있겠지만 대부분의 경우에는 이를 상속하는 구조체를 사용해야 할 것이다.
그럼 실제로 이 메시지를 수용할 클래스의 정의를 보기로 하자.
//header
struct Message;
class Spacecraft;
typedef void (__thiscall Spacecraft::*MessageReceiver) (Message*);
class Spacecraft
: public Entity
{
public:
Spacecraft(void);
virtual ~Spacecraft(void);
virtual void receive(Message* msg);
protected:
std::map<int, MessageReceiver> receiver;
private:
void onAdvance(Message* msg);
};
//source
...
Spacecraft::Spacecraft(void)
{
receiver[0] = &Spacecraft::onAdvance;
}
void Spacecraft::receive(Message* msg)
{
if (receiver.find(msg->type) == receiver.end())
return;
( this->*(receiver[msg->type]) ) (msg);
}
void Spacecraft::onAdvance(Message* msg)
{
// 우주선을 전진시킨다. 직접 위치값을 조작하든, 물리엔진을 쓰든.
}
...
현재 수용자 목록에 해당 메시지의 타입을 받는 수용자가 없다면 메소드는 그냥 리턴한다. 존재한다면, 수용자에 메시지를 넘기고 호출한다. 멤버 포인터를 호출할 때의 연산자에 주목하자. 메소드의 호출규약 __thiscall의 작동방식을 이해할 필요가 있다. C++ 클래스의 메소드는 묵시적으로 첫번째 인자에 클래스 자신, this를 받는다. 그러므로 메소드를 포인터로 호출하는데 this를 명시해주지 않으면 C2064 오류가 발생할 것이다. 오류에 대한 상세한 설명은 MSDN을 참고.
이제 클래스의 생성자나 초기화 함수에서 메시지의 타입을 지정해 두면, 다음과 같은 방식으로 메시지를 전달할 수 있다.
ControlMessage m; // Message에서 파생된 구조체
Spacecraft s;
/*초기화 작업 및 인자설정 등등*/
s.receive( &m );
같은 방식으로, Spacecraft에서 파생된 클래스에도 receive메소드의 호출로 해당 클래스 특유의 메시지 수용자를 호출할 수 있다. 다만 파생 클래스의 멤버 포인터는 Spacecraft가 아니라 파생클래스를 this로 인식한다는 점을 상기할 필요가 있다. 따라서 상위 클래스의 멤버 포인터로서 등록되려면 캐스팅이 필요하다. 이미 MessageReceiver로 멤버 포인터가 재정의되어 있으니 이것으로 캐스팅해주면 간단하다.
//Spacecraft를 상속하는 클래스.
TestShip::TestShip(void)
{
//메시지 타입이 1인 경우에 이를 호출한다.
receiver[1] = (MessageReceiver) &TestShip::onTemp;
}
...
void TestShip::onTemp(Message* msg)
{
// 뭔가를 한다.
같은 방식으로, Spacecraft에서 파생된 클래스에도 receive메소드의 호출로 해당 클래스 특유의 메시지 수용자를 호출할 수 있다. 다만 파생 클래스의 멤버 포인터는 Spacecraft가 아니라 파생클래스를 this로 인식한다는 점을 상기할 필요가 있다. 따라서 상위 클래스의 멤버 포인터로서 등록되려면 캐스팅이 필요하다. 이미 MessageReceiver로 멤버 포인터가 재정의되어 있으니 이것으로 캐스팅해주면 간단하다.
//Spacecraft를 상속하는 클래스.
TestShip::TestShip(void)
{
//메시지 타입이 1인 경우에 이를 호출한다.
receiver[1] = (MessageReceiver) &TestShip::onTemp;
}
...
void TestShip::onTemp(Message* msg)
{
// 뭔가를 한다.
}
이제 준비가 끝났다. 이제 이 클래스들은 메시지와 수용자라는 단일한 창구로서 상호작용이 가능하다. 메시지가 키보드나 마우스 입력을 위한 것이든, 네트웍을 타고 오는 것이든, 단일한 메시지 타입만 정의되어 있다면 적절한 처리가 가능하다.
또한 리플레이 시스템을 만들 경우에도 메시지가 중앙 통제 객체를 통과해서 전달되도록 하면 도움이 될 것 같다.
추가적으로, 스크립트 언어를 사용한다면 메시지를 스크립트에 그대로 넘기고 스크립트의 조작을 받고 또 그 결과값을 받아오게 한다면, 굳이 파생클래스를 유도할 필요가 없이 스크립트로 다양한 파생 객체를 만들어 낼 수 있을 것이다.
클래스 멤버 포인터에 대해서는 다음 문서에 잘 정리되어 있다.
이제 준비가 끝났다. 이제 이 클래스들은 메시지와 수용자라는 단일한 창구로서 상호작용이 가능하다. 메시지가 키보드나 마우스 입력을 위한 것이든, 네트웍을 타고 오는 것이든, 단일한 메시지 타입만 정의되어 있다면 적절한 처리가 가능하다.
또한 리플레이 시스템을 만들 경우에도 메시지가 중앙 통제 객체를 통과해서 전달되도록 하면 도움이 될 것 같다.
추가적으로, 스크립트 언어를 사용한다면 메시지를 스크립트에 그대로 넘기고 스크립트의 조작을 받고 또 그 결과값을 받아오게 한다면, 굳이 파생클래스를 유도할 필요가 없이 스크립트로 다양한 파생 객체를 만들어 낼 수 있을 것이다.
클래스 멤버 포인터에 대해서는 다음 문서에 잘 정리되어 있다.


