Ett spel är vanligtvis gjord av flera olika enheter som interagerar med varandra. Dessa interaktioner tenderar att vara mycket dynamiska och djupt kopplade till spel. Denna handledning täcker konceptet och genomförandet av ett meddelandekössystem som kan förena samverkan mellan enheter, göra din kod hanterbar och lätt att underhålla eftersom den växer i komplexitet.
En bomb kan interagera med en karaktär genom att explodera och orsaka skador, en medicinsk utrustning kan läka en enhet, en nyckel kan öppna en dörr och så vidare. Interaktioner i ett spel är oändliga, men hur kan vi hålla spelkoden hanterbar medan du fortfarande kan hantera alla dessa interaktioner? Hur ser vi till att koden kan förändras och fortsätter att fungera när nya och oväntade interaktioner uppstår?
Interaktioner i ett spel tenderar att växa i komplexitet mycket snabbt.Eftersom interaktioner läggs till (speciellt de oväntade), kommer din kod att se mer och mer rörig ut. En naiv implementering leder snabbt till att du ställer frågor som:
"Detta är enhet A, så jag borde ringa metod skada()
på det, eller hur? Eller är det damageByItem ()
? Kanske detta damageByWeapon ()
Metod är den rätta? "
Föreställ dig att det rörliga kaoset sprider sig till alla dina spelföretag, eftersom de alla interagerar med varandra på olika och märkliga sätt. Lyckligtvis finns det ett bättre, enklare och mer hanterbart sätt att göra det.
Gå in i meddelandekö. Grundidén bakom detta koncept är att genomföra alla spelinteraktioner som ett kommunikationssystem (som fortfarande används idag): meddelandeutbyte. Människor har kommunicerat via meddelanden (brev) i århundraden eftersom det är ett effektivt och enkelt system.
I våra riktiga posttjänster kan innehållet i varje meddelande skilja sig, men det sätt de fysiskt skickas och mottas är detsamma. En avsändare lägger informationen i ett kuvert och adresserar det till en destination. Destinationen kan svara (eller inte) genom att följa samma mekanism, bara ändra "från / till" -fälten på kuvertet.
Interaktioner gjorda med ett meddelande kösystem.Genom att tillämpa den här idén på ditt spel kan alla interaktioner mellan enheter ses som meddelanden. Om en spelande enhet vill interagera med en annan (eller en grupp av dem), är allt som krävs för att skicka ett meddelande. Destinationen kommer att hantera eller reagera på meddelandet baserat på innehållet och vem avsändaren är.
I detta tillvägagångssätt blir kommunikationen mellan spelorgan förenade. Alla enheter kan skicka och ta emot meddelanden. Oavsett hur komplex eller märklig interaktionen eller meddelandet är, förbli kommunikationskanalen alltid densamma.
I de följande avsnitten beskriver jag hur du faktiskt kan implementera den här meddelandekönan i ditt spel.
Låt oss börja med att utforma kuvertet, vilket är det mest grundläggande elementet i meddelandekössystemet.
Ett kuvert kan beskrivas som i figuren nedan:
Strukturen av ett meddelande.De två första fälten (avsändare
och destination
) är referenser till den enhet som skapade och den enhet som kommer att få detta meddelande, respektive. Med hjälp av dessa fält kan både avsändaren och mottagaren berätta var meddelandet kommer och varifrån det kom.
De andra två fälten (typ
och data
) arbetar tillsammans för att säkerställa att meddelandet hanteras korrekt. De typ
fältet beskriver vad detta meddelande handlar om till exempel om typen är "skada"
, Mottagaren hanterar detta meddelande som en order att minska sina hälsopunkter; om typen är "bedriva"
, mottagaren tar det som en instruktion att driva något - och så vidare.
De data
fältet är direkt anslutet till typ
fält. Använd de tidigare exemplen om meddelandetypen är "skada"
, sedan data
Fältet kommer att innehålla ett tal-säg, 10
-som beskriver hur mycket skada mottagaren ska tillämpa på hälsopunkterna. Om meddelandetypen är "bedriva"
, data
kommer att innehålla ett objekt som beskriver målet som ska följas.
De data
Fältet kan innehålla all information som gör kuvertet ett mångsidigt kommunikationsmedel. Allting kan placeras i det fältet: heltal, floats, strängar och även andra objekt. Tumregeln är att mottagaren måste veta vad som finns i data
fält baserat på vad som finns i typ
fält.
All den teorin kan översättas till en mycket enkel klass som heter Meddelande
. Den innehåller fyra egenskaper, en för varje fält:
Meddelande = funktion (till, från, typ, data) // Egenskaper this.to = to; // en hänvisning till den enhet som kommer att få detta meddelande this.from = from; // en hänvisning till den enhet som skickade detta meddelande this.type = type; // typen av det här meddelandet this.data = data; // innehållet / data för detta meddelande;
Som ett exempel på detta i bruk, om en enhet en
vill skicka en "skada"
meddelande till enheten B
, allt man behöver göra är att omedelbara ett föremål för klassen Meddelande
, ställa in egenskapen till
till B
, ställa in egenskapen från
till sig själv (enhet en
), uppsättning typ
till "skada"
och slutligen sätta data
till ett visst antal (10
, till exempel):
// Instantiate de två enheterna var entityA = new Entity (); var entityB = ny Entity (); // Skapa ett meddelande till entityB, från entityA, // med typ "skada" och data / värde 10. var msg = new Message (); msg.to = entityB; msg.from = entityA; msg.type = "skada"; msg.data = 10; // Du kan också genomsöka meddelandet direkt // överföra informationen som krävs, så här: var msg = new Message (entityB, entityA, "damage", 10);
Nu när vi har ett sätt att skapa meddelanden är det dags att tänka på den klass som kommer att lagra och leverera dem.
Klassen som är ansvarig för att lagra och leverera meddelanden kommer att ringas Meddelandekö
. Det kommer att fungera som postkontor: alla meddelanden lämnas till denna klass, vilket säkerställer att de kommer att skickas till deras destination.
För nu är det Meddelandekö
klassen kommer att ha en mycket enkel struktur:
/ ** * Den här klassen är ansvarig för att ta emot meddelanden och * skickar dem till destinationen. * / MessageQueue = funktion () this.messages = []; // lista över meddelanden som ska skickas; // Lägg till ett nytt meddelande till köen. Meddelandet måste vara en // förekomst av klassmeddelandet. MessageQueue.prototype.add = funktion (meddelande) this.messages.push (meddelande); ;
Egendomen meddelanden
är en array. Det lagrar alla meddelanden som ska levereras av Meddelandekö
. Metoden Lägg till()
tar emot ett objekt av klassen Meddelande
som en parameter och lägger till det objektet i listan med meddelanden.
Så här är vårt tidigare exempel på enheten en
meddelande enhet B
om skador skulle fungera med hjälp av Meddelandekö
klass:
// Instantiate de två enheterna och meddelandekön var entityA = new Entity (); var entityB = ny Entity (); var messageQueue = new MessageQueue (); // Skapa ett meddelande till entityB, från entityA, // med typen "skada" och data / värde 10. var msg = new Message (entityB, entityA, "damage", 10); // Lägg till meddelandet i kömeddelandetQueue.add (msg);
Vi har nu ett sätt att skapa och lagra meddelanden i en kö. Det är dags att få dem att nå sin destination.
För att göra Meddelandekö
klassen skickar faktiskt de postade meddelandena, först måste vi definiera på vilket sätt enheter hanterar och tar emot meddelanden. Det enklaste sättet är att lägga till en metod som heter onMessage ()
till varje enhet som kan ta emot meddelanden:
/ ** * Den här klassen beskriver en generisk enhet. * / Entity = function () // Initialisera något här, t.ex. Phaser-saker; // Den här metoden är påkallad av MessageQueue // när det finns ett meddelande till den här enheten. Entity.prototype.onMessage = funktion (meddelande) // Hantera nytt meddelande här;
De Meddelandekö
klassen kommer att påkalla onMessage ()
Metod för varje enhet som måste ta emot ett meddelande. Parametern som skickas till den metoden är att meddelandet levereras av kösystemet (och mottas av destinationen).
De Meddelandekö
klassen skickar meddelandena i sin kö på en gång, i avsändande()
metod:
/ ** * Den här klassen är ansvarig för att ta emot meddelanden och * skickar dem till destinationen. * / MessageQueue = funktion () this.messages = []; // lista över meddelanden som ska skickas; MessageQueue.prototype.add = funktion (meddelande) this.messages.push (meddelande); ; // Skicka alla meddelanden i kön till deras destination. MessageQueue.prototype.dispatch = function () var jag, enhet, msg; // Iterave över listan över meddelanden för (i = 0; this.messages.length; i ++) // Få meddelandet om den aktuella iterationen msg = this.messages [i]; // Är det giltigt? om (msg) // Hämta enheten som ska få det här meddelandet // (det i fältet "till") = msg.to; // Om den enheten existerar, leverera meddelandet. om (enhet) entity.onMessage (msg); // Radera meddelandet från kön this.messages.splice (jag, 1); jag--; ;
Denna metod repeterar över alla meddelanden i kön och för varje meddelande, till
fält används för att hämta en referens till mottagaren. De onMessage ()
Mottagarens metod anropas sedan med det aktuella meddelandet som en parameter, och det levererade meddelandet tas sedan bort från Meddelandekö
lista. Denna process upprepas tills alla meddelanden skickas.
Det är dags att se alla detaljerna i denna implementering tillsammans. Låt oss använda vårt meddelandekössystem i en mycket enkel demo som består av några rörliga enheter som interagerar med varandra. För enkelhetens skull arbetar vi med tre enheter: Botemedel
, Löpare
och Jägare
.
De Löpare
har en hälsorang och rör sig slumpmässigt. De Botemedel
kommer att läka något Löpare
som passerar i närheten å andra sidan Jägare
kommer att medföra skador på någon i närheten Löpare
. Alla interaktioner hanteras med meddelandekössystemet.
Låt oss börja med att skapa PlayState
som innehåller en lista över enheter (healers, löpare och jägare) och en förekomst av Meddelandekö
klass:
var PlayState = funktion () var enheter; // lista över enheter i spelet var messageQueue; // meddelandekön (avsändare) this.create = function () // Initiera meddelandekön messageQueue = new MessageQueue (); // Skapa en grupp av enheter. enheter = this.game.add.group (); ; this.update = function () // Gör alla meddelanden i meddelandekön // nå sin destination. messageQueue.dispatch (); ; ;
I spelet loop, representerad av uppdatering()
metod, meddelandekönens avsändande()
Metoden åberopas, så alla meddelanden levereras i slutet av varje spelram.
De Löpare
klassen har följande struktur:
/ ** * Den här klassen beskriver en enhet som bara * vandrar runt. * / Runner = funktion () // initiera Phaser-grejer här ...; // Inbjudna av spelet på varje ram Runner.prototype.update = function () // Gör saker flytta här ... // Den här metoden är påkallad av meddelandekön // för att göra löpare hantera inkommande meddelanden. Runner.prototype.onMessage = funktion (meddelande) var mängd; // Kontrollera meddelandetypen så det är möjligt att // bestämma om det här meddelandet ska ignoreras eller inte. om (message.type == "damage") // Meddelandet handlar om skada. // Vi måste minska våra hälsopunkter. Mängden // denna minskning informerades av meddelandets avsändare // i fältet "data". mängd = message.data; this.addHealth (-amount); annars om (message.type == "heal") // Meddelandet handlar om läkning. // Vi måste öka våra hälsopunkter. Återigen informerades meddelandet avsändaren // i fältet "data" om hur mycket / / hälsa poäng som skulle öka. mängd = message.data; this.addHealth (mängd); else // Här handlar vi om meddelanden som vi inte kan bearbeta. // Förmodligen bara ignorera dem :);
Den viktigaste delen är onMessage ()
metod som anropas av meddelandekön varje gång det finns ett nytt meddelande för denna instans. Som tidigare förklarats, fältet typ
i meddelandet används för att bestämma vad denna kommunikation handlar om.
Baserat på typen av meddelandet utförs den korrekta åtgärden: om meddelandetypen är "skada"
, hälsopoängen minskar om meddelandetypen är "läka"
, hälsopunkter ökar. Antalet hälsopunkter att öka eller minska med definieras av avsändaren i data
fältet i meddelandet.
I PlayState
, vi lägger till några löpare i listan över enheter:
var PlayState = funktion () // (...) this.create = function () // (...) // Lägg till löpare för (i = 0; i < 4; i++) entities.add(new Runner(this.game, this.game.world.width * Math.random(), this.game.world.height * Math.random())); ; // (… ) ;
Resultatet är att fyra löpare flyttar slumpmässigt:
De Jägare
klassen har följande struktur:
/ ** * Den här klassen beskriver en enhet som bara * vandrar runt och skadar löpare som passerar. * / Hunter = funktion (spel, x, y) // initiera Phaser-saker här; // Kontrollera om entiteten är giltig, är en löpare och ligger inom attackområdet. Hunter.prototype.canEntityBeAttacked = funktion (enhet) return entity && entity! = Detta && (instans instanceof Runner) &&! (Entitet instans av 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(); ;
Jägarna kommer också att röra sig, men de kommer att skada alla löpare som är nära. Detta beteende är implementerat i uppdatering()
metod där alla enheter i spelet inspekteras och löpare skickas om skador.
Skador meddelandet skapas enligt följande:
msg = nytt meddelande (enhet, detta, "skada", 2);
Meddelandet innehåller information om destinationen (entitet
, i det här fallet, vilket är den enhet som analyseras i den aktuella iterationen), avsändaren (detta
, vilket representerar jägaren som utför attacken), typen av meddelandet ("skada"
) och mängden skada (2
, i det här fallet tilldelas data
fältet i meddelandet).
Meddelandet skickas sedan till destinationen via kommandot this.getMessageQueue (). lägg (msg)
, som lägger till det nyskapade meddelandet i meddelandekön.
Slutligen lägger vi till Jägare
till listan över enheter i PlayState
:
var PlayState = funktion () // (...) this.create = function () // (...) // Lägg till jägare på position (20, 30) entities.add (new Hunter (this.game, 20, 30 )); ; // (...);
Resultatet är att några löpare flyttar sig och tar emot meddelanden från jägaren när de kommer nära varandra:
Jag lade till flygande kuvert som visuellt hjälpmedel för att hjälpa till att visa vad som händer.
De Botemedel
klassen har följande struktur:
/ ** * Den här klassen beskriver en enhet som * kan läka någon löpare som passerar i närheten. * / Healer = funktion (spel, x, y) // Initializer Phaser-grejer här; Healer.prototype.update = function () var enheter, jag, storlek, enhet, msg; // Listan över enheter i spelets enheter = this.getPlayState (). GetEntities (); för (i = 0, storlek = 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 och strukturen är mycket liknar Jägare
klass, förutom några skillnader. På samma sätt som jägarens genomförande är healarens uppdatering()
Metoden iterates över listan över enheter i spelet, meddelar någon enhet inom sin helande räckvidd:
msg = nytt meddelande (enhet, detta, "läka", 2);
Meddelandet har också en destination (entitet
), en avsändare (detta
, vilken läkare utför åtgärden), en meddelandetyp ("läka"
) och antalet läkningspunkter (2
, tilldelad i data
fältet i meddelandet).
Vi lägger till Botemedel
till listan över enheter i PlayState
på samma sätt som vi gjorde med Jägare
och resultatet är en scen med löpare, en jägare och en healer:
Och det är allt! Vi har tre olika enheter som interagerar med varandra genom att utbyta meddelanden.
Detta meddelande kösystem är ett mångsidigt sätt att hantera interaktioner i ett spel. Samspelet utförs via en kommunikationskanal som är enhetlig och har ett enkelt gränssnitt som är lätt att använda och implementera.
Eftersom ditt spel växer i komplexitet kan nya interaktioner behövas. Vissa av dem kan vara helt oväntade, så du måste anpassa din kod för att hantera dem. Om du använder ett meddelandekössystem handlar det här om att lägga till ett nytt meddelande någonstans och hantera det i en annan.
Tänk dig att du vill göra Jägare
interagera med Botemedel
; du måste bara göra Jägare
skicka ett meddelande med den nya interaktionen, till exempel, "fly"
-och se till att Botemedel
kan hantera det i onMessage
metod:
// I jägarklassen: Hunter.prototype.someMethod = function () // Hämta en hänvisning till en närliggande healer var healer = this.getNearbyHealer (); // Skapa meddelande om att flytta en plats var plats = x: 30, y: 40; var msg = nytt meddelande (enhet, detta, "fly", plats); // Skicka meddelandet bort! this.getMessageQueue () sätt (msg).; ; // I Healer-klassen: Healer.prototype.onMessage = funktion (meddelande) if (message.type == "flee") // Få platsen att fly från datafältet i meddelandet var place = message.data ; // Använd platsinformationen flytta (place.x, place.y); ;
Även om utbyte av meddelanden bland enheter kan vara användbart, kanske du tänker varför Meddelandekö
behövs trots allt. Kan du inte bara ringa till mottagaren onMessage ()
Metod själv istället för att förlita sig på Meddelandekö
, som i koden nedan?
Hunter.prototype.someMethod = function () // Hämta en referens till en närliggande healer var healer = this.getNearbyHealer (); // Skapa meddelande om att flytta en plats var plats = x: 30, y: 40; var msg = nytt meddelande (enhet, detta, "fly", plats); // Bypass MessageQueue och leverera direkt // meddelandet till healen. healer.onMessage (msg); ;
Du skulle definitivt kunna implementera ett meddelandesystem, men användningen av a Meddelandekö
har några fördelar.
Genom att centralisera sändningen av meddelanden kan du exempelvis implementera några svala funktioner som fördröjda meddelanden, förmågan att meddela en grupp enheter och visuell felsökning (t.ex. flygande kuvert som används i denna handledning).
Det finns utrymme för kreativitet i Meddelandekö
klass, det är upp till dig och ditt spel krav.
Hantering av interaktioner mellan spelobjekt med ett meddelandekössystem är ett sätt att hålla din kod organiserad och redo för framtiden. Nya interaktioner kan enkelt och snabbt läggas till, även dina mest komplexa idéer, så länge de inkapslas som meddelanden.
Som diskuteras i handledningen kan du ignorera användningen av en central meddelandekön och bara skicka meddelanden direkt till enheterna. Du kan också centralisera kommunikationen med en avsändning (the Meddelandekö
klass i vårt fall) för att skapa plats för nya funktioner i framtiden, till exempel försenade meddelanden.
Jag hoppas att du hittar detta tillvägagångssätt användbart och lägger till det i ditt spelutvecklingsverktygsband. Metoden kan verka som överkill för små projekt, men det kommer säkert att spara dig några huvudvärk i längden för större spel.