Systèmes d'entités / composants en C ++, Comment puis-je découvrir des types et des composants de construction?

37

Je travaille sur un système de composants d'entité en C ++ que j'espère suivre le style d'Artemis (http://piemaster.net/2011/07/entity-component-artemis/) dans lequel les composants sont principalement des poches de données et c'est le Systèmes qui contiennent la logique. J'espère tirer parti de l'approche centrée sur les données de cette approche et créer de bons outils de contenu.

Cependant, je ne parviens pas à comprendre comment extraire une chaîne d'identifiant ou un GUID d'un fichier de données et l'utiliser pour construire un composant pour une entité. Évidemment, je ne pourrais avoir qu'une grosse fonction d'analyse:

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

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

    else {
        return NULL:
    }
}

Mais c'est vraiment moche. J'ai l'intention d'ajouter et de modifier fréquemment des composants et, espérons-le, de construire une sorte de ScriptedComponentComponent, de sorte que vous puissiez implémenter un composant et un système dans Lua à des fins de prototypage. J'aimerais pouvoir écrire une classe héritant d'une BaseComponentclasse, peut-être ajouter quelques macros pour que tout fonctionne, puis laisser la classe disponible pour une instanciation au moment de l'exécution.

En C # et Java, cela serait assez simple, car vous avez de belles API de réflexion pour rechercher des classes et des constructeurs. Mais je le fais en C ++ parce que je veux augmenter ma maîtrise de cette langue.

Alors, comment cela se fait-il en C ++? J'ai lu sur l'activation de RTTI, mais il semble que la plupart des gens s'en méfient, en particulier dans une situation où je n'en ai besoin que pour un sous-ensemble de types d'objets. Si j'ai besoin d'un système RTTI personnalisé, où puis-je aller pour commencer à apprendre à en écrire un?

michael.bartnett
la source
1
Commentaire assez différent: Si vous souhaitez maîtriser le C ++, utilisez C ++ et non le C en ce qui concerne les chaînes. Désolé pour cela, mais il fallait le dire.
Chris dit Réintégrer Monica le
Je vous entends, c’était un exemple de jouet et je n’ai pas mémorisé l’API std :: string. . . encore!
michael.bartnett
@bearcdp J'ai posté une mise à jour majeure de ma réponse. La mise en œuvre est maintenant doit être plus robuste et efficace.
Paul Manta
@PaulManta Merci beaucoup d'avoir mis à jour votre réponse! Il y a beaucoup de petites choses à apprendre.
michael.bartnett le

Réponses:

36

Un commentaire:
L'implémentation d'Artemis est intéressante. J'ai proposé une solution similaire, sauf que j'ai appelé mes composants "Attributs" et "Comportements". Cette approche de la séparation des types de composants a très bien fonctionné pour moi.

En ce qui concerne la solution:
le code est facile à utiliser, mais sa mise en œuvre risque d’être difficile à suivre si vous n’êtes pas expérimenté en C ++. Alors...

L'interface souhaitée

Ce que j'ai fait est d'avoir un référentiel central de tous les composants. Chaque type de composant est associé à une chaîne donnée (qui représente le nom du composant). Voici comment vous utilisez le système:

// 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 mise en oeuvre

L'implémentation n'est pas si mauvaise, mais c'est quand même assez complexe. cela nécessite une certaine connaissance des modèles et des pointeurs de fonction.

Remarque: Joe Wreschnig a fait quelques remarques intéressantes dans les commentaires, notamment sur le fait que mon implémentation précédente reposait sur trop d'hypothèses sur l'efficacité du compilateur dans l'optimisation du code. le problème n'était pas préjudiciable, mais je m'en foutais aussi. J'ai également remarqué que l'ancienne COMPONENT_REGISTERmacro ne fonctionnait pas avec les modèles.

J'ai changé le code et maintenant tous ces problèmes devraient être résolus. La macro fonctionne avec des modèles et les problèmes soulevés par Joe ont été résolus: il est maintenant beaucoup plus facile pour les compilateurs d’optimiser le code inutile.

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

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

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

Étendre avec Lua

Je dois noter qu'avec un peu de travail (ce n'est pas très difficile), cela peut être utilisé pour travailler de manière transparente avec des composants définis en C ++ ou en Lua, sans jamais avoir à y penser.

Paul Manta
la source
Merci! Vous avez raison, je ne suis pas encore assez au courant des techniques noires des modèles C ++ pour bien comprendre cela. Mais la macro d'une ligne correspond exactement à ce que je cherchais et, en plus de cela, je l'utiliserai pour commencer à comprendre plus en profondeur les modèles.
michael.bartnett
6
Je conviens que c’est fondamentalement la bonne approche, mais deux choses me tiennent à coeur: 1. Pourquoi ne pas simplement utiliser une fonction basée sur un modèle et stocker une carte de pointeurs de fonction au lieu de créer des instances ComponentTypeImpl qui vont fuir à la sortie ( vous créez un fichier .SO / DLL ou autre) 2. L’objet ComponentRegistry peut tomber en panne en raison du "fiasco d’ordre d’initialisation statique". Pour s'assurer que composantRegistry est créé en premier, vous devez créer une fonction qui renvoie une référence à une variable statique locale et l'appeler au lieu d'utiliser directement composantRegistry.
Lucas
@ Lucas Ah, tu as tout à fait raison à propos de ceux-là. J'ai changé le code en conséquence. Je ne pense pas qu'il y ait eu de fuites dans le code précédent cependant, puisque j'ai utilisé shared_ptr, mais votre conseil est toujours bon.
Paul Manta
1
@Paul: D'accord, mais ce n'est pas théorique, vous devriez au moins le rendre statique pour éviter d'éventuelles plaintes de visibilité des symboles / éditeur de liens. De plus, votre commentaire "Vous devez gérer cette erreur comme bon vous semble" devrait plutôt indiquer "Ceci n'est pas une erreur".
1
@PaulManta: Les fonctions et les types sont parfois autorisés à "violer" l'ODR (par exemple, des modèles). Cependant, ici nous parlons d' instances et celles-ci doivent toujours suivre le règlement en ligne. Les compilateurs ne sont pas obligés de détecter et de signaler ces erreurs si elles se produisent dans plusieurs TU (c'est généralement impossible) et vous entrez donc dans le domaine du comportement indéfini. Si vous devez absolument appliquer du caca sur toute la définition de votre interface, la rendre statique au moins garde le programme bien défini - mais Coyote a la bonne idée.
9

On dirait que ce que vous voulez, c'est une usine.

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

Ce que vous pouvez faire est de faire en sorte que vos divers composants enregistrent auprès de l’usine le nom auquel ils correspondent, puis vous disposez d’une carte identifiant la chaîne identifiant la signature de la méthode du constructeur pour générer vos composants.

Tetrad
la source
1
Donc, il me faudrait quand même une section de code connaissant toutes mes Componentclasses, des appels ComponentSubclass::RegisterWithFactory(), non? Y a-t-il un moyen de le configurer plus dynamiquement et automagiquement? Le flux de travail que je recherche est de 1. Écrivez une classe en ne regardant que l’en-tête et le fichier cpp correspondants 2. Re-compiler le jeu 3. Ouvrez l’éditeur de niveaux et la nouvelle classe de composants pouvant être utilisés.
michael.bartnett
2
Il n'y a vraiment aucun moyen pour que cela se produise automatiquement. Vous pouvez toutefois le décomposer en un appel de macro d'une ligne, script par script. La réponse de Paul va un peu dans ce sens.
Tetrad
1

J'ai travaillé avec la conception de Paul Manta à partir de la réponse choisie pendant un certain temps et suis finalement arrivé à cette implémentation d'usine plus générique et concise que je suis prêt à partager avec tous ceux qui abordent cette question à l'avenir. Dans cet exemple, chaque objet fabrique dérive de la Objectclasse de base:

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

La classe Factory statique est la suivante:

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 pour enregistrer un sous-type de Objectest la suivante:

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

Maintenant, l'utilisation est la suivante:

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é de nombreux identificateurs de chaîne par sous-type était utile dans mon application, mais la restriction à un seul identificateur par sous-type serait relativement simple.

J'espère que cela a été utile!

alter igel
la source
1

En me basant sur la réponse de @TimStraubinger , j'ai construit une classe fabrique à l'aide de normes C ++ 14 permettant de stocker des membres dérivés avec un nombre arbitraire d'arguments . Mon exemple, contrairement à celui de Tim, ne prend qu'un nom / touche par fonction. Comme Tim, chaque classe stockée est dérivée d'une classe de base , la mienne étant appelée 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;
}

Sortie

Derived 1:  67
Derived 2:  6

J'espère que cela aidera les personnes ayant besoin d'utiliser une conception Factory qui ne nécessite pas de constructeur d'identité pour fonctionner. C'était amusant de concevoir, j'espère donc que cela aidera les personnes qui ont besoin de plus de flexibilité dans leurs conceptions d' usine .

Kenneth Cornett
la source