Проектування гри на основі компонентів


16

Я пишу шутер (як, наприклад, 1942, класична двовимірна графіка), і я хотів би використовувати підхід на основі компонентів. Поки я думав про наступний дизайн:

  1. Кожен ігровий елемент (дирижабль, снаряд, боєприпас, ворог) - це Сутність

  2. Кожна організація - це набір компонентів, які можна додавати або видаляти під час виконання. Приклади: положення, спрайт, здоров'я, IA, пошкодження, BoundingBox тощо.

Ідея полягає в тому, що Airship, Projectile, Enemy, Powerup НЕ ігрові класи. Суб'єкт господарювання визначається лише компонентами, якими він володіє (і які можуть змінюватися протягом часу). Отже, дирижабль гравця починається з компонентів Sprite, Position, Health та Input. Блок живлення має 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. Давши сутність, отримайте компонент типу "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 (завдяки алгоритмам 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: кожен тип компонента живе у своєму списку. Отже, є список спрайтів, список здоров'я, список пошкоджень тощо. Я знаю, що вони належать до тієї ж логічної сутності через ідентифікатор сутності. Це простіше, але повільніше: компонентам ІА потрібен доступ в основному до всіх інших компонентів сутності, і це потребує пошуку списку кожного іншого компонента на кожному кроці.

Який підхід, на вашу думку, є кращим? карта boost :: fusion підходить для використання таким чином?


2
чому потік? Що не так у цьому питанні?
Еміліано

Відповіді:


6

Я виявив, що дизайн на основі компонентів та орієнтований на дані дизайн йдуть рука об руку. Ви говорите, що наявність однорідних списків компонентів та усунення першокласного об'єкта сутності (замість того, щоб вибрати ідентифікатор сутності на самих компонентах) буде "повільніше", але це ні тут, ні там, оскільки ви фактично не профілювали жодного реального коду, який реалізує обидва підходи для досягнення цього висновку. Власне кажучи, я майже можу вам гарантувати, що гомогенізація ваших компонентів та уникнення традиційної важкої віртуалізації відбуватимуться швидше за рахунок різних переваг дизайну, орієнтованого на дані - простішої паралелізації, використання кешу, модульності тощо.

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

Безумовно, Google займається дизайном, орієнтованим на дані, оскільки це стосується компонентних систем, оскільки ця тема виникає багато, і там є досить багато дискусій та анекдотичних даних.


що ви розумієте під "орієнтованими на дані"?
Еміліано

В Google є багато інформації, але тут виникла гідна стаття, яка повинна забезпечити високий рівень огляду, а потім обговорення, що стосується компонентних систем: gamesfromwithin.com/data-oriented-design , gamedev . net / topic /…
Skyler York

Я не можу погодитися з усіма питаннями про DOD, оскільки я думаю, що це не може бути повноцінним, я маю на увазі, що лише DOD може запропонувати дуже хороший підхід для зберігання даних, але для виклику функцій та процедур вам потрібно використовувати або процедурні, або Підхід OOP, я маю на увазі, що проблема полягає в тому, як поєднати ці два методи, щоб отримати максимальну користь як для продуктивності, так і для легкості кодування, наприклад. в структурі, на яку я думаю, виникне проблема продуктивності, коли всі суб'єкти не поділяють деякі компоненти, але це можна легко вирішити за допомогою 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

3
Ваше рішення насправді не є компонентним рішенням, оскільки компоненти не відокремлені від ваших ігрових класів. Усі ваші випадки покладаються на відношення IS (спадкування), а не на відношення (склад) HAS A. Виконання цього способу композиції (сутності звертаються до декількох компонентів) дає вам багато переваг перед моделлю успадкування (як правило, саме тому ви використовуєте компоненти). Ваше рішення не дає жодних переваг компонентного рішення та вводить деякі химерності (багаторазове успадкування тощо). Ні локалізації даних, ні окремого оновлення компонентів. Немає змін часу виконання компонентів.
недійсна

Перш за все, питання задає структуру, згідно з якою кожен екземпляр компонента пов'язаний лише з однією сутністю, і ви можете активувати та дезактивувати компоненти, лише додавши bool isActiveбазовий клас компонентів. все ще потрібно вводити корисні компоненти, коли ви визначаєте enteties, але я не вважаю це проблемою, і все ж у вас є окремі оновлення dynamic_cast<componnet*>(entity)->update()
компонентів

і я погоджуюся, що все ще буде проблема, коли він хоче мати компонент, який може обмінюватися даними, але враховуючи те, про що він попросив, я думаю, що для цього проблеми не буде, і знову є деякі хитрощі для цієї проблеми, і якщо ви хочу, я можу пояснити.
Ali1S232

Хоча я згоден, що реалізувати це можна таким чином, я не думаю, що це гарна ідея. Ви дизайнери не можете складати об'єкти самостійно, якщо у вас є один клас über, який успадковує всі можливі компоненти. І хоча ви можете викликати оновлення лише для одного компонента, воно не матиме гарного розташування в пам'яті, у складеній моделі всі екземпляри компонентів одного типу можуть зберігатися близько до пам’яті та повторюватись без пропусків кешу. Ви також покладаєтесь на RTTI, який зазвичай відключається в іграх через причини продуктивності. Хороше відсортоване розташування об'єктів виправляє, що в основному.
порожнеча
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.