Un joc este de obicei compus din mai multe entități diferite care interacționează între ele. Aceste interacțiuni tinde să fie foarte dinamice și profund legate de modul de joc. Acest tutorial acoperă conceptul și implementarea unui sistem de coadă de mesaje care poate unifica interacțiunile entităților, făcând codul dvs. ușor de întreținut, pe măsură ce crește complexitatea.
O bomba poate interacționa cu un personaj prin explodarea și provocarea pagubelor, un kit de medic poate vindeca o entitate, o cheie poate deschide o ușă și așa mai departe. Interacțiunile într-un joc sunt nesfârșite, dar cum putem păstra codul jocului ușor de gestionat, în timp ce încă putem gestiona toate aceste interacțiuni? Cum asigurăm că codul se poate modifica și poate continua să funcționeze atunci când apar noi și neașteptate interacțiuni?
Interacțiunile într-un joc tind să crească în complexitate foarte repede.Pe măsură ce se adaugă interacțiuni (mai ales cele neașteptate), codul dvs. va arăta din ce în ce mai aglomerat. O implementare naivă va duce rapid la întrebări precum:
"Aceasta este entitatea A, așa că ar trebui să apel metodă deteriora()
pe asta, nu? Sau este damageByItem ()
? Poate asta damageByWeapon ()
metoda este cea potrivita? "
Imaginați-vă că aglomerația haosului se răspândește la toate entitățile de joc, deoarece toate interacționează între ele în moduri diferite și ciudate. Din fericire, există o modalitate mai bună, mai simplă și mai ușor de gestionat.
Introduceți coadă de mesaje. Ideea de bază a acestui concept este de a implementa toate interacțiunile jocului ca un sistem de comunicare (care este încă în uz astăzi): schimb de mesaje. Oamenii au comunicat prin mesaje (scrisori) timp de secole, deoarece este un sistem eficient și simplu.
În serviciile postale din lumea reală, conținutul fiecărui mesaj poate fi diferit, dar modul în care acestea sunt trimise fizic și primite rămâne același. Un expeditor pune informațiile într-un plic și le adresează unei destinații. Destinația poate răspunde (sau nu) urmând același mecanism, schimbând doar câmpurile "din / în" de pe plic.
Interacțiunile efectuate utilizând un sistem de coadă de mesaje.Aplicând această idee la jocul dvs., toate interacțiunile dintre entități pot fi văzute ca mesaje. Dacă o entitate de joc dorește să interacționeze cu un alt grup (sau un grup de ei), tot ce trebuie să faceți este să trimiteți un mesaj. Destinația se va ocupa sau va reacționa la mesajul în funcție de conținutul său și de cine este expeditorul.
În această abordare, comunicarea între entitățile de joc devine unificată. Toate entitățile pot trimite și primi mesaje. Indiferent cât de complexă sau particulară este interacțiunea sau mesajul, canalul de comunicare rămâne mereu același.
Pe parcursul următoarelor secțiuni, voi descrie modul în care puteți implementa această abordare a coadă de mesaje în jocul dvs..
Să începem prin proiectarea plicului, care este cel mai elementar element al sistemului de coadă de mesaje.
Un plic poate fi descris ca în figura de mai jos:
Structura unui mesaj.Primele două câmpuri (expeditor
și destinaţie
) sunt referiri la entitatea care a creat și entitatea care va primi respectivul mesaj. Folosind aceste câmpuri, atât expeditorul, cât și receptorul pot să spună unde va ajunge mesajul și de unde a provenit.
Celelalte două domenii (tip
și date
) colaborează pentru a asigura că mesajul este gestionat corespunzător. tip
câmpul descrie despre ce este vorba acest mesaj; de exemplu, dacă tipul este "deteriora"
, receptorul se va ocupa de acest mesaj ca o comandă de scădere a punctelor sale de sănătate; dacă tipul este „Urmăresc“
, receptorul o va lua ca o instrucțiune de a urmări ceva - și așa mai departe.
date
câmpul este conectat direct la tip
camp. Folosind exemplele anterioare, dacă este tipul de mesaj "deteriora"
, apoi date
câmpul va conține un număr - de exemplu, 10
-care descrie cantitatea de daune pe care receptorul trebuie să le aplice la punctele sale de sănătate. Dacă este tipul de mesaj „Urmăresc“
, date
va conține un obiect care descrie ținta care trebuie urmărită.
date
câmpul poate conține orice informație, ceea ce face ca plicul să fie un mijloc versatil de comunicare. Orice poate fi plasat în acel câmp: întregi, flotoare, șiruri și chiar alte obiecte. Regula principală este că receptorul trebuie să știe ce este în date
domeniu pe baza a ceea ce este în tip
camp.
Toată această teorie poate fi tradusă într-o clasă foarte simplă numită Mesaj
. Acesta conține patru proprietăți, câte unul pentru fiecare câmp:
Message = funcție (de la, tip, date) // Proprietăți this.to = to; // o referință la entitatea care va primi acest mesaj this.from = from; // o referință la entitatea care a trimis acest mesaj this.type = type; // tipul acestui mesaj this.data = date; // conținutul / datele din acest mesaj;
Ca exemplu al acestei utilizări, dacă o entitate A
vrea să trimită un mesaj "deteriora"
mesaj către entitate B
, tot ce trebuie să faceți este să instanțiați un obiect al clasei Mesaj
, setați proprietatea la
la B
, setați proprietatea din
la sine (entitatea A
), a stabilit tip
la "deteriora"
și, în sfârșit, stabilit date
la un anumit număr (10
, de exemplu):
// Instanțiați cele două entități var entityA = Entită nouă (); var entityB = entitate nouă (); // Crearea unui mesaj către entitateaB, de la entităA, // cu tipul "damage" și date / valoare 10. var msg = new Message (); msg.to = entitateB; msg.from = entityA; msg.type = "deteriorare"; msg.data = 10; // Puteți, de asemenea, să instanțiați mesajul direct // prin transmiterea informațiilor necesare, cum ar fi: var msg = mesaj nou (entityB, entityA, "damage", 10);
Acum, că avem o modalitate de a crea mesaje, este timpul să ne gândim la clasa care le va stoca și le va livra.
Se va solicita clasa responsabilă pentru stocarea și difuzarea mesajelor MessageQueue
. Acesta va funcționa ca un oficiu poștal: toate mesajele sunt înmânate acestei clase, ceea ce asigură că acestea vor fi expediate la destinație.
Pentru moment, MessageQueue
clasa va avea o structură foarte simplă:
/ ** * Această clasă este responsabilă pentru primirea mesajelor și pentru expedierea acestora la destinație. * / MessageQueue = funcția () this.messages = []; // lista mesajelor care vor fi expediate; // Adăugați un mesaj nou în coadă. Mesajul trebuie să fie un // instanță a mesajului din clasă. MessageQueue.prototype.add = funcție (mesaj) this.messages.push (mesaj); ;
Proprietatea mesaje
este o matrice. Acesta va stoca toate mesajele care urmează să fie livrate de către MessageQueue
. Metoda adăuga()
primește un obiect al clasei Mesaj
ca parametru, și adaugă acel obiect la lista de mesaje.
Iată cum exemplul nostru precedent de entitate A
entitate de mesagerie B
despre daune ar funcționa folosind MessageQueue
clasă:
// Instanțiați cele două entități și coada de mesaje var entityA = Entită nouă (); var entityB = entitate nouă (); var mesajQueue = Mesaj nou (); // Crearea unui mesaj către entitateaB, de la entitateaA, // cu tipul "damage" și date / valoare 10. var msg = nou Mesaj (entitateB, entitateA, "daune", 10); // Adăugați mesajul la coada messageQueue.add (msg);
Acum avem o modalitate de a crea și de a stoca mesaje într-o coadă. E timpul să-i facem să ajungă la destinație.
Pentru a face MessageQueue
clasa expediaza mesajele postate, mai intai trebuie sa definim Cum entitățile vor gestiona și primi mesaje. Cea mai ușoară cale este prin adăugarea unei metode numite onMessage ()
fiecărei entități capabile să primească mesaje:
/ ** * Această clasă descrie o entitate generică. * / Entity = function () // Initializeaza ceva aici, de ex. Lucruri Phaser; // Această metodă este invocată de MessageQueue // atunci când există un mesaj către această entitate. Entity.prototype.onMessage = funcție (mesaj) // Efectuați un mesaj nou aici;
MessageQueue
clasa va invoca onMessage ()
metodă a fiecărei entități care trebuie să primească un mesaj. Parametrul trecut la această metodă este mesajul livrat de sistemul de coadă (și fiind recepționat de destinație).
MessageQueue
clasa va expedia mesajele în coada de așteptare în același timp, în expediere()
metodă:
/ ** * Această clasă este responsabilă pentru primirea mesajelor și pentru expedierea acestora la destinație. * / MessageQueue = funcția () this.messages = []; // lista mesajelor care vor fi expediate; MessageQueue.prototype.add = funcție (mesaj) this.messages.push (mesaj); ; // Trimiteți toate mesajele din coada de așteptare la destinația lor. MessageQueue.prototype.dispatch = funcția () var i, entitate, msg; // Iterave deasupra listei de mesaje pentru (i = 0; this.messages.length; i ++) // Obțineți mesajul curent al iterației msg = this.messages [i]; // Este valabil? if (msg) // Returnați entitatea care ar trebui să primească acest mesaj // (cel din câmpul 'to') entity = msg.to; // Dacă această entitate există, trimiteți mesajul. dacă (entitate) entity.onMessage (msg); // Ștergeți mesajul din coada this.messages.splice (i, 1); Eu--; ;
Această metodă trece peste toate mesajele din coadă și, pentru fiecare mesaj, la
câmpul este utilizat pentru a prelua o referință la receptor. onMessage ()
metoda receptorului este invocată, mesajul curent fiind parametru, iar mesajul livrat este apoi eliminat din MessageQueue
listă. Acest proces se repetă până când toate mesajele sunt expediate.
Este timpul să vedem că toate detaliile acestei implementări funcționează împreună. Să folosim sistemul nostru de coadă de mesaje într-un demo foarte simplu compus din câteva entități în mișcare care interacționează între ele. Din motive de simplitate, vom lucra cu trei entități: Vindecător
, alergător
și Vânător
.
alergător
are un bar de sănătate și se mută în jurul valorii de aleatoriu. Vindecător
va vindeca orice alergător
care trece aproape; pe de altă parte, Vânător
va provoca daune pe orice apropiere alergător
. Toate interacțiunile vor fi gestionate utilizând sistemul de coadă de mesaje.
Să începem prin crearea PlayState
care conține o listă de entități (vindecători, alergători și vânători) și o instanță a MessageQueue
clasă:
var PlayState = funcția () var entități; // lista entităților din jocul var messageQueue; // coada de mesaje (dispecer) this.create = function () // Initializeaza mesajul de coada mesajuluiQueue = new MessageQueue (); // Creați un grup de entități. entități = this.game.add.group (); ; this.update = function () // Efectuați toate mesajele din coada mesajului // să ajungă la destinație. messageQueue.dispatch (); ; ;
În bucla de joc, reprezentată de Actualizați()
metoda, coada de mesaje expediere()
metoda este invocată, astfel încât toate mesajele sunt livrate la sfârșitul fiecărui cadru de joc.
alergător
clasa are următoarea structură:
/ ** * Această clasă descrie o entitate care doar * se rătăcește în jur. * / Runner = function () // initializeaza chestiile Phaser aici ...; // Invocate de joc pe fiecare cadru Runner.prototype.update = function () // Make things move here ... // Această metodă este invocată de coada mesajului // pentru a face ca alergătorul să se ocupe cu mesajele primite. Runner.prototype.onMessage = funcție (mesaj) suma var; // Verificați tipul mesajului astfel încât să puteți // decide dacă acest mesaj trebuie ignorat sau nu. dacă (message.type == "damage") // Mesajul este despre daune. // Trebuie să scădem punctele noastre de sănătate. Valoarea acestei scăderi a fost informată de expeditorul mesajului // în câmpul "date". suma = message.data; this.addHealth (-AMOUNT); altfel dacă (message.type == "vindecă") // Mesajul este despre vindecare. // Trebuie să ne creștem punctele de sănătate. Din nou, valoarea // punctelor de sănătate care au crescut este informată de expeditorul mesajului // în câmpul "date". suma = message.data; this.addHealth (suma); altceva // Aici avem de-a face cu mesaje pe care nu le putem procesa. // Probabil ignorați-le doar :);
Cea mai importantă parte este onMessage ()
, invocată de coada de mesaje de fiecare dată când există un mesaj nou pentru această instanță. După cum am explicat anterior, câmpul tip
în mesaj este folosit pentru a decide ce este vorba despre această comunicare.
Pe baza tipului mesajului, se efectuează acțiunea corectă: dacă este tipul de mesaj "deteriora"
, punctele de sănătate sunt scăzute; dacă este tipul de mesaj "vindeca"
, punctele de sanatate sunt crescute. Numărul de puncte de sănătate care trebuie să crească sau să scadă este definit de expeditor în date
domeniul mesajului.
În PlayState
, adăugăm câțiva alergători la lista entităților:
var PlayState = funcția () // (...) this.create = funcția () // (...) // Adăugarea alergătorilor pentru (i = 0; i < 4; i++) entities.add(new Runner(this.game, this.game.world.width * Math.random(), this.game.world.height * Math.random())); ; // (… ) ;
Rezultatul este că patru alergători se deplasează în mod aleatoriu:
Vânător
clasa are următoarea structură:
/ ** * Această clasă descrie o entitate care doar * se rătăcește în jurul valorii de rănit pe alergătorii care trec. * / Hunter = funcția (joc, x, y) // inițializați chestiile Phaser aici; // Verificați dacă entitatea este validă, este un alergător și se află în intervalul de atac. Hunter.prototype.canEntityBeAttacked = funcția (entitate) entitate retur & & entitate! = Această && (exemplul entității Runner) &&! (Entitate exemplu 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(); ;
Vânătorii se vor muta, de asemenea, dar vor provoca daune tuturor alergătorilor apropiați. Acest comportament este implementat în Actualizați()
, în care toate entitățile jocului sunt inspectate, iar alergătorii sunt informați despre daune.
Mesajul privind daunele este creat după cum urmează:
msg = mesaj nou (entitate, aceasta, "daune", 2);
Mesajul conține informațiile despre destinație (entitate
, în acest caz, care este entitatea analizată în actuala iterație), expeditorul (acest
, care reprezintă vânătorul care efectuează atacul), tipul mesajului ("deteriora"
) și valoarea prejudiciului (2
, în acest caz, atribuit date
câmpul mesajului).
Mesajul este apoi trimis către destinație prin intermediul comenzii this.getMessageQueue (). add (msg)
, care adaugă mesajul nou creat în coada de mesaje.
În cele din urmă, adăugăm Vânător
la lista entităților din PlayState
:
var PlayState = funcția () // (...) this.create = funcția () // (...) // Adăugați vânător la poziția (20, 30) entities.add (Hunter nou (this.game, 20, 30 )); ; // (...);
Rezultatul este că unii alergători se deplasează, primind mesaje de la vânător în timp ce se apropie unul de celălalt:
Am adăugat plicurile zboară ca un ajutor vizual pentru a vă arăta ce se întâmplă.
Vindecător
clasa are următoarea structură:
/ ** * Această clasă descrie o entitate care * este capabilă să vindece orice alergător care trece în apropiere. * / Healer = funcție (joc, x, y) // Initializer Phaser stuff here; Healer.prototype.update = funcția () var entități, i, mărime, entitate, msg; // lista entităților din entitățile de joc = this.getPlayState () getEntities (); pentru (i = 0, mărime = 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(); ;
Codul și structura sunt foarte asemănătoare cu Vânător
clasa, cu excepția câtorva diferențe. În mod similar cu implementarea vânătorului, vindecătorul Actualizați()
metoda se iterează pe lista entităților din joc, comunicând orice entitate în limitele sale de vindecare:
msg = mesaj nou (entitate, aceasta, "vindecă", 2);
Mesajul are, de asemenea, o destinație (entitate
), un expeditor (acest
, care este vindecătorul care efectuează acțiunea), un tip de mesaj ("vindeca"
) și numărul de puncte de vindecare (2
, atribuit în date
câmpul mesajului).
Adăugăm Vindecător
la lista entităților din PlayState
la fel cum am făcut și cu Vânător
iar rezultatul este o scenă cu alergători, un vânător și un vindecător:
Si asta e! Avem trei entități diferite care interacționează între ele prin schimbul de mesaje.
Acest sistem de coadă de mesaje este un mod versatil de a gestiona interacțiunile într-un joc. Interacțiunile sunt realizate printr-un canal de comunicații care este unificat și are o singură interfață ușor de folosit și implementată.
Pe măsură ce jocul dvs. crește în complexitate, ar putea fi necesare noi interacțiuni. Unele dintre ele ar putea fi complet neașteptate, așa că trebuie să vă adaptați codul pentru a le rezolva. Dacă utilizați un sistem de coadă de mesaje, este vorba de adăugarea unui mesaj nou undeva și de manipularea lui în altul.
De exemplu, imaginați-vă că doriți să faceți Vânător
interacționează cu Vindecător
; trebuie doar să faci Vânător
trimiteți un mesaj cu noua interacțiune - de exemplu, „Fugi“
-și să se asigure că Vindecător
se poate descurca în onMessage
metodă:
// În clasa Hunter: Hunter.prototype.someMethod = funcție () // Obțineți o referință la un vindecator din apropiere var healer = this.getNearbyHealer (); // Creați un mesaj despre fuga de la o locație var place = x: 30, y: 40; var msg = mesaj nou (entitate, aceasta, "fugi", locul); / Trimite mesajul departe! this.getMessageQueue () se adaugă (msg).; ; // În clasa Healer: Healer.prototype.onMessage = funcția (mesajul) if (message.type == "flee") // Obțineți locul de fugit din câmpul de date din mesajul var place = message.data ; // Utilizați informațiile despre loc (Fly) (loc.x, place.y); ;
Deși schimbul de mesaje între entități poate fi util, s-ar putea să vă gândiți de ce MessageQueue
este nevoie, la urma urmei. Nu puteți invoca receptorul onMessage ()
te în loc să te bazezi pe MessageQueue
, ca în codul de mai jos?
Hunter.prototype.someMethod = functie () // Obțineți o referință la un vindecator din apropiere var healer = this.getNearbyHealer (); // Creați un mesaj despre fuga de la o locație var place = x: 30, y: 40; var msg = mesaj nou (entitate, aceasta, "fugi", locul); // Bypass MessageQueue și trimite direct // mesajul vindecătorului. healer.onMessage (msg); ;
S-ar putea să puneți în aplicare cu siguranță un sistem de mesaje ca acesta, dar utilizarea unui a MessageQueue
are câteva avantaje.
De exemplu, prin centralizarea expedierii mesajelor, puteți implementa câteva funcții interesante cum ar fi mesaje întârziate, abilitatea de a trimite un mesaj către un grup de entități și informații vizuale de depanare (cum ar fi plicurile zburate utilizate în acest tutorial).
Există loc pentru creativitate în MessageQueue
clasă, depinde de dvs. și cerințele jocului dvs..
Manipularea interacțiunilor dintre entitățile de joc utilizând un sistem de coadă de mesaje este o modalitate de a vă menține codul organizat și pregătit pentru viitor. Noile interacțiuni pot fi adăugate cu ușurință și rapid, chiar și cu cele mai complexe idei, atât timp cât sunt încapsulate ca mesaje.
După cum sa discutat în tutorial, puteți ignora utilizarea unei coadă de mesaje centrale și puteți trimite mesaje direct entităților. De asemenea, puteți centraliza comunicarea utilizând o expediție ( MessageQueue
clasa în cazul nostru) pentru a face loc unor noi caracteristici în viitor, cum ar fi mesajele întârziate.
Sper că veți găsi această abordare utilă și o veți adăuga la centura de utilități pentru dezvoltatori de jocuri. Metoda ar putea părea o depășire pentru proiecte mici, dar cu siguranță vă va salva câteva dureri de cap pe termen lung pentru jocuri mai mari.