Slik implementerer og bruker du en meldingskø i spillet ditt

Et spill er vanligvis laget av flere forskjellige enheter som samhandler med hverandre. Disse samspillene har en tendens til å være veldig dynamisk og dypt knyttet til gameplay. Denne opplæringen dekker konseptet og implementeringen av et meldingskøesystem som kan forene samspillet mellom enheter, gjør koden håndterbar og enkel å vedlikeholde ettersom den vokser i kompleksitet. 

Introduksjon

En bombe kan samhandle med et tegn ved å eksplodere og forårsake skade, et medisinsk sett kan helbrede en enhet, en nøkkel kan åpne en dør og så videre. Interaksjoner i et spill er uendelige, men hvordan kan vi holde spillkoden håndterlig mens du fortsatt klarer å håndtere alle disse interaksjonene? Hvordan sikrer vi at koden kan endres og fortsetter å fungere når nye og uventede samspill oppstår?

Interaksjoner i et spill har en tendens til å vokse i kompleksitet veldig raskt.

Da samhandlingene blir lagt til (spesielt de uventede), vil koden din se mer og mer rotete. En naiv implementering vil raskt føre til at du stiller spørsmål som:

"Dette er enhet A, så jeg bør ringe metode skader() på det, ikke sant? Eller er det damageByItem ()? Kanskje dette damageByWeapon () Metoden er den rette? "

Tenk deg at cluttering kaos sprer seg til alle spillets enheter, fordi de alle samhandler med hverandre på forskjellige og spesielle måter. Heldigvis er det en bedre, enklere og mer overkommelig måte å gjøre det på.

Meldingskø

Tast inn meldingskø. Den grunnleggende ideen bak dette konseptet er å implementere alle spillinteraksjoner som et kommunikasjonssystem (som fortsatt er i bruk i dag): meldingsutveksling. Folk har kommunisert via meldinger (brev) i århundrer fordi det er et effektivt og enkelt system.

I våre virkelige posttjenester kan innholdet i hver melding variere, men måten de blir fysisk sendt og mottatt, forblir det samme. En avsender legger opp informasjonen i en konvolutt og adresserer den til en destinasjon. Destinasjonen kan svare (eller ikke) ved å følge samme mekanisme, bare å endre "fra / til" -feltene på konvolutten. 

Interaksjoner gjort ved hjelp av et meldings køsystem.

Ved å bruke den ideen til spillet ditt, kan alle interaksjoner mellom enheter bli sett på som meldinger. Hvis en spill-enhet ønsker å samhandle med en annen (eller en gruppe av dem), er alt det å gjøre, å sende en melding. Destinasjonen vil håndtere eller reagere på meldingen basert på innholdet og av hvem avsenderen er.

I denne tilnærmingen blir kommunikasjon mellom spill enheter enhetlig. Alle enheter kan sende og motta meldinger. Uansett hvor kompleks eller merkelig samspillet eller meldingen er, forblir kommunikasjonskanalen alltid den samme.

I de neste avsnittene beskriver jeg hvordan du faktisk kan implementere denne meldingen kø-tilnærmingen i spillet ditt. 

Utforming av en konvolutt (melding)

La oss starte med å utforme konvolutten, som er det mest grunnleggende elementet i meldingen køsystemet. 

En konvolutt kan beskrives som i figuren nedenfor:

Struktur av en melding.

De to første feltene (avsender og mål) er referanser til enheten som opprettet, og enheten som vil motta denne meldingen, henholdsvis. Ved å bruke disse feltene, kan både avsenderen og mottakeren fortelle hvor meldingen skal og hvor den kommer fra.

De to andre feltene (type og data) jobber sammen for å sikre at meldingen håndteres riktig. De type feltet beskriver hva denne meldingen handler om; for eksempel hvis typen er "skader", mottakeren vil håndtere denne meldingen som en ordre for å redusere helsepunktene; hvis typen er "forfølge", mottakeren vil ta det som en instruksjon for å forfølge noe - og så videre.

De data feltet er direkte koblet til type felt. Bruk de forrige eksemplene, hvis meldtypen er "skader", og så data feltet vil inneholde et tall-si, 10-som beskriver hvor mye skade mottakeren skal bruke på helsepunktene. Hvis meldingstypen er "forfølge"data vil inneholde et objekt som beskriver målet som må følges.

De data feltet kan inneholde informasjon som gjør konvolutten til en allsidig kommunikasjonsmåte. Alt kan plasseres i dette feltet: heltall, flyter, strenger og til og med andre objekter. Tommelfingerregelen er at mottakeren må vite hva som er i data feltet basert på hva som er i type felt.

All den teorien kan oversettes til en veldig enkel klasse som heter Budskap. Den inneholder fire egenskaper, ett for hvert felt:

Melding = funksjon (til, fra, type, data) // Egenskaper this.to = to; // en referanse til enheten som vil motta denne meldingen this.from = from; // en henvisning til enheten som sendte denne meldingen this.type = type; // typen av denne meldingen this.data = data; // innholdet / dataene til denne meldingen;

Som et eksempel på dette i bruk, hvis en enhet EN ønsker å sende en "skader" melding til enheten B, alt du trenger å gjøre er å skape et objekt av klassen Budskap, sett eiendommen til til B, sett eiendommen fra til seg selv (enhet EN), sett type til "skader" og endelig sett data til noen nummer (10, for eksempel):

// Instantiate de to enhetene var entityA = new Entity (); var entityB = ny entitet (); // Opprett en melding til entityB, fra entityA, // med type "skade" og data / verdi 10. var msg = new Message (); msg.to = entityB; msg.from = entityA; msg.type = "skade"; msg.data = 10; // Du kan også instantiere meldingen direkte // overføre informasjonen den krever, slik: var msg = ny melding (entityB, entityA, "damage", 10);

Nå som vi har en måte å skape meldinger på, er det på tide å tenke på klassen som vil lagre og levere dem.

Implementere en kø

Klassen som er ansvarlig for lagring og levering av meldingene, vil bli kalt MessageQueue. Det vil fungere som postkontor: Alle meldinger blir levert til denne klassen, som sikrer at de vil bli sendt til deres destinasjon.

For nå, den MessageQueue klassen vil ha en veldig enkel struktur:

/ ** * Denne klassen er ansvarlig for å motta meldinger og * sende dem til destinasjonen. * / MessageQueue = funksjon () this.messages = []; // liste over meldinger som skal sendes; // Legg til en ny melding i køen. Meldingen må være en // forekomst av klassen Melding. MessageQueue.prototype.add = funksjon (melding) this.messages.push (melding); ;

Eiendommen meldinger er en matrise. Det lagrer alle meldingene som skal leveres av MessageQueue. Metoden Legg til() mottar et objekt av klassen Budskap som en parameter, og legger til objektet i listen over meldinger. 

Slik er vårt tidligere eksempel på enhet EN meldingsenhet B om skade ville fungere ved hjelp av MessageQueue klasse:

// Instantiate de to enhetene og meldingen kø var entityA = new Entity (); var entityB = ny entitet (); var messageQueue = new MessageQueue (); // Opprett en melding til entityB, fra entityA, // med type "skade" og data / verdi 10. var msg = new Message (entityB, entityA, "damage", 10); // Legg til meldingen i køen messageQueue.add (msg);

Vi har nå en måte å opprette og lagre meldinger i en kø på. Det er på tide å få dem til å nå målet.

Levering av meldinger

For å gjøre MessageQueue Klassen sender faktisk meldte meldinger, først må vi definere hvordan enheter vil håndtere og motta meldinger. Den enkleste måten er å legge til en metode som heter onMessage () til hver enhet som kan motta meldinger:

/ ** * Denne klassen beskriver en generisk enhet. * / Entity = function () // Initialiser noe her, f.eks. Phaser ting; // Denne metoden er påkalt av MessageQueue // når det er en melding til denne enheten. Entity.prototype.onMessage = funksjon (melding) // Håndter ny melding her;

De MessageQueue klassen vil påkalle onMessage () Metode for hver enhet som må motta en melding. Parameteren som overføres til den metoden er at meldingen blir levert av køsystemet (og mottas av målet). 

De MessageQueue klassen vil sende meldingene i køen på en gang, i avsendelse () metode:

/ ** * Denne klassen er ansvarlig for å motta meldinger og * sende dem til destinasjonen. * / MessageQueue = funksjon () this.messages = []; // liste over meldinger som skal sendes; MessageQueue.prototype.add = funksjon (melding) this.messages.push (melding); ; // Send alle meldinger i køen til deres destinasjon. MessageQueue.prototype.dispatch = funksjon () var jeg, enhet, msg; // Iterave over listen over meldinger for (i = 0; this.messages.length; i ++) // Få meldingen til gjeldende iterasjon msg = this.messages [i]; // Er det gyldig? hvis (msg) // Hent enheten som skal motta denne meldingen // (den i feltet til "til") = msg.to; // Hvis den enheten eksisterer, leverer meldingen. hvis (enhet) entity.onMessage (msg);  // Slett meldingen fra køen this.messages.splice (jeg, 1); Jeg--; ;

Denne metoden iterates over alle meldinger i køen, og for hver melding, er til feltet brukes til å hente en referanse til mottakeren. De onMessage () Metoden til mottakeren blir deretter påkalt, med den nåværende meldingen som en parameter, og den leverte meldingen blir deretter fjernet fra MessageQueue liste. Denne prosessen gjentas til alle meldinger er sendt.

Bruke en meldingskø

Det er på tide å se alle detaljene i denne implementeringen arbeider sammen. La oss bruke vårt meldingskøesystem i en veldig enkel demo som består av noen bevegelige enheter som samhandler med hverandre. For enkelhets skyld skal vi jobbe med tre enheter: Helbreder, Løper og Jeger.

De Løper har en helsestang og beveger seg tilfeldig. De Helbreder vil helbrede noen Løper som går i nærheten på den annen side, Jeger vil forårsake skade på noen i nærheten Løper. Alle interaksjoner vil bli håndtert ved hjelp av meldingen køsystemet.

Legge til meldingskøen

La oss begynne med å opprette PlayState som inneholder en liste over enheter (healere, løpere og jegere) og en forekomst av MessageQueue klasse:

var PlayState = funksjon () var enheter; // liste over enheter i spillet var messageQueue; // meldingskøen (dispatcher) this.create = function () // Initialiser meldingen køen messageQueue = new MessageQueue (); // Opprett en gruppe enheter. enheter = this.game.add.group (); ; this.update = function () // Lag alle meldinger i meldingskøen // nå målet. messageQueue.dispatch (); ; ;

I spillsløyfen, representert av Oppdater() metode, meldingen køen er avsendelse () Metoden er påkalt, så alle meldinger leveres på slutten av hver spillramme.

Legge til løpere

De Løper klassen har følgende struktur:

/ ** * Denne klassen beskriver et enhet som bare * vandrer rundt. * / Runner = funksjon () // initialiser Phaser ting her ...; // Oppfordret av spillet på hver ramme Runner.prototype.update = function () // Gjør ting flytte her ... // Denne metoden er påkalt av meldingskøen // for å få løperen til å håndtere innkommende meldinger. Runner.prototype.onMessage = funksjon (melding) var mengde; // Sjekk meldingstypen slik at det er mulig å // bestemme om denne meldingen skal ignoreres eller ikke. hvis (message.type == "skade") // Meldingen handler om skade. // Vi må redusere helsepunktene våre. Mengden // denne nedgangen ble informert av meldingen avsender // i feltet 'data'. amount = message.data; this.addHealth (-AMOUNT);  annet hvis (message.type == "heal") // Meldingen handler om helbredelse. // Vi må øke helsepunktene våre. Igjen ble mengden // helsepoeng å øke informert av meldings avsenderen // i feltet 'data'. amount = message.data; this.addHealth (mengde);  ellers // Her behandler vi meldinger vi ikke kan behandle. // Sannsynligvis bare ignorere dem :);

Den viktigste delen er onMessage () metode, påkalt av meldingskøen hver gang det er en ny melding for denne forekomsten. Som tidligere forklart, er feltet type i meldingen brukes til å bestemme hva denne kommunikasjonen handler om.

Basert på typen av meldingen, utføres den riktige handlingen: hvis meldingstypen er "skader", Helsepunktene er redusert; hvis meldingstypen er "helbrede", helsepoengene øker. Antall helsepoeng som skal økes eller reduseres med, er definert av avsenderen i data feltet av meldingen.

I PlayState, Vi legger til noen løpere i listen over enheter:

var PlayState = funksjon () // (...) this.create = function () // (...) // Legg til løpere for (i = 0; i < 4; i++)  entities.add(new Runner(this.game, this.game.world.width * Math.random(), this.game.world.height * Math.random()));  ; // (… ) ;

Resultatet er at fire løpere flyttes tilfeldig:

Legge til Hunteren

De Jeger klassen har følgende struktur:

/ ** * Denne klassen beskriver et foretak som bare * vandrer rundt og skader løpene som går forbi. * / Hunter = funksjon (spill, x, y) // initialiser Phaser ting her; // Sjekk om entiteten er gyldig, er en løper, og ligger innenfor angrepsområdet. Hunter.prototype.canEntityBeAttacked = funksjon (enhet) return entity && entity! = Dette && (entity instanceof Runner) &&! (Entity 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(); ;

Jegerne vil også bevege seg rundt, men de vil skade alle løpere som er nært. Denne oppførselen er implementert i Oppdater() metode, hvor alle enheter i spillet blir inspisert og løpere blir meldt om skade.

Skadesmeldingen er opprettet som følger:

msg = ny melding (enhet, dette, "skade", 2);

Meldingen inneholder informasjonen om destinasjonen (enhet, i dette tilfellet, hvilken enhet analyseres i den nåværende iterasjonen), sender avsenderen (dette, som representerer jegeren som utfører angrepet), typen av meldingen ("skader") og mengden skade (2, i dette tilfellet tilordnet data feltet av meldingen).

Meldingen sendes deretter til destinasjonen via kommandoen this.getMessageQueue (). til (msg), som legger til den nyopprettede meldingen til meldingen køen.

Til slutt legger vi til Jeger til listen over enheter i PlayState:

var PlayState = funksjon () // (...) this.create = function () // (...) // Legg til jeger på posisjon (20, 30) entities.add (new Hunter (this.game, 20, 30 )); ; // (...);

Resultatet er at noen løpere beveger seg rundt, mottar meldinger fra jegeren når de kommer nær hverandre:

Jeg la til flygende konvolutter som et visuelt hjelpemiddel for å vise hva som skjer.

Legge til healeren

De Helbreder klassen har følgende struktur:

/ ** * Denne klassen beskriver en enhet som * kan helbrede alle løpere som går i nærheten. * / Healer = funksjon (spill, x, y) // Initializer Phaser ting her; Healer.prototype.update = function () var enheter, jeg, størrelse, enhet, msg; // Listen over enheter i spillet enheter = this.getPlayState (). GetEntities (); for (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(); ;

Koden og strukturen er veldig lik den Jeger klasse, bortsett fra noen få forskjeller. På samme måte som jagerens gjennomføring, er helbrederens Oppdater() Metoden iterates over listen over enheter i spillet, og melder enhver enhet innenfor sin helbredende rekkevidde:

msg = ny melding (enhet, dette, "helbrede", 2);

Meldingen har også en destinasjon (enhet), en avsender (dette, som er healeren som utfører handlingen), en meldingstype ("helbrede") og antall helbredende poeng (2, tildelt i data feltet av meldingen).

Vi legger til Helbreder til listen over enheter i PlayState På samme måte som vi gjorde med Jeger og resultatet er en scene med løpere, en jeger og en healer:

Og det er det! Vi har tre forskjellige enheter som samhandler med hverandre ved å utveksle meldinger.

Diskusjon om fleksibilitet

Dette meldings køsystemet er en allsidig måte å administrere samspill på i et spill. Samspillet utføres via en kommunikasjonskanal som er forenet og har et enkelt grensesnitt som er enkelt å bruke og implementere.

Etter hvert som spillet ditt vokser i kompleksitet, kan det hende at nye interaksjoner måtte være nødvendig. Noen av dem kan være helt uventede, så du må tilpasse koden din for å håndtere dem. Hvis du bruker et meldingskøesystem, handler dette om å legge til en ny melding et sted og håndtere det i en annen.

For eksempel, tenk deg at du vil gjøre Jeger samhandle med Helbreder; du må bare lage Jeger send en melding med den nye interaksjonen, for eksempel, "flykte"-og sørg for at Helbreder kan håndtere det i onMessage metode:

// I Hunter-klassen: Hunter.prototype.someMethod = function () // Få en referanse til en nærliggende healer var healer = this.getNearbyHealer (); // Opprett melding om å fly et sted var sted = x: 30, y: 40; var msg = ny melding (enhet, dette, "flykte", sted); // Send beskjeden vekk! this.getMessageQueue () tilsett (msg.); ; // I Healer klassen: Healer.prototype.onMessage = funksjon (melding) if (message.type == "flee") // Få stedet å fly fra datafeltet i meldingen var place = message.data ; // Bruk stedet informasjon flykte (place.x, place.y); ;

Hvorfor ikke bare sende meldinger direkte?

Selv om utveksling av meldinger mellom enheter kan være nyttig, kan du tenke på hvorfor MessageQueue trengs trods alt. Kan du ikke bare påkalle mottakerens onMessage () Metode deg selv i stedet for å stole på MessageQueue, som i koden nedenfor?

Hunter.prototype.someMethod = function () // Få en referanse til en nærliggende healer var healer = this.getNearbyHealer (); // Opprett melding om å fly et sted var sted = x: 30, y: 40; var msg = ny melding (enhet, dette, "flykte", sted); // Bypass MessageQueue og send direkte // meldingen til healeren. healer.onMessage (msg); ;

Du kan definitivt implementere et meldingssystem slik, men bruken av a MessageQueue har noen fordeler.

Ved å sentralisere utsendelse av meldinger kan du for eksempel implementere noen kule funksjoner som forsinkede meldinger, muligheten til å beskrive en gruppe enheter og visuell feilsøking (for eksempel flygende konvolutter brukt i denne opplæringen).

Det er plass til kreativitet i MessageQueue klassen, det er opp til deg og spillets krav.

Konklusjon

Håndtere samspill mellom spill enheter ved hjelp av et meldings køsystem er en måte å holde koden din organisert og klar for fremtiden. Nye samspill kan enkelt og raskt legges til, selv dine mest komplekse ideer, så lenge de er innkapslet som meldinger.

Som diskutert i opplæringen, kan du ignorere bruken av en sentral meldingskø og bare sende meldinger direkte til enhetene. Du kan også sentralisere kommunikasjonen ved hjelp av en forsendelse (the MessageQueue klasse i vårt tilfelle) for å gi plass til nye funksjoner i fremtiden, for eksempel forsinkede meldinger.

Jeg håper du finner denne tilnærmingen nyttig og legger den til spillutviklerverktøyet ditt. Metoden kan virke som overkill for små prosjekter, men det vil sikkert spare deg for noen hodepine i lengden for større spill.