Sistemas de entidades / componentes en C ++, ¿cómo descubro tipos y construyo componentes?

37

Estoy trabajando en un sistema de componentes de entidad en C ++ que espero seguir el estilo de Artemis (http://piemaster.net/2011/07/entity-component-artemis/) en que los componentes son principalmente bolsas de datos y es el Sistemas que contienen la lógica. Espero aprovechar el enfoque centrado en los datos de este enfoque y construir algunas buenas herramientas de contenido.

Sin embargo, un obstáculo con el que me encuentro es cómo tomar una cadena de identificación o GUID de un archivo de datos y usarlo para construir un componente para una Entidad. Obviamente, podría tener una gran función de análisis:

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

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

    else {
        return NULL:
    }
}

Pero eso es realmente feo. Tengo la intención de agregar y modificar componentes con frecuencia, y espero construir algún tipo de ScriptedComponentComponent, de modo que pueda implementar un componente y sistema en Lua con el propósito de crear prototipos. Me gustaría poder escribir una clase heredada de alguna BaseComponentclase, tal vez agregar un par de macros para que todo funcione y luego tener la clase disponible para instanciación en tiempo de ejecución.

En C # y Java, esto sería bastante sencillo, ya que obtienes buenas API de reflexión para buscar clases y constructores. Pero, estoy haciendo esto en C ++ porque quiero aumentar mi dominio en ese lenguaje.

Entonces, ¿cómo se logra esto en C ++? He leído sobre cómo habilitar RTTI, pero parece que la mayoría de las personas desconfían de eso, especialmente en una situación en la que solo lo necesito para un subconjunto de tipos de objetos. Si un sistema RTTI personalizado es lo que necesito allí, ¿dónde puedo ir para comenzar a aprender a escribir uno?

michael.bartnett
fuente
1
Comentario bastante no relacionado: si desea dominar C ++, utilice C ++ y no C con respecto a las cadenas. Lo siento, pero tenía que decirlo.
Chris dice que reinstala a Mónica el
Te escuché, fue un ejemplo de juguete y no tengo la API std :: string memorizada. . . ¡todavía!
michael.bartnett
@bearcdp He publicado una actualización importante en mi respuesta. La implementación ahora debe ser más robusta y eficiente.
Paul Manta el
@PaulManta ¡Muchas gracias por actualizar tu respuesta! Hay muchas pequeñas cosas que aprender de él.
michael.bartnett

Respuestas:

36

Un comentario:
La implementación de Artemis es interesante. Se me ocurrió una solución similar, excepto que llamé a mis componentes "Atributos" y "Comportamientos". Este enfoque de separar tipos de componentes me ha funcionado muy bien.

En cuanto a la solución:
el código es fácil de usar, pero la implementación puede ser difícil de seguir si no tiene experiencia con C ++. Asi que...

La interfaz deseada

Lo que hice fue tener un repositorio central de todos los componentes. Cada tipo de componente se asigna a una determinada cadena (que representa el nombre del componente). Así es como usa el 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);
}

La implementación

La implementación no es tan mala, pero sigue siendo bastante compleja; requiere algunos conocimientos de plantillas y punteros de función.

Nota: Joe Wreschnig ha hecho algunos buenos comentarios en los comentarios, principalmente sobre cómo mi implementación anterior hizo demasiados supuestos sobre qué tan bueno es el compilador para optimizar el código; El problema no fue perjudicial, en mi opinión, pero también me molestó. También noté que la COMPONENT_REGISTERmacro anterior no funcionaba con plantillas.

He cambiado el código y ahora todos esos problemas deberían solucionarse. La macro funciona con plantillas y se han abordado los problemas que planteó Joe: ahora es mucho más fácil para los compiladores optimizar el código innecesario.

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

Extendiéndose con Lua

Debo señalar que con un poco de trabajo (no es muy difícil), esto se puede usar para trabajar sin problemas con componentes definidos en C ++ o Lua, sin tener que pensar en ello.

Paul Manta
fuente
¡Gracias! Tienes razón, todavía no soy lo suficientemente fluido en las artes negras de las plantillas de C ++ para entenderlo por completo. Pero, la macro de una línea es exactamente lo que estaba buscando, y además usaré esto para comenzar a comprender más profundamente las plantillas.
michael.bartnett
66
Estoy de acuerdo en que este es básicamente el enfoque correcto, pero hay dos cosas que me llaman la atención: 1. ¿Por qué no usar una función con plantilla y almacenar un mapa de punteros de función en lugar de crear instancias ComponentTypeImpl que se filtren al salir? (No es realmente un problema a menos que está haciendo un .SO / DLL o algo así) 2. El objeto componentRegistry podría romperse debido al llamado "fiasco de orden de inicialización estática". Para asegurarse de que componentRegistry se realice primero, debe hacer una función que devuelva una referencia a una variable estática local y llamarla en lugar de usar componentRegistry directamente.
Lucas
@Lucas Ah, tienes toda la razón sobre eso. Cambié el código en consecuencia. Sin embargo, no creo que haya habido filtraciones en el código anterior, ya que lo usé shared_ptr, pero su consejo sigue siendo bueno.
Paul Manta
1
@Paul: De acuerdo, pero no es teórico, al menos debes hacerlo estático para evitar posibles fugas de visibilidad de símbolos / quejas de enlazador. También su comentario "Debería manejar este error como mejor le parezca" debería decir "Esto no es un error".
1
@PaulManta: a veces se permite que las funciones y los tipos "violen" el ODR (por ejemplo, como usted dice, plantillas). Sin embargo, aquí estamos hablando de instancias y esas siempre deben seguir el ODR. No se requiere que los compiladores detecten e informen estos errores si se producen en múltiples TU (generalmente es imposible) y, por lo tanto, ingresa al ámbito del comportamiento indefinido. Si absolutamente debe manchar la caca en toda la definición de su interfaz, hacerla estática al menos mantiene el programa bien definido, pero Coyote tiene la idea correcta.
9

Parece que lo que quieres es una fábrica.

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

Lo que puede hacer es hacer que sus diversos componentes se registren en la fábrica a qué nombre corresponden, y luego tiene un mapa del identificador de cadena a la firma del método del constructor para generar sus componentes.

Tétrada
fuente
1
Así que aún necesito tener alguna sección de código que conozca todas mis Componentclases, llamando ComponentSubclass::RegisterWithFactory(), ¿verdad? ¿Hay alguna forma de configurar esto de manera más dinámica y automática? El flujo de trabajo que estoy buscando es 1. Escriba una clase, mirando solo el encabezado correspondiente y el archivo cpp 2. Vuelva a compilar el juego 3. El editor de nivel de inicio y la nueva clase de componente están disponibles para su uso.
michael.bartnett
2
Realmente no hay forma de que ocurra de forma automática. Sin embargo, puede dividirlo en una llamada de macro de 1 línea por script. La respuesta de Paul entra un poco en eso.
Tetrad
1

Trabajé con el diseño de Paul Manta a partir de la respuesta elegida por un tiempo y finalmente llegué a esta implementación de fábrica más genérica y concisa a continuación que estoy dispuesto a compartir para cualquier persona que venga a esta pregunta en el futuro. En este ejemplo, cada objeto de fábrica deriva de la Objectclase base:

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

La clase estática Factory es la siguiente:

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 para registrar un subtipo de Objectes la siguiente:

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

Ahora el uso es el siguiente:

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 capacidad de muchas ID de cadena por subtipo fue útil en mi aplicación, pero la restricción a una sola identificación por subtipo sería bastante sencilla.

¡Espero que esto haya sido útil!

alter igel
fuente
1

Partiendo de la respuesta de @TimStraubinger , construí una clase de fábrica utilizando estándares C ++ 14 que pueden almacenar miembros derivados con un número arbitrario de argumentos . Mi ejemplo, a diferencia de Tim, solo toma un nombre / tecla por función. Al igual que Tim, cada clase que se almacena se deriva de una clase Base , la mía se llama 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;
}

Salida

Derived 1:  67
Derived 2:  6

Espero que esto ayude a las personas que necesitan usar un diseño de Fábrica que no requiere un constructor de identidad para funcionar. Fue divertido diseñar, así que espero que ayude a las personas que necesitan más flexibilidad en sus diseños de fábrica .

Kenneth Cornett
fuente