Comment implémenter et utiliser une file de messages dans votre jeu

Un jeu est généralement composé de plusieurs entités différentes qui interagissent les unes avec les autres. Ces interactions tendent à être très dynamiques et profondément liées au gameplay. Ce tutoriel couvre le concept et la mise en œuvre d'un système de file d'attente de messages capable d'unifier les interactions d'entités, rendant votre code facile à gérer et à gérer, car il devient de plus en plus complexe.. 

introduction

Une bombe peut interagir avec un personnage en explosant et en causant des dégâts, un kit médical peut soigner une entité, une clé peut ouvrir une porte, etc. Les interactions dans un jeu sont infinies, mais comment pouvons-nous garder le code du jeu gérable tout en restant capable de gérer toutes ces interactions? Comment pouvons-nous nous assurer que le code peut changer et continuer à fonctionner lorsque des interactions nouvelles et inattendues se produisent?

Les interactions dans un jeu ont tendance à devenir de plus en plus complexes.

Au fur et à mesure que les interactions s'ajoutent (en particulier les inattendues), votre code sera de plus en plus encombré. Une mise en œuvre naïve vous conduira rapidement à poser des questions telles que:

"Ceci est l'entité A, donc je devrais appeler la méthode dommage() dessus, non? Ou est-ce damageByItem ()? Peut être ça dégâtsParWeapon () la méthode est la bonne? "

Imaginez que ce chaos encombrant se propage à toutes vos entités de jeu, car elles interagissent les unes avec les autres de manière différente et particulière. Heureusement, il existe un moyen plus efficace, plus simple et plus simple de le faire..

Message Queue

Entrer le file d'attente des messages. L'idée de base de ce concept est de mettre en œuvre toutes les interactions de jeu en tant que système de communication (encore utilisé aujourd'hui): échange de messages. Les gens ont communiqué via des messages (lettres) pendant des siècles parce que c'est un système simple et efficace.

Dans nos services postaux du monde réel, le contenu de chaque message peut différer, mais la manière dont ils sont physiquement envoyés et reçus reste la même. Un expéditeur place les informations dans une enveloppe et l’adresse à une destination. La destination peut répondre (ou non) en suivant le même mécanisme, en modifiant simplement les champs "de / à" de l'enveloppe.. 

Interactions effectuées à l'aide d'un système de file d'attente de messages.

En appliquant cette idée à votre jeu, toutes les interactions entre entités peuvent être considérées comme des messages. Si une entité de jeu souhaite interagir avec une autre (ou un groupe d’entre elles), il lui suffit d’envoyer un message. La destination traitera ou réagira le message en fonction de son contenu et du destinataire de l'expéditeur..

Dans cette approche, la communication entre les entités de jeu devient unifiée. Toutes les entités peuvent envoyer et recevoir des messages. Quelle que soit la complexité ou la particularité de l'interaction ou du message, le canal de communication reste toujours le même.

Dans les sections suivantes, je décrirai comment vous pouvez réellement implémenter cette approche de file d'attente de messages dans votre jeu.. 

Conception d'une enveloppe (message)

Commençons par concevoir l'enveloppe, qui est l'élément le plus fondamental du système de file d'attente de messages.. 

Une enveloppe peut être décrite comme dans la figure ci-dessous:

Structure d'un message.

Les deux premiers champs (expéditeur et destination) sont des références à l'entité qui a créé et à l'entité qui recevra ce message, respectivement. À l'aide de ces champs, l'expéditeur et le destinataire peuvent indiquer où le message est envoyé et d'où il vient..

Les deux autres champs (type et Les données) travaillent ensemble pour s’assurer que le message est correctement traité. le type champ décrit le sujet de ce message; par exemple, si le type est "dommage", le destinataire traitera ce message comme un ordre visant à diminuer ses points de fonctionnement; si le type est "poursuivre", le destinataire le prendra comme instruction de poursuivre quelque chose, etc..

le Les données le champ est directement connecté au type champ. En utilisant les exemples précédents, si le type de message est "dommage", puis le Les données champ contiendra un nombre dire, dix-qui décrit le montant des dommages que le récepteur devrait appliquer à ses points de vie. Si le type de message est "poursuivre"Les données contiendra un objet décrivant la cible à poursuivre.

le Les données Ce champ peut contenir n’importe quelle information, ce qui fait de l’enveloppe un moyen de communication polyvalent. Tout peut être placé dans ce champ: entiers, flottants, chaînes et même d'autres objets. En règle générale, le destinataire doit savoir ce qui se trouve dans le Les données champ basé sur ce qui est dans le type champ.

Toute cette théorie peut être traduite en une classe très simple nommée Message. Il contient quatre propriétés, une pour chaque champ:

Message = function (to, from, type, data) // Propriétés this.to = to; // une référence à l'entité qui recevra ce message this.from = from; // une référence à l'entité qui a envoyé ce message this.type = type; // le type de ce message this.data = data; // le contenu / les données de ce message;

A titre d'exemple, si une entité UNE veut envoyer un "dommage" message à l'entité B, tout ce qu'il a à faire est d'instancier un objet de la classe Message, définir la propriété à à B, définir la propriété de à lui-même (entité UNE), ensemble type à "dommage" et, enfin, mis Les données à un certain nombre (dix, par exemple):

// Instancie les deux entités var entityA = new Entity (); var entityB = new Entity (); // Crée un message à entityB, à partir de entityA, // avec le type "damage" et data / value 10. var msg = new Message (); msg.to = entityB; msg.from = entityA; msg.type = "damage"; msg.data = 10; // Vous pouvez également instancier directement le message // en transmettant les informations requises, comme ceci: var msg = new Message (entityB, entityA, "damage", 10);

Maintenant que nous avons un moyen de créer des messages, il est temps de penser à la classe qui les stockera et les livrera..

Mettre en place une file d'attente

La classe chargée de stocker et de remettre les messages sera appelée MessageQueue. Cela fonctionnera comme un bureau de poste: tous les messages sont remis à cette classe, ce qui garantit qu'ils seront envoyés à leur destination..

Pour l'instant, le MessageQueue la classe aura une structure très simple:

/ ** * Cette classe est responsable de la réception des messages * et de leur envoi vers la destination. * / MessageQueue = function () this.messages = []; // liste des messages à envoyer; // Ajoute un nouveau message à la file d'attente. Le message doit être une instance // de la classe Message. MessageQueue.prototype.add = function (message) this.messages.push (message); ;

La propriété messages est un tableau. Il stockera tous les messages sur le point d'être livrés par le MessageQueue. La méthode ajouter() reçoit un objet de la classe Message en tant que paramètre et ajoute cet objet à la liste des messages. 

Voici comment notre exemple précédent d'entité UNE entité de messagerie B sur les dommages fonctionneraient en utilisant le MessageQueue classe:

// Instancie les deux entités et la file de messages var entityA = new Entity (); var entityB = new Entity (); var messageQueue = new MessageQueue (); // Crée un message à entityB, à partir de entityA, // avec le type "damage" et la donnée / valeur 10. var msg = new Message (entityB, entityA, "damage", 10); // Ajoute le message à la file d'attente messageQueue.add (msg);

Nous avons maintenant un moyen de créer et de stocker des messages dans une file d'attente. Il est temps de les amener à destination.

Diffuser des messages

Afin de faire la MessageQueue classe envoie effectivement les messages postés, nous devons d’abord définir Comment les entités vont gérer et recevoir les messages. Le plus simple est d’ajouter une méthode nommée onMessage () à chaque entité pouvant recevoir des messages:

/ ** * Cette classe décrit une entité générique. * / Entity = function () // Initialise quoi que ce soit ici, par exemple. Trucs Phaser; // Cette méthode est appelée par MessageQueue // lorsqu'un message est envoyé à cette entité. Entity.prototype.onMessage = function (message) // Gère le nouveau message ici;

le MessageQueue la classe invoquera le onMessage () méthode de chaque entité devant recevoir un message. Le paramètre transmis à cette méthode est le message envoyé par le système de file d'attente (et reçu par la destination).. 

le MessageQueue classe enverra les messages dans sa file d'attente en une fois, dans le envoi() méthode:

/ ** * Cette classe est responsable de la réception des messages * et de leur envoi vers la destination. * / MessageQueue = function () this.messages = []; // liste des messages à envoyer; MessageQueue.prototype.add = function (message) this.messages.push (message); ; // envoie tous les messages de la file d'attente à leur destination. MessageQueue.prototype.dispatch = function () var i, entity, msg; // Iterave sur la liste des messages pour (i = 0; this.messages.length; i ++) // Récupère le message de l'itération en cours msg = this.messages [i]; // Est-ce valide? if (msg) // Récupère l'entité qui devrait recevoir ce message // (celui du champ 'à') entity = msg.to; // Si cette entité existe, remettez le message. if (entity) entity.onMessage (msg);  // Supprimer le message de la file d'attente this.messages.splice (i, 1); je--; ;

Cette méthode parcourt tous les messages de la file d'attente et, pour chaque message, le à Le champ est utilisé pour extraire une référence au destinataire. le onMessage () La méthode du destinataire est ensuite appelée, avec le message actuel en tant que paramètre, puis le message remis est supprimé de la liste. MessageQueue liste. Ce processus est répété jusqu'à ce que tous les messages soient envoyés.

Utiliser une file de messages

Il est temps de voir tous les détails de cette implémentation fonctionner ensemble. Utilisons notre système de file d'attente de messages dans une démonstration très simple composée de quelques entités en mouvement qui interagissent les unes avec les autres. Par souci de simplicité, nous allons travailler avec trois entités: Guérisseur, Coureur et chasseur.

le Coureur a une barre de santé et se déplace au hasard. le Guérisseur va guérir tout Coureur qui passe tout près; d'autre part, le chasseur infligera des dégâts sur tout à proximité Coureur. Toutes les interactions seront traitées à l'aide du système de file d'attente de messages.

Ajout de la file d'attente de messages

Commençons par créer le PlayState qui contient une liste d’entités (guérisseurs, coureurs et chasseurs) et une instance du MessageQueue classe:

var PlayState = function () var entités; // liste des entités du jeu var messageQueue; // la file d'attente de messages (répartiteur) this.create = function () // Initialise la file d'attente de messages messageQueue = new MessageQueue (); // Crée un groupe d'entités. entités = this.game.add.group (); ; this.update = function () // Rendre tous les messages de la file d'attente des messages // atteindre leur destination. messageQueue.dispatch (); ; ;

Dans la boucle de jeu, représentée par le mettre à jour() méthode, la file de messages envoi() la méthode est invoquée, ainsi tous les messages sont remis à la fin de chaque cadre de jeu.

Ajout des coureurs

le Coureur La classe a la structure suivante:

/ ** * Cette classe décrit une entité qui * vagabonde. * / Runner = function () // initialise le contenu de Phaser ici…; // Appelé par le jeu sur chaque image Runner.prototype.update = function () // Fait bouger les choses ici… // Cette méthode est appelée par la file d'attente des messages // pour que le coureur gère les messages entrants. Runner.prototype.onMessage = function (message) montant var; // Vérifiez le type de message afin qu'il soit possible // de décider si ce message doit être ignoré ou non. if (message.type == "damage") // Le message concerne les dommages. // Nous devons diminuer nos points de santé. Le montant de // cette diminution a été informé par l'expéditeur du message // dans le champ 'data'. quantité = message.data; this.addHealth (-amount);  else if (message.type == "heal") // Le message concerne la guérison. // Nous devons augmenter nos points de santé. Là encore, la quantité de // points de santé à augmenter a été indiquée par l'expéditeur du message // dans le champ "data". quantité = message.data; this.addHealth (montant);  else // Nous traitons ici les messages que nous ne pouvons pas traiter. // Probablement juste les ignorer :);

La partie la plus importante est la onMessage () méthode, invoquée par la file de messages chaque fois qu’un nouveau message est créé pour cette instance. Comme expliqué précédemment, le champ type dans le message est utilisé pour décider ce que cette communication est tout au sujet.

En fonction du type du message, l’action correcte est exécutée: si le type de message est "dommage", les points de santé sont diminués; si le type de message est "guérir", les points de santé sont augmentés. Le nombre de points de santé à augmenter ou à diminuer est défini par l'expéditeur dans le Les données champ du message.

dans le PlayState, nous ajoutons quelques coureurs à la liste des entités:

var PlayState = function () // (…) this.create = function () // (…) // Ajouter des coureurs pour (i = 0; i < 4; i++)  entities.add(new Runner(this.game, this.game.world.width * Math.random(), this.game.world.height * Math.random()));  ; // (… ) ;

Le résultat est quatre coureurs se déplaçant de manière aléatoire:

Ajout du chasseur

le chasseur La classe a la structure suivante:

/ ** * Cette classe décrit une entité qui * vagabonde juste au point de blesser les coureurs qui passent. * / Hunter = function (jeu, x, y) // initialise ici le contenu du Phaser; // Vérifie si l'entité est valide, est un coureur et se trouve dans la plage d'attaque. Hunter.prototype.canEntityBeAttacked = fonction (entité) entité de retour && entité! = This && (instance instance de Runner) &&! (Entité instance de Hunter) && entity.position.distance (this.position) <= 150; ; // Invoked by the game during the game loop. Hunter.prototype.update = function()  var entities, i, size, entity, msg; // Get a list of entities entities = this.getPlayState().getEntities(); for(i = 0, size = entities.length; i < size; i++)  entity = entities.getChildAt(i); // Is this entity a runner and is it close? if(this.canEntityBeAttacked(entity))  // Yeah, so it's time to cause some damage! msg = new Message(entity, this, "damage", 2); // Send the message away! this.getMessageQueue().add(msg); // or just entity.onMessage(msg); if you want to bypass the message queue for some reasong.   ; // Get a reference to the game's PlayState Hunter.prototype.getPlayState = function()  return this.game.state.states[this.game.state.current]; ; // Get a reference to the game's message queue. Hunter.prototype.getMessageQueue = function()  return this.getPlayState().getMessageQueue(); ;

Les chasseurs se déplaceront également, mais ils causeront des dégâts à tous les coureurs proches. Ce comportement est implémenté dans le mettre à jour() méthode, où toutes les entités du jeu sont inspectées et les coureurs sont informés des dommages.

Le message de dommage est créé comme suit:

msg = nouveau message (entité, ceci, "dommage", 2);

Le message contient les informations sur la destination (entité, dans ce cas, qui est l’entité analysée dans l’itération en cours), l’émetteur (ce, qui représente le chasseur qui effectue l'attaque), le type du message ("dommage") et le montant des dommages (2, dans ce cas, assigné à la Les données champ du message).

Le message est ensuite envoyé à la destination via la commande this.getMessageQueue (). add (msg), qui ajoute le message nouvellement créé à la file de messages.

Enfin, nous ajoutons le chasseur à la liste des entités du PlayState:

var PlayState = function () // (…) this.create = function () // (…) // Ajouter un chasseur à la position (20, 30) entités.add (nouveau chasseur (this.game, 20, 30 )); ; // (…);

Le résultat est que quelques coureurs se déplacent, recevant des messages du chasseur à mesure qu'ils se rapprochent les uns des autres:

J'ai ajouté les enveloppes volantes comme aide visuelle pour montrer ce qui se passe..

Ajout du guérisseur

le Guérisseur La classe a la structure suivante:

/ ** * Cette classe décrit une entité * capable de soigner tout coureur qui passe à proximité. * / Guérisseur = fonction (jeu, x, y) // Initialiseur Trucs Phaser ici; Healer.prototype.update = function () var entités, i, taille, entité, msg; // La liste des entités dans les entités du jeu = this.getPlayState (). GetEntities (); pour (i = 0, taille = entités.longueur; i < size; i++)  entity = entities.getChildAt(i); // Is it a valid entity? if(entity)  // Check if the entity is within the healing radius if(this.isEntityWithinReach(entity))  // The entity can be healed! // First of all, create a new message regaring the healing msg = new Message(entity, this, "heal", 2); // Send the message away! this.getMessageQueue().add(msg); // or just entity.onMessage(msg); if you want to bypass the message queue for some reasong.    ; // Check if the entity is neither a healer nor a hunter and is within the healing radius. Healer.prototype.isEntityWithinReach = function(entity)  return !(entity instanceof Healer) && !(entity instanceof Hunter) && entity.position.distance(this.position) <= 200; ; // Get a reference to the game's PlayState Healer.prototype.getPlayState = function()  return this.game.state.states[this.game.state.current]; ; // Get a reference to the game's message queue. Healer.prototype.getMessageQueue = function()  return this.getPlayState().getMessageQueue(); ;

Le code et la structure sont très similaires à la chasseur classe, à quelques différences près. Comme pour la mise en œuvre du chasseur, le guérisseur mettre à jour() méthode itère sur la liste des entités du jeu, en envoyant une messagerie à toute entité se trouvant dans son rayon de guérison:

msg = new Message (entity, this, "heal", 2);

Le message a aussi une destination (entité), un expéditeur (ce, qui est le guérisseur effectuant l’action), un type de message ("guérir") et le nombre de points de guérison (2, attribué dans le Les données champ du message).

Nous ajoutons le Guérisseur à la liste des entités du PlayState de la même manière que nous avons fait avec le chasseur et le résultat est une scène avec des coureurs, un chasseur et un guérisseur:

Et c'est tout! Nous avons trois entités différentes qui interagissent les unes avec les autres en échangeant des messages.

Discussion sur la flexibilité

Ce système de file d'attente de messages est un moyen polyvalent de gérer les interactions dans un jeu. Les interactions sont effectuées via un canal de communication unifié et doté d'une interface unique facile à utiliser et à mettre en œuvre..

Au fur et à mesure que votre jeu gagne en complexité, de nouvelles interactions peuvent être nécessaires. Certaines d'entre elles peuvent être complètement inattendues, vous devez donc adapter votre code pour les gérer. Si vous utilisez un système de file d'attente de messages, vous devez ajouter un nouveau message quelque part et le traiter dans un autre dossier..

Par exemple, imaginons que vous vouliez faire la chasseur interagir avec le Guérisseur; il suffit de faire la chasseur envoyer un message avec la nouvelle interaction, par exemple, "fuir"-et veiller à ce que le Guérisseur peut le gérer dans le onMessage méthode:

// Dans la classe Hunter: Hunter.prototype.someMethod = function () // Obtient une référence à un guérisseur proche var healer = this.getNearbyHealer (); // Crée un message sur la fuite d'un endroit var place = x: 30, y: 40; var msg = nouveau Message (entité, ceci, "fuit", lieu); // Envoie le message! this.getMessageQueue (). add (msg); ; // Dans la classe Healer: Healer.prototype.onMessage = function (message) if (message.type == "flee") // Obtenir l'emplacement à fuir du champ de données du message var place = message.data ; // Utilise les informations de lieu flee (place.x, place.y); ;

Pourquoi ne pas simplement envoyer des messages directement?

Échanger des messages entre entités peut être utile, mais vous vous demandez peut-être pourquoi MessageQueue est nécessaire après tout. Ne pouvez-vous pas simplement invoquer le destinataire onMessage () méthode vous au lieu de compter sur le MessageQueue, comme dans le code ci-dessous?

Hunter.prototype.someMethod = function () // Obtenir une référence à un guérisseur proche var healer = this.getNearbyHealer (); // Crée un message sur la fuite d'un endroit var place = x: 30, y: 40; var msg = nouveau Message (entité, ceci, "fuit", lieu); // Ignore la MessageQueue et remet directement // le message au guérisseur. guérisseur.onMessage (msg); ;

Vous pouvez certainement implémenter un système de messagerie comme celui-ci, mais l'utilisation d'un MessageQueue a quelques avantages.

Par exemple, en centralisant l'envoi de messages, vous pouvez implémenter certaines fonctionnalités intéressantes, telles que les messages différés, la possibilité d'envoyer des messages à un groupe d'entités et des informations de débogage visuelles (telles que les enveloppes volantes utilisées dans ce didacticiel)..

Il y a de la place pour la créativité dans le MessageQueue classe, à vous et aux exigences de votre jeu.

Conclusion

La gestion des interactions entre les entités de jeu à l'aide d'un système de file d'attente de messages est un moyen de garder votre code organisé et prêt pour l'avenir. De nouvelles interactions peuvent être facilement et rapidement ajoutées, même vos idées les plus complexes, à condition qu'elles soient encapsulées sous forme de messages..

Comme indiqué dans le didacticiel, vous pouvez ignorer l'utilisation d'une file d'attente de messages centrale et simplement envoyer des messages directement aux entités. Vous pouvez également centraliser la communication à l’aide d’un dispatch (le MessageQueue classe dans notre cas) pour faire place à de nouvelles fonctionnalités dans le futur, telles que les messages différés.

J'espère que vous trouverez cette approche utile et que vous l'ajouterez à votre ceinture d'utilitaires pour développeurs de jeux. La méthode peut sembler exagérée pour de petits projets, mais elle vous épargnera certainement des maux de tête à long terme pour des jeux plus importants..