Systemy encji / komponentów w C ++, jak odkrywać typy i konstruować komponenty?

37

Pracuję nad systemem komponentów encji w C ++, który, mam nadzieję, będzie podążał za Artemisem (http://piemaster.net/2011/07/entity-component-artemis/), ponieważ komponenty to głównie torby danych, a to Systemy zawierające logikę. Mam nadzieję, że wykorzystam podejście skoncentrowane na danych i zbuduję fajne narzędzia do obsługi treści.

Jednak jeden garb, z którym się spotykam, to jak pobrać jakiś ciąg identyfikatora lub identyfikator GUID z pliku danych i użyć go do skonstruowania komponentu dla encji. Oczywiście mógłbym mieć tylko jedną dużą funkcję parsowania:

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

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

    else {
        return NULL:
    }
}

Ale to naprawdę brzydkie. Zamierzam często dodawać i modyfikować komponenty i mam nadzieję, że zbuduję coś w rodzaju ScriptedComponentComponent, tak aby można było zaimplementować komponent i system w Lua do celów prototypowania. Chciałbym móc napisać klasę dziedziczącą po pewnej BaseComponentklasie, być może wrzucić kilka makr, aby wszystko działało, a następnie mieć klasę dostępną do tworzenia instancji w czasie wykonywania.

W języku C # i Javie byłoby to dość proste, ponieważ masz ładne interfejsy API refleksji do wyszukiwania klas i konstruktorów. Ale robię to w C ++, ponieważ chcę zwiększyć swoją biegłość w tym języku.

Jak to się dzieje w C ++? Czytałem o włączaniu RTTI, ale wygląda na to, że większość ludzi jest tego nieufna, szczególnie w sytuacji, gdy potrzebuję jej tylko dla podzbioru typów obiektów. Jeśli potrzebuję niestandardowego systemu RTTI, to gdzie mogę przejść, aby rozpocząć naukę pisania?

michael.bartnett
źródło
1
Dość niepowiązany komentarz: jeśli chcesz biegle posługiwać się językiem C ++, użyj C ++, a nie C, w odniesieniu do łańcuchów. Przepraszam za to, ale trzeba to powiedzieć.
Chris mówi Przywróć Monikę
Słyszę, to był zabawkowy przykład i nie zapamiętałem api std :: string. . . jeszcze!
michael.bartnett
@bearcdp Opublikowałem ważną aktualizację mojej odpowiedzi. Wdrożenie musi być teraz bardziej niezawodne i wydajne.
Paul Manta
@PaulManta Dziękujemy bardzo za aktualizację odpowiedzi! Można się z tego wiele nauczyć.
michael.bartnett

Odpowiedzi:

36

Komentarz:
Implementacja Artemis jest interesująca. Wymyśliłem podobne rozwiązanie, tyle że nazwałem swoje komponenty „Atrybutami” i „Zachowaniami”. Takie podejście do rozdzielania typów komponentów działało dla mnie bardzo dobrze.

Jeśli chodzi o rozwiązanie:
kod jest łatwy w użyciu, ale implementacja może być trudna do wykonania, jeśli nie masz doświadczenia z C ++. Więc...

Pożądany interfejs

To, co zrobiłem, to mieć centralne repozytorium wszystkich komponentów. Każdy typ komponentu jest mapowany na określony ciąg znaków (który reprezentuje nazwę komponentu). Oto jak korzystasz z systemu:

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

Implementacja

Wdrożenie nie jest takie złe, ale wciąż dość skomplikowane; wymaga pewnej wiedzy na temat szablonów i wskaźników funkcji.

Uwaga: Joe Wreschnig poczynił kilka dobrych uwag w komentarzach, głównie na temat tego, jak moja poprzednia implementacja przyjęła zbyt wiele założeń dotyczących tego, jak dobry jest kompilator w optymalizacji kodu; problem nie był szkodliwy, imo, ale również mnie zaskoczył. Zauważyłem również, że poprzednie COMPONENT_REGISTERmakro nie działało z szablonami.

Zmieniłem kod i teraz wszystkie te problemy powinny zostać naprawione. Makro współpracuje z szablonami i rozwiązano problemy, które podniósł Joe: teraz kompilatorom znacznie łatwiej jest zoptymalizować niepotrzebny kod.

składnik / składnik. 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

składnik / szczegół. 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 / 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;
}

Rozszerzanie z Luą

Powinienem zauważyć, że przy odrobinie pracy (nie jest to bardzo trudne), można to wykorzystać do płynnej pracy ze składnikami zdefiniowanymi w C ++ lub Lua, bez konieczności myślenia o tym.

Paul Manta
źródło
Dziękuję Ci! Masz rację, nie jestem jeszcze wystarczająco biegły w czarnej sztuce szablonów C ++, aby całkowicie to zrozumieć. Ale makro w jednym wierszu jest dokładnie tym, czego szukałem, a do tego użyję tego, aby głębiej zrozumieć szablony.
michael.bartnett
6
Zgadzam się, że jest to w zasadzie właściwe podejście, ale dwie rzeczy, które mi się wyróżniają: 1. Dlaczego nie użyć po prostu funkcji szablonowej i przechowywać mapę wskaźników funkcji zamiast tworzyć instancje ComponentTypeImpl, które będą przeciekać przy wyjściu (Naprawdę nie jest to problem, chyba że tworzysz .SO / DLL lub coś takiego) 2. Obiekt componentRegistry może się zepsuć z powodu tak zwanego „fiasku kolejności inicjalizacji statycznej”. Aby upewnić się, że najpierw utworzono składnik componentRegistry, należy utworzyć funkcję, która zwraca odwołanie do lokalnej zmiennej statycznej i wywołać ją zamiast bezpośrednio używać składnika componentRegistry.
Lucas
@Lucas Ah, masz całkowitą rację co do nich. Odpowiednio zmieniłem kod. Nie sądzę jednak, żeby w poprzednim kodzie były wycieki, odkąd go użyłem shared_ptr, ale twoja rada jest nadal dobra.
Paul Manta
1
@Paul: Dobra, ale to nie jest teoretyczne, powinieneś przynajmniej ustawić ją na statyczną, aby uniknąć ewentualnych skarg na wyciek widoczności symboli / skargi linkera. Również twój komentarz „Powinieneś poradzić sobie z tym błędem, który uważasz za odpowiedni”, powinien zamiast tego powiedzieć „To nie jest błąd”.
1
@PaulManta: Funkcje i typy mogą czasem „naruszać” ODR (np. Szablony). Jednak tutaj mówimy o instancjach, które zawsze muszą podążać za ODR. Kompilatory nie są wymagane do wykrywania i zgłaszania tych błędów, jeśli występują one w wielu jednostkach językowych (jest to na ogół niemożliwe), dlatego wchodzisz w sferę nieokreślonego zachowania. Jeśli absolutnie musisz rozmazać kupę po całej definicji interfejsu, ustawienie go w statyczny sposób przynajmniej utrzyma dobrze zdefiniowany program - ale Coyote ma właściwy pomysł.
9

Wydaje się, że to, czego chcesz, to fabryka.

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

To, co możesz zrobić, to zarejestrować w fabryce różne komponenty pod jaką nazwą one odpowiadają, a następnie masz mapę identyfikatora łańcucha do podpisu metody konstruktora w celu wygenerowania komponentów.

Tetrad
źródło
1
Więc nadal potrzebowałbym sekcji kodu, która byłaby świadoma wszystkich moich Componentklas, dzwoniąc ComponentSubclass::RegisterWithFactory(), prawda? Czy istnieje sposób, aby to ustawić, zrób to bardziej dynamicznie i automagicznie? Przepływ pracy, którego szukam, to 1. Napisz klasę, patrząc tylko na odpowiedni nagłówek i plik cpp 2. Ponownie skompiluj grę 3. Edytor poziomu początkowego i nowa klasa komponentów jest dostępna do użycia.
michael.bartnett
2
Naprawdę nie ma mowy, żeby stało się to automagicznie. Możesz jednak podzielić go na wywołanie makra z 1 linią dla poszczególnych skryptów. Odpowiedź Paula dotyczy tego trochę.
Tetrad
1

Przez jakiś czas pracowałem z projektem Paula Manty z wybranej odpowiedzi i ostatecznie doszedłem do bardziej ogólnej i zwięzłej implementacji fabryki poniżej, którą chętnie podzielę się z każdym, kto przyjdzie na to pytanie w przyszłości. W tym przykładzie każdy obiekt fabryki pochodzi od Objectklasy podstawowej:

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

Statyczna klasa fabryczna jest następująca:

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;

Makro do rejestrowania podtypu Objectjest następujące:

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

Teraz użycie jest następujące:

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

Pojemność wielu identyfikatorów ciągów dla podtypu była przydatna w mojej aplikacji, ale ograniczenie do jednego identyfikatora dla każdego podtypu byłoby dość proste.

Mam nadzieję, że było to przydatne!

alter igel
źródło
1

Opierając się na odpowiedzi @TimStraubinger , zbudowałem klasę fabryczną przy użyciu standardów C ++ 14, które mogą przechowywać elementy pochodne z dowolną liczbą argumentów . Mój przykład, w przeciwieństwie do Tima, bierze tylko jedną nazwę / klawisz na funkcję. Podobnie jak Tim, każda przechowywana klasa pochodzi z klasy Base , a moja nazywana jest 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;
}

Wydajność

Derived 1:  67
Derived 2:  6

Mam nadzieję, że pomoże to osobom potrzebującym użyć projektu fabryki, który nie wymaga konstruktora tożsamości do pracy. To było fajne projektowanie, więc mam nadzieję, że pomoże ludziom potrzebującym większej elastyczności w projektach fabrycznych .

Kenneth Cornett
źródło