write   top   keyword   local   rss   guestbook 
levites
  카테고리
  달력
  검색
  요즘 올라온 글
  요즘 달린 댓글
  요즘 받은 트랙백
  글 보관함
  링크사이트
  방문자 집계
admin
Object-Oriented vs Component-Oriented
개발 - 프로그래밍 | 10/01/07 23:21
.. 얼마만의 프로그래밍 관련 포스팅인지..
죄송합니다.
저 프로그래머 아닌거 같습니다.


암튼, MMORPG 를 만든다고 하겠습니다.
개발 초창기, 전사와 마법사가 확정 되어서 다음과 같이 코딩을 시작했습니다.

class CCharacter
{
    protected:
        int m_curHP;
        int m_maxHP;
};

class CFighter : public CCharacter
{
    protected:
        int m_curRage;
        int m_maxRage;
};

class CWizard : public CCharacter
{
    protected:
        int m_curMP;
        int m_maxMP;
};

HP 는 모든 캐릭터가 사용하니 Character 에 넣고, Rage 는 전사만, MP 는 마법사만 갖게 해두었습니다. 헌데 기획팀에서 갑자기 Rage 와 MP 를 동시에 쓰는 드루이드를 만들자고 합니다. 결국 Rage 와 MP 를 Character 로 올릴수 밖에 없는 상황.

개인적으론 프로젝트를 진행 하다 보면 위와 같은 상황을 꽤 자주 만나게 됩니다. '상속' 에 지나치게 의지하고 있었던 탓인데.. 이런 문제엔 결국 모든 기능을 들고 있는 슈퍼 클래스로 상황을 해결 할 수 밖에 없었습니다. 그리고 그러한 크고 무거운 슈퍼 클래스는.. 확실히 유지 보수에 좋지 않았고 말이죠.

수많은 함수들과 전역 변수 관리하기 바빴던 C 에서 벗어나 C++ 로 넘어가면서 Objected-Oriented 를 처음 봤을때의 그 감격은 잊혀지지가 않습니다. 하지만 전 거기에 너무 오래 얽매여 있었던것 같습니다. '상속' 은 결코 모든 것을 표현 할 수 있는 만능의 개념이 아님에도 불구하고.


링크 : 김학규님의 'OO Programming - 상속의 남용을 막자'

그렇다면 C++ GUI 개발자들은 C# 이나 다른 언어로 갈아타야만 하는 것인가? 가장 타당한 대안은
스크립트 언어를 사용하는 것이다

Window *w = new Window();
Button *b = new Button();
b->SetHandler("onclick", "MsgBox 'Hello'" }   // MsgBox 'Hello' 는 루아 코드
w->AddChild(b);
w->Show();

마침 글을 쓸려는 타이밍에 김학규님이 재밌는 글을 올리셨습니다. 요컨데, 상속의 표현력에는 한계가 있고, 그것을 극복하기 위해 스크립트가 필요하다. 라는 것. 기획자들의 접근성을 위한다는 측면에서 비슷한 형태로 스크립트의 활용도는 높아져 가고 있지만.. 하지만 역시, 개인적으론 스크립트 언어는 성능 상의 문제와 안정성 측면에서 신뢰도가 떨어지기에 프로젝트 전반에 걸쳐 적용할만한 궁극적인 대안이라 보기엔 좀 힘들지 않을까.. 라고 생각힙니다.


링크 : noerror 님의 번역 '게임 오브젝트 컴포넌트 설계 방식 이론과 실제'

얼마전 XBox 360 으로 발매된 Prototype 의 프로그래머가 자신들이 개발할때 사용한 게임 오브젝트 컴포넌트 설계 방식에 대해 GDC 에서 발표한 문서.

요약하자면, 객체를 조립하자. 라는 것입니다. 예를들자면, 전사에게는 "체력" 클래스와 "분노" 클래스를 붙여주고, 마법사에게는 "체력", "마나" 클래스를, 드루이드 에게는 "체력", "분노", "마나" 클래스를 붙여준다는 것이죠.

코드로 보면..

class ICharacterBehaviour
{
    public:
        virtual void OnUpdate() {}
        virtual void OnMessage(IMessage * pMessage) {}

    protected:
        CCharacter * m_pCharacter;
};

class CCharacterBehaviour_HP : public ICharacterBehaviour
{
    public:
        void OnMessage(IMessage * pMessage) override
        {
            switch (pMessage->GetType())
            {
                case MSG_DAMAGE:
                    m_curHP -= ((CMessage_Damage*)pMessage)->GetDamage();
                    if (m_curHP <= 0)
                        m_pCharacter->SendMessage(new CMessage_Dead);
                    break;
            }
        }

    protected:
        int m_curHP;
        int m_maxHP;
};

class CCharacterBehaviour_MP : public ICharacterBehaviour
{
    protected:
        int m_curMP;
        int m_maxMP;
};

class CCharacterBehaviour_Rage : public ICharacterBehaviour
{
    protected:
        int m_curRage;
        int m_maxRage;
};

class CCharacter
{
    public:
        void AppendCharacterBehaviour(ICharacterBehaviour * pCharacterBehaviour)
        {
            m_characterBehaviourList.push_back(pCharacterBehaviour);
        }

    public:
        void SendMessage(IMessage * pMessage)
        {
            std::list::iterator itor = m_characterBehaviourList.begin();
            std::list::iterator itor_end = m_characterBehaviourList.end();
            for (; itor != itor_end; ++itor)
            {
                ICharacterBehaviour * pCharacterBehaviour = *itor;

                pCharacterBehaviour->OnMessage(pMessage);
            }
        }

    protected:
        std::list m_characterBehaviourList;
};

/////
/////

CCharacter * CreateFighter()
{
    CCharacter * pFighter = new CCharacter;

    pFighter->AppendCharacterBehaviour(new CCharacterBehaviour_HP);
    pFighter->AppendCharacterBehaviour(new CCharacterBehaviour_Rage);

    return pFighter;
}

CCharacter * CreateWizard()
{
    CCharacter * pWizard = new CCharacter;

    pWizard->AppendCharacterBehaviour(new CCharacterBehaviour_HP);
    pWizard->AppendCharacterBehaviour(new CCharacterBehaviour_MP);

    return pWizard;
}

CCharacter * CreateDruid()
{
    CCharacter * pDruid = new CCharacter;

    pDruid->AppendCharacterBehaviour(new CCharacterBehaviour_HP);
    pDruid->AppendCharacterBehaviour(new CCharacterBehaviour_Rage);
    pDruid->AppendCharacterBehaviour(new CCharacterBehaviour_Rage);

    return pDruid;
}

근래 읽었던 테크니컬 문서 중에서 가장 감명 깊게 본 듯. 뒤쪽에도 꽤 재밌는 뒷이야기들이 있는데, 인상적인 내용들만 대충 추려보면..

+ 조립을 코드에서 하지 않고 기획자들이 스크립트 상에서 할 수 있게 했음.
+ 덕분에 새로운 엔티티를 쉽게 만들어 추가 할 수 있었음. (주인공, 탈것, 멀리 있는 시민, 가까이 있는 시민 등)
+ Behaviour 는 재사용이 가능하다.
+ 코드 상에 게임 오브젝트에 대한 정의는 존재하지 않는다.
+ 즉, 제네릭한 코드 작성이 가능하다.

- 제네릭한 코드를 작성"해야만" 한다.
- 직접적으로 작성하는 것보다는 빠르지 않다. (메세지를 모든 Behaviour 에 뿌려야 하므로)
- 복잡해 하고, 귀찮아 하는 프로그래머들이 많았다.

> 1544 종의 게임 오브젝트 정의
> 145 종의 Behaviours
> 145 종의 Behaviours
> 335 종의 고유한 데이터형
> 156 종의 단일 데이터
> 게임내 6400개의 클래스

6400!? 읽었을 당시에는 잔손만 많이 갈것 같고 실제로는 그다지 효율이 좋지 않을것 같았는데.. 자그마한 프로젝트에 적용해봤더니 엄청난 효율을 보여주길래 놀랐습니다. 실제 대규모 프로젝트에도 쓸 수 있을까 하고 여러가지로 고민해 보고 있는데.. 역시 가장 큰 이슈는 속도일듯. 우선은 작은 프로젝트들에서 여러가지로 테스트를 많이 해봐야 할 것 같습니다. 저는 아직 겨우 이런 레벨인데..

링크 : 매운맛나리님의 '게임의 객체 시스템'

저 글을 읽고서 기억난 매운 맛나리님의 깊이 있는 고찰. 당시에 올라왔었을때는 감흥이 별로 없었는데, 컴퍼넌트 기반 프로그래밍을 해보고 나서 읽었을때의 감동이란..

예를 들어 로직 인벤토리가 하나의 컴퍼넌트라고 치면 아이템을 넣고 빼는 연산과 자리가 비어서 아이템을 넣을 수 있는지, 아이템이 있어서 꺼낼 수 있는지 확인하는 함수 외에는 아무 것도 구현하지 않는 것이죠.

그러면 도대체 인벤토리에 아이템을 넣으려면 어떻게 하잔 거냐? 여기에 트랜잭션 객체란 개념을 도입합니다. 마우스 컨트롤러 컴퍼넌트가 인벤토리 컴퍼넌트에 메시지를 보내는 게 아니라, 트랜잭션 객체 생성자가 업데이트 과정에 마우스 컨트롤러를 확인해서 아이템을 인벤토리에 넣어야겠구나, 이런 생각이 들면 아이템을 커서에서 꺼내서 인벤토리에 집어넣는 트랜잭션 객체를 생성해서 트랜잭션 큐에 밀어넣는 겁니다.

(아까도 언급했 듯이, 로직 인벤토리가 노출하는 아이템 추가됨 이벤트에 프리젠테이션 인벤토리가 자신을 등록하는 게 훨씬 이쁩니다만 설명을 위해서) 이 트랜잭션 객체는 1. 원본 로직 인벤토리에서 아이템을 제거하고, 2. 원본 프리젠테이션 인벤토리에서 아이템 이미지를 제거한 뒤에, 3. 커서 컴퍼넌트에서 프록시 아이템을 제거하고 4. 타겟 로직 인벤토리에 아이템을 추가하고, 5. 타겟 프리젠테이션 인벤토리에 아이템 이미지를 추가하는, 다섯 가지 단계로 구현할 수 있을 겁니다.

트랜잭션 객체를 도입하면 각각의 컴퍼넌트는 임의의 트랜잭션 객체를 구현하는데 필요한 최소한의 연산만을 구현하면 땡이기 때문에 전혀 수정이 필요없으므로 수정에 닫혀있어야 한다는 조건을 만족하는 동시에, 새로운 기능의 추가는 새로운 트랜잭션 객체의 구현으로 깔끔하게 해결할 수 있으므로 확장에 열려있어야 한다는 조건도 만족할 수 있습니다. 요는 어떤 기능의 개념에 대한 지식은 컴퍼넌트에, 컴퍼넌트 간의 관계와 작업 절차에 관련된 지식은 트랜잭션 객체에 모으는 것인데, 이로써 컴퍼넌트와 트랜잭션 객체 모두 단일책임 원칙(Single Responsibility Principle)과 개방-폐쇄 원칙(Open-Closed Principle)을 잘 지킬 수 있게 되지요.

트랜잭션 객체 역시 메시지와 유사한 속성을 가지고 있기 때문에 메시지를 사용하는 컴퍼넌트 모델과 마찬가지로 서버-클라이언트 모델에도 잘 어울리고, 멀티 스레딩 모델에도 문제없이 대응됩니다. 꽤 오랜 시간 고민하면서 개발중인 코드에 적용해 본 결과 큰 구멍없이 게임을 완성할 수 있겠다는 확신도 얻었습니다.

요는 컴퍼넌트 모델을 고려하시는 분들께서는 메시지 핸들링 루틴을 컴퍼넌트에 포함시키는 대신에 트랜잭션 객체의 도입을 한 번쯤 고려해보시는 것도 나쁘지 않을 것 같을 것 같습니다- 뭐 이런 얘기네요.

로직 처리에 컴퍼넌트 기반 프로그래밍을!? ... 이제야 컴퍼넌트 기반 프로그래밍을 깨달았다는게 부끄러울 따름입니다. 좀 더 많은 공부와 정진이 필요 할 듯. 열심히 하겠습니다.
트랙백 | 댓글(4)
스팸 방지를 위해 트랙백을 잠시 막아 두었습니다.
백승민 10/01/10 11:10 R X
실제로 쓰다보면
A 행동을 추가하기 위해서는 B C D 데이터 타입과 E행동이 꼭 추가되어 있어야 한다던가
혹은 A와 B가 같이 추가됐을땐 이런식으로 동작하는데 A와 C가 같이 추가되면 이런식으로 예측 못하게 동작한다던가...
하는 식으로 복잡한 기획 때문에 컴포넌트들이 깔끔하게 독립되지 못하고 상호작용을 다양하게 하는 경우가 많아서 이론처럼 깔끔하게 되기는 좀 힘든듯.
뭐 어떤 방법이 단점 없이 모든 문제를 해결해 주겠냐만은...
┗ 
levites 10/01/10 13:17 X
오.. 써보셨군요!
다음에 좀 더 자세한 경험담을 듣고 싶습니다. :$
백승민 10/01/13 00:59 R X
뭐 전체적으로 방법론을 도입했다기 보다는 일부 유사한 구조를 써본 경험에서 문제점을 예측해본 정도랄까.
┗ 
levites 10/01/13 03:22 X
으음.. 부분적으로 도입했는데도 쉽지 않았던거군요..
전면적으로 사용하기에 꽤 부담스러워지는.. ;
이름
비밀번호
홈페이지 비밀글로 저장
  submit
PREV 1 2 3 4 5 6 7 8 9 ... 313 NEXT
Powered by  TATTER TOOLS / 
Skin by Lutris