Entity / Component Systems в C ++. Как мне обнаружить типы и создать компоненты?

37

Я работаю над системой компонентов сущностей в C ++, которая, я надеюсь, будет следовать стилю Artemis (http://piemaster.net/2011/07/entity-component-artemis/), так как компоненты в основном представляют собой пакеты данных, и это Системы, содержащие логику. Я надеюсь использовать преимущества этого подхода, ориентированного на данные, и создать несколько хороших инструментов для работы с контентом.

Тем не менее, я сталкиваюсь с одним затруднением в том, как взять некоторую строку идентификатора или GUID из файла данных и использовать его для создания компонента для сущности. Очевидно, я мог бы просто иметь одну большую функцию разбора:

Component* ParseComponentType(const std::string &typeName)
{
    if (typeName == "RenderComponent") {
        return new RenderComponent();
    }

    else if (typeName == "TransformComponent") {
        return new TransformComponent();
    }

    else {
        return NULL:
    }
}

Но это действительно ужасно. Я намерен часто добавлять и изменять компоненты, и, надеюсь, создать какой-то ScriptedComponentComponent, чтобы вы могли реализовать компонент и систему в Lua для целей создания прототипов. Я хотел бы иметь возможность написать класс, унаследованный от некоторого BaseComponentкласса, возможно, добавить пару макросов, чтобы все работало, и затем иметь класс, доступный для реализации во время выполнения.

В C # и Java это было бы довольно просто, так как у вас есть хорошие API отражения для поиска классов и конструкторов. Но я делаю это на C ++, потому что хочу повысить свой уровень владения этим языком.

Итак, как это достигается в C ++? Я читал о включении RTTI, но, похоже, большинство людей настороженно относится к этому, особенно в ситуации, когда мне это нужно только для подмножества типов объектов. Если мне нужна нестандартная система RTTI, где я могу начать изучать ее?

michael.bartnett
источник
1
Весьма несвязанный комментарий: Если вы хотите стать опытным в C ++, тогда используйте C ++, а не C, в отношении строк. Извините за это, но это нужно было сказать.
Крис говорит восстановить Монику
Я слышал, это был игрушечный пример, и я не запомнил API-интерфейс std :: string. , , еще!
Майкл Бартнетт
@bearcdp Я опубликовал большое обновление моего ответа. Реализация теперь должна быть более надежной и эффективной.
Пол Манта
@PaulManta Большое спасибо за обновление вашего ответа! Из этого можно многому научиться.
Michael.bartnett

Ответы:

36

Комментарий:
реализация Artemis интересна. Я придумал подобное решение, за исключением того, что назвал свои компоненты «Атрибуты» и «Поведения». Такой подход разделения типов компонентов работал очень хорошо для меня.

Относительно решения:
код прост в использовании, но его реализация может быть затруднена, если у вас нет опыта работы с C ++. Так...

Желаемый интерфейс

Я сделал центральное хранилище всех компонентов. Каждый тип компонента сопоставляется с определенной строкой (которая представляет имя компонента). Вот как вы используете систему:

// Every time you write a new component class you have to register it.
// For that you use the `COMPONENT_REGISTER` macro.
class RenderingComponent : public Component
{
    // Bla, bla
};
COMPONENT_REGISTER(RenderingComponent, "RenderingComponent")

int main()
{
    // To then create an instance of a registered component all you have
    // to do is call the `create` function like so...
    Component* comp = component::create("RenderingComponent");

    // I found that if you have a special `create` function that returns a
    // pointer, it's best to have a corresponding `destroy` function
    // instead of using `delete` directly.
    component::destroy(comp);
}

Реализация

Реализация не так уж и плоха, но все же довольно сложна; это требует определенных знаний о шаблонах и указателях функций.

Примечание: Джо Вершниг высказал несколько хороших замечаний в комментариях, в основном о том, как моя предыдущая реализация сделала слишком много предположений о том, насколько хорош компилятор в оптимизации кода; проблема не была пагубной, но, тем не менее, меня это тоже беспокоило. Я также заметил, что предыдущий COMPONENT_REGISTERмакрос не работает с шаблонами.

Я изменил код, и теперь все эти проблемы должны быть исправлены. Макрос работает с шаблонами, и проблемы, поднятые Джо, были устранены: теперь компиляторам намного проще оптимизировать ненужный код.

Компонент / component.h

#ifndef COMPONENT_COMPONENT_H
#define COMPONENT_COMPONENT_H

// Standard libraries
#include <string>

// Custom libraries
#include "detail.h"


class Component
{
    // ...
};


namespace component
{
    Component* create(const std::string& name);
    void destroy(const Component* comp);
}

#define COMPONENT_REGISTER(TYPE, NAME)                                        \
    namespace component {                                                     \
    namespace detail {                                                        \
    namespace                                                                 \
    {                                                                         \
        template<class T>                                                     \
        class ComponentRegistration;                                          \
                                                                              \
        template<>                                                            \
        class ComponentRegistration<TYPE>                                     \
        {                                                                     \
            static const ::component::detail::RegistryEntry<TYPE>& reg;       \
        };                                                                    \
                                                                              \
        const ::component::detail::RegistryEntry<TYPE>&                       \
            ComponentRegistration<TYPE>::reg =                                \
                ::component::detail::RegistryEntry<TYPE>::Instance(NAME);     \
    }}}


#endif // COMPONENT_COMPONENT_H

Компонент / detail.h

#ifndef COMPONENT_DETAIL_H
#define COMPONENT_DETAIL_H

// Standard libraries
#include <map>
#include <string>
#include <utility>

class Component;

namespace component
{
    namespace detail
    {
        typedef Component* (*CreateComponentFunc)();
        typedef std::map<std::string, CreateComponentFunc> ComponentRegistry;

        inline ComponentRegistry& getComponentRegistry()
        {
            static ComponentRegistry reg;
            return reg;
        }

        template<class T>
        Component* createComponent() {
            return new T;
        }

        template<class T>
        struct RegistryEntry
        {
          public:
            static RegistryEntry<T>& Instance(const std::string& name)
            {
                // Because I use a singleton here, even though `COMPONENT_REGISTER`
                // is expanded in multiple translation units, the constructor
                // will only be executed once. Only this cheap `Instance` function
                // (which most likely gets inlined) is executed multiple times.

                static RegistryEntry<T> inst(name);
                return inst;
            }

          private:
            RegistryEntry(const std::string& name)
            {
                ComponentRegistry& reg = getComponentRegistry();
                CreateComponentFunc func = createComponent<T>;

                std::pair<ComponentRegistry::iterator, bool> ret =
                    reg.insert(ComponentRegistry::value_type(name, func));

                if (ret.second == false) {
                    // This means there already is a component registered to
                    // this name. You should handle this error as you see fit.
                }
            }

            RegistryEntry(const RegistryEntry<T>&) = delete; // C++11 feature
            RegistryEntry& operator=(const RegistryEntry<T>&) = delete;
        };

    } // namespace detail

} // namespace component

#endif // COMPONENT_DETAIL_H

Компонент / component.cpp

// Matching header
#include "component.h"

// Standard libraries
#include <string>

// Custom libraries
#include "detail.h"


Component* component::create(const std::string& name)
{
    detail::ComponentRegistry& reg = detail::getComponentRegistry();
    detail::ComponentRegistry::iterator it = reg.find(name);

    if (it == reg.end()) {
        // This happens when there is no component registered to this
        // name. Here I return a null pointer, but you can handle this
        // error differently if it suits you better.
        return nullptr;
    }

    detail::CreateComponentFunc func = it->second;
    return func();
}

void component::destroy(const Component* comp)
{
    delete comp;
}

Расширение с Lua

Я должен отметить, что немного поработав (это не очень сложно), это можно использовать для беспроблемной работы с компонентами, определенными в C ++ или Lua, без необходимости думать об этом.

Пол Манта
источник
Спасибо! Вы правы, я еще недостаточно хорошо разбираюсь в черных искусствах шаблонов C ++, чтобы полностью это понять. Но однострочный макрос - это именно то, что я искал, и, кроме того, я воспользуюсь этим, чтобы начать более глубоко понимать шаблоны.
Майкл Бартнетт
6
Я согласен, что это в основном правильный подход, но две вещи, которые мне нравятся: 1. Почему бы просто не использовать шаблонную функцию и хранить карту указателей на функции вместо создания экземпляров ComponentTypeImpl, которые будут просачиваться при выходе (на самом деле это не проблема, если только вы делаете .SO / DLL или что-то еще) 2. Объект componentRegistry может сломаться из-за так называемого «фиаско статического порядка инициализации». Чтобы убедиться, что componentRegistry создан первым, вам нужно создать функцию, которая возвращает ссылку на локальную статическую переменную, и вызывать ее вместо непосредственного использования componentRegistry.
Лукас
@ Лукас Ах, вы абсолютно правы в этом. Я изменил код соответственно. Я не думаю, что были какие-то утечки в предыдущем коде, хотя, так как я использовал shared_ptr, но ваш совет все еще хорош.
Пол Манта
1
@Paul: Хорошо, но это не теоретически, вы должны, по крайней мере, сделать его статическим, чтобы избежать возможных утечек видимости символов / жалоб компоновщика. Кроме того, ваш комментарий «Вы должны обрабатывать эту ошибку так, как считаете нужным», должен вместо этого сказать: «Это не ошибка».
1
@PaulManta: функциям и типам иногда разрешается «нарушать» ODR (например, как вы говорите, шаблоны). Однако здесь мы говорим о случаях, и они всегда должны следовать за ODR. Компиляторы не обязаны обнаруживать и сообщать об этих ошибках, если они происходят в нескольких TU (это обычно невозможно), и поэтому вы входите в область неопределенного поведения. Если вам абсолютно необходимо размазать poo по всему определению интерфейса, то, сделав его статичным, по крайней мере, программа будет четко определена - но у Coyote есть правильная идея.
9

Кажется, что вы хотите, это фабрика.

http://en.wikipedia.org/wiki/Factory_method_pattern

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

тетрада
источник
1
Так что мне все еще нужен какой-то раздел кода, который бы знал обо всех моих Componentклассах, вызывая ComponentSubclass::RegisterWithFactory(), верно? Есть ли способ настроить это сделать это более динамично и автоматически? Рабочий процесс, который я ищу: 1. Напишите класс, просматривая только соответствующий заголовок и файл cpp 2. Перекомпилируйте игру 3. Запустите редактор уровней и новый класс компонентов доступен для использования.
Майкл Бартнетт
2
Там действительно нет никакого способа, чтобы это произошло автоматически. Вы можете разбить его на 1-строчный макрос для каждого сценария. Пол отвечает на это немного.
Тетрад
1

Некоторое время я работал с дизайном Пола Манты из выбранного ответа и в конечном итоге пришел к этой более общей и краткой фабричной реализации, приведенной ниже, которой я готов поделиться для всех, кто придет к этому вопросу в будущем. В этом примере каждый объект фабрики является производным от Objectбазового класса:

struct Object {
    virtual ~Object(){}
};

Статический класс Factory выглядит следующим образом:

struct Factory {
    // the template used by the macro
    template<class ObjectType>
    struct RegisterObject {
        // passing a vector of strings allows many id's to map to the same sub-type
        RegisterObject(std::vector<std::string> names){
            for (auto name : names){
                objmap[name] = instantiate<ObjectType>;
            }
        }
    };

    // Factory method for creating objects
    static Object* createObject(const std::string& name){
        auto it = objmap.find(name);
        if (it == objmap.end()){
            return nullptr;
        } else {
            return it->second();
        }
    }

    private:
    // ensures the Factory cannot be instantiated
    Factory() = delete;

    // the map from string id's to instantiator functions
    static std::map<std::string, Object*(*)(void)> objmap;

    // templated sub-type instantiator function
    // requires that the sub-type has a parameter-less constructor
    template<class ObjectType>
    static Object* instantiate(){
        return new ObjectType();
    }
};
// pesky outside-class initialization of static member (grumble grumble)
std::map<std::string, Object*(*)(void)> Factory::objmap;

Макрос для регистрации подтипа Objectвыглядит следующим образом:

#define RegisterObject(type, ...) \
namespace { \
    ::Factory::RegisterObject<type> register_object_##type({##__VA_ARGS__}); \
}

Теперь использование выглядит следующим образом:

struct SpecialObject : Object {
    void beSpecial(){}
};
RegisterObject(SpecialObject, "SpecialObject", "Special", "SpecObj");

...

int main(){
    Object* obj1 = Factory::createObject("SpecialObject");
    Object* obj2 = Factory::createObject("SpecObj");
    ...
    if (obj1){
        delete obj1;
    }
    if (obj2){
        delete obj2;
    }
    return 0;
}

Возможность для многих идентификаторов строк для каждого подтипа была полезна в моем приложении, но ограничение на один идентификатор для каждого подтипа было бы довольно простым.

Я надеюсь, что это было полезно!

Alter Igel
источник
1

Основываясь на ответе @TimStraubinger , я создал фабричный класс, используя стандарты C ++ 14, которые могут хранить производные члены с произвольным числом аргументов . Мой пример, в отличие от Тима, использует только одно имя / ключ для каждой функции. Как и у Тима, каждый сохраняемый класс является производным от базового класса, а мой называется Base .

Base.h

#ifndef BASE_H
#define BASE_H

class Base{
    public:
        virtual ~Base(){}
};

#endif

EX_Factory.h

#ifndef EX_COMPONENT_H
#define EX_COMPONENT_H

#include <string>
#include <map>
#include "Base.h"

struct EX_Factory{
    template<class U, typename... Args>
    static void registerC(const std::string &name){
        registry<Args...>[name] = &create<U>;
    }
    template<typename... Args>
    static Base * createObject(const std::string &key, Args... args){
        auto it = registry<Args...>.find(key);
        if(it == registry<Args...>.end()) return nullptr;
        return it->second(args...);
    }
    private:
        EX_Factory() = delete;
        template<typename... Args>
        static std::map<std::string, Base*(*)(Args...)> registry;

        template<class U, typename... Args>
        static Base* create(Args... args){
            return new U(args...);
        }
};

template<typename... Args>
std::map<std::string, Base*(*)(Args...)> EX_Factory::registry; // Static member declaration.


#endif

main.cpp

#include "EX_Factory.h"
#include <iostream>

using namespace std;

struct derived_1 : public Base{
    derived_1(int i, int j, float f){
        cout << "Derived 1:\t" << i * j + f << endl;
    }
};
struct derived_2 : public Base{
    derived_2(int i, int j){
        cout << "Derived 2:\t" << i + j << endl;
    }
};

int main(){
    EX_Factory::registerC<derived_1, int, int, float>("derived_1"); // Need to include arguments
                                                                    //  when registering classes.
    EX_Factory::registerC<derived_2, int, int>("derived_2");
    derived_1 * d1 = static_cast<derived_1*>(EX_Factory::createObject<int, int, float>("derived_1", 8, 8, 3.0));
    derived_2 * d2 = static_cast<derived_2*>(EX_Factory::createObject<int, int>("derived_2", 3, 3));
    delete d1;
    delete d2;
    return 0;
}

Выход

Derived 1:  67
Derived 2:  6

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

Kenneth Cornett
источник