컴포넌트 기반 게임 디자인


16

1942, 클래식 2D 그래픽과 같은 슈팅 게임을 작성 중이며 구성 요소 기반 접근 방식을 사용하고 싶습니다. 지금까지 나는 다음과 같은 디자인에 대해 생각했습니다.

  1. 각 게임 요소 (비행선, 발사체, 파워 업, 적)는 개체입니다

  2. 각 엔터티는 런타임에 추가하거나 제거 할 수있는 구성 요소 집합입니다. 예를 들면 Position, Sprite, Health, IA, Damage, BoundingBox 등이 있습니다.

비행선, 발사체, 적, 파워 업은 게임 클래스가 아닙니다. 엔터티는 소유 한 구성 요소에 의해서만 정의되며 시간에 따라 변경 될 수 있습니다. 비행선 플레이어는 스프라이트, 위치, 상태 및 입력 구성 요소로 시작합니다. 파워 업에는 Sprite, Position, BoundingBox가 있습니다. 등등.

메인 루프는 게임 "물리", 즉 구성 요소가 서로 상호 작용하는 방식을 관리합니다.

foreach(entity (let it be entity1) with a Damage component)
    foreach(entity (let it be entity2) with a Health component)
    if(the entity1.BoundingBox collides with entity2.BoundingBox)
    {
        entity2.Health.decrease(entity1.Damage.amount());
    }

foreach(entity with a IA component)
    entity.IA.update(); 

foreach(entity with a Sprite component)
    draw(entity.Sprite.surface()); 

...

구성 요소는 기본 C ++ 응용 프로그램에서 하드 코딩됩니다. 엔티티는 XML 파일 (루아 또는 파이썬 파일의 IA 부분)에서 정의 할 수 있습니다.

메인 루프는 엔터티에 대해 큰 관심을 갖지 않으며 구성 요소 만 관리합니다. 소프트웨어 설계는 다음을 허용해야합니다.

  1. 컴포넌트가 주어지면, 속한 엔티티를 얻는다

  2. 엔터티가 주어지면 "type"유형의 구성 요소를 가져옵니다.

  3. 모든 엔티티에 대해 무언가를 수행하십시오.

  4. 모든 엔티티의 구성 요소에 대해 무언가를 수행하십시오 (예 : 직렬화).

나는 다음에 대해 생각하고 있었다.

class Entity;
class Component { Entity* entity; ... virtual void serialize(filestream, op) = 0; ...}
class Sprite : public Component {...};
class Position : public Component {...};
class IA : public Component {... virtual void update() = 0; };

// I don't remember exactly the boost::fusion map syntax right now, sorry.
class Entity
{
   int id; // entity id
   boost::fusion::map< pair<Sprite, Sprite*>, pair<Position, Position*> > components;
   template <class C> bool has_component() { return components.at<C>() != 0; }
   template <class C> C* get_component() { return components.at<C>(); }
   template <class C> void add_component(C* c) { components.at<C>() = c; }
   template <class C> void remove_component(C* c) { components.at<C>() = 0; }
   void serialize(filestream, op) { /* Serialize all componets*/ }
...
};

std::list<Entity*> entity_list;

이 디자인을 사용하면 # 1, # 2, # 3 (부스트 :: fusion :: map 알고리즘) 및 # 4를 얻을 수 있습니다. 또한 모든 것이 O (1)입니다 (정확하지는 않지만 여전히 빠릅니다).

보다 일반적인 접근 방식도 있습니다.

class Entity;
class Component { Entity* entity; ... virtual void serialize(filestream, op) = 0; ...}
class Sprite : public Component { static const int type_id = 0; };
class Position : public Component { static const int type_id = 1; };

class Entity
{
   int id; // entity id
   std::vector<Component*> components;
   bool has_component() { return components[i] != 0; }
   template <class C> C* get_component() { return dynamic_cast<C> components[C::id](); } // It's actually quite safe
...
};

또 다른 접근법은 Entity 클래스를 제거하는 것입니다. 각 구성 요소 유형은 자체 목록에 있습니다. Sprite 목록, Health 목록, Damage 목록 등이 있습니다. 엔터티 ID로 인해 동일한 로직 엔터티에 속한다는 것을 알고 있습니다. IA 구성 요소는 기본적으로 다른 모든 엔터티의 구성 요소에 액세스해야하며 각 단계에서 서로 다른 구성 요소 목록을 검색해야합니다.

어떤 접근법이 더 낫다고 생각합니까? boost :: fusion map은 그런 식으로 사용하기에 적합합니까?


2
왜 공감해야합니까? 이 질문에 어떤 문제가 있습니까?
Emiliano

답변:


6

구성 요소 기반 디자인과 데이터 지향 디자인이 서로 밀접한 관련이 있음을 알게되었습니다. 동종의 구성 요소 목록이 있고 1 급 엔터티 개체를 제거하는 대신 (구성 요소 자체에서 엔터티 ID를 선택하는 대신) "느린"것이지만 실제로는 실제 코드를 프로파일 링하지 않았으므로 여기도 없습니다 결론에 도달하기 위해 두 가지 접근 방식을 모두 구현합니다. 사실, 데이터 지향 설계의 다양한 장점 (더 쉬운 병렬화, 캐시 활용도, 모듈화 등)로 인해 구성 요소를 균질화하고 기존의 대량 가상화를 피하는 것이 더 빠르다 는 것을 거의 보장 할 수 있습니다 .

나는이 접근 방식이 모든 것에 이상적이라고 말하지는 않지만 기본적으로 모든 프레임에서 동일한 변환을 수행해야하는 데이터 모음 인 구성 요소 시스템은 단순히 데이터 지향적으로 비명을 지른다. 구성 요소가 다른 유형의 다른 구성 요소와 통신해야하는 경우가 있지만 이는 어느 쪽이든 필요한 악이 될 것입니다. 그러나 메시지 대기열 및 선물 과 같은 모든 구성 요소가 병렬로 처리되는 극단적 인 경우 에도이 문제를 해결할 수있는 방법이 있기 때문에 디자인을 추진해서는 안됩니다. .

구성 요소 기반 시스템과 관련된 데이터 지향 설계를위한 Google은 확실히 존재합니다. 왜냐하면이 주제는 많이 나오고 거기에는 꽤 많은 토론과 일화적인 데이터가 있기 때문입니다.


"데이터 지향"이란 무엇입니까?
Emiliano

이 구글에 대한 많은 정보가 있지만 여기가 구성 요소를 시스템에 관련된 토론에 이어 높은 수준의 개요를 제공한다 팝업하는 괜찮은 기사입니다 : gamesfromwithin.com/data-oriented-design , gamedev. 순 / 주제 /…
Skyler York

나는 그 자체가 완전 할 수 없다고 생각하기 때문에 국방부에 관한 모든 것에 동의 할 수는 없다. 국방성만이 데이터를 저장하는 데 아주 좋은 aprroch를 제안 할 수 있지만 함수 또는 절차를 호출하려면 절차 적 또는 절차를 사용해야한다. OOP 접근 방식, 문제는 성능과 코딩 용이성 모두에서 가장 큰 이점을 얻기 위해이 두 가지 방법을 결합하는 방법입니다. 구조에서 모든 엔터티가 일부 구성 요소를 공유하지 않지만 DOD를 사용하여 쉽게 해결할 수있을 때 성능 문제가 발생한다고 제안합니다. 다른 유형의 entieties에 대해서만 다른 배열을 만들어야합니다.
Ali1S232 4:14에

이것은 내 질문에 직접 대답하지는 않지만 매우 유익합니다. 나는 대학 시절의 데이터 흐름에 대해 무언가를 기억했습니다. 지금까지 가장 좋은 답변이며 "승리"합니다.
Emiliano

-1

내가 그런 코드를 작성한다면 오히려이 접근법을 사용하지 않을 것입니다 (그리고 당신에게 중요하다면 부스트를 사용하지 않을 것입니다). 원하는 모든 것을 할 수 있지만 문제는 엔터티가 너무 많을 때입니다 일부 componnet을 공유하지 않고 시간이 오래 걸리는 것들을 찾습니다. 그 외에는 내가 할 수있는 다른 문제가 없습니다.

// declare components here------------------------------
class component
{
};

class health:public component
{
public:
    int value;
};

class boundingbox:public component
{
public :
    int left,right,top,bottom;
    bool collision(boundingbox& other)
    {
        if (left < other.right || right > other.left)
            if (top < other.bottom || bottom > other.top)
                return true;
        return false;
    }
};

class damage : public component
{
public:
    int value;
};

// declare enteties here------------------------------

class entity
{
    virtual int id() = 0;
    virtual int size() = 0;
};

class aircraft :public entity, public health,public boundingbox
{
    virtual int id(){return 1;}
    virtual int size() {return sizeof(*this);};
};

class bullet :public entity, public damage, public boundingbox
{
    virtual int id(){return 2;}
    virtual int size() {return sizeof(*this);};
};

int main()
{
    entity* gameobjects[3];
    gameobjects[0] = new aircraft;
    gameobjects[1] = new bullet;
    gameobjects[2] = new bullet;
    for (int i=0;i<3;i++)
        for(int j=0;j<3;j++)
            if (dynamic_cast<boundingbox*>(gameobjects[i]) && dynamic_cast<boundingbox*>(gameobjects[j]) &&
                dynamic_cast<boundingbox*>(gameobjects[i])->collision(*dynamic_cast<boundingbox*>(gameobjects[j])))
                if (dynamic_cast<health*>(gameobjects[i]) && dynamic_cast<damage*>(gameobjects[j]))
                    dynamic_cast<health*>(gameobjects[i])->value -= dynamic_cast<damage*>(gameobjects[j])->value;
}

이 접근법에서 모든 컴포넌트는 엔티티의 기본이므로 컴포넌트가 주어지면 포인터도 엔티티입니다! 두 번째로 요청하는 것은 일부 엔티티의 구성 요소에 직접 액세스하는 것입니다. 내가 사용하는 내 기관 중 하나에 액세스 손상을 필요로 할 때 dynamic_cast<damage*>(entity)->value, 그래서 만약 entity손상 구성 요소가이 값을 반환합니다. entity구성 요소 손상이 있는지 확실 하지 않은 경우 캐스트가 유효하지 않고 동일한 포인터이지만 유효하면 요청 된 유형을 갖는 경우 if (dynamic_cast<damage*> (entity))반환 값 dynamic_cast이 항상 NULL인지 쉽게 확인할 수 있습니다 . 그래서 당신은 아래와 같이 할 수 entities있는 모든 것을 가지고 무언가를하기 위해component

for (int i=0;i<enteties.size();i++)
    if (dynamic_cast<component*>(enteties[i]))
        //do somthing here

다른 질문이 있으면 답변 해 드리겠습니다.


왜 내가 투표권을 얻었습니까? 내 솔루션에 어떤 문제가 있습니까?
Ali1S232

3
컴포넌트는 게임 클래스와 분리되지 않으므로 솔루션은 실제로 컴포넌트 기반 솔루션이 아닙니다. 인스턴스는 모두 HAS A 관계 (구성) 대신 IS A 관계 (상속)에 의존합니다. 구성 방식 (엔티티가 여러 구성 요소를 처리 함)을 수행하면 상속 모델에 비해 많은 이점이 있습니다 (일반적으로 구성 요소를 사용하는 이유). 솔루션은 구성 요소 기반 솔루션의 이점을 제공하지 않으며 몇 가지 단점 (다중 상속 등)을 소개합니다. 데이터 지역, 별도의 구성 요소 업데이트가 없습니다. 구성 요소의 런타임 수정이 없습니다.
무효

우선 모든 질문은 모든 구성 요소 인스턴스가 하나의 엔터티에만 관련이있는 구조를 요구하며 bool isActive기본 구성 요소 클래스에 추가하여 구성 요소를 활성화 및 비활성화 할 수 있습니다 . 엔터티를 정의 할 때 여전히 사용 가능한 구성 요소를 소개해야하지만 문제로 생각하지는 않지만 여전히 업데이트를 따로 따로 따로 보관해야합니다 dynamic_cast<componnet*>(entity)->update().
Ali1S232

그리고 데이터를 공유 할 수있는 구성 요소를 원하지만 요청한 것을 고려할 때 여전히 문제가 없을 것이라는 데 동의합니다. 나는 그것에 대해 문제가 없을 것이라고 생각합니다. 내가 설명 할 수 있기를 바랍니다.
Ali1S232

이 방법으로 구현하는 것이 가능하다는 데 동의하지만 좋은 생각은 아닙니다. 가능한 모든 구성 요소를 상속하는 하나의 클래스가 없으면 디자이너는 객체 자체를 작성할 수 없습니다. 또한 하나의 구성 요소에서만 업데이트를 호출 할 수 있지만 메모리 내 레이아웃이 좋지는 않습니다. 구성된 모델에서는 동일한 유형의 모든 구성 요소 인스턴스를 메모리에서 가깝게 유지하고 캐시 누락없이 반복 할 수 있습니다. 또한 성능상의 이유로 게임에서 일반적으로 해제되는 RTTI에 의존하고 있습니다. 잘 정렬 된 개체 레이아웃은 대부분 그 문제를 해결합니다.
무효
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.