Entity / Component Systems in C ++, Come posso scoprire tipi e costruire componenti?


37

Sto lavorando su un sistema di componenti di entità in C ++ che spero di seguire lo stile di Artemis (http://piemaster.net/2011/07/entity-component-artemis/) in quanto i componenti sono principalmente data bag ed è il Sistemi che contengono la logica. Spero di trarre vantaggio dalla centralità dei dati di questo approccio e creare alcuni strumenti di contenuto interessanti.

Tuttavia, una gobba che sto incontrando è come prendere una stringa identificativa o GUID da un file di dati e usarlo per costruire un componente per un'entità. Ovviamente potrei avere solo una grande funzione di analisi:

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

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

    else {
        return NULL:
    }
}

Ma è davvero brutto. Ho intenzione di aggiungere e modificare componenti frequentemente e, si spera, di costruire una sorta di ScriptedComponentComponent, in modo da poter implementare un componente e un sistema in Lua ai fini della prototipazione. Mi piacerebbe essere in grado di scrivere una classe che eredita da una BaseComponentclasse, magari aggiungere un paio di macro per far funzionare tutto e quindi avere la classe disponibile per l'istanza in fase di esecuzione.

In C # e Java questo sarebbe piuttosto semplice, dal momento che ottieni belle API di riflessione per cercare classi e costruttori. Ma lo sto facendo in C ++ perché voglio aumentare la mia competenza in quella lingua.

Quindi, come viene realizzato in C ++? Ho letto di abilitare RTTI, ma sembra che la maggior parte delle persone sia cauta a riguardo, specialmente in una situazione in cui ne ho bisogno solo per un sottoinsieme di tipi di oggetti. Se un sistema RTTI personalizzato è ciò di cui ho bisogno lì, dove posso andare per iniziare a imparare a scriverne uno?


1
Commento abbastanza non correlato: se si desidera acquisire competenze in C ++, utilizzare C ++ e non C, per quanto riguarda le stringhe. Scusami, ma bisogna dirlo.
Chris dice di reintegrare Monica il

Ti sento, è stato un esempio di giocattolo e non ho memorizzato l'apd :: string api. . . ancora!
michael.bartnett,

@bearcdp Ho pubblicato un importante aggiornamento sulla mia risposta. L'implementazione ora deve essere più solida ed efficiente.
Paul Manta,

@PaulManta Grazie mille per aver aggiornato la tua risposta! Ci sono molte piccole cose da imparare da esso.
michael.bartnett,

Risposte:


36

Un commento:
l'implementazione di Artemis è interessante. Ho trovato una soluzione simile, tranne per il fatto che ho chiamato i miei componenti "Attributi" e "Comportamenti". Questo approccio alla separazione dei tipi di componenti ha funzionato molto bene per me.

Per quanto riguarda la soluzione:
il codice è facile da usare, ma l'implementazione potrebbe essere difficile da seguire se non si ha esperienza con C ++. Così...

L'interfaccia desiderata

Quello che ho fatto è stato avere un repository centrale di tutti i componenti. Ogni tipo di componente è associato a una determinata stringa (che rappresenta il nome del componente). Ecco come si utilizza il sistema:

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

L'implemento

L'implementazione non è poi così male, ma è ancora piuttosto complessa; richiede una certa conoscenza dei modelli e dei puntatori a funzione.

Nota: Joe Wreschnig ha espresso alcuni punti positivi nei commenti, principalmente su come la mia precedente implementazione abbia fatto troppe ipotesi su quanto il compilatore sia bravo a ottimizzare il codice; il problema non è stato dannoso, imo, ma mi ha anche infastidito. Ho anche notato che la precedente COMPONENT_REGISTERmacro non funzionava con i modelli.

Ho cambiato il codice e ora tutti questi problemi dovrebbero essere risolti. La macro funziona con i modelli e sono stati risolti i problemi sollevati da Joe: ora è molto più facile per i compilatori ottimizzare il codice non necessario.

componente / 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

componente / 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

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

Estensione con Lua

Dovrei notare che con un po 'di lavoro (non è molto difficile), questo può essere usato per lavorare senza problemi con componenti definiti in C ++ o Lua, senza mai pensarci.


Grazie! Hai ragione, non sono ancora abbastanza fluente nelle arti nere dei modelli C ++ per capirlo totalmente. Ma la macro a una riga è esattamente quello che stavo cercando, e per di più userò questo per iniziare a capire più profondamente i template.
michael.bartnett,

6
Concordo sul fatto che questo è fondamentalmente l'approccio giusto, ma due cose che mi colpiscono: 1. Perché non usare semplicemente una funzione basata su modelli e archiviare una mappa di puntatori a funzione invece di creare istanze ComponentTypeImpl che perdano all'uscita (Non è un problema a meno che stai creando un file .SO / DLL o qualcosa del genere) 2. L'oggetto componentRegistry potrebbe rompersi a causa del cosiddetto "fiasco dell'ordine di inizializzazione statico". Per assicurarsi che componentRegistry sia creato per primo, è necessario creare una funzione che restituisca un riferimento a una variabile statica locale e chiamarla invece di utilizzare direttamente componentRegistry.
Lucas,

@Lucas Ah, hai perfettamente ragione su quelli. Ho modificato il codice di conseguenza. Non credo ci siano state perdite nel codice precedente, dal momento che l'ho usato shared_ptr, ma il tuo consiglio è ancora buono.
Paul Manta,

1
@Paul: Okay, ma non è teorico, dovresti almeno renderlo statico per evitare possibili perdite di visibilità dei simboli / reclami del linker. Anche il tuo commento "Dovresti gestire questo errore come ritieni opportuno" dovrebbe invece dire "Questo non è un errore".

1
@PaulManta: a volte funzioni e tipi possono "violare" l'ODR (ad esempio, come dici tu, modelli). Tuttavia qui stiamo parlando di casi e quelli devono sempre seguire l'ODR. I compilatori non sono tenuti a rilevare e segnalare questi errori se si verificano in più TU (in genere è impossibile) e quindi si entra nel regno del comportamento indefinito. Se devi assolutamente imbrattare la cacca per tutta la definizione della tua interfaccia, renderla statica almeno mantiene il programma ben definito - ma Coyote ha l'idea giusta.

9

Sembra che quello che vuoi sia una fabbrica.

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

Quello che puoi fare è avere i tuoi vari componenti registrati in fabbrica a quale nome corrispondono, e quindi hai una mappa dell'identificatore di stringa con la firma del metodo del costruttore per generare i tuoi componenti.


1
Quindi avrei ancora bisogno di avere una sezione di codice che sia a conoscenza di tutte le mie Componentclassi, chiamante ComponentSubclass::RegisterWithFactory(), giusto? C'è un modo per impostare questo farlo più dinamicamente e automagicamente? Il flusso di lavoro che sto cercando è 1. Scrivere una classe, guardando solo l'intestazione e il file cpp corrispondenti 2. Ricompilare il gioco 3. L'editor di livello iniziale e la nuova classe di componenti sono disponibili per l'uso.
michael.bartnett,

2
Non c'è davvero modo che accada automagicamente. Tuttavia, è possibile suddividerlo in una chiamata di macro a 1 riga in base allo script. La risposta di Paul ci approfondisce un po '.
Tetrad,

1

Ho lavorato per un po 'con il design di Paul Manta dalla risposta scelta e alla fine sono arrivato a questa implementazione di fabbrica più generica e concisa di seguito che sono disposto a condividere per chiunque venga a questa domanda in futuro. In questo esempio, ogni oggetto factory deriva dalla Objectclasse base:

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

La classe Factory statica è la seguente:

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;

La macro per la registrazione di un sottotipo di Objectè la seguente:

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

Ora l'utilizzo è il seguente:

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

La capacità di molti ID stringa per sottotipo era utile nella mia applicazione, ma la restrizione a un singolo ID per sottotipo sarebbe abbastanza semplice.

Spero sia stato utile!


1

Partendo dalla risposta di @TimStraubinger , ho creato una classe di fabbrica utilizzando gli standard C ++ 14 che possono memorizzare membri derivati ​​con un numero arbitrario di argomenti . Il mio esempio, a differenza di quello di Tim, accetta solo un nome / tasto per funzione. Come quello di Tim, ogni classe memorizzata deriva da una classe Base , la mia viene chiamata 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;
}

Produzione

Derived 1:  67
Derived 2:  6

Spero che questo aiuti le persone che hanno bisogno di usare un design Factory che non richiede un costruttore di identità per funzionare. È stato divertente progettare, quindi spero che aiuti le persone che necessitano di maggiore flessibilità nei loro progetti di fabbrica .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.