Maak een Hockey Game AI met behulp van stuurgedrag spelmechanica

In eerdere berichten in deze serie hebben we ons gericht op de concepten achter de kunstmatige intelligentie die we hebben leren kennen. In dit deel verpakken we de implementatie in een volledig speelbaar hockeyspel. Je leert hoe je de ontbrekende stukjes kunt toevoegen die nodig zijn om dit in een spel te veranderen, zoals score, power-ups en een beetje game-ontwerp.

Eindresultaat

Hieronder vindt u de game die wordt geïmplementeerd met alle elementen die in deze zelfstudie worden beschreven.

Thinking Game Design

De vorige delen van deze serie waren gericht op het uitleggen hoe de game AI werkt. Elk deel detailleerde een bepaald aspect van het spel, zoals hoe atleten bewegen en hoe aanval en verdediging worden geïmplementeerd. Ze waren gebaseerd op concepten zoals stuurgedrag en stack-gebaseerde eindige-toestandsmachines.

Om een ​​volledig speelbare game te maken, moeten al die aspecten echter in een kern worden ingepakt spelmonteur. De meest voor de hand liggende keuze zou zijn om alle officiële regels voor een officiële hockeywedstrijd te implementeren, maar dat zou veel werk en tijd vergen. Laten we in plaats daarvan een eenvoudiger fantasiebenadering nemen.

Alle hockeyregels worden vervangen door een enkele: als je de puck draagt ​​en wordt aangeraakt door een tegenstander, bevriest en valt je uiteen in een miljoen stukjes! Het maakt het spel eenvoudiger te spelen en leuk voor beide spelers: degene met de puck en degene die probeert het te herstellen.

Om deze monteur te verbeteren, voegen we een paar power-ups toe. Ze helpen de speler te scoren en maken het spel een beetje dynamischer.

Het vermogen om te scoren toevoegen

Laten we beginnen met het scoresysteem, verantwoordelijk voor het bepalen wie wint of verliest. Een team scoort elke keer dat de puck het doel van de tegenstander bereikt.

De gemakkelijkste manier om dit te implementeren is door twee overlappende rechthoeken te gebruiken:

Overlappende rechthoeken die het doelgebied beschrijven. Als de puck in botsing komt met de rode rechthoek, scoort het team.

De groene rechthoek geeft het gebied weer dat wordt ingenomen door de doelstructuur (het frame en het net). Het werkt als een solide blok, zodat de puck en de atleten er niet doorheen kunnen bewegen; ze zullen terugveren.

De rode rechthoek geeft het "scoregebied" weer. Als de puck deze rechthoek overlapt, betekent dit dat een team net heeft gescoord.

De rode rechthoek is kleiner dan de groene en wordt ervoor geplaatst, dus als de puck het doel raakt aan elke kant maar aan de voorkant, dan stuitert hij terug en wordt er geen score toegevoegd:

Een paar voorbeelden van hoe de puck zich zou gedragen als hij de rechthoeken tijdens het bewegen zou raken.

Alles organiseren nadat iemand scoort

Nadat een team scoort, moeten alle atleten terugkeren naar hun oorspronkelijke positie en moet de puck opnieuw in het midden van de ijsbaan worden geplaatst. Na dit proces kan de match worden voortgezet.

Atleten naar hun oorspronkelijke positie verplaatsen

Zoals uitgelegd in het eerste deel van deze serie, hebben alle atleten een AI-status genoemd prepareForMatch dat zal hen in de oorspronkelijke positie brengen en ervoor zorgen dat ze daar soepel tot stilstand komen.

Wanneer de puck een van de "scoregebieden" overlapt, wordt elke momenteel actieve AI-status van alle atleet verwijderd en prepareForMatch wordt in de hersenen geduwd. Waar de atleten ook zijn, na enkele seconden zullen ze terugkeren naar hun oorspronkelijke positie:

De camera naar de piste verplaatsen

Aangezien de camera altijd de puck volgt, wordt de huidige weergave abrupt gewijzigd als deze rechtstreeks naar het centrum van de ijsbaan wordt geteleporteerd nadat iemand heeft gescoord, wat lelijk en verwarrend zou zijn.

Een betere manier om dit te doen is door de puck soepel naar het midden van de ijsbaan te verplaatsen; aangezien de camera de puck volgt, schuift dit sierlijk het zicht van het doel naar het midden van de ijsbaan. 

Dit kan worden bereikt door de snelheidsvector van de puck te veranderen nadat deze een doelgebied heeft geraakt. De nieuwe snelheidsvector moet de puck "duwen" naar het midden van de ijsbaan, zodat deze kan worden berekend als:

var c: Vector3D = getRinkCenter (); var p: Vector3D = puck.position; var v: Vector3D = c - p; v = normaliseren (v) * 100; puck.velocity = v;

Door de positie van het ijsbaancentrum af te trekken van de huidige positie van de puck, is het mogelijk om een ​​vector te berekenen die direct naar het ijsbaancentrum wijst.

Nadat deze vector is genormaliseerd, kan deze op elke waarde worden geschaald, zoals 100, die bepaalt hoe snel de puck beweegt naar het midden van de ijsbaan.

Hieronder is een afbeelding met een weergave van de nieuwe snelheidsvector:

Berekening van een nieuwe snelheidsvector die de puck naar het midden van de ijsbaan zal bewegen.

Deze vector V wordt gebruikt als de snelheidsvector van de puck, dus de puck zal naar het midden van de ijsbaan bewegen zoals bedoeld.

Om vreemd gedrag te voorkomen terwijl de puck in de richting van het midden van de ijsbaan beweegt, zoals een interactie met een atleet, wordt de puck gedeactiveerd tijdens het proces. Als gevolg hiervan stopt het de interactie met atleten en wordt het gemarkeerd als onzichtbaar. De speler ziet de puck niet bewegen, maar de camera zal deze nog steeds volgen.

Om te beslissen of de puck al in positie is, wordt de afstand tussen deze puck en het midden van de ijsbaan berekend tijdens de beweging. Als het minder is dan 10, de puck is bijvoorbeeld dichtbij genoeg om direct in het midden van de ijsbaan te worden geplaatst en opnieuw te worden geactiveerd, zodat de wedstrijd kan doorgaan.

Power-ups toevoegen

Het idee achter power-ups is om de speler te helpen het primaire doel van het spel te bereiken, namelijk scoren door de puck naar het doel van de tegenstander te dragen.

Omwille van de reikwijdte heeft ons spel maar twee power-ups: Ghost Help en Angst voor de puck. De eerstgenoemde voegt drie extra atleten toe aan het team van de speler voor een tijdje, terwijl de laatste de tegenstanders voor een paar seconden van de puck laat vluchten.

Power-ups worden aan beide teams toegevoegd wanneer iemand scoort.

Implementatie van de "Ghost Help" Power-up

Aangezien alle atleten zijn toegevoegd door de Ghost Help power-up is tijdelijk, de Atleet klasse moet worden aangepast om een ​​atleet toe te staan ​​om als een "geest" te worden gemarkeerd. Als een atleet een geest is, zal deze zichzelf na een paar seconden uit het spel verwijderen.

Hieronder staat de Atleet klasse, alleen de toevoegingen gemarkeerd die gemaakt zijn om de geestfunctionaliteit te accommoderen:

public class Athlete // (...) private var mGhost: Boolean; // vertelt of de atleet een geest is (een power-up die nieuwe atleten toevoegt om de puck te stelen). private var mGhostCounter: Number; // telt de tijd dat een geest actief blijft. Openbare functie Atleet (thePosX: Number, thePosY: Number, theTotalMass: Number) // (...) mGhost = false; mGhostCounter = 0; // (...) public function setGhost (theStatus: Boolean, theDuration: Number): void mGhost = theStatus; mGhostCounter = theDuration;  openbare functie amIAGhost (): Boolean return mGhost;  public function update (): void // (...) // Update powerup-counters en stuff updatePowerups (); // (...) update voor publieke functie updatePowerups (): void // TODO. 

Het eigendom mGhost is een booleaans die aangeeft of de atleet een geest is of niet, terwijl mGhostCounter bevat het aantal seconden dat de sporter moet wachten voordat hij zichzelf uit het spel verwijdert.

Die twee eigenschappen worden gebruikt door de updatePowerups () methode:

persoonlijke functie updatePowerups (): void // Als de atleet een geest is, heeft deze een teller die // bestuurt wanneer deze moet worden verwijderd. if (amIAGhost ()) mGhostCounter - = time_elapsed; if (mGhostCounter <= 2)  // Make athlete flicker when it is about to be removed. flicker(0.5);  if (mGhostCounter <= 0)  // Time to leave this world! (again) kill();   

De updatePowerups () methode, genoemd binnen de atleet bijwerken() routine, zal alle power-up verwerking in de atleet afhandelen. Op dit moment is het alleen maar controleren of de huidige atleet een geest is of niet. Als dat zo is, dan is het mGhostCounter eigenschap wordt verlaagd met de hoeveelheid tijd die is verstreken sinds de laatste update.

Wanneer de waarde van mGhostCounter bereikt nul, dit betekent dat de tijdelijke atleet lang genoeg actief is geweest, dus hij moet zichzelf uit het spel verwijderen. Om de speler hiervan bewust te maken, begint de atleet de laatste twee seconden te knipperen voordat hij verdwijnt.

Ten slotte is het tijd om het proces van het toevoegen van de tijdelijke sporters te implementeren wanneer de power-up is geactiveerd. Dat wordt uitgevoerd in de powerupGhostHelp () methode, beschikbaar in de spellogica:

private function powerupGhostHelp (): void var aAthlete: Athlete; for (var i: int = 0; i < 3; i++)  // Add the new athlete to the list of athletes aAthlete = addAthlete(RINK_WIDTH / 2, RINK_HEIGHT - 100); // Mark the athlete as a ghost which will be removed after 10 seconds. aAthlete.setGhost(true, 10);  

Deze methode itereert over een lus die overeenkomt met het aantal tijdelijke atleten dat wordt toegevoegd. Elke nieuwe atleet wordt toegevoegd aan de onderkant van de baan en gemarkeerd als een geest. 

Zoals eerder beschreven, zullen ghost-atleten zichzelf uit het spel verwijderen.

Implementatie van de Power-Up "Fear The Puck"

De Angst voor de puck power-up zorgt ervoor dat alle tegenstanders een paar seconden de puck verlaten. 

Net als de Ghost Help power-up, de Atleet klasse moet worden aangepast om aan die functionaliteit te voldoen:

openbare klas Atleet // (...) private var mFearCounter: Number; // telt de tijd die de sporter moet ontwijken uit puck (wanneer powerup van angst actief is). openbare functie Atleet (thePosX: Number, thePosY: Number, theTotalMass: Number) // (...) mFearCounter = 0; // (...) public function fearPuck (theDuration: Number = 2): void mFearCounter = theDuration;  // Geeft true als de mFearCounter een waarde heeft en de atleet // niet inactief is of zich voorbereidt op een match. private function shouldIEvadeFromPuck (): Boolean return mFearCounter> 0 && mBrain.getCurrentState ()! = idle && mBrain.getCurrentState ()! = prepareForMatch;  private function updatePowerups (): void if (mFearCounter> 0) mFearCounter - = elapsed_time;  // (...) update openbare functie (): void // (...) // Update powerup-counters en stuff updatePowerups (); // Als de atleet een AI-gestuurde tegenstander is als (amIAnAiControlledOpponent ()) // Controleer of "angst voor de puck" power-up actief is. // Als dat waar is, ontwijk dan van puck. if (shouldIEvadeFromPuck ()) evadeFromPuck ();  // (...) public function evadeFromPuck (): void // TODO

Eerst de updatePowerups () methode is gewijzigd om het te verlagen mFearCounter eigenschap, die de tijd bevat dat de atleet de puck moet vermijden. De mFearCounter eigenschap wordt elke keer de methode gewijzigd fearPuck () wordt genoemd.

In de Atleet's bijwerken() methode, wordt een test toegevoegd om te controleren of de power-up moet plaatsvinden. Als de atleet een tegenstander is die wordt bestuurd door de AI (amIAnAiControlledOpponent () komt terug waar) en de atleet moet de puck ontwijken (shouldIEvadeFromPuck () komt terug waar ook), de evadeFromPuck () methode wordt aangeroepen.

De evadeFromPuck () methode maakt gebruik van het ontwijkgedrag, waardoor een entiteit elk object en zijn traject helemaal vermijdt:

private function evadeFromPuck (): void mBoid.steering = mBoid.steering + mBoid.evade (getPuck (). getBoid ()); 

Al de evadeFromPuck () methode is om een ​​ontwijkende kracht toe te voegen aan de stuurkracht van de huidige atleet. Het zorgt ervoor dat hij de puck ontwijkt zonder de reeds toegevoegde stuurkrachten te negeren, zoals degene die is gemaakt door de actieve AI-status.

Om ontastbaar te zijn, moet de puck zich gedragen als een boid, zoals alle atleten doen (meer informatie hierover in het eerste deel van de serie). Dientengevolge moet een boid-eigenschap, die de huidige positie en snelheid van de puck bevat, worden toegevoegd aan de puck klasse:

class Puck // (...) private var mBoid: Boid; // (...) update openbare functie () // (...) mBoid.update ();  openbare functie getBoid (): Boid return mBoid;  // (...)

Ten slotte werken we de hoofdlogica bij om tegenstanders bang te maken voor de puck wanneer de power-up wordt geactiveerd:

privéfunctie powerupFearPuck (): void var i: uint, atleten: Array = rightTeam.members, size: uint = athletes.length; voor (i = 0; i < size; i++)  if (athletes[i] != null)  // Make athlete fear the puck for 3 seconds. athletes[i].fearPuck(3);   

De methode itereert over alle sporters van de tegenstander (in dit geval het juiste team), en roept de fearkPuck () methode van elk van hen. Dit zal de logica activeren waardoor de atleten de puck gedurende een paar seconden vrezen, zoals eerder uitgelegd.

Bevriezen en verbrijzelen

De laatste toevoeging aan het spel is het bevriezende en versplinterende gedeelte. Het wordt uitgevoerd in de hoofdlogica, waarbij een routine controleert of de atleten van het linkerteam overlappen met de atleten van het juiste team.

Deze overlappende controle wordt automatisch uitgevoerd door de Flixel-game-engine, die elke keer dat er een overlapping wordt gevonden een callback oproept:

privéfunctie atletenOverlapped (theLeftAthlete: Athlete, theRightAthlete: Athlete): void // Heeft de puck een eigenaar? if (mPuck.owner! = null) // Ja, dat klopt. if (mPuck.owner == theLeftAthlete) // De eigenaar van Puck is de linker atleet theLeftAthlete.shatter (); mPuck.setOwner (theRightAthlete);  else if (mPuck.owner == theRightAthlete) // De eigenaar van Puck is de juiste atleet theRightAthlete.shatter (); mPuck.setOwner (theLeftAthlete); 

Deze callback ontvangt als parameters de atleten van elk team dat overlapt. Een test controleert of de eigenaar van de puck niet nul is, wat betekent dat deze door iemand wordt gedragen.

In dat geval wordt de eigenaar van de puck vergeleken met de atleten die elkaar net overlappen. Als een van hen de puck bij zich heeft (dus hij is de eigenaar van de puck), wordt hij verbrijzeld en gaat het eigendom van de puck over op de andere atleet.

De breken() methode in de Atleet klasse markeert de atleet als inactief en plaatst deze na een paar seconden op de bodem van de ijsbaan. Het zal ook verschillende deeltjes uitzenden die ijsstukken voorstellen, maar dit onderwerp zal in een andere post worden behandeld.

Conclusie

In deze tutorial hebben we enkele elementen geïmplementeerd die nodig zijn om van ons hockeyprototype een volledig speelbaar spel te maken. Ik heb bewust de focus gelegd op de concepten achter elk van die elementen, in plaats van hoe ze daadwerkelijk in game-engine X of Y kunnen worden geïmplementeerd.

De bevriezing en verbrijzeling van de game klinkt misschien te fantastisch, maar helpt het project beheersbaar te houden. Sportregels zijn heel specifiek en de uitvoering ervan kan lastig zijn.

Door een paar schermen en enkele HUD-elementen toe te voegen, kun je vanuit deze demo je eigen volledige hockeyspel maken!

Referenties

  • Rink: Hockey Stadium op GraphicRiver
  • Sprites: Hockey Players door Taylor J Glidden
  • Pictogrammen: Game-Icons door Lorc
  • Muiscursor: Cursor door Iwan Gabovitch
  • Instructietoetsen: Keyboard Pack van Nicolae Berbece
  • Crosshair: Crosshairs Pack van Bryan
  • SFX / Music: shatter by Michel Baradari, puck hit and cheer door gr8sfx, music by DanoSongs.com