Sistemas de entidade / componente em C ++, Como descubro tipos e construo componentes?

37

Estou trabalhando em um sistema de componentes de entidade em C ++ que espero seguir o estilo de Artemis (http://piemaster.net/2011/07/entity-component-artemis/) em que os componentes são principalmente sacos de dados e é o Sistemas que contêm a lógica. Espero tirar proveito da centralização de dados dessa abordagem e criar algumas boas ferramentas de conteúdo.

No entanto, um problema é como pegar uma sequência identificadora ou GUID de um arquivo de dados e usá-la para construir um componente para uma Entidade. Obviamente, eu poderia ter apenas uma grande função de análise:

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

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

    else {
        return NULL:
    }
}

Mas isso é realmente feio. Pretendo adicionar e modificar componentes com freqüência, e espero criar algum tipo de ScriptedComponentComponent, para que você possa implementar um componente e sistema em Lua para fins de prototipagem. Eu gostaria de poder escrever uma classe herdada de alguma BaseComponentclasse, talvez lançar algumas macros para fazer tudo funcionar e depois ter a classe disponível para instanciação em tempo de execução.

Em C # e Java, isso seria bem simples, pois você obtém boas APIs de reflexão para procurar classes e construtores. Mas, estou fazendo isso em C ++ porque quero aumentar minha proficiência nesse idioma.

Então, como isso é feito em C ++? Eu li sobre a ativação do RTTI, mas parece que a maioria das pessoas tem receio disso, especialmente em uma situação em que eu só preciso dele para um subconjunto de tipos de objetos. Se eu preciso de um sistema RTTI personalizado, onde posso começar a aprender a escrever um?

michael.bartnett
fonte
11
Comentário bastante não relacionado: Se você deseja obter proficiência em C ++, use C ++ e não C, em relação às strings. Desculpe por isso, mas tinha que ser dito.
Chris diz Reinstate Monica
Ouvi dizer que foi um exemplo de brinquedo e não tenho a API std :: string memorizada. . . ainda!
michael.bartnett
@bearcdp Publiquei uma grande atualização na minha resposta. A implementação agora deve ser mais robusta e eficiente.
Paul Manta
@PaulManta Muito obrigado por atualizar sua resposta! Há muitas pequenas coisas para aprender com isso.
precisa saber é o seguinte

Respostas:

36

Um comentário:
A implementação da Artemis é interessante. Eu vim com uma solução semelhante, exceto que chamei meus componentes de "Atributos" e "Comportamentos". Essa abordagem de separar tipos de componentes funcionou muito bem para mim.

Em relação à solução:
O código é fácil de usar, mas pode ser difícil seguir a implementação se você não tiver experiência com C ++. Tão...

A interface desejada

O que fiz foi ter um repositório central de todos os componentes. Cada tipo de componente é mapeado para uma determinada sequência (que representa o nome do componente). É assim que você usa o 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);
}

A implementação

A implementação não é tão ruim, mas ainda é bastante complexa; requer algum conhecimento de modelos e ponteiros de função.

Nota: Joe Wreschnig fez alguns pontos positivos nos comentários, principalmente sobre como minha implementação anterior fez muitas suposições sobre o quão bom o compilador é na otimização de código; a questão não foi prejudicial, imo, mas também me incomodou. Notei também que a COMPONENT_REGISTERmacro anterior não funcionava com modelos.

Alterei o código e agora todos esses problemas devem ser corrigidos. A macro trabalha com modelos e os problemas que Joe levantou foram abordados: agora é muito mais fácil para os compiladores otimizarem códigos desnecessários.

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

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

Estendendo com Lua

Devo observar que, com um pouco de trabalho (não é muito difícil), isso pode ser usado para trabalhar perfeitamente com componentes definidos em C ++ ou Lua, sem ter que pensar nisso.

Paul Manta
fonte
Obrigado! Você está certo, ainda não sou fluente o suficiente nas artes negras dos modelos C ++ para entender totalmente isso. Mas, a macro de uma linha é exatamente o que eu estava procurando e, além disso, usarei isso para começar a entender mais profundamente os modelos.
michael.bartnett
6
Concordo que essa é basicamente a abordagem correta, mas há duas coisas que me destacam: 1. Por que não usar uma função de modelo e armazenar um mapa de ponteiros de função em vez de criar instâncias ComponentTypeImpl que vazarão na saída (não é realmente um problema, a menos que você está criando um .SO / DLL ou algo assim) 2. O objeto componentRegistry pode quebrar devido ao chamado "fiasco da ordem de inicialização estática". Para garantir que o componentRegistry seja feito primeiro, você precisa criar uma função que retorne uma referência a uma variável estática local e chamar isso em vez de usar o componentRegistry diretamente.
Lucas
@ Lucas Ah, você está totalmente certo sobre isso. Eu mudei o código de acordo. Eu não acho que houve algum vazamento no código anterior, desde que eu usei shared_ptr, mas seu conselho ainda é bom.
Paul Manta
11
@Paul: Ok, mas não é teórico, você deve pelo menos torná-lo estático para evitar possíveis queixas de vazamento de visibilidade de símbolo / vinculador. Além disso, seu comentário "Você deve lidar com esse erro como achar melhor" deve dizer "Isso não é um erro".
11
@PaulManta: Às vezes, funções e tipos podem "violar" o ODR (por exemplo, como você diz, modelos). No entanto, aqui estamos falando de instâncias e essas sempre devem seguir o ODR. Os compiladores não precisam detectar e relatar esses erros se eles ocorrerem em várias TUs (geralmente é impossível) e, portanto, você entra no domínio do comportamento indefinido. Se você absolutamente precisa espalhar cocô em toda a sua definição de interface, torná-lo estático pelo menos mantém o programa bem definido - mas o Coiote tem a idéia certa.
9

Parece que o que você quer é uma fábrica.

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

O que você pode fazer é ter seus vários componentes registrados na fábrica a que nome eles correspondem e, em seguida, você tem algum mapa do identificador de string para a assinatura do método construtor para gerar seus componentes.

Tetrad
fonte
11
Então, eu ainda precisaria ter uma seção de código que esteja ciente de todas as minhas Componentclasses, chamando ComponentSubclass::RegisterWithFactory(), certo? Existe uma maneira de configurar isso de forma mais dinâmica e automagicamente? O fluxo de trabalho que procuro é 1. Escreva uma classe, observando apenas o cabeçalho correspondente e o arquivo cpp 2. Re-compile o jogo 3. Inicie o editor de nível e a nova classe de componente estará disponível para uso.
michael.bartnett
2
Realmente não há como acontecer automaticamente. Você pode dividi-lo em uma chamada de macro de 1 linha por script, no entanto. A resposta de Paulo entra nisso um pouco.
Tetrad
1

Trabalhei com o design de Paul Manta a partir da resposta escolhida por um tempo e, finalmente, cheguei a esta implementação de fábrica mais genérica e concisa abaixo, que estou disposto a compartilhar para qualquer pessoa que venha a essa pergunta no futuro. Neste exemplo, todo objeto de fábrica deriva da Objectclasse base:

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

A classe estática Factory é a seguinte:

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;

A macro para registrar um subtipo de Objecté a seguinte:

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

Agora o uso é o seguinte:

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

A capacidade de muitos IDs de string por subtipo foi útil no meu aplicativo, mas a restrição a um único ID por subtipo seria bastante direta.

Espero que isso tenha sido útil!

alterar igel
fonte
1

Com base na resposta de @TimStraubinger , construí uma classe de fábrica usando os padrões C ++ 14 que podem armazenar membros derivados com um número arbitrário de argumentos . Meu exemplo, ao contrário do Tim, leva apenas um nome / chave por função. Como o de Tim, toda classe armazenada é derivada de uma classe Base , a minha sendo chamada 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;
}

Saída

Derived 1:  67
Derived 2:  6

Espero que isso ajude as pessoas que precisam usar um design de fábrica que não exija um construtor de identidade para funcionar. Foi divertido projetar, por isso espero que ajude as pessoas que precisam de mais flexibilidade em seus projetos de fábrica .

Kenneth Cornett
fonte