Conception basée sur les composants: gestion de l'interaction des objets

9

Je ne sais pas exactement comment les objets font les choses avec d'autres objets dans une conception basée sur des composants.

Dis que j'ai une Objclasse. Je fais:

Obj obj;
obj.add(new Position());
obj.add(new Physics());

Comment pourrais-je alors avoir un autre objet non seulement pour déplacer la balle mais aussi pour appliquer ces principes physiques. Je ne recherche pas les détails de l'implémentation mais plutôt la manière dont les objets communiquent. Dans une conception basée sur une entité, vous pourriez simplement avoir:

obj1.emitForceOn(obj2,5.0,0.0,0.0);

Tout article ou explication pour mieux comprendre une conception axée sur les composants et comment faire des choses de base serait vraiment utile.

jmasterx
la source

Réponses:

10

Cela se fait généralement à l'aide de messages. Vous pouvez trouver beaucoup de détails dans d'autres questions sur ce site, comme ici ou - bas .

Pour répondre à votre exemple spécifique, une solution consiste à définir une petite Messageclasse que vos objets peuvent traiter, par exemple:

struct Message
{
    Message(const Objt& sender, const std::string& msg)
        : m_sender(&sender)
        , m_msg(msg) {}
    const Obj* m_sender;
    std::string m_msg;
};

void Obj::Process(const Message& msg)
{
    for (int i=0; i<m_components.size(); ++i)
    {
        // let components do some stuff with msg
        m_components[i].Process(msg);
    }
}

De cette façon, vous ne "polluez" pas votre Objinterface de classe avec les méthodes liées aux composants. Certains composants peuvent choisir de traiter le message, d'autres peuvent simplement l'ignorer.

Vous pouvez commencer par appeler cette méthode directement à partir d'un autre objet:

Message msg(obj1, "EmitForce(5.0,0.0,0.0)");
obj2.ProcessMessage(msg);

Dans ce cas, obj2's Physicschoisira le message et effectuera le traitement nécessaire. Une fois terminé, il sera soit:

  • Envoyez un message "SetPosition" à vous-même, que le Positioncomposant choisira;
  • Ou accédez directement au Positioncomposant pour les modifications (ce qui est tout à fait faux pour une conception basée uniquement sur des composants, car vous ne pouvez pas supposer que chaque objet a un Positioncomposant, mais le Positioncomposant peut être une exigence de Physics).

C'est généralement une bonne idée de retarder le traitement réel du message à la mise à jour du composant suivant . Le traiter immédiatement pourrait signifier l'envoi de messages à d'autres composants d'autres objets, donc l'envoi d'un seul message pourrait rapidement signifier une pile de spaghetti inextricable.

Vous devrez probablement opter pour un système plus avancé plus tard: files d'attente de messages asynchrones, envoi de messages à un groupe d'objets, enregistrement / désinscription par composant des messages, etc.

La Messageclasse peut être un conteneur générique pour une chaîne simple comme indiqué ci-dessus, mais le traitement des chaînes au moment de l'exécution n'est pas vraiment efficace. Vous pouvez opter pour un conteneur de valeurs génériques: chaînes, entiers, flottants ... Avec un nom ou mieux encore, un identifiant, pour distinguer différents types de messages. Ou vous pouvez également dériver une classe de base pour répondre à des besoins spécifiques. Dans votre cas, vous pouvez imaginer un EmitForceMessagequi dérive Messageet ajoute le vecteur de force souhaité, mais méfiez-vous du coût d' exécution de RTTI si vous le faites.

Laurent Couvidou
la source
3
Je ne m'inquiéterais pas de la "non pureté" de l'accès direct aux composants. Les composants sont utilisés pour répondre aux besoins fonctionnels et de conception, pas pour le milieu universitaire. Vous voulez vérifier qu'un composant existe (par exemple, la valeur de retour de vérification n'est pas nulle pour l'appel de composant get).
Sean Middleditch
J'y ai toujours pensé comme vous l'avez dit pour la dernière fois, en utilisant RTTI, mais tant de gens ont dit tant de mauvaises choses sur RTTI
jmasterx
@SeanMiddleditch Bien sûr, je le ferais de cette façon, en mentionnant simplement que pour qu'il soit clair que vous devriez toujours vérifier ce que vous faites lorsque vous accédez à d'autres composants de la même entité.
Laurent Couvidou
@Milo Le RTTI implémenté par le compilateur et son dynamic_cast peut devenir un goulot d'étranglement, mais je ne m'en inquiéterai pas pour l'instant. Vous pouvez toujours optimiser cela plus tard si cela devient un problème. Les identificateurs de classe basés sur CRC fonctionnent comme un charme.
Laurent Couvidou
´template <typename T> uint32_t class_id () {static uint32_t v; return (uint32_t) & v; } ´ - pas besoin de RTTI.
arul
3

Ce que j'ai fait pour résoudre un problème similaire à ce que vous montrez, c'est d'ajouter des gestionnaires de composants spécifiques et d'ajouter une sorte de système de résolution d'événements.

Ainsi, dans le cas de votre objet "Physique", une fois initialisé, il s'ajouterait à un gestionnaire central d'objets physiques. Dans la boucle du jeu, ces types de gestionnaires ont leur propre étape de mise à jour, donc lorsque ce PhysicsManager est mis à jour, il calcule toutes les interactions physiques et les ajoute dans une file d'attente d'événements.

Après avoir produit tous vos événements, vous pouvez résoudre votre file d'attente d'événements en vérifiant simplement ce qui s'est passé et en prenant des mesures en conséquence, dans votre cas, il devrait y avoir un événement indiquant que les objets A et B ont interagi d'une manière ou d'une autre, vous appelez donc votre méthode emitForceOn.

Avantages de cette méthode:

  • Conceptuellement, c'est vraiment simple à suivre.
  • Vous donne de la place pour des optimisations spécifiques comme l'utilisation de quadtress ou tout ce dont vous auriez besoin.
  • Cela finit par être vraiment "plug and play". Les objets avec physique n'interagissent pas avec les objets non physiques car ils n'existent pas pour le gestionnaire.

Les inconvénients:

  • Vous vous retrouvez avec beaucoup de références qui se déplacent, il peut donc devenir un peu compliqué de tout nettoyer correctement si vous ne faites pas attention (de votre composant au propriétaire du composant, du gestionnaire au composant, de l'événement aux participants, etc. ).
  • Vous devez accorder une attention particulière à l'ordre dans lequel vous résolvez tout. Je suppose que ce n'est pas votre cas, mais j'ai fait face à plus d'une boucle infinie où un événement a créé un autre événement et je l'ajoutais directement à la file d'attente des événements.

J'espère que ça aide.

PS: Si quelqu'un a une solution plus propre / meilleure pour résoudre ce problème, j'aimerais vraiment l'entendre.

Carlos
la source
1
obj->Message( "Physics.EmitForce 0.0 1.1 2.2" );
// and some variations such as...
obj->Message( "Physics.EmitForce", "0.0 1.1 2.2" );
obj->Message( "Physics", "EmitForce", "0.0 1.1 2.2" );

Quelques points à noter sur cette conception:

  • Le nom du composant est le premier paramètre - c'est pour éviter d'avoir trop de code à travailler sur le message - nous ne pouvons pas savoir quels composants un message peut déclencher - et nous ne voulons pas qu'ils mâchent tous un message avec un échec de 90% taux qui se convertit en beaucoup de branches inutiles et de strcmp .
  • Le nom du message est le deuxième paramètre.
  • Le premier point (en # 1 et # 2) n'est pas nécessaire, c'est juste pour faciliter la lecture (pour les personnes, pas pour les ordinateurs).
  • Il est compatible avec sscanf, iostream, you-name-it. Aucun sucre syntaxique qui ne fait rien pour simplifier le traitement du message.
  • Un paramètre de chaîne: passer les types natifs n'est pas moins cher en termes de besoins en mémoire car vous devez prendre en charge un nombre inconnu de paramètres de type relativement inconnu.
snake5
la source