Solitamente un gioco è composto da diverse entità che interagiscono tra loro. Queste interazioni tendono ad essere molto dinamiche e profondamente connesse al gameplay. Questo tutorial copre il concetto e l'implementazione di un sistema di code di messaggi che può unificare le interazioni delle entità, rendendo il codice gestibile e di facile manutenzione man mano che cresce nella complessità.
Una bomba può interagire con un personaggio esplodendo e causando danni, un kit medico può guarire un'entità, una chiave può aprire una porta e così via. Le interazioni in un gioco sono infinite, ma come possiamo mantenere il codice di gioco gestibile pur essendo ancora in grado di gestire tutte quelle interazioni? Come possiamo garantire che il codice possa cambiare e continuare a funzionare quando sorgono interazioni nuove e inaspettate?
Le interazioni in un gioco tendono a crescere in complessità molto rapidamente.Con l'aggiunta di interazioni (specialmente quelle inaspettate), il tuo codice apparirà sempre più ingombrante. Un'implementazione ingenua ti porterà rapidamente a fare domande come:
"Questa è l'entità A, quindi dovrei chiamare metodo danno()
su di esso, giusto? O è damageByItem ()
? Forse questo damageByWeapon ()
il metodo è quello giusto? "
Immagina quel caos disordinato che si diffonde a tutte le tue entità di gioco, perché interagiscono tra loro in modi diversi e peculiari. Fortunatamente, c'è un modo migliore, più semplice e più gestibile di farlo.
Inserisci il coda dei messaggi. L'idea alla base di questo concetto è di implementare tutte le interazioni di gioco come un sistema di comunicazione (che è ancora in uso oggi): lo scambio di messaggi. Le persone hanno comunicato tramite messaggi (lettere) per secoli perché è un sistema efficace e semplice.
Nei nostri servizi postali reali, il contenuto di ciascun messaggio può essere diverso, ma il modo in cui sono fisicamente inviati e ricevuti rimane lo stesso. Un mittente mette le informazioni in una busta e le indirizza a una destinazione. La destinazione può rispondere (o meno) seguendo lo stesso meccanismo, semplicemente cambiando i campi "da / a" sulla busta.
Interazioni effettuate utilizzando un sistema di messaggi in coda.Applicando questa idea al tuo gioco, tutte le interazioni tra entità possono essere viste come messaggi. Se un'entità di gioco vuole interagire con un altro (o un gruppo di essi), tutto ciò che deve fare è inviare un messaggio. La destinazione tratterà o reagirà al messaggio in base al suo contenuto e a chi è il mittente.
In questo approccio, la comunicazione tra le entità del gioco diventa unificata. Tutte le entità possono inviare e ricevere messaggi. Indipendentemente dalla complessità o peculiarità dell'interazione o del messaggio, il canale di comunicazione rimane sempre lo stesso.
Nelle prossime sezioni descriverò come effettivamente implementare questo approccio alla coda di messaggi nel tuo gioco.
Iniziamo disegnando la busta, che è l'elemento più basilare nel sistema di code messaggi.
Una busta può essere descritta come nella figura seguente:
Struttura di un messaggio.I primi due campi (mittente
e destinazione
) sono riferimenti all'entità che ha creato e all'entità che riceverà questo messaggio, rispettivamente. Usando questi campi, sia il mittente che il ricevente possono dire dove sta andando il messaggio e da dove proviene.
Gli altri due campi (genere
e dati
) lavorare insieme per garantire che il messaggio sia gestito correttamente. Il genere
campo descrive di cosa tratta questo messaggio; per esempio, se il tipo è "danno"
, il destinatario gestirà questo messaggio come un ordine per diminuire i suoi punti vita; se il tipo è "perseguire"
, il ricevitore lo prenderà come un'istruzione per perseguire qualcosa - e così via.
Il dati
il campo è direttamente collegato al genere
campo. Utilizzando gli esempi precedenti, se il tipo di messaggio è "danno"
, poi il dati
il campo conterrà un numero, per esempio, 10
-che descrive la quantità di danno che il ricevitore dovrebbe applicare ai suoi punti di salute. Se il tipo di messaggio è "perseguire"
, dati
conterrà un oggetto che descrive il bersaglio che deve essere perseguito.
Il dati
campo può contenere qualsiasi informazione, che rende la busta un mezzo versatile di comunicazione. Tutto ciò che può essere inserito in quel campo: interi, float, stringhe e persino altri oggetti. La regola generale è che il ricevitore deve sapere cosa c'è nel dati
campo basato su ciò che è nel genere
campo.
Tutta quella teoria può essere tradotta in una classe molto semplice chiamata Messaggio
. Contiene quattro proprietà, una per ogni campo:
Message = function (to, from, type, data) // Proprietà this.to = a; // un riferimento all'entità che riceverà questo messaggio this.from = from; // un riferimento all'entità che ha inviato questo messaggio this.type = type; // il tipo di questo messaggio this.data = data; // il contenuto / i dati di questo messaggio;
Come esempio di questo in uso, se un'entità UN
vuole inviare un "danno"
messaggio all'entità B
, tutto ciò che deve fare è istanziare un oggetto della classe Messaggio
, imposta la proprietà a
a B
, imposta la proprietà a partire dal
a se stesso (entità UN
), impostato genere
a "danno"
e, infine, impostare dati
ad un certo numero (10
, per esempio):
// Istanzia le due entità var entityA = new Entity (); var entityB = new Entity (); // Crea un messaggio all'entitàB, da entityA, // con tipo "damage" e data / value 10. var msg = new Message (); msg.to = entityB; msg.from = entityA; msg.type = "damage"; msg.data = 10; // Puoi anche istanziare direttamente il messaggio // passando le informazioni richieste, come questo: var msg = new Message (entityB, entityA, "damage", 10);
Ora che abbiamo un modo per creare messaggi, è tempo di pensare alla classe che li memorizzerà e li consegnerà.
La classe responsabile della memorizzazione e consegna dei messaggi sarà chiamata MessageQueue
. Funzionerà come un ufficio postale: tutti i messaggi vengono consegnati a questa classe, che assicura che verranno spediti a destinazione.
Per ora, il MessageQueue
la classe avrà una struttura molto semplice:
/ ** * Questa classe è responsabile della ricezione dei messaggi e * della spedizione alla destinazione. * / MessageQueue = function () this.messages = []; // elenco dei messaggi da inviare; // Aggiungi un nuovo messaggio alla coda. Il messaggio deve essere una // istanza del messaggio di classe. MessageQueue.prototype.add = function (message) this.messages.push (message); ;
La proprietà messaggi
è un array. Memorizzerà tutti i messaggi che stanno per essere consegnati dal MessageQueue
. Il metodo Inserisci()
riceve un oggetto della classe Messaggio
come parametro e aggiunge quell'oggetto all'elenco di messaggi.
Ecco come il nostro precedente esempio di entità UN
entità di messaggistica B
circa il danno funzionerebbe usando il MessageQueue
classe:
// Istanzia le due entità e la coda dei messaggi var entityA = new Entity (); var entityB = new Entity (); var messageQueue = new MessageQueue (); // Crea un messaggio all'entitàB, da entityA, // con tipo "damage" e data / value 10. var msg = new Message (entityB, entityA, "damage", 10); // Aggiungi il messaggio al messaggio codeQueue.add (msg);
Ora abbiamo un modo per creare e archiviare i messaggi in una coda. È tempo di farli raggiungere la loro destinazione.
Al fine di rendere il MessageQueue
la classe effettivamente invia i messaggi postati, prima dobbiamo definire Come le entità gestiranno e riceveranno messaggi. Il modo più semplice è aggiungere un metodo chiamato onMessage ()
a ogni entità in grado di ricevere messaggi:
/ ** * Questa classe descrive un'entità generica. * / Entity = function () // Inizializza qualsiasi cosa qui, ad es. Roba Phaser; // Questo metodo è invocato dal MessageQueue // quando c'è un messaggio a questa entità. Entity.prototype.onMessage = function (message) // Gestisci il nuovo messaggio qui;
Il MessageQueue
la classe invocherà il onMessage ()
metodo di ciascuna entità che deve ricevere un messaggio. Il parametro passato a quel metodo è il messaggio che viene consegnato dal sistema di coda (e ricevuto dalla destinazione).
Il MessageQueue
class invierà i messaggi nella sua coda tutto in una volta, nel spedizione()
metodo:
/ ** * Questa classe è responsabile della ricezione dei messaggi e * della spedizione alla destinazione. * / MessageQueue = function () this.messages = []; // elenco dei messaggi da inviare; MessageQueue.prototype.add = function (message) this.messages.push (message); ; // Invia tutti i messaggi nella coda alla loro destinazione. MessageQueue.prototype.dispatch = function () var i, entity, msg; // Iterave sull'elenco di messaggi per (i = 0; this.messages.length; i ++) // Ottieni il messaggio dell'attuale iterazione msg = this.messages [i]; // è valido? if (msg) // Recupera l'entità che dovrebbe ricevere questo messaggio // (quello nel campo 'a') entity = msg.to; // Se quell'entità esiste, consegnare il messaggio. if (entity) entity.onMessage (msg); // Elimina il messaggio dalla coda this.messages.splice (i, 1); io--; ;
Questo metodo esegue iterazioni su tutti i messaggi in coda e, per ciascun messaggio, su a
campo viene utilizzato per recuperare un riferimento al ricevitore. Il onMessage ()
viene quindi richiamato il metodo del destinatario, con il messaggio corrente come parametro, e il messaggio consegnato viene quindi rimosso dal MessageQueue
elenco. Questo processo viene ripetuto finché non vengono inviati tutti i messaggi.
È tempo di vedere tutti i dettagli di questa implementazione lavorare insieme. Usiamo il nostro sistema di messaggi in una demo molto semplice composta da poche entità in movimento che interagiscono tra loro. Per semplicità, lavoreremo con tre entità: Guaritore
, Corridore
e Cacciatore
.
Il Corridore
ha una barra della salute e si muove in giro in modo casuale. Il Guaritore
guarirà qualsiasi Corridore
che passa vicino; d'altra parte, il Cacciatore
infliggerà danni a qualsiasi vicino Corridore
. Tutte le interazioni verranno gestite utilizzando il sistema di code messaggi.
Iniziamo creando il visualizzarloState
che contiene un elenco di entità (guaritori, corridori e cacciatori) e un'istanza del MessageQueue
classe:
var PlayState = function () var entità; // lista di entità nel gioco var messageQueue; // la coda dei messaggi (dispatcher) this.create = function () // Inizializza la coda dei messaggi messageQueue = new MessageQueue (); // Crea un gruppo di entità. entities = this.game.add.group (); ; this.update = function () // Crea tutti i messaggi nella coda dei messaggi // raggiunge la loro destinazione. messageQueue.dispatch (); ; ;
Nel ciclo di gioco, rappresentato dal aggiornare()
metodo, la coda dei messaggi spedizione()
viene invocato il metodo, quindi tutti i messaggi vengono consegnati alla fine di ogni frame di gioco.
Il Corridore
la classe ha la seguente struttura:
/ ** * Questa classe descrive un'entità che * si aggira solo intorno. * / Runner = function () // initialize Phaser stuff here ...; // Invocato dal gioco su ogni frame Runner.prototype.update = function () // Fai spostare le cose qui ... // Questo metodo viene invocato dalla coda dei messaggi // per far sì che il runner gestisca i messaggi in arrivo. Runner.prototype.onMessage = function (message) var amount; // Controlla il tipo di messaggio in modo che sia possibile // decidere se questo messaggio debba essere ignorato o meno. if (message.type == "damage") // Il messaggio riguarda il danno. // Dobbiamo ridurre i nostri punti di salute. La quantità di // questa diminuzione è stata informata dal mittente del messaggio // nel campo "dati". amount = message.data; this.addHealth (-quantità); else if (message.type == "heal") // Il messaggio riguarda la guarigione. // Dobbiamo aumentare i nostri punti salute. Anche in questo caso la quantità di punti salute da aumentare è stata informata dal mittente del messaggio // nel campo "dati". amount = message.data; this.addHealth (quantità); else // Qui trattiamo i messaggi che non siamo in grado di elaborare. // Probabilmente li ignoro :);
La parte più importante è la onMessage ()
metodo, invocato dalla coda dei messaggi ogni volta che c'è un nuovo messaggio per questa istanza. Come precedentemente spiegato, il campo genere
nel messaggio viene utilizzato per decidere di cosa tratta questa comunicazione.
In base al tipo di messaggio, viene eseguita l'azione corretta: se il tipo di messaggio è "danno"
, i punti di salute sono diminuiti; se il tipo di messaggio è "guarire"
, i punti di salute sono aumentati. Il numero di punti vita da aumentare o diminuire è definito dal mittente nel file dati
campo del messaggio.
Nel visualizzarloState
, aggiungiamo alcuni corridori all'elenco delle entità:
var PlayState = function () // (...) this.create = function () // (...) // Aggiungi i corridori per (i = 0; i < 4; i++) entities.add(new Runner(this.game, this.game.world.width * Math.random(), this.game.world.height * Math.random())); ; // (… ) ;
Il risultato sono quattro corridori che si muovono a caso:
Il Cacciatore
la classe ha la seguente struttura:
/ ** * Questa classe descrive un'entità che * si aggira solo per ferire i corridori che passano. * / Hunter = function (game, x, y) // initialize Phaser stuff here; // Controlla se l'entità è valida, è un corridore e si trova all'interno dell'intervallo di attacco. Hunter.prototype.canEntityBeAttacked = function (entity) return entity && entity! = This && (entity instanceof Runner) &&! (Entità instanceof 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(); ;
Anche i cacciatori si muoveranno, ma causeranno danni a tutti i corridori vicini. Questo comportamento è implementato nel aggiornare()
metodo, in cui tutte le entità del gioco sono ispezionate e i corridori sono informati del danno.
Il messaggio di danno è creato come segue:
msg = new Message (entity, this, "damage", 2);
Il messaggio contiene le informazioni sulla destinazione (entità
, in questo caso, che è l'entità analizzata nell'iterazione corrente), il mittente (Questo
, che rappresenta il cacciatore che sta eseguendo l'attacco), il tipo di messaggio ("danno"
) e la quantità di danno (2
, in questo caso, assegnato a dati
campo del messaggio).
Il messaggio viene quindi inviato alla destinazione tramite il comando this.getMessageQueue (). aggiungi (msg)
, che aggiunge il messaggio appena creato alla coda dei messaggi.
Infine, aggiungiamo il Cacciatore
alla lista di entità nel visualizzarloState
:
var PlayState = function () // (...) this.create = function () // (...) // Aggiungi hunter in posizione (20, 30) entities.add (new Hunter (this.game, 20, 30 )); ; // (...);
Il risultato sono alcuni corridori che si muovono, ricevendo messaggi dal cacciatore mentre si avvicinano l'uno all'altro:
Ho aggiunto le buste volanti come aiuto visivo per mostrare cosa sta succedendo.
Il Guaritore
la classe ha la seguente struttura:
/ ** * Questa classe descrive un'entità che * è in grado di guarire qualsiasi corridore che passa nelle vicinanze. * / Healer = function (game, x, y) // Initializer Phaser stuff here; Healer.prototype.update = function () var entità, i, dimensione, entità, msg; // L'elenco delle entità nelle entità del gioco = this.getPlayState (). GetEntities (); per (i = 0, size = entities.length; 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(); ;
Il codice e la struttura sono molto simili al Cacciatore
classe, tranne per alcune differenze. Analogamente all'implementazione del cacciatore, quella del guaritore aggiornare()
il metodo esegue iterazioni sull'elenco di entità nel gioco, inviando messaggi a qualsiasi entità all'interno della sua portata di guarigione:
msg = new Message (entity, this, "heal", 2);
Il messaggio ha anche una destinazione (entità
), un mittente (Questo
, che è il guaritore che esegue l'azione), un tipo di messaggio ("guarire"
) e il numero di punti di cura (2
, assegnato nel dati
campo del messaggio).
Aggiungiamo il Guaritore
alla lista di entità nel visualizzarloState
allo stesso modo che abbiamo fatto con Cacciatore
e il risultato è una scena con corridori, un cacciatore e un guaritore:
E questo è tutto! Abbiamo tre diverse entità che interagiscono l'una con l'altra scambiando messaggi.
Questo sistema di messaggi in coda è un modo versatile per gestire le interazioni in un gioco. Le interazioni vengono eseguite tramite un canale di comunicazione unificato e dotato di un'unica interfaccia facile da utilizzare e implementare.
Man mano che il tuo gioco cresce in complessità, potrebbero essere necessarie nuove interazioni. Alcuni di essi potrebbero essere completamente inaspettati, quindi è necessario adattare il codice per gestirli. Se si utilizza un sistema di code messaggi, si tratta di aggiungere un nuovo messaggio da qualche parte e gestirlo in un altro.
Ad esempio, immagina di voler creare il Cacciatore
interagire con il Guaritore
; devi solo fare il Cacciatore
invia un messaggio con la nuova interazione, ad esempio, "fuggire"
-e assicurarsi che il Guaritore
in grado di gestirlo nel onMessage
metodo:
// Nella classe Hunter: Hunter.prototype.someMethod = function () // Ottieni un riferimento a un healer nelle vicinanze var healer = this.getNearbyHealer (); // Crea un messaggio sulla fuga da un posto var place = x: 30, y: 40; var msg = new Message (entity, this, "flee", luogo); // manda via il messaggio! this.getMessageQueue () aggiungere (msg).; ; // Nella classe Healer: Healer.prototype.onMessage = function (message) if (message.type == "flee") // Ottieni il posto dove fuggire dal campo dati nel messaggio var place = message.data ; // Usa le informazioni sul luogo in fuga (place.x, place.y); ;
Sebbene lo scambio di messaggi tra entità possa essere utile, potresti pensare perché MessageQueue
è necessario dopotutto. Non puoi semplicemente invocare il ricevitore onMessage ()
metodo te stesso invece di fare affidamento sul MessageQueue
, come nel codice qui sotto?
Hunter.prototype.someMethod = function () // Ottieni un riferimento a un healer nelle vicinanze var healer = this.getNearbyHealer (); // Crea un messaggio sulla fuga da un posto var place = x: 30, y: 40; var msg = new Message (entity, this, "flee", luogo); // Ignora MessageQueue e invia direttamente // il messaggio al guaritore. healer.onMessage (msg); ;
Si potrebbe sicuramente implementare un sistema di messaggi del genere, ma l'uso di a MessageQueue
ha alcuni vantaggi.
Ad esempio, centralizzando il dispacciamento dei messaggi, è possibile implementare alcune funzioni interessanti come i messaggi in ritardo, la possibilità di inviare messaggi a un gruppo di entità e informazioni di debug visivo (come le buste volanti utilizzate in questo tutorial).
C'è spazio per la creatività in MessageQueue
classe, dipende da te e dai requisiti del tuo gioco.
Gestire le interazioni tra le entità di gioco utilizzando un sistema di code di messaggi è un modo per mantenere il codice organizzato e pronto per il futuro. Nuove interazioni possono essere aggiunte facilmente e rapidamente, anche le tue idee più complesse, purché siano incapsulate come messaggi.
Come discusso nel tutorial, è possibile ignorare l'uso di una coda di messaggi centrale e inviare semplicemente messaggi direttamente alle entità. È inoltre possibile centralizzare la comunicazione utilizzando una spedizione (il MessageQueue
classe nel nostro caso) per fare spazio a nuove funzionalità in futuro, come i messaggi in ritardo.
Spero che tu possa trovare utile questo approccio e aggiungerlo al tuo programma di utilità per sviluppatori di giochi. Il metodo potrebbe sembrare eccessivo per i piccoli progetti, ma sicuramente ti farà risparmiare alcuni grattacapi a lungo termine per i giochi più grandi.