Comment puis-je accéder correctement aux composants de mes systèmes d'entités-composants C ++?

18

(Ce que je décris est basé sur cette conception: qu'est-ce qu'un framework de système d'entité?, Faites défiler vers le bas et vous le trouverez)

J'ai des problèmes pour créer un système de composants d'entité en C ++. J'ai ma classe Component:

class Component { /* ... */ };

Ce qui est en fait une interface, pour la création d'autres composants. Donc, pour créer un composant personnalisé, je viens d'implémenter l'interface et d'ajouter les données qui seront utilisées dans le jeu:

class SampleComponent : public Component { int foo, float bar ... };

Ces composants sont stockés dans une classe Entity, qui donne à chaque instance d'Entity un ID unique:

class Entity {
     int ID;
     std::unordered_map<string, Component*> components;
     string getName();
     /* ... */
};

Les composants sont ajoutés à l'entité en hachant le nom du composant (ce n'est probablement pas une si bonne idée). Lorsque j'ajoute un composant personnalisé, il est stocké en tant que type de composant (classe de base).

Maintenant, d'un autre côté, j'ai une interface système, qui utilise une interface Node à l'intérieur. La classe Node est utilisée pour stocker certains des composants d'une seule entité (car le système n'est pas intéressé à utiliser tous les composants de l'entité). Lorsque le système doit le faire update(), il n'a qu'à parcourir les nœuds qu'il a stockés, créés à partir de différentes entités. Donc:

/* System and Node implementations: (not the interfaces!) */

class SampleSystem : public System {
        std::list<SampleNode> nodes; //uses SampleNode, not Node
        void update();
        /* ... */
};

class SampleNode : public Node {
        /* Here I define which components SampleNode (and SampleSystem) "needs" */
        SampleComponent* sc;
        PhysicsComponent* pc;
        /* ... more components could go here */
};

Maintenant, le problème: disons que je construis les SampleNodes en passant une entité au SampleSystem. Le SampleNode "vérifie" ensuite si l'entité possède les composants requis à utiliser par le SampleSystem. Le problème apparaît lorsque j'ai besoin d'accéder au composant souhaité à l'intérieur de l'entité: le composant est stocké dans une collection Component(classe de base), donc je ne peux pas accéder au composant et le copier sur le nouveau nœud. J'ai temporairement résolu le problème en convertissant le Componentdown en un type dérivé, mais je voulais savoir s'il y avait une meilleure façon de le faire. Je comprends si cela signifierait repenser ce que j'ai déjà. Merci.

Federico
la source

Réponses:

23

Si vous allez stocker les Components dans une collection tous ensemble, vous devez utiliser une classe de base commune comme type stocké dans la collection, et donc vous devez convertir le type correct lorsque vous essayez d'accéder aux Components dans la collection. Les problèmes d'essayer de transtyper vers la mauvaise classe dérivée peuvent être éliminés par une utilisation intelligente des modèles et de la typeidfonction, cependant:

Avec une carte déclarée ainsi:

std::unordered_map<const std::type_info* , Component *> components;

une fonction addComponent comme:

components[&typeid(*component)] = component;

et un getComponent:

template <typename T>
T* getComponent()
{
    if(components.count(&typeid(T)) != 0)
    {
        return static_cast<T*>(components[&typeid(T)]);
    }
    else 
    {
        return NullComponent;
    }
}

Vous n'obtiendrez pas d'erreur. En effet typeid, renvoie un pointeur sur les informations de type du type d'exécution (le type le plus dérivé) du composant. Étant donné que le composant est stocké avec ces informations de type en tant que clé, la distribution ne peut pas provoquer de problèmes en raison de types incompatibles. Vous obtenez également la vérification du type de temps de compilation sur le type de modèle car il doit être un type dérivé de Component ou les static_cast<T*>types auront des correspondances avec leunordered_map .

Cependant, vous n'avez pas besoin de stocker les composants de différents types dans une collection commune. Si vous abandonnez l'idée d'un s Entitycontenant Componentet que vous avez à la place chaque Componentmagasin un Entity(en réalité, ce ne sera probablement qu'un ID entier), vous pouvez stocker chaque type de composant dérivé dans sa propre collection du type dérivé au lieu de type de base commun, et trouver le Components "appartenant à" unEntity via cet ID.

Cette deuxième implémentation est un peu plus intuitive à penser que la première, mais elle pourrait probablement être masquée en tant que détails d'implémentation derrière une interface afin que les utilisateurs du système n'aient pas à s'en soucier. Je ne commenterai pas ce qui est mieux car je n'ai pas vraiment utilisé le second, mais je ne vois pas l'utilisation de static_cast comme un problème avec une garantie sur les types aussi forte que celle fournie par la première implémentation. Notez qu'il nécessite RTTI qui peut ou non être un problème en fonction de la plate-forme et / ou des convictions philosophiques.

Chewy Gumball
la source
3
J'utilise C ++ depuis près de 6 ans maintenant, mais chaque semaine, j'apprends une nouvelle astuce.
knight666
Merci de répondre. J'essaierai d'abord d'utiliser la première méthode, et si je le fais peut-être plus tard, je penserai à une façon d'utiliser l'autre. Mais, la addComponent()méthode ne devrait-elle pas également être une méthode de modèle? Si je définis un addComponent(Component* c), tout sous-composant que j'ajoute sera stocké dans un Componentpointeur et typeidfera toujours référence à la Componentclasse de base.
Federico
2
Typeid vous donnera le type réel de l'objet pointé même si le pointeur est d'une classe de base
Chewy Gumball
J'ai vraiment aimé la réponse de chewy, j'ai donc essayé de l'implémenter sur mingw32. J'ai rencontré le problème mentionné par fede rico où addComponent () stocke tout en tant que composant car typeid renvoie le composant en tant que type pour tout. Quelqu'un ici a mentionné que typeid devrait donner le type réel de l'objet pointé même si le pointeur est sur une classe de base, mais je pense que cela peut varier en fonction du compilateur, etc. Quelqu'un d'autre peut-il confirmer cela? J'utilisais g ++ std = c ++ 11 mingw32 sur Windows 7. J'ai fini par modifier getComponent () pour être un modèle, puis j'ai enregistré le type de celui-ci dans th
shwoseph
Ce n'est pas spécifique au compilateur. Vous n'aviez probablement pas la bonne expression comme argument de la fonction typeid.
Chewy Gumball
17

Chewy a raison, mais si vous utilisez C ++ 11, vous pouvez utiliser de nouveaux types.

À la place d'utiliser const std::type_info* comme clé dans votre carte, vous pouvez utiliser std::type_index( voir cppreference.com ), qui est un wrapper autour du std::type_info. Pourquoi l'utiliseriez-vous? Le std::type_indexstocke en fait la relation avec le en std::type_infotant que pointeur, mais c'est un pointeur de moins dont vous devez vous soucier.

Si vous utilisez effectivement C ++ 11, je recommanderais de stocker les Componentréférences dans des pointeurs intelligents. La carte pourrait donc ressembler à ceci:

std::map<std::type_index, std::shared_ptr<Component> > components

L'ajout d'une nouvelle entrée peut se faire de la manière suivante:

components[std::type_index(typeid(*component))] = component

componentest de type std::shared_ptr<Component>. La récupération d'une référence à un type donné de Componentpourrait ressembler à:

template <typename T>
std::shared_ptr<T> getComponent()
{
    std::type_index index(typeid(T));
    if(components.count(std::type_index(typeid(T)) != 0)
    {
        return static_pointer_cast<T>(components[index]);
    }
    else
    {
        return NullComponent
    }
}

Notez également l'utilisation de static_pointer_castau lieu de static_cast.

vijoc
la source
1
J'utilise en fait ce genre d'approche dans mon propre projet.
vijoc
C'est en fait assez pratique, car j'ai appris le C ++ en utilisant la norme C ++ 11 comme référence. Une chose que j'ai remarquée cependant, c'est que tous les systèmes de composants d'entité que j'ai trouvés sur le Web utilisent une sorte de cast. Je commence à penser qu'il serait impossible de mettre en œuvre cela, ou une conception de système similaire sans transtypages.
Federico
@Fede Le stockage de Componentpointeurs dans un seul conteneur nécessite nécessairement leur conversion vers le type dérivé. Mais, comme l'a souligné Chewy, vous avez d'autres options à votre disposition, qui ne nécessitent pas de casting. Moi-même, je ne vois rien de "mauvais" à avoir ce type de plâtres dans la conception, car ils sont relativement sûrs.
vijoc
@vijoc Ils sont parfois considérés comme mauvais en raison du problème de cohérence de mémoire qu'ils peuvent introduire.
akaltar