Entitäts- / Komponentensysteme in C ++, Wie erkenne ich Typen und konstruiere Komponenten?

37

Ich arbeite an einem Entity-Komponentensystem in C ++, das hoffentlich dem Stil von Artemis (http://piemaster.net/2011/07/entity-component-artemis/) entspricht, da es sich bei den Komponenten hauptsächlich um Datentaschen handelt Systeme, die die Logik enthalten. Ich hoffe, dass ich die Datenzentrierung dieses Ansatzes nutzen und ein paar nette Content-Tools erstellen kann.

Ein Buckel, auf den ich stoße, ist jedoch, wie man einen Bezeichner-String oder eine GUID aus einer Datendatei nimmt und daraus eine Komponente für eine Entität erstellt. Offensichtlich könnte ich nur eine große Analysefunktion haben:

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

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

    else {
        return NULL:
    }
}

Aber das ist wirklich hässlich. Ich beabsichtige, Komponenten häufig hinzuzufügen und zu ändern und hoffentlich eine Art ScriptedComponentComponent zu erstellen, sodass Sie eine Komponente und ein System in Lua zum Zwecke des Prototyping implementieren können. Ich möchte in der Lage sein, eine Klasse zu schreiben, die von einer Klasse erbt BaseComponent, vielleicht ein paar Makros einzufügen, damit alles funktioniert, und dann die Klasse zur Laufzeit für die Instanziierung verfügbar zu haben.

In C # und Java wäre dies ziemlich einfach, da Sie nette Reflection-APIs zum Nachschlagen von Klassen und Konstruktoren erhalten. Aber ich mache das in C ++, weil ich meine Kenntnisse in dieser Sprache verbessern möchte.

Wie wird dies in C ++ erreicht? Ich habe über das Aktivieren von RTTI gelesen, aber es scheint, dass die meisten Leute diesbezüglich vorsichtig sind, insbesondere in einer Situation, in der ich es nur für eine Teilmenge von Objekttypen benötige. Wenn ich dort ein benutzerdefiniertes RTTI-System benötige, wo kann ich anfangen, das Schreiben eines Systems zu lernen?

michael.bartnett
quelle
1
Ganz anderer Kommentar: Wenn Sie C ++ beherrschen möchten, verwenden Sie C ++ und nicht C in Bezug auf Zeichenfolgen. Entschuldigung, aber es musste gesagt werden.
Chris sagt Reinstate Monica
Ich habe gehört, es war ein Spielzeugbeispiel und ich habe nicht die std :: string api auswendig gelernt. . . noch!
michael.bartnett
@bearcdp Ich habe ein umfangreiches Update zu meiner Antwort veröffentlicht. Die Implementierung muss jetzt robuster und effizienter sein.
Paul Manta
@PaulManta Vielen Dank, dass Sie Ihre Antwort aktualisiert haben! Es gibt viele kleine Dinge, die man daraus lernen kann.
michael.bartnett

Antworten:

36

Ein Kommentar:
Die Artemis-Implementierung ist interessant. Ich habe eine ähnliche Lösung gefunden, mit der Ausnahme, dass ich meine Komponenten "Attribute" und "Verhalten" nannte. Dieser Ansatz der Trennung von Komponententypen hat bei mir sehr gut funktioniert.

In Bezug auf die Lösung:
Der Code ist einfach zu verwenden, aber die Implementierung ist möglicherweise schwer zu befolgen, wenn Sie keine Erfahrung mit C ++ haben. So...

Die gewünschte Schnittstelle

Ich wollte ein zentrales Repository für alle Komponenten haben. Jeder Komponententyp ist einer bestimmten Zeichenfolge zugeordnet (die den Komponentennamen darstellt). So nutzen Sie das System:

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

Die Umsetzung

Die Implementierung ist nicht so schlecht, aber immer noch ziemlich komplex. Es erfordert einige Kenntnisse über Vorlagen und Funktionszeiger.

Anmerkung: Joe Wreschnig hat in den Kommentaren einige gute Punkte angesprochen, vor allem, wie meine vorherige Implementierung zu viele Annahmen darüber gemacht hat, wie gut der Compiler den Code optimiert. Das Problem war nicht schädlich, aber es hat mich auch gestört. Mir ist auch aufgefallen, dass das vorherige COMPONENT_REGISTERMakro mit Vorlagen nicht funktioniert hat.

Ich habe den Code geändert und jetzt sollten alle diese Probleme behoben sein. Das Makro arbeitet mit Vorlagen und die Probleme, die Joe angesprochen hat, wurden behoben: Jetzt ist es für Compiler viel einfacher, unnötigen Code zu optimieren.

komponente / komponente.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

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

Mit Lua erweitern

Ich sollte beachten, dass dies mit ein wenig Arbeit (es ist nicht sehr schwer) verwendet werden kann, um nahtlos mit Komponenten zu arbeiten, die entweder in C ++ oder Lua definiert sind, ohne jemals darüber nachdenken zu müssen.

Paul Manta
quelle
Danke! Sie haben Recht, ich beherrsche die schwarzen Künste von C ++ - Vorlagen noch nicht fließend genug, um das vollständig zu verstehen. Aber das einzeilige Makro ist genau das, wonach ich gesucht habe, und darüber hinaus werde ich damit beginnen, Vorlagen besser zu verstehen.
michael.bartnett
6
Ich bin damit einverstanden, dass dies im Grunde der richtige Ansatz ist, aber zwei Dinge, die mir auffallen: 1. Warum nicht einfach eine Template-Funktion verwenden und eine Map von Funktionszeigern speichern, anstatt ComponentTypeImpl-Instanzen zu erzeugen, die beim Beenden auslaufen (kein wirkliches Problem, es sei denn Sie machen eine .SO / DLL oder so) 2. Das componentRegistry-Objekt könnte aufgrund des sogenannten "statischen Fiaskos der Initialisierungsreihenfolge" beschädigt werden. Um sicherzustellen, dass componentRegistry zuerst erstellt wird, müssen Sie eine Funktion erstellen, die einen Verweis auf eine lokale statische Variable zurückgibt, und diese aufrufen, anstatt componentRegistry direkt zu verwenden.
Lucas
@Lucas Ah, da hast du vollkommen recht. Ich habe den Code entsprechend geändert. Ich glaube nicht, dass es im vorherigen Code irgendwelche Undichtigkeiten gab, seit ich das getan habe shared_ptr, aber Ihr Rat ist immer noch gut.
Paul Manta
1
@Paul: Okay, aber es ist nicht theoretisch, Sie sollten es zumindest statisch machen, um mögliche Symbolsichtbarkeitslecks / Linkerbeschwerden zu vermeiden. Auch Ihr Kommentar "Sie sollten diesen Fehler so behandeln, wie Sie es für richtig halten" sollte stattdessen "Dies ist kein Fehler" sagen.
1
@PaulManta: Funktionen und Typen dürfen manchmal die ODR "verletzen" (z. B. wie Sie sagen, Vorlagen). Hier geht es jedoch um Instanzen, die immer der ODR folgen müssen. Compiler müssen diese Fehler nicht erkennen und melden, wenn sie in mehreren TUs auftreten (dies ist im Allgemeinen nicht möglich), sodass Sie in den Bereich undefinierten Verhaltens eintreten. Wenn Sie Ihre Schnittstellendefinition unbedingt verputzen müssen, bleibt das Programm durch die statische Darstellung zumindest klar definiert - aber Coyote hat die richtige Idee.
9

Es scheint, als ob Sie eine Fabrik wollen.

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

Sie können Ihre verschiedenen Komponenten beim Hersteller registrieren lassen, welchem ​​Namen sie entsprechen, und dann haben Sie eine Zuordnung der Zeichenfolgen-ID zur Signatur der Konstruktormethode, um Ihre Komponenten zu generieren.

Tetrade
quelle
1
Ich brauche also immer noch einen Codeabschnitt, der alle meine ComponentKlassen kennt und aufruft ComponentSubclass::RegisterWithFactory(), oder? Gibt es eine Möglichkeit, dies dynamischer und automatischer einzurichten? Der Arbeitsablauf, den ich suche, ist 1. Schreiben Sie eine Klasse und betrachten Sie nur den entsprechenden Header und die zugehörige CPP-Datei. 2. Kompilieren Sie das Spiel erneut.
michael.bartnett
2
Es gibt wirklich keine Möglichkeit, dass dies automatisch geschieht. Sie können es jedoch in einen einzeiligen Makroaufruf pro Skript aufteilen. Pauls Antwort geht ein wenig darauf ein.
Tetrad
1

Ich habe eine Weile mit Paul Mantas Design aus der gewählten Antwort gearbeitet und bin schließlich zu dieser allgemeineren und prägnanteren Factory-Implementierung gekommen, die ich jedem mitteilen möchte, der sich in Zukunft mit dieser Frage befasst. In diesem Beispiel stammt jedes Factory-Objekt von der ObjectBasisklasse:

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

Die statische Factory-Klasse lautet wie folgt:

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;

Das Makro zum Registrieren eines Untertyps von Objectlautet wie folgt:

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

Jetzt ist die Verwendung wie folgt:

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

Die Kapazität für viele Zeichenfolgen-IDs pro Untertyp war in meiner Anwendung nützlich, aber die Beschränkung auf eine einzelne ID pro Untertyp wäre ziemlich einfach.

Ich hoffe das war hilfreich!

Alter Igel
quelle
1

Aufbauend auf der Antwort von @TimStraubinger habe ich eine Factory-Klasse mit C ++ 14- Standards erstellt, in der abgeleitete Member mit einer beliebigen Anzahl von Argumenten gespeichert werden können . In meinem Beispiel wird im Gegensatz zu Tim nur ein Name / eine Taste pro Funktion verwendet. Wie Tims, wobei jede Klasse gespeichert ist , von einer abgeleiteten Basisklasse, Mine genannt wird Basis .

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

Ausgabe

Derived 1:  67
Derived 2:  6

Ich hoffe, dies hilft Menschen, die ein Factory- Design benötigen, für dessen Arbeit kein Identitätskonstruktor erforderlich ist. Das Entwerfen hat Spaß gemacht, daher hoffe ich, dass es den Menschen hilft, die mehr Flexibilität bei ihren Factory- Designs benötigen .

Kenneth Cornett
quelle