Як я можу правильно отримати доступ до компонентів у своїх системах C ++ Entity-Component System?


18

(Те, що я описую, ґрунтується на такому дизайні: що таке система системи?) , Прокрутіть униз і ви знайдете його)

У мене виникають проблеми зі створенням системи-компонентів сутності в C ++. У мене клас компонентів:

class Component { /* ... */ };

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

class SampleComponent : public Component { int foo, float bar ... };

Ці компоненти зберігаються всередині класу Entity, який надає кожному екземпляру Entity унікальний ідентифікатор:

class Entity {
     int ID;
     std::unordered_map<string, Component*> components;
     string getName();
     /* ... */
};

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

Тепер, з іншого боку, у мене є системний інтерфейс, який використовує інтерфейс Node всередині. Клас Node використовується для зберігання деяких компонентів однієї сутності (оскільки Система не зацікавлена ​​у використанні всіх компонентів сутності). Коли Системі належить update(), їй потрібно лише переглядати збережені у неї вузли, створені з різних об'єктів. Так:

/* System and Node implementations: (not the interfaces!) */

class SampleSystem : public System {
        std::list<SampleNode> nodes; //uses SampleNode, not Node
        void update();
        /* ... */
};

class SampleNode : public Node {
        /* Here I define which components SampleNode (and SampleSystem) "needs" */
        SampleComponent* sc;
        PhysicsComponent* pc;
        /* ... more components could go here */
};

Тепер проблема: скажімо, я будую SampleNodes, передаючи сутність в SampleSystem. Потім SampleNode "перевіряє", чи має об'єкт необхідні компоненти для використання в SampleSystem. Проблема з'являється, коли мені потрібен доступ до потрібного компонента всередині Entity: компонент зберігається у Componentколекції (базовий клас), тому я не можу отримати доступ до компонента та скопіювати його на новий вузол. Я тимчасово вирішив проблему, перекинувши її Componentна похідний тип, але хотів дізнатися, чи є кращий спосіб зробити це. Я розумію, чи це означатиме перепроектування того, що я вже маю. Спасибі.

Відповіді:


23

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

З картою, оголошеною так:

std::unordered_map<const std::type_info* , Component *> components;

функція addComponent на зразок:

components[&typeid(*component)] = component;

та getComponent:

template <typename T>
T* getComponent()
{
    if(components.count(&typeid(T)) != 0)
    {
        return static_cast<T*>(components[&typeid(T)]);
    }
    else 
    {
        return NullComponent;
    }
}

Ви не отримаєте промаху. Це відбувається тому typeid, що поверне вказівник на інформацію про тип типу виконання (найбільш похідний тип) компонента. Оскільки компонент зберігається з інформацією про цей тип як його ключовий, команда не може викликати проблеми через невідповідні типи. Ви також можете перевірити тип часу компіляції для типу шаблону, оскільки він повинен бути типом, похідним від Component, або ж static_cast<T*>типи будуть невідповідними типами з unordered_map.

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

Ця друга реалізація трохи не інтуїтивно думати, ніж перша, але вона, ймовірно, може бути прихована як деталі реалізації за інтерфейсом, так що користувачі системи не повинні піклуватися. Я не буду коментувати, що краще, оскільки я не використовував другого, але не вважаю, що використання static_cast є проблемою з настільки сильною гарантією на типи, як надає перша реалізація. Зауважте, що для цього потрібна RTTI, яка може бути або не бути проблемою в залежності від платформи та / або філософських переконань.


3
Я використовую C ++ вже майже 6 років, але щотижня я вивчаю якийсь новий трюк.
knight666

Дякую за відповідь. Спершу спробую скористатися першим методом, і якщо я, можливо, пізніше придумаю спосіб використання другого. Але хіба addComponent()метод не повинен бути методом шаблонів? Якщо я визначу a addComponent(Component* c), будь-який доданий до нього компонент зберігатиметься в Componentпокажчику і typeidзавжди буде посилатися на Componentбазовий клас.
Федеріко

2
Typeid надасть вам фактичний тип об'єкта, на який вказують, навіть якщо вказівник базового класу
Chewy Gumball

Мені дуже сподобалася відповідь chewy, тому я спробував її реалізувати на mingw32. Я зіткнувся з проблемою, згаданою Федерацією Ріко, де addComponent () зберігає все як компонент, тому що typeid повертає компонент як тип для всього. Хтось тут згадував, що typeid повинен давати фактичний тип об'єкта, на який вказують, навіть якщо вказівник на базовий клас, але я думаю, що він може змінюватися залежно від компілятора і т. Д. Чи може хтось ще підтвердити це? Я використовував g ++ std = c ++ 11 mingw32 на Windows 7. Я в кінцевому підсумку просто змінив getComponent () на шаблон, а потім
зберег

Це не конкретно для компілятора. Ви, мабуть, не мали правильного виразу як аргументу функції typeid.
Chewy Gumball

17

Chewy це правильно, але якщо ви використовуєте C ++ 11, ви можете використовувати нові типи.

Замість використання const std::type_info*в якості ключа на карті ви можете використовувати std::type_index( див. Cppreference.com ), який є обгорткою навколо std::type_info. Для чого б ти ним користувався? std::type_indexФактично зберігає відносини з std::type_infoяк покажчик, але це один покажчик менше для вас , щоб турбуватися про.

Якщо ви справді використовуєте C ++ 11, я б рекомендував зберігати Componentпосилання всередині смарт-покажчиків. Тож карта може бути чимось на зразок:

std::map<std::type_index, std::shared_ptr<Component> > components

Додавання нового запису можна зробити так:

components[std::type_index(typeid(*component))] = component

де componentє тип std::shared_ptr<Component>. Отримання посилання на певний тип Componentможе виглядати так:

template <typename T>
std::shared_ptr<T> getComponent()
{
    std::type_index index(typeid(T));
    if(components.count(std::type_index(typeid(T)) != 0)
    {
        return static_pointer_cast<T>(components[index]);
    }
    else
    {
        return NullComponent
    }
}

Зверніть увагу також на використання static_pointer_castзамість static_cast.


1
Я фактично використовую такий підхід у власному проекті.
vijoc

Це насправді досить зручно, оскільки я вивчав C ++, використовуючи стандарт C ++ 11 як орієнтир. Одне, що я помітив, - це те, що всі суті-компоненти, які я знайшов в Інтернеті, використовують якесь таке cast. Я починаю думати, що неможливо було б реалізувати це, або подібний дизайн системи без закидів.
Федеріко

@Fede Зберігання Componentпокажчиків в одному контейнері обов'язково вимагає відкидання їх до похідного типу. Але, як зазначив Чеві, у вас є інші варіанти, які вам не потрібні. Я сам не бачу нічого «поганого» в тому, щоб мати такий тип ролях у дизайні, оскільки вони відносно безпечні.
vijoc

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