게임 오브젝트 설계 #4 - 메시지 통신(messaging) 아키텍처 이야기

컴포넌트 기반 게임 오브젝트에 메시지 통신을 사용하는 방법은

컴포넌트끼리 의존하는 경우가 많아지면서 생기는 문제들을 보완하고자 제안된 방법입니다.

Game Programming Gems 6권에 나온 '게임 객체 구성요소 시스템'에서는

메시지 통신을 하지 않고 컴포넌트끼리 직접 접근하는 방식으로 컴포넌트 기반 설계가 소개되었는데요.

GDC, KGC, NDC 등 개발 컨퍼런스에서 컴포넌트 기반 게임 오브젝트로 발표된

대부분의 자료에서 메시지 통신이 언급되는 것을 보면 보편적으로 사용되는 방법이라고 볼 수 있을 것 같습니다.


메시지 통신을 설명하기 전에 먼저 예시를 하나 만들어봅시다.

게임 오브젝트가 A와 B라는 컴포넌트를 가지고 있다고 가정하고,

A는 B에게 종이를 가져와서 종이학을 접는 일을 한다고 생각해 봅시다.

메시지 통신을 하지 않는 컴포넌트에서는 A가 B에게 직접 종이를 달라고 말하고

그렇게 종이를 받아와서 종이학을 접는 형태가 될 것입니다.

따라서 B가 없어지고 종이를 줄 수 있는 컴포넌트가 C로 변경된다거나,

또는 B에게 종이를 받기 위한 인터페이스가 달라진다면

B를 사용하고 있던 A도 역시 수정을 해야 합니다.

이것이 문제점으로 지적되었던 컴포넌트끼리의 의존성입니다.


그렇다면 메시지 통신을 하면 어떻게 될까요?

메시지는 편지나 쪽지와 같다고 생각하면 이해하기가 편합니다.

직접 가서 "종이를 내놓으시지 B군" 이라고 말하는 것이 아니라

"나 종이 필요해" 라고 쪽지를 남겨놓는 것입니다.

그리고 그 쪽지를 본 컴포넌트들 중에 종이를 줄 수 있는 컴포넌트가 있다면

그 컴포넌트가 A에게 종이를 주는 것이죠.

이렇게 만들면 A는 종이가 있으면 종이학을 접고 종이가 없으면 쪽지를 남겨놓고 기다리면 되는것입니다.

종이를 줄 수 있는 B가 없어지고 C가 줄 수 있게 되거나, B의 인터페이스가 변경되는것은

A에게 아무런 영향을 주지 않습니다. 이렇게 의존성이 사라진것입니다.

class GameObject
{
    ...

    void OnMessage( Message message )
    {
        컴포넌트들 돌면서
        {
            컴포넌트->OnMessage( message );
        }
    }
};

 

class ComponentBase
{
...

virtual void OnMessage( Message message ) = 0;
};

위 코드가 메시지 통신을 하는 컴포넌트의 기본적인 형태입니다.

컴포넌트들을 알고 있는 게임 오브젝트 또는 메시지 통신을 중개하는 특정 객체에 메시지를 보내면

메시지를 받아야 할 컴포넌트들을 돌면서 각 컴포넌트들에게 메시지를 전달하고

컴포넌트들은 메시지를 받게 되면 자신이 처리할 수 있는 메시지인지 확인 후에

해당 메시지를 처리하는 형태입니다.

class ComponentB
{
    ...

    virtual void OnMessage( Message message )
    {
        switch( message.GetType() )
        {
            case eGOCMessage::나에게 종이를 달라:
                {
                    종이를 달라고 한 컴포넌트에게 종이를 준다.
                }
                break;

            case eGOCMessage::나에게 풀을 달라:
                {
                    풀을 달라고 한 컴포넌트에게 풀을 준다.
                }
                break;
        }
    }
};

이런 식으로 B에서 처리할 메시지인지를 판단하고 처리해야 하는 메시지이면

그에 맞는 처리를 해주는 형태입니다.

이렇게 하면 컴포넌트들끼리 직접 접근을 하지 않고도 서로 메시지를 주고 받으며 복잡한 처리도 가능하게 됩니다.

그렇기 때문에 어떤 컴포넌트의 인터페이스가 변경되거나 삭제되거나 하더라도

해당 컴포넌트를 사용하던 모든 코드를 뒤져서 수정해야 하는 문제는 없어집니다.

따라서 컴포넌트의 추가와 삭제가 수월해져 컴포넌트 기반 설계의 장점이 더욱 빛나게 되는 것이지요.


하지만 메시지 통신에도 문제는 있습니다.

컴포넌트에서 메시지를 받았을 때 메시지의 타입을 확인하는 과정은 어떻게 해도 없앨 수 없기 때문에

switch case의 폭발은 언제 봐도 유쾌하지 않은 문제입니다.

그리고 더 중요한 문제는 복잡한 일을 처리하기 위해 여러 개의 컴포넌트가 같이 통신을 해야 하는 경우

컴포넌트끼리 서로를 모르기 때문에 어떤 컴포넌트에서 어떤 일을 먼저 처리 해야 하는지 알 수 없어서

처리하는 일의 순서를 보장하기가 쉽지 않다는 것입니다.


덧글

  • 초보자 2013/07/26 21:37 # 삭제 답글

    좋은 글 감사해요. 정말 쉽게 이해할 수 있었습니다.
  • 2014/07/04 14:11 # 삭제 답글

    감사합니다. 더 강의 안하시나요 ㅎㅎ 조금 아쉽네요
  • 2017/07/29 07:55 # 삭제 답글

    꽤 오래전 포스트네요. 메시지 처리를 위해 switch-case 문을 주구장창 달 일은 없습니다. 메시지를 객체화 후 함포나 메시지 처리 인터페이스 개체를 설정하는 방식으로 해결하거든요. 메시지 설정자는 각 컴포넌트가 메시지 설정자 콜백을 설정하여 처리하구요.
댓글 입력 영역