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



오늘 올릴 내용은.. 템플릿과 함께하는 컴포넌트 설계! 입니다..
따라서 개인취향에 따라.. 이번 글은 패스하셔도 욕하지 않겠습ㄴ.. 
 
 


이전글에 나오는 컴포넌트를 사용하는 방식으로 게임 오브젝트를 만든다고 해봅시다.

GameObject* pGameObject = new GameObject();
RenderComponent* pRenderComponent = new RenderComponent();
pGameObject->InsertComponent( pRenderComponent );

...

ComponentBase* pComponent = pGameObject->GetComponent( "rander" ); 



아무 문제없이 돌아가야 할 것 같습니다. 그런데!! 화면에 게임 오브젝트가 그려지지 않습니다!
컴파일에러도 안나고 아무리 봐도 문제가 없을것 같은 코드인데도 제대로 된 동작을 하지 않는다면 버그를 찾아서 고쳐야 합니다. 모든 코드를 하나하나 다 살펴보고 테스트 해보고 디버깅해보고.. 머리는 아파오고 집에는 못가고 그렇게 야근을 하다보면 집에 늦게 가고 집에 가면서도 잡지 못한 버그 생각에 머리는 또 아프고.. 늦게 퇴근해서 다음날 늦잠을 자고 지각을 하고 이런 악순환이 계속될 것입니다.


 
 

아마 몇몇 분들은 눈치채셨을지도 모르겠습니다만.. 위 코드의 문제는 무엇이었을까요?!!
바로 오타!! 입니다.. RenderComponent는 "render"라는 이름을 가지지만 "rander"를 얻어오려 하니 당연히 얻어올 수 없죠.. Orz


참으로 어처구니 없는 실수입니다..




물론 위와 같은 경우는 꽤나 쉽게 찾을 수 있는 버그입니다. 하지만 좀 더 복잡한 상황이라면.. 또는 그렇지 않더라도 애초에 이런 실수를 할 수 없도록 설계를 한다면 더 좋을 것입니다.



그래서 저는 템플릿을 선택하였습니다. 



템플릿을 이용한 이유는 속도도 다른 이유도 아닌 이런 수준의 실수는 컴파일 타임에 잡아내기 위해서입니다.

GameObject* pGameObject = new GameObject();
pGameObject->InsertComponent<RenderComponent>();


...

RenderComponent* pRenderComponent = pGameObject->GetComponent<RenderComponent>();

 
위와 같은 코드가 제가 설명하려는 템플릿을 이용한 컴포넌트를 사용하는 코드입니다.


이걸 설명하기 전에 먼저 RTTI에 대해 설명해야 할 것 같습니다. 왜냐하면 RTTI를 이용한 방법이거든요..


그렇다면 RTTI란 무엇인가?!!!
 

감사합니다. 친절한티스님.


저는 컴포넌트 기반 설계를 할 때에 RTTI를 사용합니다. 왜냐하면 ComponentBase*에서 자식 객체로 변환하는일이 많기 때문입니다. 그래서 RTTI를 이미 쓰고 있습니다. 그리고 RTTI의 특성을 이용하여 위의 문제도 해결하고 있습니다.




바로 이녀석을 이용하는 것입니다. RTTI 구현의 핵심이기도 한 이녀석은 특성상 클래스별로 하나만 존재하는 객체입니다. 따라서 이 객체를 이용하면 타입별 아이디를 만드는것도 가능합니다.


[주의] 위 코드는 비표준 문법이 사용되어 작성된 코드입니다.
Visual Studio 이외의 컴파일러에서는 컴파일이 되지 않을겁니다..



일단 제가 사용중인 RTTI 코드를 첨부하였습니다. 이 코드는 현재 프로젝트에서 쓰는 코드와는 차이도 있고 원형은 GPG4권에 나온걸 기준으로 수정을 한 코드이니 이 코드를 공개하는것은 문제가 없을 듯 합니다.




RTTI를 사용하는 클래스의 static객체인 s_RTTI의 주소를 ID로 사용하는 방식입니다. 이것을 이용하면 클래스 이름 또는 객체로 ID를 얻어올 수 있습니다. 

이 RTTI의 GetTypeID()를 이용하여 GameObject에 컴포넌트의 추가와 컴포넌트를 얻어오는 부분을 다음과 같이 구현하는 것입니다.

Cpp2Html[-] Collapse

template<typename T>
T* InsertComponent()
{
    unsigned int componentID = RTTI::GetTypeID<T>();
    if( m_components.find( componentID ) != m_components.end() )
    {
        return NULL;
    }

    T* pComponent = new T();
    m_components.insert( std::make_pair( componentID, pComponent ) );
    pComponent->SetOwner( this );
    return pComponent;
}

Cpp2Html[-] Collapse
template<typename T>
T* GetComponent()
{
    ComponentTable::const_iterator iter = m_components.find( RTTI::GetTypeID<T>() );
    if( iter != m_components.end() )
    {
        return static_cast<T*>( iter->second.get() );
    }

    return NULL;
}



이렇게 하면 이제 가장 위에서 봤던 코드가 가능해집니다!!



그러나 아직 해결하지 않은 한가지 문제가 더 있습니다. 바로 패밀리식별자 입니다..




위와 같은 구조로 컴포넌트가 만들어져있다고 생각해봅시다. AAA1과 AAA2는 AAA를 상속받습니다. 이들의 패밀리식별자는 "AAA"정도로 만들 수 있겠네요. 게임오브젝트에 추가되어 있는 AAA1를 AAA의 이름으로 얻어오려면 어떻게 해야 할까요?


AAAComponent* pAAAComponent = pGameObject->GetComponent<AAAComponent>();



이렇게 얻어오면 되는걸까요? AAAComponent로 추가하지 않으면 현재의 GetComponent함수로는 원하는 동작을 기대할 수 없습니다.......... 




그래서 생각한 방법은 AAA를 기준으로 관리가 되게 만드는 것입니다. 추가할 때에는 AAA1로 추가를 하면서 자신 이외에 AAA로부터 상속받은 다른 컴포넌트가 이미 추가되어있는지를 확인하고, 컴포넌트를 얻어올 때에는 AAA로도 얻어올 수 있다면 문제가 해결될 것 같습니다.




그래서 제가 생각한 방법은 이런 관리를 하기 위한 특별한 컴포넌트 계층인 SingleComponent를 만드는 것입니다. SingleComponent를 상속받은 컴포넌트는 무조건 게임오브젝트에 하나만 넣을 수 있고, SingleComponent의 바로 아래 클래스를 기준으로 관리를 하는 것으로 기존의 패밀리식별자를 대처하는 것입니다.

이렇게 구현하기 위해서는 AAA1Component로 SingleComponent의 아래에 있는 AAAComponent를 알아내야 합니다. 또는 AAA밑에 다른 계층이 있어도 그 클래스로 AAAComponent를 알아내야 하죠. 그래서 고민을 한 끝에 RTTI에 이런 기능을 넣게 되었습니다.



먼저 템플릿에서 현재 클래스와 상위 클래스를 사용하기 위해 MyType과 BaseType를 만들고..
(__super 키워드는 비표준 문법입니다. __super를 안쓰려면 상위 클래스의 이름을 따로 받아야 할 것 같습니다..)


그리고 MyType과 BaseType을 사용해서 이런 간단한 유틸리티 템플릿 구조체를 만들었습니다.

one_step_direct_descendant는 Derived부터 Base까지 상위 타입이 Base와 같은지를 비교해가면서 Base의 바로 아래 타입을 알아올 수 있는 템플릿 구조체 입니다.

이것을 이용하여 다음과 같이 작성을 하면 AAAComponent의 타입을 사용할 수 있습니다.

one_step_direct_descendant<SingleComponent, AAA1Component>::type;


이제 이렇게 SingleComponent와 AAA1Component로 AAAComponent를 얻어올 수 있게 되었습니다!


그래서 저는 이 템플릿 구조체를 사용하여 InsertComponent를 할때에 SingleComponent를 상속하고 있는 컴포넌트이면 SingleComponent의 바로 아래 타입으로 저장을 하고 상속하고 있지 않는 컴포넌트이면 해당 컴포넌트의 타입으로 저장을 하는식으로 InsertComponent 함수를 작성하였습니다.

Cpp2Html [-] Collapse
template<typename T>
T* InsertComponent()
{
    unsigned int componentID = boost::mpl::if_<type_traits::is_base_of<SingleComponent, T>,
        single_component_type,
        component_type>::type::Invoke<T>();

    if( m_components.find( componentID ) != m_components.end() )
    {
        return NULL;
    }

    T* pComponent = new T();
    m_components.insert( std::make_pair( componentID, pComponent ) );
    pComponent->SetOwner( this );
    return pComponent;
}

먼저 if_와 is_base_of를 사용하여 SingleComponent 로부터 상속된 타입인지를 검사합니다. SingleComponent로부터 상속된 타입이면 single_component_type의 Invoke 함수를 실행하고 아니면 component_type의 Invoke 함수를 실행합니다.

if_ : 
http://www.boost.org/doc/libs/1_49_0/libs/mpl/doc/refmanual/if.html 참조
is_base_of :  http://www.boost.org/doc/libs/1_49_0/libs/type_traits/doc/html/boost_typetraits/reference/is_base_of.html  참조

Cpp2Html [-] Collapse
struct single_component_type
{
    template<typename T>
    unsigned int Invoke()
    {
        return RTTI::GetTypeID<typename RTTI::one_step_direct_descendant<SingleComponent, T>::type>();
    }
};

struct component_type
{
    template<typename T>
    unsigned int Invoke()
    {
        return RTTI::GetTypeID<T>();
    }
};

single_component_type의 Invoke 함수는 InsertComponent에 들어온 타입과 SingleComponent로부터 SingleComponent의 바로 아래 타입의 ID를 반환하고, component_type의 Invoke 함수는 현재 타입의 ID를 반환합니다.

GetComponent 함수도 역시 마찬가지입니다.

Cpp2Html [-] Collapse
template<typename T>
T* GetComponent()
{
    unsigned int componentID = boost::mpl::if_<type_traits::is_base_of<SingleComponent, T>,
        single_component_type,
        component_type>::type::Invoke<T>();

    ComponentTable::const_iterator iter = m_components.find( componentID );
    if( iter != m_components.end() )
    {
        return static_cast<T*>( iter->second.get() );
    }

    return NULL;
}

InsertComponent와 같은 방법으로 컴포넌트의 ID를 얻어오고 그 ID로 컴포넌트를 찾아서 반환해주는 것입니다.

이렇게 되면 컴포넌트를 만들때 하나만 관리되어야 하는 컴포넌트이면 SingleComponent를 상속받고 그렇지 않으면 ComponentBase를 상속하여 구현을 하는 것으로 관리가 될 것입니다. 


(사실 위의 코드는 문제가 조금 있습니다. 따라서 현재 저희 프로젝트에서 사용하는 코드와 조금 다릅니다. 위 코드의 문제점에 대해 알아내신 분은 kgun86@dragonflygame.com으로 연락주세요.. 저희팀에서 어떤식으로 처리를 했는지에 대해 설명해드리고 같이 토론을 해보면 좋을 것 같습니다.. ㅎㅎ)


일단 이번 연재는 이정도로 마무리를 하도록 하고.. 다음 연재에서는 매크로를 사용한 컴포넌트 설계에 대해 설명을 하게 될 것 같습니다.




ps. 저희팀에서 현재 프로그래머를 모집하고 있습니다!!

경력자만 모집하고 있구요.. 네트워크, 그래픽스, 게임플레이, 툴, DB 등등등 모든 분야를 모집하고 있습니다!!

사무라이쇼다운 온라인을 MMORPG로 개발하고 있구요. 저희팀으로 오시면 하고 싶은거 시켜드립니다! 자세한건 메일 보내주시면 설명해드리겠습니다!


 
위 초대장의 빈칸을 채워서 kgun86@dragonflygame.com 으로 보내주세요!!! 

반응형
,