In deze tutorial blijven we kunstmatige intelligentie coderen voor een hockeywedstrijd met behulp van stuurgedrag en eindige toestandsmachines. In dit deel van de serie leer je over de AI die game-entiteiten nodig hebben om een aanval te coördineren, waarbij de puck wordt onderschept en naar het doel van de tegenstander wordt gedragen.
Het coördineren en uitvoeren van een aanval in een coöperatieve sportgame is een zeer complexe taak. In de echte wereld, wanneer mensen een hockeywedstrijd spelen, maken ze verscheidene beslissingen gebaseerd op vele variabelen.
Die beslissingen houden berekeningen in en begrijpen wat er aan de hand is. Een mens kan vertellen waarom een tegenstander beweegt op basis van de acties van een andere tegenstander, bijvoorbeeld: "hij komt in een betere strategische positie terecht." Het is niet triviaal om dat begrip naar een computer over te dragen.
Als gevolg hiervan, als we proberen de AI te coderen om alle menselijke nuances en percepties te volgen, zal het resultaat een enorme en enge stapel code zijn. Bovendien is het resultaat mogelijk niet precies of eenvoudig aanpasbaar.
Dat is de reden waarom onze aanval AI zal proberen om het na te bootsen resultaat van een groep mensen die speelt, niet de menselijke perceptie zelf. Die benadering zal leiden tot benaderingen, maar de code zal gemakkelijker te begrijpen en aan te passen zijn. Het resultaat is goed genoeg voor verschillende gebruikscasussen.
We breken het aanvalsproces op in kleinere stukjes, met elk een zeer specifieke actie. Die stukken zijn de toestanden van een op een stapel gebaseerde eindige toestandsmachine. Zoals eerder uitgelegd, zal elke staat een stuurkracht produceren die ervoor zorgt dat de atleet zich overeenkomstig gedraagt.
De orkestratie van die staten en de voorwaarden om daarover te schakelen, zullen de aanval bepalen. De onderstaande afbeelding geeft de volledige FSM weer die in het proces is gebruikt:
Een op een stapel gebaseerde eindige toestandsmachine die het aanvalsproces vertegenwoordigt.Zoals geïllustreerd door het beeld, zijn de voorwaarden om tussen de staten te schakelen uitsluitend gebaseerd op de afstand en het eigendom van de puck. Bijvoorbeeld, team heeft de puck
of puck is te ver weg
.
Het aanvalsproces bestaat uit vier staten: nutteloos
, aanval
, stealPuck
, en pursuePuck
. De nutteloos
status was al geïmplementeerd in de vorige tutorial en het is het startpunt van het proces. Van daaruit gaat een atleet naar aanval
als het team de puck heeft, naar stealPuck
als het team van de tegenstander de puck heeft of naar pursuePuck
als de puck geen eigenaar heeft en deze dichtbij genoeg is om te worden verzameld.
De aanval
staat vertegenwoordigt een aanvallende beweging. Terwijl in die staat, de atleet die de puck draagt (genaamd leider
) zal proberen het doel van de tegenstander te bereiken. Teamgenoten gaan mee en proberen de actie te ondersteunen.
De stealPuck
staat vertegenwoordigt iets tussen een defensieve en een aanvallende beweging. Terwijl in die staat, zal een atleet zich richten op het nastreven van de tegenstander die de puck draagt. Het doel is om de puck te herstellen, zodat het team weer kan aanvallen.
eindelijk, de pursuePuck
staat is niet gerelateerd aan aanval of verdediging; het zal de atleten gewoon leiden wanneer de puck geen eigenaar heeft. Terwijl in die staat, zal een atleet proberen de puck te krijgen die zich vrij op de ijsbaan beweegt (bijvoorbeeld nadat iemand door een stok is geraakt).
De nutteloos
staat die eerder geïmplementeerd was, geen overgangen had. Omdat deze status het startpunt is voor de hele AI, laten we het bijwerken en het in staat stellen over te schakelen naar andere toestanden.
De nutteloos
staat heeft drie overgangen:
Als het team van de atleet de puck heeft, nutteloos
zou uit de hersenen moeten worden geplukt en aanval
moet worden ingedrukt. Evenzo, als het team van de tegenstander de puck heeft, nutteloos
moet worden vervangen door stealPuck
. De overblijvende overgang gebeurt wanneer niemand de puck bezit en deze zich dicht bij de atleet bevindt; in dat geval, pursuePuck
moet in de hersenen worden geduwd.
De bijgewerkte versie van nutteloos
is als volgt (alle andere staten zullen later worden geïmplementeerd):
class Athlete // (...) private function idle (): void var aPuck: Puck = getPuck (); stopAndlookAt (aPuck); // Dit is een hack om de AI te testen. als (mStandStill) terugkomt; // Heeft de puck een eigenaar? if (getPuckOwner ()! = null) // Ja, dat is het geval. mBrain.popState (); if (doesMyTeamHaveThePuck ()) // Mijn team heeft net de puck gekregen, het is attack time! mBrain.pushState (attack); else // Het tegenspelers team heeft de puck, laten we proberen het te stelen. mBrain.pushState (stealPuck); else if (afstand (dit, aPuck) < 150) // The puck has no owner and it is nearby. Let's pursue it. mBrain.popState(); mBrain.pushState(pursuePuck); private function attack() :void private function stealPuck() :void private function pursuePuck() :void
Laten we doorgaan met de implementatie van de andere staten.
Nu heeft de sporter enige perceptie over de omgeving opgedaan en kan hij overschakelen van nutteloos
in welke staat dan ook, laten we ons concentreren op het nastreven van de puck wanneer deze geen eigenaar heeft.
Een atleet schakelt over naar pursuePuck
onmiddellijk nadat de wedstrijd begint, omdat de puck zonder eigenaar in het midden van de baan zal worden geplaatst. De pursuePuck
staat heeft drie overgangen:
De eerste overgang is puck is te ver weg
, en het probeert te simuleren wat er gebeurt in een echte game met betrekking tot het achtervolgen van de puck. Om strategische redenen is de atleet die het dichtst bij de puck is degene die probeert hem te vangen, terwijl de anderen wachten of proberen te helpen.
Zonder over te schakelen naar nutteloos
wanneer de puck ver weg is, zou elke AI-gecontroleerde atleet de puck tegelijkertijd achtervolgen, zelfs als ze er niet bij zijn. Door de afstand tussen de atleet en de puck te controleren, pursuePuck
springt uit het brein en duwt nutteloos
wanneer de puck te ver weg is, wat betekent dat de atleet gewoon "het opgeven" van de puck heeft opgegeven:
class Atleet // (...) privéfunctie pursuePuck (): void var aPuck: Puck = getPuck (); if (distance (this, aPuck)> 150) // Puck is te ver weg van onze huidige positie, dus laten we opgeven // de puck nastreven en hopen dat iemand dichter bij de puck komt // voor ons. mBrain.popState (); mBrain.pushState (idle); else // De puck is dichtbij, laten we proberen hem te grijpen. // (...)
Wanneer de puck in de buurt is, moet de atleet er achteraan gaan, wat gemakkelijk kan worden bereikt met het zoekgedrag. Gebruikmakend van de positie van de puck als de zoekbestemming, zal de atleet gracieus de puck najagen en zijn baan aanpassen terwijl de puck beweegt:
class Atleet // (...) privéfunctie pursuePuck (): void var aPuck: Puck = getPuck (); mBoid.steering = mBoid.steering + mBoid.separation (); if (distance (this, aPuck)> 150) // Puck is te ver weg van onze huidige positie, dus laten we opgeven // de puck nastreven en hopen dat iemand dichter bij de puck komt // voor ons. mBrain.popState (); mBrain.pushState (idle); else // De puck is dichtbij, laten we proberen hem te grijpen. if (aPuck.owner == null) // Niemand heeft de puck, het is onze kans om het te zoeken en te krijgen! mBoid.steering = mBoid.steering + mBoid.seek (aPuck.position); else // Iemand heeft net de puck ontvangen. Als de nieuwe puck-eigenaar tot mijn team behoort, // moeten we overschakelen naar 'attack', anders moet ik overschakelen naar 'stealPuck' // en probeer de puck terug te krijgen. mBrain.popState (); mBrain.pushState (doesMyTeamHaveThePuck ()? attack: stealPuck);
De resterende twee overgangen in de pursuePuck
staat, team heeft de puck
en tegenstander heeft de puck
, zijn gerelateerd aan het vangen van de puck tijdens het vervolgproces. Als iemand de puck vangt, moet de atleet de pursuePuck
staat en duwt een nieuwe in de hersenen.
De te pushen status is afhankelijk van het eigendom van de puck. Als de oproep naar doesMyTeamHaveThePuck ()
komt terug waar
, het betekent dat een teamgenoot de puck heeft, dus de atleet moet duwen aanval
, wat betekent dat het tijd is om te stoppen met het nastreven van de puck en beginnen te bewegen naar het doel van de tegenstander. Als een tegenstander de puck heeft, moet de atleet duwen stealPuck
, waardoor het team probeert de puck te herstellen.
Als een kleine verbetering mogen atleten niet te dicht bij elkaar blijven tijdens de pursuePuck
staat, omdat een "drukke" achtervolgende beweging onnatuurlijk is. Scheiding toevoegen aan de stuurkracht van de staat (lijn 6
in de bovenstaande code) zorgt ervoor dat atleten een minimale afstand tussen hen houden.
Het resultaat is een team dat in staat is om de puck na te streven. Om te testen, in deze demo, wordt de puck om de paar seconden in het midden van de ijsbaan geplaatst om de atleten continu te laten bewegen:
Na het behalen van de puck moeten een atleet en zijn team zich naar het doel van de tegenstander begeven om te scoren. Dat is het doel van de aanval
staat:
De aanval
staat heeft slechts twee overgangen: tegenstander heeft de puck
en puck heeft geen eigenaar
. Omdat de staat alleen is ontworpen om atleten naar het doel van de tegenstander te laten bewegen, heeft het geen zin om te blijven aanvallen als de puck niet meer in het bezit is van het team..
Wat betreft de beweging naar het doel van de tegenstander: de atleet met de puck (leider) en de teamgenoten die hem helpen, moeten zich anders gedragen. De leider moet het doel van de tegenstander bereiken en de teamgenoten moeten hem onderweg helpen.
Dit kan worden geïmplementeerd door te controleren of de atleet die de code uitvoert de puck heeft:
class Athlete // (...) private function attack (): void var aPuckOwner: Athlete = getPuckOwner (); // Heeft de puck een eigenaar? if (aPuckOwner! = null) // Ja, dat is het geval. Laten we kijken of de eigenaar tot het team van de tegenstander behoort. if (doesMyTeamHaveThePuck ()) if (amIThePuckOwner ()) // Mijn team heeft de puck en ik ben degene die het heeft! Laten we // naar het doel van de tegenstander bewegen. mBoid.steering = mBoid.steering + mBoid.seek (getOpponentGoalPosition ()); else // Mijn team heeft de puck, maar een teamgenoot heeft het. Laten we hem volgen // om wat steun te bieden tijdens de aanval. mBoid.steering = mBoid.steering + mBoid.followLeader (aPuckOwner.boid); mBoid.steering = mBoid.steering + mBoid.separation (); else // De tegenstander heeft de puck! Stop de aanval // en probeer het te stelen. mBrain.popState (); mBrain.pushState (stealPuck); else // Puck heeft geen eigenaar, dus het heeft geen zin om te blijven // aan te vallen. Het is tijd om opnieuw te organiseren en de puck te gaan nastreven. mBrain.popState (); mBrain.pushState (pursuePuck);
Als amIThePuckOwner ()
komt terug waar
(regel 10), heeft de atleet die de code uitvoert de puck. In dat geval zal hij alleen de doelpositie van de tegenstander opzoeken. Dat is vrijwel dezelfde logica die wordt gebruikt om de puck in de pursuePuck
staat.
Als amIThePuckOwner ()
komt terug vals
, de atleet heeft de puck niet, dus hij moet de leider helpen. Het helpen van de leider is een gecompliceerde taak, dus we zullen het vereenvoudigen. Een atleet zal de leider helpen door alleen een positie voor hem te zoeken:
Als de leider beweegt, wordt hij omringd door teamgenoten als ze de volgen verder
punt. Dit geeft de leider enkele opties om de puck door te geven aan als er problemen zijn. Net als in een echte game, moeten de omliggende teamgenoten ook uit de buurt blijven van de leider.
Dit hulppatroon kan worden bereikt door een enigszins aangepaste versie van het leidende volgende gedrag toe te voegen (regel 18). Het enige verschil is dat atleten een punt zullen volgen verder van de leider, in plaats van één achter hem zoals oorspronkelijk geïmplementeerd in dat gedrag.
Atleten die de leider helpen, moeten ook een minimale afstand tussen elkaar houden. Dat wordt geïmplementeerd door een scheidingskracht toe te voegen (regel 19).
Het resultaat is een team dat in staat is om te bewegen naar het doel van de tegenstander, zonder verdringing en tijdens het simuleren van een geassisteerde aanvalsbeweging:
De huidige implementatie van de aanval
staat is goed genoeg voor sommige situaties, maar het heeft een fout. Wanneer iemand de puck vangt, wordt hij de leider en wordt hij onmiddellijk gevolgd door teamgenoten.
Wat gebeurt er als de leider op weg is naar zijn eigen doel wanneer hij de puck vangt? Bekijk de demo hierboven en merk het onnatuurlijke patroon op wanneer teamgenoten de leider gaan volgen.
Wanneer de leider de puck vangt, neemt het zoekgedrag enige tijd in beslag om het traject van de leider te corrigeren en hem effectief naar het doel van de tegenstander te laten gaan. Zelfs wanneer de leider "manoeuvreert", zullen teamgenoten proberen de zijne te vinden verder
punt, wat betekent dat ze naar toe zullen bewegen hun eigen doel (of de plaats waar de leider naar kijkt).
Wanneer de leider eindelijk in positie is en klaar om te bewegen naar het doel van de tegenstander, zullen teamgenoten "manoeuvreren" om de leider te volgen. De leider zal dan zonder teamgenootondersteuning bewegen zolang de anderen hun trajecten aanpassen.
Deze fout kan worden opgelost door na te gaan of de teamgenoot voorloopt op de leider wanneer het team de puck herstelt. Hier betekent de voorwaarde "vooruit" "dichter bij het doel van de tegenstander":
class Atleet // (...) private function isAheadOfMe (theBoid: Boid): Boolean var aTargetDistance: Number = distance (getOpponentGoalPosition (), theBoid); var aMyDistance: Number = distance (getOpponentGoalPosition (), mBoid.position); return aTargetDistance <= aMyDistance; private function attack() :void var aPuckOwner :Athlete = getPuckOwner(); // Does the puck have an owner? if (aPuckOwner != null) // Yeah, it has. Let's find out if the owner belongs to the opponents team. if (doesMyTeamHaveThePuck()) if (amIThePuckOwner()) // My team has the puck and I am the one who has it! Let's move // towards the opponent's goal. mBoid.steering = mBoid.steering + mBoid.seek(getOpponentGoalPosition()); else // My team has the puck, but a teammate has it. Is he ahead of me? if (isAheadOfMe(aPuckOwner.boid)) // Yeah, he is ahead of me. Let's just follow him to give some support // during the attack. mBoid.steering = mBoid.steering + mBoid.followLeader(aPuckOwner.boid); mBoid.steering = mBoid.steering + mBoid.separation(); else // Nope, the teammate with the puck is behind me. In that case // let's hold our current position with some separation from the // other, so we prevent crowding. mBoid.steering = mBoid.steering + mBoid.separation(); else // The opponent has the puck! Stop the attack // and try to steal it. mBrain.popState(); mBrain.pushState(stealPuck); else // Puck has no owner, so there is no point to keep // attacking. It's time to re-organize and start pursuing the puck. mBrain.popState(); mBrain.pushState(pursuePuck);
Als de leider (die de eigenaar van de puck is) voorloopt op de atleet die de code uitvoert, moet de atleet de leider volgen zoals hij eerder deed (regels 27 en 28). Als de leider achter hem staat, moet de atleet zijn huidige positie behouden, waarbij hij een minimale afstand tussen de anderen moet houden (regel 33).
Het resultaat is een beetje overtuigender dan de initiaal aanval
implementatie:
Tip: Door de afstandberekeningen en vergelijkingen in de isAheadOfMe ()
methode is het mogelijk om de manier waarop sporters hun huidige posities behouden te wijzigen.
De laatste staat in het aanvallende proces is stealPuck
, die actief wordt wanneer het andere team de puck heeft. Het hoofddoel van de stealPuck
staat is om de puck te stelen van de tegenstander die hem draagt, zodat het team weer kan aanvallen:
Omdat het idee achter deze status is om de puck van de tegenstander te stelen, als de puck wordt hersteld door het team of als het wordt vrijgespeeld (dat wil zeggen, het heeft geen eigenaar), stealPuck
zal zichzelf uit het brein halen en de juiste staat pushen om met de nieuwe situatie om te gaan:
class Athlete // (...) private function stealPuck (): void // Heeft de puck een eigenaar? if (getPuckOwner ()! = null) // Ja, dat is het, maar wie heeft het? if (doesMyTeamHaveThePuck ()) // Mijn team heeft de puck, dus het is tijd om te stoppen met proberen / de puck te stelen en te gaan aanvallen. mBrain.popState (); mBrain.pushState (attack); else // Een tegenstander heeft de puck. var aOpponentLeader: Athlete = getPuckOwner (); // Laten we hem achtervolgen terwijl hij een bepaalde scheiding van // de anderen inneemt, om te voorkomen dat iedereen dezelfde // positie in achtervolgt. mBoid.steering = mBoid.steering + mBoid.pursuit (aOpponentLeader.boid); mBoid.steering = mBoid.steering + mBoid.separation (); else // De puck heeft geen eigenaar, hij loopt waarschijnlijk vrij rond op de ijsbaan. // Het heeft geen zin om te blijven proberen het te stelen, dus laten we de 'stealPuck'-status // voltooien en overschakelen naar' pursuePuck '. mBrain.popState (); mBrain.pushState (pursuePuck);
Als de puck een eigenaar heeft en hij behoort tot het team van de tegenstander, moet de atleet de tegenstander volgen en proberen de puck te stelen. Om de leider van de tegenstander te achtervolgen, moet een atleet dat doen voorspellen waar hij in de nabije toekomst zal zijn, zodat hij kan worden onderschept in zijn traject. Dat is iets anders dan alleen de tegenstander zoeken.
Gelukkig kan dit eenvoudig worden bereikt met het achtervolgingsgedrag (regel 19). Door een achtervolging te gebruiken in de stealPuck
staat, atleten zullen proberen onderscheppen de leider van de tegenstander, in plaats van hem alleen te volgen:
De huidige implementatie van stealPuck
werkt, maar in een echt spel naderen slechts één of twee atleten de tegenstander om de puck te stelen. De rest van het team blijft in de omliggende gebieden proberen om te helpen, wat een overrompeld stelen voorkomt.
Het kan worden opgelost door een afstandscontrole toe te voegen (regel 17) vóór de leiderachtervolging van de tegenstander:
class Athlete // (...) private function stealPuck (): void // Heeft de puck een eigenaar? if (getPuckOwner ()! = null) // Ja, dat is het, maar wie heeft het? if (doesMyTeamHaveThePuck ()) // Mijn team heeft de puck, dus het is tijd om te stoppen met proberen / de puck te stelen en te gaan aanvallen. mBrain.popState (); mBrain.pushState (attack); else // Een tegenstander heeft de puck. var aOpponentLeader: Athlete = getPuckOwner (); // Zit de tegenstander met de puck dicht bij me? if (afstand (aOpponentLeader, this) < 150) // Yeah, he is close! Let's pursue him while mantaining a certain // separation from the others to avoid that everybody will ocuppy the same // position in the pursuit. mBoid.steering = mBoid.steering.add(mBoid.pursuit(aOpponentLeader.boid)); mBoid.steering = mBoid.steering.add(mBoid.separation(50)); else // No, he is too far away. In the future, we will switch // to 'defend' and hope someone closer to the puck can // steal it for us. // TODO: mBrain.popState(); // TODO: mBrain.pushState(defend); else // The puck has no owner, it is probably running freely in the rink. // There is no point to keep trying to steal it, so let's finish the 'stealPuck' state // and switch to 'pursuePuck'. mBrain.popState(); mBrain.pushState(pursuePuck);
In plaats van blindelings de leider van de tegenstander na te streven, controleert een atleet of de afstand tussen hem en de tegenstander kleiner is dan bijvoorbeeld, 150
. Als dat zo is waar
, de achtervolging gebeurt normaal, maar als de afstand groter is dan 150
, het betekent dat de atleet te ver van de tegenstander is verwijderd.
Als dat gebeurt, heeft het geen zin door te gaan met het stelen van de puck, omdat deze te ver weg is en er waarschijnlijk al teamgenoten zijn die hetzelfde proberen te doen. De beste optie is pop stealPuck
uit de hersenen en druk op de verdediging
staat (wat in de volgende tutorial zal worden uitgelegd). Voor nu zal een atleet gewoon zijn huidige positie behouden als de tegenstander van de tegenstander te ver weg is.
Het resultaat is een meer overtuigend en natuurlijk stelenpatroon (geen verdringing):
Er is nog een laatste truc die de atleten moeten leren om effectief aan te vallen. Op dit moment bewegen ze zich naar het doel van de tegenstander zonder de tegenstanders onderweg te overwegen. Een tegenstander moet als een bedreiging worden gezien en moet worden vermeden.
Door gebruik te maken van het botsingsvermijdingsgedrag kunnen atleten tegenstanders ontwijken terwijl ze bewegen:
Botsingsvermijdingsgedrag dat wordt gebruikt om tegenstanders te vermijden.Tegenstanders worden gezien als cirkelvormige obstakels. Als gevolg van de dynamische aard van stuurgedrag, die wordt bijgewerkt in elke gamelus, werkt het vermijdingspatroon gracieus en soepel voor bewegende obstakels (wat hier het geval is).
Om te zorgen dat atleten tegenstanders (obstakels) vermijden, moet een enkele regel worden toegevoegd aan de aanvalsstaat (regel 14):
class Athlete // (...) private function attack (): void var aPuckOwner: Athlete = getPuckOwner (); // Heeft de puck een eigenaar? if (aPuckOwner! = null) // Ja, dat is het geval. Laten we kijken of de eigenaar tot het team van de tegenstander behoort. if (doesMyTeamHaveThePuck ()) if (amIThePuckOwner ()) // Mijn team heeft de puck en ik ben degene die het heeft! Laten we // naar het doel van de tegenstander bewegen, avonende tegenstanders langs de weg. mBoid.steering = mBoid.steering + mBoid.seek (getOpponentGoalPosition ()); mBoid.steering = mBoid.steering + mBoid.collisionAvoidance (getOpponentTeam (). leden); else // Mijn team heeft de puck, maar een teamgenoot heeft het. Loopt hij voor me uit? if (isAheadOfMe (aPuckOwner.boid)) // Ja, hij staat voor me. Laten we hem maar volgen om wat steun te bieden // tijdens de aanval. mBoid.steering = mBoid.steering + mBoid.followLeader (aPuckOwner.boid); mBoid.steering = mBoid.steering + mBoid.separation (); else // Nee, de teamgenoot met de puck staat achter me. In dat geval // laten we onze huidige positie vasthouden met enige afstand van de // andere, zodat we crowding voorkomen. mBoid.steering = mBoid.steering + mBoid.separation (); else // De tegenstander heeft de puck! Stop de aanval // en probeer het te stelen. mBrain.popState (); mBrain.pushState (stealPuck); else // Puck heeft geen eigenaar, dus het heeft geen zin om te blijven // aan te vallen. Het is tijd om opnieuw te organiseren en de puck te gaan nastreven. mBrain.popState (); mBrain.pushState (pursuePuck);
Deze lijn voegt een botsingsvermijdingskracht toe aan de atleet, die wordt gecombineerd met de krachten die al bestaan. Als gevolg hiervan zal de atleet obstakels vermijden op hetzelfde moment als het doel van de tegenstander zoeken.
Hieronder is een demonstratie van een atleet die de aanval
staat. Tegenstanders zijn onwrikbaar om het gedrag ter voorkoming van botsingen te benadrukken:
Deze tutorial verklaarde de implementatie van het aanvalspatroon dat de atleten gebruiken om te stelen en de puck naar het doel van de tegenstander te dragen. Door een combinatie van stuurgedrag te gebruiken, zijn atleten nu in staat complexe bewegingspatronen uit te voeren, zoals het volgen van een leider of het nastreven van de tegenstander met de puck.
Zoals eerder besproken, is de aanvalsimplementatie gericht op het simuleren van wat mensen do, dus het resultaat is een benadering van een echt spel. Door de staten die de aanval samenstellen aan te passen, kunt u een betere simulatie maken, of een simulatie die aan uw behoeften voldoet.
In de volgende tutorial leer je hoe je atleten kunt verdedigen. De AI wordt feature-compleet, in staat om aan te vallen en te verdedigen, resulterend in een match met 100% AI-gestuurde teams die tegen elkaar spelen.