프로그래밍 언어 입문서가 아닌 프로그래밍 기초 개념 입문서
문과생, 비전공자를 위한 프로그래밍 입문책입니다.
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 으로 보내주세요!!! 

댓글을 달아 주세요

  1. Favicon of https://gamedevforever.com 친절한티스 2012.03.15 15:43 신고  댓글주소  수정/삭제  댓글쓰기

    하고 싶은거 시켜주신다니!! 전 사장님 하고 싶어요.

  2. 이군 2012.03.15 21:34  댓글주소  수정/삭제  댓글쓰기

    이번 회도 잘 읽었습니다. 전 enum과 템플릿 메서드로 해결했었는데 이런 방법도 있었군요.. 잘배워가도록 하겠습니다. 매번 좋은글 올려주셔서 감사합니다.

    • Favicon of https://gamedevforever.com 끼로 2012.03.16 01:04 신고  댓글주소  수정/삭제

      읽어주셔서 감사합니다 ㅎㅎ 그리고 이군님이 해결한 방법도 궁금하네요..

    • 이군 2012.03.16 02:25  댓글주소  수정/삭제

      저 같은 경우 패밀리 식별자로만 관리했습니다. insert는 ComponentBase로 받아서 패밀리 식별자를 쓰고, get을 할 때는 템플릿을 써서 타입과 식별자를 같이 쓰도록했습니다.

      부끄럽지만 코드도 같이 첨부해보도록 하겠습니다.

      void AttachComponent(LP_COMPONENT Component);
      template <class Component>
      Component* GetComponent(ComponentType GettingComponentType)
      {
      return dynamic_cast<Component*>(Components[GettingComponentType]);
      };

    • Favicon of https://gamedevforever.com 끼로 2012.03.22 22:04 신고  댓글주소  수정/삭제

      우와! 코드까지 올려주시다니! 감사합니다 ㅎㅎ GettingComponentType은 어떤 정보를 표현하는건가요?

  3. 액션가면 2012.03.22 20:51  댓글주소  수정/삭제  댓글쓰기

    좋은 글 잘 보았습니다.
    FamilyComponent부분을 이렇게 해도 되지 않을까 해서 한번 코드를 적어보겠습니다.

    template <typename T>
    T* GetFamilyComponent(unsigned int componentId)
    {
    ComponentTable::const_iterator iter = m_components.find(componentId);
    if (iter != components.end())
    {
    if (iter->second->IsKindOf(&T::ms_RTTI))
    return static_cast<T*>(iter->second);
    }

    }

    • Favicon of https://gamedevforever.com 끼로 2012.03.22 22:07 신고  댓글주소  수정/삭제

      그렇게 하게 되면 componentId를 따로 받아야 하는데 처음에 이렇게 만들기 시작했을때 생각은 추가로 Id를 부여하지 않겠다는데서 출발했기 때문이었거든요.. Id를 따로 관리했을때에 문제가 되지 않는 경우라면 액션가면님의 코드가 훨씬 더 좋을것 같습니다

    • 액션가면 2012.03.23 14:14  댓글주소  수정/삭제

      제가 설명이 부족햇네요.
      본문에 나와있는대로
      AAA , AAA1, AAA2컴포넌트가 있을경우
      GetFamilyComponent<AAA>(RTTI::GetTypeID<AAA1>())
      사용하면 어떨까 해서 올린 코드였읍니다. .

    • Favicon of https://gamedevforever.com 끼로 2012.03.23 16:17 신고  댓글주소  수정/삭제

      아하! 그런 방향도 괜찮네요 ㅎㅎ GetFamilyComponent<AAA, AAA1>(); 같은 형태도 좋을것 같습니다

    • Favicon of http://pak2536.tistory.com 몽상귀 2012.03.23 17:26  댓글주소  수정/삭제

      그럼 AAA1 이 사용중이라는 것도 알아야 되는 것 아닌가요?

      사실 컴포넌트 쓰는건 외부 소스(XML/YAML/JSON/DB/ETC Script..)와의 결합일 때 더 빛을 발할 것 같은데..

      템플릿으로 하면 컴파일 타임에 어느정도 까진 정해져서 좋긴 하겠는데, 데이터 드리븐이라고 해야되나?
      이걸 구현하기가 쉽진 않을 것 같기도 해요.

      컴포넌트 테이블과 ID 형식으로 하면 어느정도 쉽게 갈것같은데 .. 이렇게 소스코드에 박혀버리면 컨트롤하기가 쉽지 않을 듯 싶어서.. 헤헤

      사실 템플릿 메타 프로그래밍도 지금 소스코드 이해하는 정도의 수준이라서용 넘 자세히는 못끼겠슴돠..;

      딴짓하며 적느라 횡설수설이네요.
      아, 근데 코드에 문제가 있다고 하는데 궁금하네요.. ^^ 나중에 살짝 힌트점,

    • Favicon of https://gamedevforever.com 끼로 2012.03.23 17:47 신고  댓글주소  수정/삭제

      안그래도 다음에 쓸 내용이 xml이나 스크립트와의 연동 관련된 내용입니다 ㅎㅎ; 아 이제 슬슬 써야 하는데..Orz

    • Favicon of https://gamedevforever.com 끼로 2012.03.23 18:03 신고  댓글주소  수정/삭제

      아 그리고 사실 문제가 있는 부분은.. 글을 급하게 쓰다보니 이 글의 코드에서 생긴 문제인데 그냥 이렇게 마무리를 해버린 것인데요.. ㅡ .-;;; 사실 위 코드처럼 만들게 되면 Insert 할때는 AAA1을 넣고 Get 할때는 AAA2로 얻어오게 될때 문제가 발생합니다.. AAA1을 넣을때 내부에서는 AAA로 관리가 되고 그 상태에서 AAA2를 얻어오려고 하면 AAA2도 AAA의 아이디를 얻어올 수 있으니 AAA1의 객체를 AAA2로 강제 변환을 하게 되버리는거죠... 굉장히 큰 문제입니다 ㅡ.ㅡ;; 저희팀에서 사용하는 코드랑 똑같이 쓴게 아니다보니.. 쓰고 보니까 이런 문제가 있는 코드더라구요..

    • Favicon of http://pak2536.tistory.com 몽상귀 2012.03.26 10:31  댓글주소  수정/삭제

      음 그 문제라면 get 할 때, RTTI.h 에 있는 방법을 쓰면 잘 될지도 모르겠다는 생각이 드네요.. ^^

      dynamic cast 가 있던데.. ㅎㅎ

      답변 감사드리고, 다음 편이 기대됩니다.

  4. Favicon of http://pak2536.tistory.com 몽상귀 2012.03.23 12:38  댓글주소  수정/삭제  댓글쓰기

    트리에서 바로 1레벨 까지만 자식을 갖출 수 있다면,

    RTTI 등록할 때 부모의 정보를 갖고 있을텐데,
    이걸 이용해서 GetRTTI 할 때, 자신이 아니라 부모껄 등록하는 건 어떤가요?

    그리고 get 하면 AAA1 은 AAA 가 될테구요.

    좀 쉽게갈 수 있을 것 같습니다. 1단계로 제약이 된다면,
    물론 이 이상으로 간다면 root RTTI 정보를 보관하는 식으로 하면 해당 트리 중 자식을 등록해도 root RTTI 가 등록 되는 방식이 되겠죠.

    • Favicon of https://gamedevforever.com 끼로 2012.03.23 16:21 신고  댓글주소  수정/삭제

      그런데 일단 그렇게 가게 되면 컴포넌트의 구현 자체에 제약이 너무 걸릴것 같습니다 일단 현재 저희 게임에서도 컴포넌트 상속 관계가 2단계나 3단계까지 가는 경우도 있거든요.. 그리고 그렇게 가게 되면 컴파일타임에 2단계 이상 상속을 받은 경우를 체크해서 에러를 내야 하는데 그것도 생각해 봐야 할 문제구요.. 사실 그래서 SingleComponent같은걸 만들게 되었습니다. 같은 패밀리 컴포넌트이면 하나만 가져야 애매한 구조를 덜 만들어낼 것 같기도 했구요. 좋은 의견 감사합니다

    • Favicon of http://pak2536.tistory.com 몽상귀 2012.03.23 17:39  댓글주소  수정/삭제

      음.

      1단계 -- ROOT
      2단계 -- 1 2
      .. -- ......
      N단계 --11 22 33 44

      였을 경우, RTTI를 찾을 때 어차피 위로 순회를 하는 코드가 있잖아요?
      그 곳에 꼽사리 껴서 ROOT까지 올라가서 찾은 후 그것을 반납.
      그리고 이걸 컨테이너에 보관하면 해당 패밀리(base = root)는 모두 root id 를 반납하니 중복체크도 자연스럽게 되구요.. ㅎㅎ;
      그런데 항상 ROOT 만 반납하니 N 단계에서 N-M(N > M > 1) 단계를 찾으려면 지금 방식으로는 안되긴 하겠네요.. 소기의 목적은 패밀리 끼리는 중복 적용안되는 것이니 ^^데헷 ㅋ

      문제는 그 root 를 지정해주는 건데.
      위의 RTTI 도 decl..root 라고 있는데 그걸 이용하면 어떨까 하는 간단한 아이디어였습니다.


      템플릿 코드는 보기가 어려워 집중해서 다시 보니, 제가 말한 내용에 근접했네요 -_-;; (ㅈㅅ 템플릿 코드는 보기가 넘 심들어요)
      이렇게도 쓸 수 있다는 점 배워갑니다.
      메타 템플릿 프로그래밍은 보면 볼수록 lisp 같은 functional lang 이랑 비슷하군요..^^

    • Favicon of https://gamedevforever.com 끼로 2012.03.23 17:51 신고  댓글주소  수정/삭제

      아아!! 말씀하신 1단계 제약이라는게 SingleComponent같은걸 만들지 않고 기준점을 ComponenetBase의 아래 단계를 기준으로 한다는 말씀이신것 같네요.. 제가 만든 템플릿 코드가 그걸 알아오는 코드입니다 ㅎㅎ;; 단지 SingleComponent같은 단계를 만든 이유는 어떤 컴포넌트들은 여러개가 하나의 게임 오브젝트에 붙을수도 있으니까 그걸 구분하기 위한 것이지요..

  5. Favicon of http://ikpil.com 최익필 2013.03.26 05:14  댓글주소  수정/삭제  댓글쓰기

    http://gamedevforever.com/119#comment8578616 댓글 중..
    "어떤 컴포넌트들은 여러개가 하나의 게임 오브젝트에 붙을수도 있으니까" 부분에서 이해가 안가는게 있어 질문 올립니다.

    질문
    컴포넌트간 구분을 타입 아이디로 결정한 상황에서 하나의 게임 오브젝트에 같은 컴포넌트를 여러개를 넣는다면, 각각의 컴포넌트는 어떻게 구별되며, 밀어 넣을 수 있나요?

  6. Favicon of http://ikpil.com 최익필 2013.03.26 05:14  댓글주소  수정/삭제  댓글쓰기

    http://gamedevforever.com/119#comment8578616 댓글 중..
    "어떤 컴포넌트들은 여러개가 하나의 게임 오브젝트에 붙을수도 있으니까" 부분에서 이해가 안가는게 있어 질문 올립니다.

    질문
    컴포넌트간 구분을 타입 아이디로 결정한 상황에서 하나의 게임 오브젝트에 같은 컴포넌트를 여러개를 넣는다면, 각각의 컴포넌트는 어떻게 구별되며, 밀어 넣을 수 있나요?

    • Favicon of https://gamedevforever.com 끼로 2013.04.19 10:17 신고  댓글주소  수정/삭제

      아 오래전에 쓴 글이라 이제서야 댓글을 확인했네요;; 일단 이때 당시에는 멀티맵으로 컴포넌트 리스트를 관리했었구요 그래서 같은 타입의 컴포넌트의 경우 컴포넌트 하나하나를 구분하는 방법은 생각하지 않았습니다. 그런데 쓰다보니 컴포넌트 여러개를 밀어넣는게 좋은 인터페이스가 아니라고 생각되어 요즘에는 싱글컴포넌트 같은걸 아예 없애고 무조건 타입으로 구분해서 하나씩만 추가가 되도록 해서 작업중인데 개발상 문제도 없고 인터페이스도 더 깔끔해서 계속 이렇게 쓸 것 같습니다 ㅎㅎ