Een spel is meestal gemaakt van verschillende entiteiten die met elkaar communiceren. Die interacties zijn over het algemeen erg dynamisch en sterk verbonden met gameplay. Deze tutorial behandelt het concept en de implementatie van een berichtenwachtrijsysteem dat de interacties van entiteiten kan verenigen, waardoor uw code beheersbaar en gemakkelijk te onderhouden is naarmate het complexer wordt.
Een bom kan interactie hebben met een personage door te exploderen en schade aan te richten, een medicijnkit kan een entiteit genezen, een sleutel kan een deur openen, enzovoort. Interacties in een game zijn eindeloos, maar hoe kunnen we de spelcode beheersbaar houden terwijl we toch al die interacties kunnen verwerken? Hoe zorgen we ervoor dat de code kan veranderen en blijven werken wanneer zich nieuwe en onverwachte interacties voordoen?
Interacties in een spel hebben de neiging om erg snel in complexiteit te groeien.Naarmate interacties worden toegevoegd (met name de onverwachte), ziet uw code er steeds rommeliger uit. Een naïeve implementatie zal snel leiden tot het stellen van vragen zoals:
"Dit is entiteit A, dus ik zou methode moeten noemen schade()
erop, toch? Of is het damageByItem ()
? Misschien dit damageByWeapon ()
methode is de juiste? "
Stel je voor dat die chaotische chaos zich verspreidt naar al je spelentiteiten, omdat ze allemaal op verschillende en vreemde manieren met elkaar omgaan. Gelukkig is er een betere, eenvoudigere en beter beheersbare manier om het te doen.
Ga naar berichtenwachtrij. Het basisidee achter dit concept is om alle game-interacties te implementeren als een communicatiesysteem (dat nog steeds in gebruik is): berichten uitwisselen. Mensen hebben eeuwenlang gecommuniceerd via berichten (brieven) omdat het een effectief en eenvoudig systeem is.
In onze real-world postservices kan de inhoud van elk bericht verschillen, maar de manier waarop ze fysiek worden verzonden en ontvangen, blijft hetzelfde. Een afzender plaatst de informatie in een envelop en adresseert deze naar een bestemming. De bestemming kan antwoorden (of niet) door hetzelfde mechanisme te volgen, alleen de "van / naar" velden op de envelop te veranderen.
Interacties gemaakt met behulp van een berichtenwachtrijsysteem.Door dat idee op uw spel toe te passen, kunnen alle interacties tussen entiteiten worden gezien als berichten. Als een game-entiteit wil communiceren met een andere (of een groep van hen), hoeft het alleen maar een bericht te verzenden. De bestemming behandelt of reageert op het bericht op basis van de inhoud en op wie de afzender is.
In deze benadering wordt communicatie tussen game-entiteiten verenigd. Alle entiteiten kunnen berichten verzenden en ontvangen. Hoe complex of eigenaardig de interactie of boodschap ook is, het communicatiekanaal blijft altijd hetzelfde.
In de volgende secties zal ik beschrijven hoe je deze Message Queue-aanpak daadwerkelijk kunt implementeren in je game.
Laten we beginnen met het ontwerpen van de envelop, het meest elementaire element in het berichtenwachtrijsysteem.
Een envelop kan worden beschreven zoals in de onderstaande afbeelding:
Structuur van een bericht.De eerste twee velden (afzender
en bestemming
) zijn verwijzingen naar de entiteit die is gemaakt en de entiteit die dit bericht respectievelijk zal ontvangen. Met behulp van die velden kunnen zowel de afzender als de ontvanger vertellen waar het bericht naartoe gaat en waar het vandaan komt.
De andere twee velden (type
en gegevens
) werk samen om ervoor te zorgen dat het bericht correct wordt afgehandeld. De type
veld beschrijft waar dit bericht over gaat; bijvoorbeeld, als het type is "schade"
, de ontvanger zal dit bericht behandelen als een opdracht om zijn gezondheidspunten te verminderen; als het type is "na te streven"
, de ontvanger neemt het als een instructie om iets na te streven, enzovoort.
De gegevens
veld is direct verbonden met de type
veld. Gebruik de voorgaande voorbeelden als het berichttype is "schade"
, dan de gegevens
veld bevat een nummer - zeg, 10
-die de hoeveelheid schade beschrijft die de ontvanger zou moeten toepassen op zijn gezondheidspunten. Als het berichttype is "na te streven"
, gegevens
bevat een object dat beschrijft welk doel moet worden nagestreefd.
De gegevens
veld kan informatie bevatten die de envelop tot een veelzijdig communicatiemiddel maakt. Alles kan in dat veld worden geplaatst: gehele getallen, drijvers, tekenreeksen en zelfs andere objecten. De vuistregel is dat de ontvanger moet weten wat er in zit gegevens
veld op basis van wat in de type
veld-.
Al die theorie kan worden vertaald in een heel eenvoudige klasse met de naam Bericht
. Het bevat vier eigenschappen, één voor elk veld:
Message = function (to, from, type, data) // Properties this.to = to; // een verwijzing naar de entiteit die dit bericht zal ontvangen this.from = from; // een verwijzing naar de entiteit die dit bericht heeft verzonden this.type = type; // het type van dit bericht this.data = data; // de inhoud / gegevens van dit bericht;
Als voorbeeld hiervan bij gebruik, als een entiteit EEN
wil een sturen "schade"
bericht aan entiteit B
, alles wat het hoeft te doen is een object van de klas instantiëren Bericht
, stel de eigenschap in naar
naar B
, stel de eigenschap in van
voor zichzelf (entiteit EEN
), instellen type
naar "schade"
en, ten slotte, ingesteld gegevens
naar een nummer (10
, bijvoorbeeld):
// Instantiate the two entities var entityA = new Entity (); var entityB = new Entity (); // Maak een bericht naar entityB, van entityA, // met type "damage" en data / value 10. var msg = new Message (); msg.to = entityB; msg.from = entityA; msg.type = "schade"; msg.data = 10; // U kunt het bericht ook direct instantiëren // de benodigde informatie doorgeven, zoals dit: var msg = new Message (entityB, entityA, "damage", 10);
Nu we een manier hebben om berichten te maken, is het tijd om na te denken over de klas die ze opslaat en bezorgt.
De klasse die verantwoordelijk is voor het opslaan en afleveren van de berichten, wordt gebeld message queue
. Het zal werken als een postkantoor: alle berichten worden overhandigd aan deze klasse, die ervoor zorgt dat ze naar hun bestemming worden verzonden.
Voor nu, de message queue
klas zal een heel eenvoudige structuur hebben:
/ ** * Deze klasse is verantwoordelijk voor het ontvangen van berichten en * het verzenden van berichten naar de bestemming. * / MessageQueue = function () this.messages = []; // lijst met berichten die moeten worden verzonden; // Voeg een nieuw bericht toe aan de wachtrij. Het bericht moet een // exemplaar van het bericht van de klasse zijn. MessageQueue.prototype.add = function (message) this.messages.push (message); ;
Het eigendom berichten
is een array. Het slaat alle berichten op die op het punt staan te worden afgeleverd door de message queue
. De methode toevoegen()
ontvangt een object van de klas Bericht
als een parameter, en voegt dat object toe aan de lijst met berichten.
Hier is hoe ons vorige voorbeeld van entiteit EEN
berichtentiteit B
over schade zou werken met behulp van de message queue
klasse:
// Start de twee entiteiten en de berichtenwachtrij var entityA = new Entity (); var entityB = new Entity (); var messageQueue = new MessageQueue (); // Maak een bericht naar entityB, van entityA, // met type "damage" en data / value 10. var msg = new Message (entityB, entityA, "damage", 10); // Voeg het bericht toe aan de queue messageQueue.add (msg);
We hebben nu een manier om berichten in een wachtrij te maken en op te slaan. Het is tijd om ze hun bestemming te laten bereiken.
Om het te maken message queue
klasse daadwerkelijk de geposte berichten verzenden, eerst moeten we definiëren hoe entiteiten zullen berichten afhandelen en ontvangen. De eenvoudigste manier is door een methode genaamd toe te voegen OnMessage ()
aan elke entiteit die berichten kan ontvangen:
/ ** * Deze klasse beschrijft een generieke entiteit. * / Entity = function () // Initialiseer hier alles, bijvoorbeeld Phaser-dingen; // Deze methode wordt aangeroepen door de MessageQueue // wanneer er een bericht is voor deze entiteit. Entity.prototype.onMessage = functie (bericht) // Nieuw bericht hier behandelen;
De message queue
klasse zal de OnMessage ()
methode van elke entiteit die een bericht moet ontvangen. De parameter die aan die methode wordt doorgegeven, is het bericht dat wordt bezorgd door het wachtrijsysteem (en wordt ontvangen door de bestemming).
De message queue
klasse verzendt de berichten in zijn wachtrij in één keer, in de verzending()
methode:
/ ** * Deze klasse is verantwoordelijk voor het ontvangen van berichten en * het verzenden van berichten naar de bestemming. * / MessageQueue = function () this.messages = []; // lijst met berichten die moeten worden verzonden; MessageQueue.prototype.add = function (message) this.messages.push (message); ; // Verzend alle berichten in de wachtrij naar hun bestemming. MessageQueue.prototype.dispatch = function () var i, entity, msg; // Iterave over de lijst met berichten voor (i = 0; this.messages.length; i ++) // Ontvang het bericht van de huidige iteratie msg = this.messages [i]; // Is het geldig? if (msg) // Haal de entiteit op die dit bericht zou moeten ontvangen // (die in het veld 'to') entity = msg.to; // Als deze entiteit bestaat, verzendt u het bericht. if (entiteit) entity.onMessage (msg); // Verwijder het bericht uit de wachtrij this.messages.splice (i, 1); ik--; ;
Deze methode wordt herhaald voor alle berichten in de wachtrij en voor elk bericht de naar
veld wordt gebruikt om een verwijzing naar de ontvanger op te halen. De OnMessage ()
methode van de ontvanger wordt vervolgens opgeroepen, met het huidige bericht als een parameter, en het geleverde bericht wordt vervolgens verwijderd uit de message queue
lijst. Dit proces wordt herhaald totdat alle berichten zijn verzonden.
Het is tijd om alle details van deze implementatie samen te voegen. Laten we ons berichtwachtrijsysteem gebruiken in een heel eenvoudige demo die bestaat uit een paar bewegende entiteiten die met elkaar communiceren. Voor de eenvoud werken we met drie entiteiten: heler
, loper
en Jager
.
De loper
heeft een gezondheidsbalk en beweegt willekeurig rond. De heler
zal iedereen genezen loper
die voorbij komt; aan de andere kant, de Jager
zal schade aanrichten in een nabije omgeving loper
. Alle interacties worden afgehandeld met behulp van het berichtenwachtrijsysteem.
Laten we beginnen met het maken van de PlayState
die een lijst met entiteiten bevat (genezers, hardlopers en jagers) en een exemplaar van de message queue
klasse:
var PlayState = function () var entities; // lijst met entiteiten in de game var messageQueue; // de berichtenwachtrij (dispatcher) this.create = function () // Initialize the message queue messageQueue = new MessageQueue (); // Maak een groep entiteiten. entities = this.game.add.group (); ; this.update = function () // Maak alle berichten in de berichtenwachtrij // bereik hun bestemming. messageQueue.dispatch (); ; ;
In de spellus, vertegenwoordigd door de bijwerken()
methode, de berichtenwachtrij verzending()
methode wordt aangeroepen, dus alle berichten worden aan het einde van elk frame geleverd.
De loper
klasse heeft de volgende structuur:
/ ** * Deze klasse beschrijft een entiteit die slechts * rondwandelt. * / Runner = function () // initialiseer Phaser-dingen hier ...; // Geroepen door het spel op elk frame Runner.prototype.update = function () // Laat dingen hier bewegen ... // Deze methode wordt aangeroepen door de berichtenwachtrij // om de runner om te laten gaan met inkomende berichten. Runner.prototype.onMessage = function (bericht) var amount; // Controleer het berichttype zodat het mogelijk is om te beslissen of dit bericht moet worden genegeerd of niet. if (message.type == "damage") // Het bericht gaat over schade. // We moeten onze gezondheidspunten verminderen. Het bedrag van // deze afname werd geïnformeerd door de afzender van het bericht // in het veld 'gegevens'. amount = message.data; this.addHealth (-amount); else if (message.type == "healing") // De boodschap gaat over genezing. // We moeten onze gezondheidspunten verhogen. Wederom werd het aantal te verhogen // health points door de afzender // in het veld 'data' geïnformeerd. amount = message.data; this.addHealth (hoeveelheid); else // Hier behandelen we berichten die we niet kunnen verwerken. // Waarschijnlijk negeer je ze gewoon :);
Het belangrijkste onderdeel is de OnMessage ()
methode, aangeroepen door de berichtenwachtrij telkens wanneer er een nieuw bericht voor deze instantie is. Zoals eerder uitgelegd, het veld type
in het bericht wordt gebruikt om te beslissen waar deze communicatie over gaat.
Op basis van het type bericht wordt de juiste actie uitgevoerd: als het berichttype is "schade"
, gezondheidspunten zijn afgenomen; als het berichttype is "genezen"
, gezondheidspunten zijn toegenomen. Het aantal gezondheidspunten dat moet worden verhoogd of verlaagd, wordt bepaald door de afzender in de gegevens
veld van het bericht.
In de PlayState
, we voegen enkele hardlopers toe aan de lijst met entiteiten:
var PlayState = function () // (...) this.create = function () // (...) // Lopers toevoegen voor (i = 0; i < 4; i++) entities.add(new Runner(this.game, this.game.world.width * Math.random(), this.game.world.height * Math.random())); ; // (… ) ;
Het resultaat is dat vier lopers willekeurig rondlopen:
De Jager
klasse heeft de volgende structuur:
/ ** * Deze les beschrijft een entiteit die * alleen ronddwaalt om de lopers die voorbij komen te verwonden. * / Hunter = functie (game, x, y) // initialiseer Phaser-dingen hier; // Controleer of de entiteit geldig is, een hardloper is en zich binnen het aanvalsbereik bevindt. Hunter.prototype.canEntityBeAttacked = function (entity) return entity && entity! = This && (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(); ;
De jagers gaan ook rond, maar ze zullen schade toebrengen aan alle lopers die dichtbij zijn. Dit gedrag is geïmplementeerd in de bijwerken()
methode, waarbij alle entiteiten van het spel worden geïnspecteerd en agenten berichten verzenden over schade.
Het schadebericht is als volgt gemaakt:
msg = nieuw bericht (entiteit, dit, "schade", 2);
Het bericht bevat de informatie over de bestemming (entiteit
, in dit geval, wat de entiteit is die in de huidige iteratie wordt geanalyseerd), de afzender (deze
, die de jager vertegenwoordigt die de aanval uitvoert), het type bericht ("schade"
) en de hoeveelheid schade (2
, in dit geval toegewezen aan de gegevens
veld van het bericht).
Het bericht wordt vervolgens via de opdracht op de bestemming gepost this.getMessageQueue (). toe te voegen (msg)
, die het nieuw gecreëerde bericht toevoegt aan de berichtenwachtrij.
Ten slotte voegen we de Jager
naar de lijst met entiteiten in de PlayState
:
var PlayState = function () // (...) this.create = function () // (...) // Voeg jager toe op positie (20, 30) entities.add (nieuwe Hunter (this.game, 20, 30) )); ; // (...);
Het resultaat is een paar lopers die rondlopen en berichten ontvangen van de jager als ze dicht bij elkaar komen:
Ik voegde de vliegende enveloppen toe als een visueel hulpmiddel om te laten zien wat er aan de hand is.
De heler
klasse heeft de volgende structuur:
/ ** * Deze klasse beschrijft een entiteit die * in staat is om elke loper die dichtbij komt te genezen. * / Healer = functie (game, x, y) // Initializer Phaser-dingen hier; Healer.prototype.update = function () var-entiteiten, i, grootte, entiteit, msg; // De lijst met entiteiten in de spelentiteiten = 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(); ;
De code en structuur lijken erg op de Jager
klas, behalve enkele verschillen. Op dezelfde manier als de uitvoering van de jager, de genezer bijwerken()
methode itereert over de lijst met entiteiten in het spel, en verzendt elke entiteit binnen zijn healingbereik:
msg = nieuw bericht (entiteit, deze, "genezen", 2);
Het bericht heeft ook een bestemming (entiteit
), een afzender (deze
, welke is de genezer die de actie uitvoert), een berichttype ("genezen"
) en het aantal genezingspunten (2
, toegewezen in de gegevens
veld van het bericht).
We voegen het toe heler
naar de lijst met entiteiten in de PlayState
op dezelfde manier als wij met de Jager
en het resultaat is een scène met lopers, een jager en een genezer:
En dat is het! We hebben drie verschillende entiteiten die op elkaar inwerken door berichten uit te wisselen.
Dit berichtenwachtrijsysteem is een veelzijdige manier om interacties in een game te beheren. De interacties worden uitgevoerd via een communicatiekanaal dat uniform is en een enkele interface heeft die gemakkelijk te gebruiken en implementeren is.
Naarmate uw spel complexer wordt, zijn mogelijk nieuwe interacties nodig. Sommigen van hen kunnen volledig onverwacht zijn, dus je moet je code aanpassen om ermee om te gaan. Als u een berichtenwachtersysteem gebruikt, is dit een kwestie van ergens een nieuw bericht toevoegen en dit in een ander bericht verwerken.
Stel u bijvoorbeeld voor dat u het wilt maken Jager
interactie met de heler
; je moet gewoon het maken Jager
stuur een bericht met de nieuwe interactie, bijvoorbeeld, "vluchten"
-en zorg ervoor dat de heler
kan het in de OnMessage
methode:
// In de klasse Hunter: Hunter.prototype.someMethod = function () // een verwijzing ophalen naar een nabijgelegen genezer var healer = this.getNearbyHealer (); // Maak een bericht over het ontvluchten van een plaats var place = x: 30, y: 40; var msg = new Message (entity, this, "flee", place); // Stuur het bericht weg! this.getMessageQueue () toe te voegen (msg).; ; // In de klasse Healer: Healer.prototype.OnMessage = function (message) if (message.type == "flee") // Haal de plaats op om te vluchten voor het gegevensveld in het bericht var place = message.data ; // Gebruik de plaatsinformatie op de vlucht (place.x, place.y); ;
Hoewel het uitwisselen van berichten tussen entiteiten nuttig kan zijn, denkt u misschien wel waarom message queue
is tenslotte nodig. Kun je niet gewoon de ontvanger aanroepen OnMessage ()
methode zelf in plaats van te vertrouwen op de message queue
, zoals in de onderstaande code?
Hunter.prototype.someMethod = function () // Krijg een verwijzing naar een nabijgelegen genezer var healer = this.getNearbyHealer (); // Maak een bericht over het ontvluchten van een plaats var place = x: 30, y: 40; var msg = new Message (entity, this, "flee", place); // Omzeil de MessageQueue en lever direct // de boodschap aan de genezer. healer.onMessage (msg); ;
Je zou zo'n berichtensysteem zeker kunnen implementeren, maar het gebruik van een message queue
heeft een paar voordelen.
Door bijvoorbeeld de verzending van berichten te centraliseren, kunt u enkele coole functies implementeren zoals vertraagde berichten, de mogelijkheid om een groep entiteiten te melden en visuele foutopsporingsinformatie (zoals de vliegende enveloppen die in deze zelfstudie worden gebruikt).
Er is ruimte voor creativiteit in de message queue
Klasse, het is aan jou en de vereisten van je spel.
Het afhandelen van interacties tussen game-entiteiten met behulp van een message-queuesysteem is een manier om uw code georganiseerd en klaar voor de toekomst te houden. Nieuwe interacties kunnen gemakkelijk en snel worden toegevoegd, zelfs uw meest complexe ideeën, zolang ze zijn ingekapseld als berichten.
Zoals besproken in de zelfstudie, kunt u het gebruik van een centrale berichtenwachtrij negeren en berichten rechtstreeks naar de entiteiten verzenden. U kunt de communicatie ook centraliseren met een verzending (de message queue
klasse in ons geval) om in de toekomst ruimte te maken voor nieuwe functies, zoals vertraagde berichten.
Ik hoop dat je deze aanpak nuttig vindt en deze aan je hulpprogrammaengamma voor ontwikkelaars toevoegt. De methode lijkt misschien te overdreven voor kleine projecten, maar het zal je op de lange termijn zeker wat hoofdbrekens besparen voor grotere games.