반응형

http://kiro86.egloos.com/663439

 

게임 오브젝트 설계 #3 - 컴포넌트 기반 설계(component based) 

 

 

게임 오브젝트의 컴포넌트 기반 설계는 계층 구조 설계의 문제점들을 보완하고자 제안된 설계 방법입니다.

컴포넌트 시스템을 게임 오브젝트에 적용하는 아이디어는 GDC2002에서 Scot Bilas에 의해 발표된

A Data Driven Game Object System에서 소개되었으며, 보편적으로 사용되기 시작한 때는

2006년에 출판된 Game Programming Gems 6권에 '게임 객체 구성요소 시스템'으로 소개된 이후부터인 것 같습니다.

구글을 뒤져봐도 component based game object에 대한 내용은 2006년 이후 자료가 대부분이기도 하고요..


컴포넌트 기반 설계는 게임 오브젝트가 해야 할 기능들을 각각 별도의 객체로 생성하여 

게임 오브젝트의 클래스 폭발과 게임 오브젝트의 비대화 등의 문제점들을 해결하는 설계입니다.

 




컴포넌트 기반 설계에서 게임 오브젝트는 각 타입에 따라 사용되는 컴포넌트들로 구성됩니다.

오브젝트마다 필요한 기능이 있다면 그 기능을 하나의 컴포넌트로 만들고 

컴포넌트들을 추가하여 하나의 게임 오브젝트가 만들어지는 것입니다. 

게임 오브젝트는 단지 컴포넌트들의 관리만 할 뿐이죠.

class ComponentBase;
class Entity
{
public:
    Entityentity_id entityID );
    ~Entity();

    void Update( float elapsedTime );

    entity_id GetEntityID() const;
    
    void SetPositionconst Vector3& position );
    const Vector3GetPosition() const;

    void SetOrientationconst Quaternion& orientation );
    const QuaternionGetOrientation() const;

    bool InsertComponent( ComponentBase* pComponent );
    ComponentBase* GetComponentconst component_id& componentID );
    const ComponentBase*GetComponentconst component_id& componentID )const;
    void ClearComponents();

private:
    entity_id m_entityID;

    Vector3 m_position;
    Quaternion m_orientation;

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


게임 오브젝트가 컴포넌트들을 관리 할 때는 컴포넌트마다 고유 식별자가 필요합니다.

그리고 이 식별자를 GetComponent 함수의 인자로 받아 해당 컴포넌트를 얻어올 수 있도록 합니다.



컴포넌트 기반 설계에서는 게임 오브젝트가 하는 거의 모든 일들이 컴포넌트에 분산되어있기 때문에

역시 컴포넌트가 가장 중요한 요소라고 할 수 있습니다.

컴포넌트는 자신이 담당한 기능을 처리하는 객체입니다.

'화면에 그려지는 기능', '생명치 관련 처리', 'AI', '물리 시뮬레이션', '피격에 대한 처리' 등등

따로 떼어낼 수 있는 모든 기능들이 컴포넌트로 만들어질 수 있습니다.

 

class Entity;
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 Updatefloat elapsedTime 
) {}

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

private:
    GameObject
* m_pOwner;
};

 

 

 


컴포넌트들은 계층 구조로 설계할 수 있습니다. 게임 오브젝트를 계층구조로 만드는 것이 아닌

하나의 기능을 담당하는 컴포넌트를 계층 구조로 만들어 복잡도를 낮추고 효율적으로 만들 수 있게 된 것입니다.

이때 상속 관계에 있는 컴포넌트들은 서로 연관된 처리를 하는 것일테고, 이렇게 연관된 컴포넌트들은

게임 오브젝트가 두 개 이상 가질 필요가 없는 경우가 존재합니다.

이것을 분간하기 위해 '패밀리 식별자' 라는것이 존재합니다.
(패밀리 라는 이름은 GPG6-게임 구성요소 시스템 에 사용된 용어를 그대로 가져왔습니다.)

이 패밀리 식별자를 사용하여 게임 오브젝트에서 컴포넌트를 얻어 사용하는 방식입니다.



예를 들어 RenderComponent를 만들려고 합니다. RenderComponent는 화면에 그려지는 기능을 처리합니다.

화면에 그리기 위한 모델데이터를 가지고 있고, 그리기 위해 Render 함수를 제공하거나

또는 외부에서 Render를 하기 위해 모델 데이터를 반환하는 함수가 필요하겠죠.


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

Entity* pEntity = new Entity();
RenderComponent* pRenderComponent = new RenderComponent();
pEntity->InsertComponentpRenderComponent );

...

ComponentBase* pComponent = pEntity->GetComponent"render" );
if( pComponent != NULL )
{

    RenderComponent* pRenderComponent = 
        static_cast<RenderComponent*>( pComponent );
    pRenderComponent->
Render();
}

그런데 만약 파일에서 읽은 모델을 그려야 하는 경우도 있고, 

프로그램 내부에서 어떠한 지오매트리 모델을 그려야 하는 등 RenderComponent를 나눠야 한다면..


이런 식으로 RenderComponent에 계층 구조를 만들어서 각자 타입에 맞게 기능을 구현할 수 있습니다.

그런데 이 경우에는 게임 오브젝트가 ModelRenderComponent와 GeometryRenderComponent를 모두 가질 수 있습니다.

같은 기능을 하는 컴포넌트이기 때문에 이런 경우는 로직이 복잡해지거나 이상해질 수 있습니다.

Entity* pEntity = new Entity();
ModelRenderComponent* pModelRenderComponent = new ModelRenderComponent();
pEntity->InsertComponent( pModelRenderComponent ); 
// ?
GeometryRenderComponent* pGeometryRenderComponent = new GeometryRenderComponent();
pEntity->InsertComponent( pGeometryRenderComponent ); // ?

...

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

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

물론 의도하여 이런 코드를 작성 할 수도 있지만 보통의 경우에는 의도한 부분이 아닐 것입니다.

이런 문제를 해결하기 위해 같은 기능을 하는 컴포넌트들을 묶어서 패밀리 식별자를 이용하여 얻어오는 것입니다.

Entity* pEntity = new Entity();
ModelRenderComponent* pModelRenderComponent = new ModelRenderComponent();
pEntity->
InsertComponent( pModelRenderComponent );


...


ComponentBase* pComponent = pEntity->GetComponent( "render" );
if( pComponent != NULL)
{

    RenderComponent* pRenderComponent =
        static_cast<RenderComponent*>( pComponent );
    pRenderComponent->
Render();
}

이런 식으로 게임 오브젝트를 설계하는 것이 컴포넌트 기반 설계의 기본입니다.

이런 방식을 기본으로 하여 조금 더 추가된 것이 '메시지 통신'을 하는 컴포넌트 기반 게임 오브젝트 인데요

컴포넌트가 RenderComponent처럼 자기 혼자서 하나의 기능을 모두 처리 할 수 있으면 좋겠지만

아쉽게도 게임이 이렇게 간단한 기능들만 요구하지는 않습니다.

하나의 로직을 처리하기 위해 여러 개의 컴포넌트가 서로를 참조하여 서로에게 데이터를 얻어 실행되어야 하는

그러니까 컴포넌트끼리 의존하게 되는 기능들이 굉장히 많이 존재하게 됩니다.

예를 들어 AIComponent를 만든다고 해도 여러 가지 데이터들을 얻어와야 하기 때문에

다른 컴포넌트에 접근 할 수 밖에 없는데요.

이 기능에 대한 로직을 처리 할 때 컴포넌트끼리의 의존성을 줄이기 위해 제안된 방법이 메시지 통신입니다.

아무래도 컴포넌트에서 다른 컴포넌트에 직접 접근하여 함수 호출을 하는 식의 의존성이 많아지게 되면

어떠한 컴포넌트의 인터페이스를 수정하거나 삭제를 했을 경우 

그 컴포넌트를 사용하고 있는 모든 컴포넌트를 수정해 주어야 하는 문제가 생겨버리겠지요..

반응형

+ Recent posts