꽃미남 프로그래머 김포프가 창립한 탑 프로그래머 양성 교육 기관 POCU 아카데미 오픈!
절찬리에 수강생 모집 중!
프로그래밍 언어 입문서가 아닌 프로그래밍 기초 개념 입문서
문과생, 비전공자를 위한 프로그래밍 입문책입니다.
jobGuid 꽃미남 프로그래머 "Pope Kim"님의 이론이나 수학에 치우치지 않고 실무에 곧바로 쓸 수 있는 실용적인 셰이더 프로그래밍 입문서 #겁나친절 jobGuid "1판의내용"에 "새로바뀐북미게임업계분위기"와 "비자관련정보", "1판을 기반으로 북미취업에 성공하신 분들의 생생한 경험담"을 담았습니다.
Posted by 알 수 없는 사용자
이 글은 2012/02/02 - [프로그래밍] - 게임 오브젝트 설계.. 나도 잘하고 싶다! #1 에서 이어지는 글입니다.

게임 오브젝트 설계.. 나도 잘하고 싶다! #1에서는 계층 구조를 사용한 오브젝트 설계를 설명하였습니다. 그런데 시간이 흐르면서 게임이 점점 복잡해짐에 따라 게임 오브젝트의 타입도 많아지고 하는일도 많아지고 요구사항들이 늘어나면서 계층 구조 설계 방식으로 게임 오브젝트를 만드는것에 어려움이 생기기 시작하였습니다.


옛날의 게임

요즘 게임들



그럼.. 무엇이 어렵지?!

일단 설명을 위해 저번 글에서 쓰였던 게임 오브젝트의 구조를 사용하여 설명을 해보려 합니다.  위와 같은 그림처럼 게임오브젝트들을 만들었다고 가정해봅시다. 여기에 Camera를 추가한다면 어떻게 해야 할까요?

카메라는 화면에 그려지는 오브젝트가 아니니까 이렇게 추가하면 되는 것일까요? 카메라는 화면에 그려지는 오브젝트는 아니지만 움직일 수는 있습니다. 

그럼 움직일 수 있는 오브젝트니까 이렇게 추가하면 되는 것일까요? 안타깝게도 카메라는 화면에 그려지는 오브젝트가 아닙니다.. 그려지진 않지만 움직일 수 있는 오브젝트가 생기니 약간 골치가 아파졌습니다. 어쩔 수 없이 Prop과 Trigger에게는 미안하지만 GameObject에서 움직임 처리를 하고 Prop과 Trigger에서는 사용하지 않기로 해봅시다.

 

이렇게 만들고 보니 클래스 계층 구조만 보면 웬지 깔끔해진 것 같습니다. 하지만 사실은 GameObject의 비중이 커져버린겁니다. 대두가 되버린 느낌인거죠.. 아쉽지만 이 정도로 타협을 보고 다른 오브젝트를 추가해봅시다.
생각해보니 Player와 Monster는 피격을 받을 수 있고 죽을 수도 있는 공통점이 있네요. 이 공통점을 묶어서 하나의 계층을 추가하는게 좋을것 같습니다. 이름은 죽을 수 있으니까 멋지게 Diable(Die + able!!) 라고 해봅시다! (그래요.. 저 영어 못해요.. Orz)

그런데 어느날 기획서를 받아보니 부서질 수 있는 Prop이 있어야 한다고 합니다. 이걸 어쩌죠 못만든다고 싸워야 할까요?! 마음을 가다듬고 새로운 계층을 추가하여 Prop에 부서질 수 있는 속성을 추가해봅니다..

그렇다면 이렇게 만들면 되는걸까요?! 하지만 BreakableProp과 DiableObject의 역활이 상당히 비슷해보입니다.

그럼 이렇게?! 이렇게 만들게 되면 Prop에 대한 기능이 Prop과 BreakableProp에 중복으로 들어가게 되버리겠네요..
안타깝지만 데미지 관련 처리도 역시 각자 알아서 하던지 GameObject에서 처리할 수 밖에 없습니다. 각자 넣게 되면 코드 중복이 심해지니 GameObject에 넣기로 하겠습니다.

언뜻 문제점들이 모두 해결된것처럼 보이지만 GameObject의 비중은 점점 더 커져만 갑니다. 이런 문제는 만들면 만들수록 다양해질수록 복잡해질수록 계속 나타나게 됩니다. 애니메이션 기능이 필요하지 않는 Prop이 존재한다면? 물리 시뮬레이션이 되어야 하는 오브젝트? 애니메이션은 하지만 물리 시뮬레이션은 하지 않고, 물리 시뮬레이션은 하지만 애니메이션은 하지 않고.. 이런 경우에는 어떤 기능을 어디에 넣어야 할까요?

오브젝트의 타입이 늘어나면 늘어날수록 기능이 추가될수록 점점 더 설계는 복잡해지고 상위 계층은 비대해집니다.

이 미래의 인간도 계층 구조로 설계되었나봅니다.. 그래서 이렇게 뇌 계층에 기능이 계속 추가되어 비대해진것이 아닐까요?


두번째.. 컴포넌트 기반 게임 오브젝트 설계!


컴포넌트 기반 설계는 게임 오브젝트가 해야 할 기능들을 각각 별도의 객체로 생성하여 게임 오브젝트에 연결하는 방식으로 게임 오브젝트의 클래스 폭발과 비대화 등의 문제점들을 해결할 수 있는 설계 방식입니다. 오브젝트마다 필요한 기능이 있다면 그 기능을 하나의 컴포넌트로 만들고 게임 오브젝트에 연결하여 게임 오브젝트가 해당 기능을 사용할 수 있도록 하는 것입니다. 실제 게임 오브젝트 클래스는 주로 컴포넌트들의 관리를 하게 됩니다.

Cpp2Html [-] Collapse
class ComponentBase;
class GameObject
{
public:
     GameObject( entity_id entityID );
    ~GameObject();

    void Update( float elapsedTime );

    entity_id GetEntityID() const;

    void SetPosition( const Vector3& position );
    const Vector3& GetPosition() const;

    void SetOrientation( const Quaternion& orientation );
    const Quaternion& GetOrientation() const;

    bool InsertComponent( ComponentBase* pComponent );
    ComponentBase* GetComponent( const component_id& componentID );
    const ComponentBase*GetComponent( const component_id& componentID const;

private:
    entity_id m_entityID;

    Vector3 m_position;
    Quaternion m_orientation;

    boost::unorderd_map<component_id, ComponentBase*> m_components;
};

게임 오브젝트가 컴포넌트들을 관리 할 때에는 컴포넌트마다 고유 식별자가 필요합니다. 그리고 이 식별자를 GetComponent함수의 인자로 받아 해당 컴포넌트를 얻어올 수 있도록 합니다.

컴포넌트 기반 설계에서는 게임 오브젝트가 하는 거의 모든 일들이 각각의 컴포넌트들에서 이루어지기 때문에 역시 컴포넌트가 가장 중요한 요소라고 할 수 있습니다. '화면에 그려지는 기능', '생명치 관련 처리', 'AI', '물리 시뮬레이션', '전투에 대한 처리' 등등 따로 떼어낼 수 있는 모든 기능들이 컴포넌트로 만들어질 수 있습니다.

Cpp2Html [-] Collapse
class GameObject;
class ComponentBase
{
public:
    ComponentBase() : m_pOwner( NULL ) {}
    virtual ~ComponentBase() = 0 {}

    virtual const component_id& GetComponentID() const = 0;
    virtual const component_id& GetFamilyID() const = 0;

    virtual void Update( float elapsedTime ) {}

    void SetOwner( GameObject* pOwner );
    GameObject* GetOwner() const;


private:
    GameObject* m_pOwner;
};

컴포넌트들은 계층 구조로 설계할 수 있습니다.

게임 오브젝트를 계층구조로 만드는 것이 아닌 하나의 기능을 담당하는 컴포넌트를 계층 구조로 만들어 복잡도를 낮추고 효율적으로 만들 수 있습니다.

이때 상속 관계에 있는 컴포넌트들은 서로 연관된 처리를 하는 것일테고, 이렇게 연관된 컴포넌트들은 게임 오브젝트가 두 개 이상 가질 필요가 없는 경우가 존재합니다. 이것을 분간하기 위해 '패밀리 식별자' 라는것을 사용합니다. (패밀리 라는 이름은 GPG6-게임 구성요소 시스템에 사용된 용어를 그대로 가져왔습니다.)
이 패밀리 식별자를 사용하여 게임 오브젝트에서 실제 연결된 컴포넌트를 얻을 수 있습니다.

예를 들어 RenderComponent를 만든다고 가정해보겠습니다. RenderComponent는 화면에 그려지는 기능을 하는 컴포넌트일 것입니다. 화면에 그리기 위한 모델데이터를 가지고 있거나, 외부에서 호출될 Render 함수를 제공하거나 또는 Render에 필요한 데이터들을 설정하거나 반환하는 함수가 있을 것입니다.


이 컴포넌트를 게임 오브젝트에 추가를 해주면 게임 오브젝트는 화면에 그려질 수 있습니다.

Cpp2Html [-] Collapse
GameObject* pGameObject = new GameObject();
RenderComponent* pRenderComponent = new RenderComponent();
pGameObject->InsertComponent( pRenderComponent );

...

ComponentBase* pComponent = pGameObject->GetComponent( "render" );
if( pComponent != NULL )
{
    RenderComponent* pRenderComponent = static_cast<RenderComponent*>( pComponent );
    pRenderComponent->Render();
}

그런데 만약 파일에서 읽은 모델을 그려야 하는 경우도 있고, 프로그램 내부에서 만든 지오매트리 모델을 그려야 하는 등 RenderComponent를 나눠야 한다면..


ModelRenderComponent와 GeometryRenderComponent를 만들고 RenderComponent를 상속받는 이런 형태로 만들 수 있을 것입니다. 그런데 이 경우에 패밀리 식별자로 관리를 해주지 않는다면 게임 오브젝트가 ModelRenderComponent와 
GeometryRenderComponent를 모두 가질 수 있게 됩니다. 같은 기능을 하는 컴포넌트이기 때문에 의도한것이 아니라면 로직이 복잡해지거나 이상해질 수 있습니다.
 

Cpp2Html [-] Collapse
GameObject* pGameObject = new GameObject();
ModelRenderComponent* pModelRenderComponent = new ModelRenderComponent();
pGameObject->InsertComponent( pModelRenderComponent ); // ?
GeometryRenderComponent* pGeometryRenderComponent = new GeometryRenderComponent();
pGameObject->InsertComponent( pGeometryRenderComponent ); // ?

...

ComponentBase* pComponent = pGameObject->GetComponent( "model_render" );
if( pComponent != NULL )
{
    ModelRenderComponent* pModelRenderComponent = static_cast<ModelRenderComponent*>( pComponent );
    pModelRenderComponent->Render(); // ?
}

pComponent = pGameObject->GetComponent( "geometry_render" );
if( pComponent != NULL )
{
    GeometryRenderComponent* pGeometryRenderComponent = static_cast<GeometryRenderComponent*>( pComponent );
    pGeometryRenderComponent->Render(); // ?
}

물론 일부러 이렇게 작업해야할 경우도 있겠지만 의도하지 않은 경우라면 문제가 생길 수 있는 부분입니다. (사실 마땅한 예제가 떠오르지 않아서..)
패밀리 식별자를 사용하면 이런 문제를 해결할 수 있습니다. 같은 기능을 하는 컴포넌트들을 묶어서 같은 패밀리 식별자를 부여하고 패밀리 식별자를 이용하여 얻어오는 것입니다.

Cpp2Html [-] Collapse
GameObject* pGameObject = new GameObject();
ModelRenderComponent* pModelRenderComponent = new ModelRenderComponent();
pGameObject->InsertComponent( pModelRenderComponent );

...

ComponentBase* pComponent = pGameObject->GetComponent( "render" );
if( pComponent != NULL)
{
    RenderComponent* pRenderComponent = static_cast<RenderComponent*>( pComponent );
    pRenderComponent->Render();
}

이런식으로 하나의 기능을 컴포넌트 단위로 구현하여 게임 오브젝트에 연결해 나가는것이 컴포넌트 기반 설계입니다.


그리고.. 컴포넌트 하면 빠질 수 없는 메시지 통신!

컴포넌트 하나가 자기 혼자서 하나의 기능을 모두 처리할 수 있으면 좋겠지만 아쉽게도 게임이 이렇게 간단한 기능들만 요구하지는 않습니다. 하나의 기능을 처리하기 위해 여러 컴포넌트가 서로를 참조하여 서로에게 데이터를 얻어 실행되어야 하는 기능들이 굉장히 많이 존재하게 되죠. 이렇게 컴포넌트끼리의 의존성이 증가하게 되면 이것 역시 개발을 힘들게 하는 요소가 됩니다. 참조하는 컴포넌트의 인터페이스가 수정되거나 해당 기능을 하는 컴포넌트가 교체되거나 할 때 참조하는 모든 컴포넌트들을 찾아서 일일히 다 수정해야 하는것부터 컴포넌트를 설계할때에도 이 의존성때문에 더 어렵고 복잡하게 설계하게 되기도 하게 되죠.. 다른 컴포넌트와의 연관성을 직접적으로 관리를 해주어야 할테니까요


이러한 컴포넌트끼리의 의존성을 없앨 수 있는 방법으로 제안된 것이 메시지 통신 입니다. 메시지 통신을 사용하여 컴포넌트가 다른 컴포넌트를 참조할때 직접 접근하지 않고 느슨하게 접근하게 됨으로써 인터페이스의 변경이나 컴포넌트의 교체 등의 수정이 이루어져도 영향을 받지 않게 되고 또 설계를 할 때에도 명확하게 구분이 가능해집니다. 그리고 멀티쓰레드의 도움도 더 쉽게 얻을 수 있습니다.

메시지 통신의 설명은 간단한 예시를 가지고 설명을 해보면 좋을것 같습니다. 게임 오브젝트가 A와 B라는 컴포넌트를 가지고 있고, A는 B에게 종이를 가져와서 종이학을 접는 일을 한다고 가정해보도록 하죠.

 

메시지 통신을 하지 않는 경우에는 A가 B에게 직접 종이를 달라고 말하고 그렇게 종이를 받아와서 종이학을 접는 형태가 될 것입니다. 따라서 B가 없어지고 종이를 줄 수 있는 컴포넌트가 C로 변경된다거나 또는 B에게 종이를 받기 위한 인터페이스가 달라진다면 B를 사용하고 있는 A도 역시 수정을 해야 합니다. 이것이 대표적인 문제점으로 지적되는 컴포넌트끼리의 의존성입니다.

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

메시지는 편지나 쪽지와 같다고 생각하면 이해하기가 편합니다. B에게 직접 가서 "종이를 내놓으시지 B군" 이라고 말하는 것이 아니라 "나는 필요하다 종이" 라고 쪽지를 남겨놓는 것입니다. 그리고 그 쪽지를 본 컴포넌트들 중 종이를 줄 수 있는 컴포넌트가 있다면 종이를 쪽지에 넣어 남겨둬서 A가 종이를 쓸 수 있도록 하는 것입니다.

이렇게 만들면 A는 종이가 있으면 종이를 가져와서 종이학을 접고 종이가 없으면 쪽지를 남겨놓고 다른 일을 하거나 기다리는 형태가 될 것입니다. B나 C가 어떤 수정이 일어나던 A에게 아무런 영향을 주지 않습니다. 물론 아무도 종이를 줄 수 없다면 A는 시간낭비만을 하게 되겠지만요.. 이렇게 의존성이 사라진 것입니다. 
Cpp2Html
[-] Collapse
class GameObject
{
    ...

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


class ComponentBase
{
    ...

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

위 코드가 메시지통신을 하는 컴포넌트의 기본적인 형태입니다. (물론 이렇게만 만들지는 않겠지만요..)
간단하게 설명해보자면 컴포넌트들을 알고 있는 게임 오브젝트 또는 메시지 통신을 중개하는 특정 객체에 메시지를 보내면 메시지를 받아야 할 컴포넌트들을 돌면서 각 컴포넌트들에게 메시지를 전달하고 컴포넌트들은 메시지를 확인하여 자신이 처리할 수 있는 메시지인지 확인 후에 해당 메시지를 처리하는 형태입니다.

그럼 다시 위의 예제로 돌아가서 B는 어떤식으로 메시지를 처리하게 될지 살펴보도록 하겠습니다.

Cpp2Html [-] Collapse
class ComponentB
{
    ...

    virtual void HangleMessage( Message message )
    {
        switch( message.GetType() )
        {
        case eGOCMessage::나는 필요하다 종이:
            {
                종이를 메시지에 싸서 던진다.
            }
            break;

        case eGOCMessage::나는 필요하다 풀:
            {
                풀을 메시지에 싸서 던진다.
            }
            break;
        }
    }
};

이런식으로 B에서는 자신이 처리할 수 있는 메시지이면 그에 맞는 처리를 해주고 아니라면 무시해주는 형태입니다. 또는 HandleMessage() 이전에 처리할 수 있는것인지를 먼저 확인하는 함수를 하나 더 두어서 처리 가능한지 아닌지 여부를 미리 확인받을수도 있구요.

어쨌든 이렇게 하면 컴포넌트끼리 직접 접근을 하지 않고도 서로 메시지를 통해 복잡한 처리가 가능해집니다. 그렇기 때문에 어떤 컴포넌트의 인터페이스가 변경되거나 삭제되거나 하더라도 해당 컴포넌트를 사용하던 모든 코드를 뒤져서 수정해야 하는 문제도 없습니다. 그리고 다른 컴포넌트와 직접 연결되는 부분이 없기 때문에 메시지를 던지고 받는 부분만 신경써주면 멀티쓰레드의 활용도 수월해집니다. 따라서 컴포넌트 기반 설계의 장점이 더욱 빛나게 되는 것이지요.


그렇다면.. 계층 구조를 사용한 오브젝트 설계방식은 버려야 하는것인가?!

절대 아닙니다!! 만들어야 하는 게임을 고려하여 게임 오브젝트의 종류와 복잡도에 따라서 결정해야 할 문제입니다!! 만약 게임 오브젝트의 수가 많지 않고 복잡하지 않는 게임이라면 컴포넌트 기반으로 만드는것이 오히려 게임 구조를 복잡하게 만들 수 있습니다. 컴포넌트를 이용하는 설계 방식이 아무래도 기본적으로 고려해야 할것도 많고 기본적인 코드의 양도 많기 때문이지요..

굉장히 다양하고 복잡한 게임 오브젝트가 요구되는 게임이라면 계층구조 설계 방식을 사용하는것보다 컴포넌트 기반으로 만드는것이 좋다고 확신합니다.


다양하고 복잡한 게임 오브젝트를 계층 구조만으로 만드는 것은 4인치짜리 갤X시s를 안방 TV로 사용하는 것처럼 불편한 일입니다..


하지만 간단하고 단순한 게임 오브젝트가 요구되는 게임이라면 컴포넌트 설계 방식을 사용하는 것이 오히려 불편해질 것입니다.


요즘 많은 분들이 말씀하시는 것이지만.. 저도 마찬가지로 게임을 만들때 중요한 것은 기술을 만드는것이 아니라 게임을 만드는 것이라고 생각합니다. 안해본것 새로나온것 다른 회사에서 쓰는것을 우리 게임에 적용하는것보다 자신이 만들고 있는 게임 자체를 이해하고 게임을 재밌게 만들기 위해서 필요한 것이 무엇인지를 고민하고 그것을 위한 코드를 만드는 것이 제일 중요한 일이니까요..


우리 모두 재밌는 게임을 만들어 봅시다!!




다음 연재에서는 오늘 설명한 컴포넌트 기반 방식을 기준으로 제가 사용하고 있는 것들을 몇 가지 소개해보려고 합니다.
아마도 컴포넌트의 ID를 템플릿을 이용하여 사용하거나 객체의 컴포넌트 내용들을 xml 로 저장하거나 읽어올 때의 매크로 활용 등이 되지 않을까 생각해봅니다.. 자세한건 그때 컨디션과 여러가지 요인에 따라서..

반응형
,