Как правильно получить доступ к компонентам в моих C ++ Entity-Component-Systems?

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. Проблема возникает, когда мне нужен доступ к нужному компоненту внутри сущности: компонент хранится в Componentколлекции (базового класса), поэтому я не могу получить доступ к компоненту и скопировать его на новый узел. Я временно решил проблему, приведя Componentк производному типу, но я хотел знать, есть ли лучший способ сделать это. Я понимаю, будет ли это означать перепроектирование того, что у меня уже есть. Благодарю.

Federico
источник

Ответы:

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что вернет указатель на информацию о типе типа времени выполнения (наиболее производного типа) компонента. Поскольку компонент хранится с информацией о типе в качестве его ключа, приведение не может вызвать проблемы из-за несовпадения типов. Вы также получаете проверку типа времени компиляции для типа шаблона, так как он должен быть типом, унаследованным от Компонента, иначе у него 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. Я столкнулся с проблемой, упомянутой fede rico, где addComponent () хранит все как компонент, потому что typeid возвращает компонент как тип для всего. Кто-то здесь упоминал, что typeid должен указывать фактический тип объекта, на который указывает указатель, даже если указатель на базовый класс, но я думаю, что он может варьироваться в зависимости от компилятора и т. Д. Кто-нибудь еще может это подтвердить? Я использовал g ++ std = c ++ 11 mingw32 на Windows 7. В итоге я просто изменил getComponent (), чтобы он стал шаблоном, а затем сохранил тип из этого в th
shwoseph
Это не зависит от компилятора. Вероятно, у вас не было правильного выражения в качестве аргумента функции 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.

vijoc
источник
1
Я на самом деле использую такой подход в своем собственном проекте.
Виджок
Это на самом деле довольно удобно, так как я изучал C ++, используя стандарт C ++ 11 в качестве справочного материала. Однако я заметил, что все системы компонентов сущностей, которые я нашел в Интернете, используют какой-то вид cast. Я начинаю думать, что было бы невозможно реализовать это или подобный дизайн системы без приведения.
Федерико
@Fede Хранение Componentуказателей в одном контейнере обязательно требует приведения их к производному типу. Но, как указывал Чуи, у вас есть другие варианты, которые не требуют кастинга. Я сам не вижу ничего «плохого» в том, чтобы использовать этот тип бросков в дизайне, поскольку они относительно безопасны.
Виджок
@vijoc Их иногда считают плохими из-за проблемы с согласованностью памяти, которую они могут создать.
akaltar