컴포넌트 기반 게임 디자인 루프는 엔터티에 대해 큰

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은 그런 식으로 사용하기에 적합합니까?



답변

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

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

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


답변

내가 그런 코드를 작성한다면 오히려이 접근법을 사용하지 않을 것입니다 (그리고 당신에게 중요하다면 부스트를 사용하지 않을 것입니다). 원하는 모든 것을 할 수 있지만 문제는 엔터티가 너무 많을 때입니다 일부 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

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