Субстанційні / компонентні системи в C ++, як я можу виявити типи та побудувати компоненти?


37

Я працюю над системою компонентів сутності в C ++, для якої я сподіваюся дотримуватися стилю Артеміди (http://piemaster.net/2011/07/entity-component-artemis/), що компоненти в основному є пакетами даних, і це Системи, що містять логіку. Я сподіваюся скористатися орієнтованістю на дані цього підходу та створити кілька приємних інструментів контенту.

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

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

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

    else {
        return NULL:
    }
}

Але це справді некрасиво. Я маю намір часто додавати та змінювати компоненти, і, сподіваюся, будувати якийсь ScriptedComponentComponent, щоб ви могли реалізувати компонент та систему в Луа для цілей прототипування. Я хотів би мати можливість написати клас у спадок від якогось BaseComponentкласу, можливо, киньте пару макросів, щоб все спрацювало, а потім мати клас доступний для екземплярів під час виконання.

У C # та Java це було б досить просто, оскільки ви отримуєте приємні API відображення для пошуку класів та конструкторів. Але я роблю це на C ++, тому що хочу збільшити рівень володіння цією мовою.

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


1
Досить незв'язаний коментар: Якщо ви хочете отримати знання C ++, тоді використовуйте C ++, а не C, що стосується рядків. Вибачте за це, але це треба було сказати.
Кріс каже, що повернеться до Моніки

Я чую вас, це був іграшковий приклад, і мені не запам’ятовується std :: string api. . . ще!
michael.bartnett

@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);
}

Впровадження

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

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

Я змінив код і тепер усі ці проблеми слід вирішити. Макрос працює з шаблонами, і проблеми, які піднімав Джо, були вирішені: зараз компіляторам набагато простіше оптимізувати непотрібний код.

компонент / компонент.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

компонент / деталь.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

компонент / компонент.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;
}

Подовження з Луа

Слід зазначити, що за допомогою трохи роботи (це не дуже важко), це можна використовувати для безперебійної роботи з компонентами, визначеними або в C ++, або в Lua, не маючи про це думати.


Дякую! Ви маєте рацію, я ще не досить добре володію чорними мистецтвами шаблонів C ++, щоб це повністю зрозуміти. Але, однорядковий макрос - це саме те, що я шукав, і на додаток до цього я скористаюся цим, щоб почати глибше розуміти шаблони.
michael.bartnett

6
Я погоджуюсь, що це в основному правильний підхід, але дві речі, які мені дотримуються: 1. Чому б не просто використовувати шаблонну функцію і зберігати карту функціональних покажчиків, а не робити екземпляри ComponentTypeImpl, які просочуться при виході (насправді це не проблема, якщо ви робите .SO / DLL або щось подібне) 2. Об'єкт компонентRegistry може зламатися через так званий "фіаско порядку статичної ініціалізації". Для забезпечення компонентаRegistry спочатку потрібно створити функцію, яка повертає посилання на локальну статичну змінну і викликати її, а не використовувати компонентRegistry безпосередньо.
Лукас

@Lucas Ах, ти з цим абсолютно правий. Я відповідно змінив код. Я не думаю, що в попередньому коді не було витоків, оскільки я користувався 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. Редактор рівня запуску та новий клас компонентів доступний для використання.
michael.bartnett

2
Насправді немає способу, щоб це відбулося автоматично. Однак ви можете розбити його на 1-рядовий макро-дзвінок на основі сценарію. Відповідь Павла трохи йде на це.
Тетрад

1

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

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

Статичний заводський клас такий:

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;
}

Ємність для багатьох рядкових ідентифікаторів для підтипу була корисною в моїй програмі, але обмеження на один ідентифікатор для підтипу було б досить просте.

Я сподіваюся, що це було корисно!


1

Спираючись на відповідь @TimStraubinger , я створив заводський клас, використовуючи стандарти C ++ 14, які можуть зберігати похідні члени з довільною кількістю аргументів . Мій приклад, на відміну від Тіма, бере лише одне ім’я / ключ на функцію. Як і у Тіма, кожен клас, який зберігається, походить від базового класу, шахта називається 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

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

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