Разработка компонентной игры

16

Я пишу шутер (например, 1942, классическая 2D графика), и я хотел бы использовать подход, основанный на компонентах. До сих пор я думал о следующем дизайне:

  1. Каждый элемент игры (дирижабль, снаряд, усиление, враг) является сущностью

  2. Каждый объект представляет собой набор компонентов, которые можно добавлять или удалять во время выполнения. Примерами являются Позиция, Спрайт, Здоровье, IA, Урон, BoundingBox и т. Д.

Идея в том, что Airship, Projectile, Enemy, Powerup НЕ являются игровыми классами. Сущность определяется только теми компонентами, которыми она владеет (и которые могут изменяться с течением времени). Итак, игрок Дирижабль начинает с компонентов Sprite, Position, Health и Input. Powerup имеет 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 в файле lua или python).

Основной цикл не заботится о сущностях: он управляет только компонентами. Дизайн программного обеспечения должен позволять:

  1. Получив компонент, получите сущность, к которой он принадлежит

  2. Получив сущность, получите компонент типа «тип»

  3. Для всех сущностей сделайте что-нибудь

  4. Для всех компонентов сущности сделайте что-нибудь (например: serialize)

Я думал о следующем:

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 (благодаря алгоритмам boost :: 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: каждый тип Component живет в своем собственном списке. Итак, есть список Sprite, список работоспособности, список повреждений и т. Д. Я знаю, что они принадлежат одной и той же логической сущности из-за идентификатора сущности. Это проще, но медленнее: компонентам IA необходим доступ в основном ко всем другим компонентам объекта, и для этого потребуется поиск по списку каждого другого компонента на каждом шаге.

Какой подход ты думаешь лучше? подходит ли boost :: fusion map для такого использования?

Эмилиано
источник
2
почему понизить? Что не так с этим вопросом?
Эмилиано

Ответы:

6

Я обнаружил, что компонентный дизайн и ориентированный на данные дизайн идут рука об руку. Вы говорите, что наличие однородных списков компонентов и исключение первоклассного объекта-сущности (вместо того, чтобы выбирать идентификатор сущности для самих компонентов) будет «медленнее», но это ни здесь, ни там, поскольку вы на самом деле не профилировали ни один настоящий код, который реализует оба подхода, чтобы прийти к такому выводу. Фактически, я могу почти гарантировать вам, что гомогенизация ваших компонентов и избежание традиционной тяжелой виртуализации будут быстрее благодаря различным преимуществам ориентированного на данные проектирования - более простое распараллеливание, использование кэша, модульность и т. Д.

Я не говорю, что этот подход идеален для всего, но системы компонентов, которые в основном представляют собой наборы данных, которые требуют одинаковых преобразований, выполняемых на каждом кадре, просто кричат, чтобы быть ориентированными на данные. Будут времена, когда компоненты должны будут взаимодействовать с другими компонентами разных типов, но в любом случае это будет неизбежным злом. Однако это не должно влиять на дизайн, поскольку существуют способы решения этой проблемы даже в крайнем случае, когда все компоненты обрабатываются параллельно, например, очереди сообщений и фьючерсы. .

Определенно, Google ищет ориентированный на данные дизайн, так как он относится к компонентным системам, потому что эта тема поднимается очень часто, и там довольно много дискуссий и анекдотических данных.

Скайлер Йорк
источник
что вы подразумеваете под "ориентированным на данные"?
Эмилиано
В Google есть много информации, но вот появилась достойная статья, которая должна предоставить обзор высокого уровня, после чего следует обсуждение , касающееся систем компонентов: gamesfromwithin.com/data-oriented-design , gamedev. net / topic /…
Skyler York
Я не могу согласиться со всем, что связано с DOD, поскольку я думаю, что оно не может быть завершено само по себе, я имею в виду, что только DOD может предложить очень хороший пример для хранения данных, но для вызова функций и процедур вам нужно использовать либо процедурный, либо ООП-подход, я имею в виду, проблема состоит в том, как объединить эти два метода, чтобы получить максимальную выгоду как для производительности, так и для простоты кодирования, например. в структуре, которую я предполагаю, будет проблема с производительностью, когда все объекты не разделяют некоторые компоненты, но это может быть легко решено с помощью DOD, вам нужно только создать различные массивы для различных типов событий.
Ali1S232
Это не отвечает на мой вопрос напрямую, но очень информативно. Я вспомнил кое-что о потоках данных еще во времена университета. Это лучший ответ, и он "выигрывает".
Эмилиано
-1

если бы я написал такой код, я бы предпочел не использовать этот подход (и я не использую никакого повышения, если это важно для вас), поскольку он может делать все, что вы хотите, но проблема в том, что слишком много энтузиастов которые не разделяют некоторые компоненты, поиск тех, у которых они есть, займет некоторое время. кроме этого нет никакой другой проблемы, которую я могу вещь:

// 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
источник
почему я получил отрицательный голос? что не так с моим решением?
Ali1S232
3
Ваше решение на самом деле не является компонентным решением, поскольку компоненты не отделены от ваших игровых классов. Все ваши экземпляры опираются на отношение IS A (наследование) вместо отношения HAS A (состав). Делая это композиционным способом (сущности обращаются к нескольким компонентам), вы получаете много преимуществ по сравнению с моделью наследования (именно поэтому обычно вы используете компоненты). Ваше решение не дает ни одного из преимуществ решения на основе компонентов и вводит некоторые особенности (множественное наследование и т. Д.). Нет данных, нет отдельного обновления компонента. Нет модификации во время выполнения компонентов.
аннулировано
В первую очередь вопрос заключается в том, что каждый экземпляр компонента связан только с одной сущностью, и вы можете активировать и деактивировать компоненты, только добавив bool isActiveбазовый компонентный класс. по-прежнему необходимо вводить используемые компоненты, когда вы определяете организации, но я не считаю это проблемой, и все же у вас есть отдельные обновления компонентов (вспомните что-то вроде dynamic_cast<componnet*>(entity)->update().
Ali1S232
и я согласен, что все еще будет проблема, когда он захочет иметь компонент, который может обмениваться данными, но учитывая то, что он просил, я думаю, что для этого не будет проблемы, и опять же, есть некоторые хитрости для этой проблемы, что если вы хочу я могу объяснить.
Ali1S232
Хотя я согласен, что это можно реализовать таким образом, я не думаю, что это хорошая идея. Вы, дизайнеры, не можете сами составлять объекты, если у вас нет одного über-класса, который наследует все возможные компоненты. И хотя вы можете вызывать update только для одного компонента, у него не будет хорошей компоновки в памяти, в составной модели все экземпляры компонентов одного типа могут храниться в памяти близко и повторяться без каких-либо промахов в кеше. Вы также полагаетесь на RTTI, который обычно отключается в играх по соображениям производительности. Хорошая сортировка объектов в основном исправляет это.
аннулировано