Creați un joc de hochei AI Folosind comportamentul de direcție Atac

În acest tutorial, continuăm să codificăm inteligența artificială pentru un joc de hochei folosind comportamente de direcție și mașini de stare finită. În această parte a seriei, veți afla despre AI cerut de entitățile de joc pentru a coordona un atac, care implică interceptarea și transportarea pucului la obiectivul adversarului.

Câteva cuvinte despre atac

Coordonarea și efectuarea unui atac într-un joc de cooperare sportivă este o sarcină foarte complexă. În lumea reală, când oamenii joacă un joc de hochei, fac asta mai mulți deciziile bazate pe numeroase variabile.

Aceste decizii implică calcule și înțelegerea a ceea ce se întâmplă. Un om poate spune de ce un adversar se mișcă pe baza acțiunilor unui alt adversar, de exemplu "el se mișcă să fie într-o poziție strategică mai bună". Nu este banal să port asta înțelegere la un computer.

Ca o consecință, dacă încercăm să codificăm AI să urmeze toate nuanțele și percepțiile umane, rezultatul va fi o grămadă mare și înfricoșătoare de cod. În plus, rezultatul poate să nu fie precis sau ușor de modificat.

Acesta este motivul pentru care AI-ul nostru de atac va încerca să-l imite rezultat a unui grup de oameni care joacă, nu percepția umană în sine. Această abordare va duce la aproximări, dar codul va fi mai ușor de înțeles și de optimizat. Rezultatul este suficient de bun pentru mai multe cazuri de utilizare.

Organizarea atacului cu state

Vom rupe procesul de atac în bucăți mai mici, fiecare executând o acțiune foarte specifică. Aceste piese sunt stările unei mașini de stat finite. După cum sa explicat anterior, fiecare stat va produce o forță de conducere care va face ca atletul să se comporte în mod corespunzător.

Orchestrarea acestor state și condițiile de a schimba între ele vor defini atacul. Imaginea de mai jos prezintă FSM completă utilizată în proces:

O mașină de stat finită bazată pe stack reprezentând procesul de atac.

După cum ilustrează imaginea, condițiile de a schimba între state vor fi bazate exclusiv pe distanța și proprietatea pucului. De exemplu, echipa are puculsau pucul este prea departe.

Procesul de atac va fi compus din patru stări: inactiv, atac, stealPuck, și pursuePuck. inactiv statul a fost deja implementat în tutorialul anterior și este punctul de plecare al procesului. De acolo, un atlet va trece la atac în cazul în care echipa are puc, la stealPuck în cazul în care echipa adversarului are puc, sau la pursuePuck dacă pucul nu are proprietar și este suficient de aproape pentru a fi colectat.

atac stat reprezintă o mișcare ofensivă. În timp ce în acel stat, atletul care transporta pucul (numit lider) va încerca să atingă obiectivul adversarului. Colegii de echipă se vor deplasa, încercând să sprijine acțiunea.

stealPuck statul reprezintă ceva între o mișcare defensivă și o ofensivă. În timp ce în acel stat, un atlet se va concentra pe urmărirea adversarului care transportă pucul. Obiectivul este de a recupera pucul, astfel încât echipa poate începe să atace din nou.

În cele din urmă, pursuePuck statul nu are legătură cu atacul sau apărarea; va îndruma doar sportivii atunci când pucul nu are proprietar. În timp ce în acel stat, un sportiv va încerca să obțină pucul care se mișcă în mod liber pe patinoar (de exemplu, după ce a fost lovit de un cuiva).

Actualizarea stării de mers în gol

inactiv stat care a fost implementat anterior fără să aibă loc tranziții. Deoarece această stare este punctul de plecare pentru întreaga AI, să o actualizăm și să o putem schimba în alte state.

inactiv statul are trei tranziții:

 Starea inactivă și tranzițiile sale în FSM, care descriu procesul de atac.

Dacă echipa sportivului are pucul, inactiv ar trebui să fie aruncat din creier și atac ar trebui împins. În mod similar, dacă echipa adversarului are pucul, inactiv ar trebui înlocuit cu stealPuck. Restul de tranziție se întâmplă atunci când nimeni nu deține pucul și este aproape de atlet; în acest caz, pursuePuck ar trebui să fie împins în creier.

Versiunea actualizată a inactiv este după cum urmează (toate celelalte state vor fi implementate mai târziu):

clasa Atlet // (...) funcția privată idle (): void var aPuck: Puck = getPuck (); stopAndlookAt (aPuck); // Aceasta este o hack pentru a ajuta la testarea AI. dacă (mStandStill) returnează; // Are pucul un proprietar? dacă (getPuckOwner ()! = null) / Da, da. mBrain.popState (); dacă (doesMyTeamHaveThePuck ()) // Echipa mea tocmai a luat pucul, este momentul atacului! mBrain.pushState (atac);  altfel // Echipa adversarului a luat pucul, să încercăm să o furăm. mBrain.pushState (stealPuck);  altceva dacă (distanța (a, 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   

Să continuăm cu punerea în aplicare a celorlalte state.

Urmărirea puckului

Acum, că atletul a câștigat o anumită percepție despre mediul înconjurător și este capabil să treacă de la inactiv la orice stat, să ne concentrăm pe urmărirea pucului atunci când nu are proprietar.

Un atlet va trece la pursuePuck imediat după începerea meciului, deoarece pucul va fi plasat în centrul patinoarului fără proprietar. pursuePuck statul are trei tranziții:

Statul "pursuePuck" și tranzițiile sale în FSM care descriu procesul de atac.

Prima tranziție este pucul este prea departe, și încearcă să simuleze ceea ce se întâmplă într-un joc real în ceea ce privește urmărirea pucului. Din motive strategice, de obicei atletul cel mai apropiat de puc este cel care încearcă să îl prindă, în timp ce ceilalți așteaptă sau încearcă să ajute.

Fără a trece la inactiv când pucul este îndepărtat, fiecare sportiv controlat de AI ar urmări pucul în același timp, chiar dacă acesta este departe de el. Verificând distanța dintre atlet și puc, pursuePuck se scoate din creier și împinge inactiv când pucul este prea îndepărtat, ceea ce înseamnă că atletul "a renunțat" urmărind pucul:

clasa Atlet // (...) funcția privată pursuePuck (): void var aPuck: Puck = getPuck (); daca (distanta (a, aPuck)> 150) // Puck este prea departe de pozitia noastra curenta, asa ca sa renuntam la urmarirea pucului si sa speram ca cineva va fi mai aproape de a lua pucul pentru noi. mBrain.popState (); mBrain.pushState (inactiv);  altfel // Pucul este aproape, să încercăm să-l luăm.  // (...)

Când pucul este aproape, sportivul trebuie să meargă după el, ceea ce se poate obține cu ușurință prin comportamentul căutării. Folosind poziția pucului ca destinație de căutare, sportivul va urmări cu pauză pucul și va ajusta traiectoria sa în timp ce pucul se mișcă:

clasa Atlet // (...) funcția privată pursuePuck (): void var aPuck: Puck = getPuck (); mBoid.steering = mBoid.steering + mBoid.separare (); daca (distanta (a, aPuck)> 150) // Puck este prea departe de pozitia noastra curenta, asa ca sa renuntam la urmarirea pucului si sa speram ca cineva va fi mai aproape de a lua pucul pentru noi. mBrain.popState (); mBrain.pushState (inactiv);  altfel // Pucul este aproape, să încercăm să-l luăm. daca (aPuck.owner == null) // Nimeni nu are pucul, este sansa noastra sa o cautam si sa o primim! mBoid.steering = mBoid.steering + mBoid.seek (aPuck.position);  altfel // Cineva a primit pucul. Dacă noul proprietar al pucului aparține echipei mele, // ar trebui să trecem la "atac", altfel ar trebui să trec la stealPuck și să încerc să-l iau înapoi. mBrain.popState (); mBrain.pushState (doesMyTeamHaveThePuck ()? atac: stealPuck); 

Celelalte două tranziții din pursuePuck stat, echipa are pucul și adversarul are pucul, sunt legate de puc fiind prins în timpul procesului de urmărire. Dacă cineva prindă pucul, atletul trebuie să explodeze pursuePuck stați și împingeți unul nou în creier. 

Statul care va fi împins depinde de proprietatea pucului. Dacă ați sunat la doesMyTeamHaveThePuck () se intoarce Adevărat, înseamnă că un coechipier a luat pucul, așa că atletul trebuie să împingă atac, ceea ce înseamnă că este timpul să opriți urmărirea pucului și să începeți să vă deplasați spre obiectivul adversarului. Dacă un adversar a luat puc, sportivul trebuie să împingă stealPuck, care va face echipa să încerce să recupereze pucul.

Ca un accesoriu mic, sportivii nu ar trebui să rămână prea apropiați unul de celălalt în timpul pursuePuck stat, pentru că mișcarea "aglomerată" care urmărește este nefiresc. Adăugarea separării la forța de direcție a statului (linia 6 în codul de mai sus) asigură sportivilor o distanță minimă între ei.

Rezultatul este o echipă care poate continua pucul. De dragul testelor, în acest demo, pucul este plasat în centrul patinoarului la fiecare câteva secunde, pentru ca sportivii să se miște continuu:

Atacul cu puckul

După obținerea pucului, un atlet și echipa lui trebuie să se deplaseze către obiectivul adversarului pentru a înscrie. Acesta este scopul atac stat:

Statul de atac și tranzițiile sale în FSM care descriu procesul de atac.

atac statul are doar două tranziții: adversarul are pucul și pucul nu are proprietar. Din moment ce statul este destinat exclusiv să facă sportivii să se deplaseze spre obiectivul adversarului, nu mai are sens să rămână atacat dacă pucul nu mai este în posesia echipei.

În ceea ce privește mișcarea spre obiectivul adversarului: atletul care poartă pucul (liderul) și coechipierii care îl ajută trebuie să se comporte diferit. Liderul trebuie să atingă obiectivul adversarului, iar coechipierii trebuie să-l ajute de-a lungul drumului.

Acest lucru poate fi implementat verificând dacă atletul care rulează codul are pucul:

clasa Atlet // (...) privat funcția atac (): void var aPuckOwner: Athlete = getPuckOwner (); // Are pucul un proprietar? dacă (aPuckOwner! = null) / Da, da. Să aflăm dacă proprietarul aparține echipei de adversari. dacă (doesMyTeamHaveThePuck ()) if (amIThePuckOwner ()) // Echipa mea are pucul și eu sunt cel care o are! Să ne mutăm // spre obiectivul adversarului. mBoid.steering = mBoid.steerung + mBoid.seek (getOpponentGoalPosition ());  altfel // Echipa mea are puc, dar un coechipier o are. Să-l urmăm doar pentru a da un sprijin în timpul atacului. mBoid.steering = mBoid.steering + mBoid.followLeader (aPuckOwner.boid); mBoid.steering = mBoid.steering + mBoid.separare ();  altfel // Adversarul are puc! Opriți atacul și încercați să-l furiți. mBrain.popState (); mBrain.pushState (stealPuck);  altceva // Puck nu are proprietar, deci nu are rost să-l păstrăm // atac. Este timpul să reorganizați și să începeți să urmăriți pucul. mBrain.popState (); mBrain.pushState (pursuePuck); 

Dacă amIThePuckOwner () se intoarce Adevărat (linia 10), atletul care rulează codul are pucul. În acest caz, el va căuta doar poziția adversarului. Aceasta este cam aceeași logică folosită pentru a urmări pucul în pursuePuck stat.

Dacă amIThePuckOwner () se intoarce fals, atletul nu are pucul, așa că trebuie să-l ajute pe lider. Ajutarea liderului este o sarcină complicată, așa că o vom simplifica. Un atlet îi va ajuta pe lider doar căutând o poziție înaintea lui:

Coechipierii asistă liderul.

Pe măsură ce liderul se mișcă, el va fi înconjurat de coechipieri în timp ce îl urmează înainte punct. Aceasta oferă liderului câteva opțiuni pentru a trece pucul în cazul în care există probleme. Ca într-un joc real, coechipierii din jur trebuie, de asemenea, să rămână în afara conducerii.

Acest model de asistență poate fi obținut prin adăugarea unei versiuni ușor modificate a liderului după comportament (linia 18). Singura diferență este că sportivii vor urma un punct înainte a liderului, în loc de unul în spatele lui, așa cum a fost inițial implementat în acest comportament.

Atleții care asistă liderul ar trebui să păstreze, de asemenea, o distanță minimă între ele. Aceasta este implementată prin adăugarea unei forțe de separare (linia 19).

Rezultatul este o echipă capabilă să se deplaseze spre obiectivul adversarului, fără a aglomerarea și simularea unei mișcări de atac asistate:

Îmbunătățirea suportului pentru atacuri

Punerea în aplicare actuală a atac statul este destul de bun pentru unele situații, dar are un defect. Când cineva captează pucul, el devine lider și este imediat urmat de coechipieri.

Ce se întâmplă dacă liderul se deplasează spre propriul său scop când captează pucul? Uitați-vă mai degrabă la demo-ul de mai sus și observați modelul nenatural atunci când coechipierii încep după lider.

Când liderul captează pucul, comportamentul căutării durează ceva timp pentru a corecta traiectoria liderului și a face efectiv să se îndrepte spre obiectivul adversarului. Chiar și atunci când liderul este "manevrat", coechipierii vor încerca să-i caute înainte punct, ceea ce înseamnă că se vor deplasa spre propriile lor obiectiv (sau locul în care liderul se uită la).

Când liderul este în cele din urmă în poziția și gata să se îndrepte spre obiectivul adversarului, coechipierii vor fi "manevrați" să urmeze liderul. Liderul se va muta apoi fără sprijinul colegilor, atâta timp cât ceilalți își ajustează traiectoriile.

Acest defect poate fi stabilit prin verificarea dacă colegul de echipă este înaintea liderului, atunci când echipa recuperează pucul. Aici, condiția "înainte" înseamnă "mai aproape de obiectivul adversarului":

clasa Atlet // (...) funcția privată isAheadOfMe (Boot: Boid): Boolean var aTargetDistance: Number = distance (getOpponentGoalPosition (), Booid); var aDistanță: Numărul = distanța (getOpponentGoalPosition (), mBoid.position); returnați 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);   

Dacă liderul (care este proprietarul pucului) este în fața sportivului care rulează codul, atunci atletul trebuie să urmeze liderul, așa cum a procedat înainte (linii 27 și 28). În cazul în care liderul se află în spatele lui, sportivul trebuie să își păstreze poziția actuală, menținând o distanță minimă între celelalte (linia 33).

Rezultatul este un pic mai convingător decât cel inițial atac implementare:

Bacsis: Prin modificarea calculelor de distanță și a comparațiilor în isAheadOfMe () , este posibil să se modifice modul în care sportivii își păstrează pozițiile curente.

Furtul puckului

Starea finală în procesul de atac este stealPuck, care devine activ când echipa adversă are pucul. Scopul principal al stealPuck stat este să fure pucul de la adversar care îl poartă, astfel încât echipa să înceapă să atace din nou:

 Starea stealPuck și tranzițiile sale în FSM care descriu procesul de atac.

Deoarece ideea din spatele acestei stări este de a fura pucul de la adversar, dacă pucul este recuperat de către echipă sau devine liber (adică nu are proprietar), stealPuck va apărea din creier și va împinge starea corectă pentru a face față noii situații:

clasa Atlet // (...) funcția privată stealPuck (): void // Pucul are vreun proprietar? if (getPuckOwner ()! = null) / Da, da, dar cine o are? dacă (doesMyTeamHaveThePuck ()) // Echipa mea are pucul, așa că este timpul să nu mai încercați să furați // pucul și să începeți ataca. mBrain.popState (); mBrain.pushState (atac);  altfel // Un adversar are pucul. var aOpponentLeader: atlet = getPuckOwner (); // Să-l urmărim în timp ce păstrăm o anumită separare de ceilalți pentru a evita ca toată lumea să ocupe aceeași poziție în căutarea. mBoid.steering = mBoid.steering + mBoid.pursuit (aOpponentLeader.boid); mBoid.steering = mBoid.steering + mBoid.separare ();  altfel // Pucul nu are proprietar, probabil că rulează liber în patinoar. // Nu are rost să continuăm să încercăm să o furăm, așa că terminăm starea 'stealPuck' și trecem la 'pursuePuck'. mBrain.popState (); mBrain.pushState (pursuePuck); 

Dacă pucul are un proprietar și el aparține echipei adversarului, atletul trebuie să urmărească liderul opus și să încerce să fure pucul. Pentru a urmări liderul adversarului, un atlet trebuie prezice unde va fi în viitorul apropiat, astfel încât el poate fi interceptat în traiectoria sa. Asta e diferit de a căuta doar liderul opus.

Din fericire, acest lucru poate fi ușor atins prin comportamentul urmărit (linia 19). Folosind o forță de urmărire în stealPuck stat, sportivii vor încerca intercepta liderul adversarului, în loc să îl urmeze:

Prevenirea unei mișcări de steal aglomerat

Actuala implementare a stealPuck dar într-un joc real, doar unul sau doi sportivi se apropie de liderul adversarului pentru a fura pucul. Restul echipei rămâne în zonele înconjurătoare care încearcă să ajute, ceea ce împiedică un model de furt agățat.

Acesta poate fi stabilit prin adăugarea unei verificări la distanță (linia 17) înainte de urmărirea liderului adversarului:

clasa Atlet // (...) funcția privată stealPuck (): void // Pucul are vreun proprietar? if (getPuckOwner ()! = null) / Da, da, dar cine o are? dacă (doesMyTeamHaveThePuck ()) // Echipa mea are pucul, așa că este timpul să nu mai încercați să furați // pucul și să începeți ataca. mBrain.popState (); mBrain.pushState (atac);  altfel // Un adversar are pucul. var aOpponentLeader: atlet = getPuckOwner (); // Este adversarul cu pucul aproape de mine? dacă (distanța (aOpponentLeader, aceasta) < 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);   

În loc să urmărească orbește pe liderul adversarului, un atlet va verifica dacă distanța dintre el și liderul oponentului este mai mică decât, să spunem, 150. Dacă asta e Adevărat, urmărirea se face în mod normal, dar dacă distanța este mai mare decât 150, înseamnă că atletul este prea departe de liderul adversarului.

Dacă se întâmplă acest lucru, nu mai are sens să continuăm încercarea de a fura pucul, deoarece este prea departe și există, probabil, colegii de echipă care deja au încercat să facă același lucru. Cea mai bună opțiune este de a pop stealPuck din creier și împingeți apărare stat (care va fi explicat în tutorialul următor). Deocamdată, un atlet își va ține poziția actuală dacă liderul adversarului este prea departe.

Rezultatul este un model de furt mai convingător și mai natural (fără aglomerări):

Evitarea oponenților în timp ce atacă

Există un ultim truc pe care trebuie să-l învețe sportivii pentru a ataca în mod eficient. În acest moment, se îndreaptă spre obiectivul adversarului, fără a lua în considerare adversarii de-a lungul drumului. Un adversar trebuie privit ca o amenințare și trebuie evitat.

Folosind comportamentul de evitare a coliziunii, sportivii pot eschiva adversarii în timp ce se mișcă:

Comportamentul de evitare a coliziunilor folosit pentru a evita adversarii.

Oponenții vor fi considerați obstacole circulare. Ca urmare a naturii dinamice a comportamentelor de direcție, care sunt actualizate în fiecare buclă de joc, modelul de evitare va lucra în mod grațios și fără probleme pentru deplasarea obstacolelor (așa cum este cazul aici).

Pentru a face ca sportivii să evite adversarii (obstacolele), trebuie adăugată o singură linie în starea de atac (linia 14):

clasa Atlet // (...) privat funcția atac (): void var aPuckOwner: Athlete = getPuckOwner (); // Are pucul un proprietar? dacă (aPuckOwner! = null) / Da, da. Să aflăm dacă proprietarul aparține echipei de adversari. dacă (doesMyTeamHaveThePuck ()) if (amIThePuckOwner ()) // Echipa mea are pucul și eu sunt cel care o are! Să ne mutăm // spre obiectivul adversarului, oferind oricărui adversar de-a lungul drumului. mBoid.steering = mBoid.steerung + mBoid.seek (getOpponentGoalPosition ()); mBoid.steering = mBoid.steering + mBoid.collisionAvoidance (membrii getOpponentTeam ().);  altfel // Echipa mea are puc, dar un coechipier o are. E înaintea mea? dacă (isAheadOfMe (aPuckOwner.boid)) // Da, el este înaintea mea. Să-l urmăm doar pentru a da un sprijin / / în timpul atacului. mBoid.steering = mBoid.steering + mBoid.followLeader (aPuckOwner.boid); mBoid.steering = mBoid.steering + mBoid.separare ();  altfel // Nu, coechipierul cu pucul este în spatele meu. În acest caz, // să ne păstrăm poziția actuală cu o anumită separare față de // alta, astfel încât să prevenim aglomerarea. mBoid.steering = mBoid.steering + mBoid.separare ();  altceva // Adversarul are puc! Opriți atacul și încercați să-l furiți. mBrain.popState (); mBrain.pushState (stealPuck);  altceva // Puck nu are proprietar, deci nu are rost să-l păstrăm // atac. Este timpul să reorganizați și să începeți să urmăriți pucul. mBrain.popState (); mBrain.pushState (pursuePuck); 

Această linie va adăuga o forță de evitare a coliziunii sportivului, care va fi combinată cu forțele care există deja. Ca rezultat, sportivul va evita obstacolele în același timp cu căutarea obiectivului adversarului.

Mai jos este o demonstrație a unui atlet care rulează atac stat. Oponenții sunt imobiliari pentru a evidenția comportamentul de evitare a coliziunilor:

Concluzie

Acest tutorial a explicat implementarea modelului de atac folosit de sportivi pentru a fura și a transporta pucul spre obiectivul adversarului. Folosind o combinație de comportamente de direcție, sportivii pot acum să efectueze modele complexe de mișcare, cum ar fi urmarea unui lider sau urmărirea adversarului cu puc.

După cum sa discutat anterior, implementarea atacului urmărește să simuleze ceea ce oamenii do, astfel încât rezultatul este o aproximare a unui joc real. Prin reglarea individuală a stărilor care compun atacul, puteți produce o simulare mai bună sau cea care se potrivește nevoilor dvs..

În următorul tutorial, veți învăța cum să faceți apărarea sportivilor. AI va deveni complet completă, capabilă să atace și să apere, rezultând într-un meci cu echipe de 100% controlate de AI care se joacă unul împotriva celuilalt.

Referințe

  • Sprite: Stadionul de hochei pe GraphicRiver
  • Sprites: Jucătorii de hochei de Taylor J Glidden