Comment fonctionne la communication d'entité?


115

J'ai deux cas d'utilisateur:

  1. Comment entity_Aenvoyer un take-damagemessage à entity_B?
  2. Comment entity_Ainterrogerait entity_BHP?

Voici ce que j'ai rencontré jusqu'à présent:

  • File d'attente
    1. entity_Acrée un take-damagemessage et l'affiche dans entity_Bla file d'attente des messages.
    2. entity_Acrée un query-hpmessage et l'affiche dans entity_B. entity_Ben retour, crée un response-hpmessage et l'envoie à entity_A.
  • Publier / S'abonner
    1. entity_Bs'abonne à des take-damagemessages (éventuellement avec un filtrage préemptif afin que seuls les messages pertinents soient remis). entity_Aproduit un take-damagemessage qui fait référence entity_B.
    2. entity_As'abonne à des update-hpmessages (éventuellement filtrés). Chaque image entity_Bdiffuse des update-hpmessages.
  • Signal / Slots
    1. ???
    2. entity_Arelie une update-hpfente à entity_B« s update-hpsignaux.

Y a-t-il quelque chose de mieux? Ai-je bien compris le lien entre ces schémas de communication et le système d'entités d'un moteur de jeu?

Réponses:


67

Bonne question! Avant de passer aux questions spécifiques que vous avez posées, je dirai: ne sous-estimez pas le pouvoir de la simplicité. Tenpn a raison. Gardez à l'esprit que ces approches ne font que chercher un moyen élégant de différer un appel de fonction ou de découpler l'appelant de l'appelé. Je peux recommander des coroutines comme un moyen étonnamment intuitif d'atténuer certains de ces problèmes, mais c'est un peu hors sujet. Parfois, il vaut mieux appeler simplement la fonction et vivre avec le fait que l'entité A est couplée directement à l'entité B. Voir YAGNI.

Cela dit, je suis satisfait du modèle signal / emplacement associé à la transmission simple de messages. Je l'ai utilisé en C ++ et Lua pour un titre iPhone assez réussi et dont le programme était très serré.

Pour le cas signal / slot, si je veux que l'entité A fasse quelque chose en réponse à quelque chose que l'entité B a fait (par exemple, déverrouiller une porte lorsque quelque chose meurt), il est possible que l'entité A souscrive directement à l'événement de mort de l'entité B. Ou peut-être que l'entité A souscrirait à chacune des entités d'un groupe, incrémenterait un compteur à chaque événement déclenché et déverrouillerait la porte après la mort de N d'entre elles. En outre, "groupe d'entités" et "N d'entre eux" seraient généralement définis par le concepteur dans les données de niveau. (En passant, c’est un domaine où les coroutines peuvent vraiment briller, par exemple WaitForMultiple ("Mourir", entA, entB, entC); door.Unlock ();)

Mais cela peut devenir fastidieux quand il s’agit de réactions étroitement liées au code C ++ ou à des événements de jeu intrinsèquement éphémères: infliger des dégâts, recharger des armes, déboguer, un retour d’information basé sur la localisation, dirigé par le joueur. C’est là que le passage du message peut combler les lacunes. Cela revient essentiellement à quelque chose comme: "Dites à toutes les entités de cette zone de subir des dégâts en 3 secondes" ou "chaque fois que vous terminez la physique pour déterminer qui j'ai tiré, dites-leur d'exécuter cette fonction de script". Il est difficile de comprendre comment faire cela en utilisant publication / abonnement ou signal / slot.

Cela peut facilement être exagéré (par rapport à l'exemple de Tenpn). Cela peut aussi être inefficace si vous avez beaucoup d'action. Mais malgré ses inconvénients, cette approche "messages et événements" se marie très bien avec le code de jeu scripté (par exemple en Lua). Le code de script peut définir et réagir à ses propres messages et événements sans que le code C ++ ne s'en soucie. De plus, le code de script peut facilement envoyer des messages qui déclenchent du code C ++, tels que la modification de niveaux, la reproduction de sons ou même le simple fait de laisser une arme définir les dégâts causés par le message TakeDamage. Cela m'a fait gagner beaucoup de temps, car je n'avais pas à m'amuser avec luabind. Et cela m'a permis de garder tout mon code luabind au même endroit, car il n'y en avait pas beaucoup. Lorsqu'il est correctement couplé,

De plus, mon expérience avec le cas d'utilisation n ° 2 est qu'il vaut mieux que vous le traitiez comme un événement dans l'autre sens. Au lieu de demander quel est l'état de santé de l'entité, déclenchez un événement / envoyez un message chaque fois que l'état de santé effectue un changement important.

En termes d'interfaces, d'ailleurs, j'ai eu trois classes pour implémenter tout cela: EventHost, EventClient et MessageClient. EventHosts crée des logements, EventClients s'y abonne / se connecte et MessageClients associe un délégué à un message. Notez que la cible déléguée d'un MessageClient n'a pas nécessairement besoin d'être le même objet que celui qui possède l'association. En d'autres termes, MessageClients peut exister uniquement pour transférer des messages vers d'autres objets. FWIW, la métaphore hôte / client est plutôt inappropriée. Source / Sink pourrait être de meilleurs concepts.

Désolé, j'ai un peu perdu la tête. C'est ma première réponse :) J'espère que cela a du sens.


Merci d'avoir répondu. Grandes idées. La raison pour laquelle je suis en train de concevoir le passage du message est à cause de Lua. J'aimerais pouvoir créer de nouvelles armes sans nouveau code C ++. Vos pensées ont donc répondu à certaines de mes questions non posées.
deft_code

En ce qui concerne les coroutines, moi aussi je crois beaucoup aux coroutines, mais je ne peux jamais jouer avec elles en C ++. J'avais un vague espoir d'utiliser des coroutines dans le code lua pour gérer le blocage des appels (par exemple, l'attente de la mort). Cela en valait-il la peine? Je crains d’être aveuglé par mon désir intense de coroutines en c ++.
deft_code

Enfin, quel était le jeu iPhone? Puis-je obtenir plus d'informations sur le système d'entité que vous avez utilisé?
deft_code

2
Le système d'entité était principalement en C ++. Il y avait donc, par exemple, une classe Imp qui gérait le comportement de l'Imp. Lua pourrait changer les paramètres de Imp à l'apparition ou via un message. L'objectif avec Lua était de respecter un calendrier serré et le débogage du code Lua prend beaucoup de temps. Nous avons utilisé Lua pour écrire des niveaux (quelles entités vont où, événements qui se produisent lorsque vous appuyez sur des déclencheurs). Ainsi, à Lua, nous dirions des choses comme SpawnEnt ("Imp"), où Imp est une association d’usine enregistrée manuellement. Cela créerait toujours un pool d’entités global. Sympa et simple. Nous avons utilisé beaucoup de smart_ptr et de faible_ptr.
Braffle

1
So BananaRaffle: Diriez-vous qu’il s’agit d’un résumé précis de votre réponse: "Les trois solutions que vous avez publiées ont leur utilité, comme le font les autres. Ne cherchez pas la solution parfaite, utilisez ce dont vous avez besoin là où cela a du sens. . "
Ipsquiggle

76
// in entity_a's code:
entity_b->takeDamage();

Vous avez demandé comment les jeux commerciaux le font. ;)


8
Un vote négatif? Sérieusement, c'est comme ça que ça se fait normalement! Les systèmes d'entités sont excellents, mais ils n'aident pas les jalons.
mardi

Je fais des jeux Flash de manière professionnelle, et c'est comme ça que je le fais. Vous appelez ennemis.damage (10), puis vous recherchez toutes les informations dont vous avez besoin dans les sites publics.
Iain

7
C'est sérieusement comment les moteurs de jeu commerciaux le font. Il ne plaisante pas. Target.NotifyTakeDamage (DamageType, DamageAmount, DamageDealer, etc.) est généralement ce qui se passe.
AA Grapsas

3
Est-ce que les jeux commerciaux mal orthographient "dommage" aussi? :-P
Ricket

15
Oui, ils causent des dégâts, entre autres choses. :)
LearnCocos2D

17

Une réponse plus sérieuse:

J'ai souvent vu les tableaux noirs utilisés. Les versions simples ne sont rien de plus que des jambes de force mises à jour avec des éléments tels que HP d'une entité, que les entités peuvent ensuite interroger.

Vos tableaux peuvent être la vue du monde de cette entité (demandez au tableau de B ce qu'est son HP), ou la vue du monde d'une entité (A interroge son tableau pour connaître la cible du HP de A).

Si vous ne mettez à jour que les tableaux noirs à un point de synchronisation dans le cadre, vous pourrez ensuite les lire ultérieurement à partir de n’importe quel fil, ce qui simplifiera l’implémentation du multithreading.

Des tableaux plus avancés peuvent ressembler davantage à des tables de hachage, mappant des chaînes à des valeurs. Ceci est plus facile à gérer mais a évidemment un coût d’exécution.

Un tableau n’est traditionnellement qu’une communication à sens unique: il ne résiste pas aux dégâts causés.


Je n'avais jamais entendu parler du modèle de tableau noir avant maintenant.
deft_code

Ils sont également utiles pour réduire les dépendances, comme le fait une file d’événements ou un modèle de publication / abonnement.
mardi

2
C’est aussi la «définition» canonique de la manière dont un système «idéal» E / C / S «devrait fonctionner». Les composants constituent le tableau; les systèmes sont le code agissant sur elle. (Les entités, bien sûr, sont juste long long ints ou similaires, dans un système ECS pur.)
BRPocock

6

J'ai étudié cette question un peu et j'ai vu une solution intéressante.

Fondamentalement, il s'agit de sous-systèmes. C'est semblable à l'idée de tableau mentionnée par tenpn.

Les entités sont constituées de composants, mais ce ne sont que des sacs de propriétés. Aucun comportement n'est implémenté dans les entités elles-mêmes.

Disons que les entités ont une composante de santé et une composante de dommages.

Ensuite, vous avez un MessageManager et trois sous-systèmes: ActionSystem, DamageSystem, HealthSystem. À un moment donné, ActionSystem effectue ses calculs sur le monde du jeu et génère un événement:

HIT, source=entity_A target=entity_B power=5

Cet événement est publié dans le MessageManager. À un moment donné, MessageManager parcourt les messages en attente et constate que DamageSystem s'est abonné aux messages HIT. Maintenant, MessageManager envoie le message HIT au DamageSystem. DamageSystem parcourt sa liste d'entités qui ont un composant Damage, calcule les points de dégâts en fonction de la puissance touchée ou d'un autre état des deux entités, etc. et publie un événement.

DAMAGE, source=entity_A target=entity_B amount=7

Le HealthSystem a souscrit aux messages DAMAGE et maintenant, lorsque MessageManager publie le message DAMAGE sur le HealthSystem, le HealthSystem a accès à la fois à entity_A et à entity_B avec leurs composants d'intégrité. au MessageManager).

Dans un tel moteur de jeu, le format des messages est le seul couplage entre tous les composants et sous-systèmes. Les sous-systèmes et entités sont complètement indépendants et ignorants les uns des autres.

Je ne sais pas si un moteur de jeu réel a implémenté cette idée ou non, mais il semble assez solide et propre et j'espère un jour l'appliquer moi-même pour mon moteur de jeu de niveau amateur.


C'est une réponse bien meilleure que la réponse acceptée, IMO. Découplé, maintenable et extensible (et pas plus qu'un désastre couplé comme la réponse blague de entity_b->takeDamage();)
Danny Yaroslavski

4

Pourquoi ne pas avoir une file de messages globale, quelque chose comme:

messageQueue.push_back(shared_ptr<Event>(new DamageEvent(entityB, 10, entityA)));

Avec:

DamageEvent(Entity* toDamage, uint amount, Entity* damageDealer);

Et à la fin de la boucle de jeu / de la gestion des événements:

while(!messageQueue.empty())
{
    Event e = messageQueue.front();
    messageQueue.pop_front();
    e.Execute();
}

Je pense que ceci est le modèle de commande. Et Execute()est un virtuel pur Event, quels dérivés définissent et font des choses. Alors ici:

DamageEvent::Execute() 
{
    toDamage->takeDamage(amount); // Or of course, you could now have entityA get points, or a recognition of damage, or anything.
}

3

Si votre jeu est en mode solo, utilisez simplement la méthode des objets cibles (comme suggéré par Tenpn).

Si vous êtes (ou souhaitez prendre en charge) le mode multijoueur (multiclient pour être exact), utilisez une file d'attente de commandes.

  • Lorsque A inflige des dommages à B sur le client 1, il suffit de mettre l’événement en attente.
  • Synchroniser les files d'attente de commandes via le réseau
  • Gérez les commandes en file d'attente des deux côtés.

2
Si vous voulez vraiment éviter de tricher, A ne vous en prie pas. Le client possédant A envoie une commande "attack B" au serveur, qui fait exactement ce que tenpn a dit; le serveur synchronise ensuite cet état avec tous les clients pertinents.

@ Joe: Oui, si un serveur est un point valide à prendre en compte, mais il est parfois correct de faire confiance au client (par exemple sur une console) pour éviter une charge de serveur lourde.
Andreas

2

Je dirais: n'utilisez ni l'un ni l'autre, tant que vous n'avez pas explicitement besoin d'un retour instantané des dégâts.

L'entité / le composant / tout ce qui doit causer des dommages doit pousser les événements vers une file d'attente d'événements locale ou un système de niveau égal contenant les événements de dégâts.

Il devrait alors y avoir un système en superposition avec un accès aux deux entités qui demande les événements à l'entité a et les transmet à l'entité b. En ne créant pas un système d'événements général que tout le monde peut utiliser n'importe où pour passer un événement à un moment quelconque, vous créez un flux de données explicite qui facilite toujours le débogage du code, facilite la mesure des performances, facilite la lecture et la compréhension, et souvent conduit à un système plus bien conçu en général.


1

Il suffit de faire l'appel. Ne faites pas request-hp suivi de query-hp - si vous suivez ce modèle, vous vous retrouverez dans un monde de blessures.

Vous voudrez peut-être aussi jeter un coup d'œil aux suites mono. Je pense que ce serait idéal pour les PNJ.


1

Alors que se passe-t-il si les joueurs A et B essaient de se toucher au cours du même cycle update ()? Supposons que la mise à jour () pour le joueur A se produise avant la mise à jour () pour le joueur B au cycle 1 (ou coche ou peu importe comment vous l'appelez). Il y a deux scénarios auxquels je peux penser:

  1. Traitement immédiat via un message:

    • joueur A.Update () voit que le joueur veut frapper B, le joueur B reçoit un message lui indiquant les dégâts.
    • le joueur B.HandleMessage () met à jour les points de vie du joueur B (il meurt)
    • le joueur B.Update () voit que le joueur B est mort .. il ne peut pas attaquer le joueur A

C'est injuste, les joueurs A et B devraient se toucher, le joueur B est mort avant de frapper A simplement parce que cette entité / cet objet a été mis à jour () plus tard.

  1. Mettre le message en attente

    • Le joueur A.Update () voit que le joueur veut frapper B, le joueur B reçoit un message l'informant des dégâts et les stocke dans une file d'attente.
    • Le joueur A.Update () vérifie sa file d'attente, elle est vide
    • Le joueur B.Update () vérifie d’abord les coups afin que le joueur B envoie un message au joueur A avec des dégâts également
    • player B.Update () gère également les messages de la file d'attente, traite les dommages causés par le lecteur A
    • Nouveau cycle (2): le joueur A veut boire une potion de santé et le joueur A.Update () est appelé et le déplacement est traité.
    • Player A.Update () vérifie la file de messages et traite les dommages causés par le lecteur B

Encore une fois, c'est injuste. Le joueur A est supposé prendre les points de vie dans le même tour / cycle / tick!


4
Vous ne répondez pas vraiment à la question, mais je pense que votre réponse en ferait une excellente question. Pourquoi ne pas aller de l'avant et demander comment résoudre une telle priorisation "injuste"?
Bummzack

Je doute que la plupart des jeux se soucient de cette injustice car ils sont mis à jour si souvent que c'est rarement un problème. Une solution simple consiste à alterner entre les itérations dans la liste d'entités lors de la mise à jour.
Kylotan

J'utilise 2 appels donc j'appelle Update () à toutes les entités, puis après la boucle, j'itère à nouveau et appelle quelque chose comme pEntity->Flush( pMessages );. Lorsque entity_A génère un nouvel événement, l'entité_B ne le lit pas dans ce cadre (il a également la possibilité de prendre la potion), puis les deux subissent des dégâts et traitent ensuite le message de guérison de la potion, qui sera le dernier dans la file d'attente. . Le joueur B meurt toujours malgré tout, car le message de potion est le dernier de la file d'attente: P, mais il peut être utile pour d'autres types de messages, tels que le nettoyage des pointeurs vers des entités mortes.
Pablo Ariel

Je pense qu'au niveau des images, la plupart des implémentations de jeux sont simplement injustes. comme dit Kylotan.
v.oddou

Ce problème est incroyablement facile à résoudre. Appliquez simplement les dommages les uns aux autres dans les gestionnaires de messages ou quoi que ce soit. Vous ne devriez certainement pas marquer le joueur comme mort à l'intérieur du gestionnaire de messages. Dans "Update ()" vous faites simplement "if (hp <= 0) die ();" (au début de "Update ()" par exemple). De cette façon, les deux peuvent s'entre-tuer en même temps. Aussi: Souvent, vous n'endommagez pas le joueur directement, mais à travers un objet intermédiaire tel qu'une balle.
Tara
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.