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.
Vous avez demandé comment les jeux commerciaux le font. ;)
la source
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.
la source
long long int
s ou similaires, dans un système ECS pur.)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:
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.
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.
la source
entity_b->takeDamage();
)Pourquoi ne pas avoir une file de messages globale, quelque chose comme:
Avec:
Et à la fin de la boucle de jeu / de la gestion des événements:
Je pense que ceci est le modèle de commande. Et
Execute()
est un virtuel purEvent
, quels dérivés définissent et font des choses. Alors ici:la source
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.
la source
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.
la source
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.
la source
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:
Traitement immédiat via un message:
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.
Mettre le message en attente
Encore une fois, c'est injuste. Le joueur A est supposé prendre les points de vie dans le même tour / cycle / tick!
la source
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.