In questo tutorial, continuiamo a codificare l'intelligenza artificiale per una partita di hockey usando comportamenti di guida e macchine a stati finiti. In questa parte della serie, imparerai l'intelligenza artificiale richiesta dalle entità del gioco per coordinare un attacco, che implica l'intercettazione e il trasporto del disco verso l'obiettivo dell'avversario.
Il coordinamento e l'esecuzione di un attacco in un gioco di sport cooperativo è un compito molto complesso. Nel mondo reale, quando gli umani giocano a una partita di hockey, lo fanno parecchi decisioni basate su molte variabili.
Queste decisioni implicano calcoli e capire cosa sta succedendo. Un umano può dire perché un avversario si muove basandosi sulle azioni di un altro avversario, ad esempio, "si sta muovendo per trovarsi in una posizione strategica migliore". Non è banale portare quella comprensione a un computer.
Di conseguenza, se proviamo a codificare l'intelligenza artificiale per seguire tutte le sfumature e le percezioni umane, il risultato sarà una pila enorme e spaventosa di codice. Inoltre, il risultato potrebbe non essere preciso o facilmente modificabile.
Questo è il motivo per cui il nostro attacco AI cercherà di imitare il risultato di un gruppo di umani che giocano, non la stessa percezione umana. Questo approccio porterà ad approssimazioni, ma il codice sarà più facile da capire e modificare. Il risultato è abbastanza buono per diversi casi d'uso.
Rompiamo il processo di attacco in pezzi più piccoli, ognuno dei quali esegue un'azione molto specifica. Questi pezzi sono gli stati di una macchina a stati finiti basata sullo stack. Come precedentemente spiegato, ogni stato produrrà una forza di governo che farà comportare l'atleta di conseguenza.
L'orchestrazione di quegli stati e le condizioni per passare da uno all'altro definiranno l'attacco. L'immagine sotto mostra l'intero FSM utilizzato nel processo:
Una macchina a stati finiti basata sullo stack che rappresenta il processo di attacco.Come illustrato dall'immagine, le condizioni per passare da uno stato all'altro saranno esclusivamente basate sulla distanza e sulla proprietà del disco. Per esempio, la squadra ha il disco
o il disco è troppo lontano
.
Il processo di attacco sarà composto da quattro stati: inattivo
, attacco
, stealPuck
, e pursuePuck
. Il inattivo
stato era già stato implementato nel precedente tutorial ed è il punto di partenza del processo. Da lì, un atleta passerà a attacco
se la squadra ha il disco, a stealPuck
se la squadra dell'avversario ha il disco, o pursuePuck
se il disco non ha proprietario ed è abbastanza vicino da essere raccolto.
Il attacco
lo stato rappresenta un movimento offensivo. Mentre si trova in quello stato, l'atleta che porta il disco (chiamato capo
) cercherà di raggiungere l'obiettivo dell'avversario. I compagni di squadra si muoveranno, provando a sostenere l'azione.
Il stealPuck
lo stato rappresenta qualcosa tra un movimento difensivo e uno offensivo. Mentre si trova in quello stato, un atleta si concentrerà sul perseguimento dell'avversario che porta il disco. L'obiettivo è recuperare il disco, quindi il team può ricominciare ad attaccare.
Finalmente, il pursuePuck
lo stato non è legato all'attacco o alla difesa; guiderà solo gli atleti quando il disco non ha alcun proprietario. Mentre si trova in quello stato, un atleta cercherà di ottenere il disco che si muove liberamente sulla pista (per esempio, dopo essere stato colpito dal bastone di qualcuno).
Il inattivo
stato precedentemente implementato non ha avuto transizioni. Poiché questo stato è il punto di partenza per l'intera intelligenza artificiale, aggiorniamolo e rendilo in grado di passare ad altri stati.
Il inattivo
lo stato ha tre transizioni:
Se la squadra dell'atleta ha il disco, inattivo
dovrebbe essere spuntato dal cervello e attacco
dovrebbe essere spinto. Allo stesso modo, se la squadra dell'avversario ha il disco, inattivo
dovrebbe essere sostituito da stealPuck
. La transizione rimanente accade quando nessuno possiede il disco ed è vicino all'atleta; in quel caso, pursuePuck
dovrebbe essere spinto nel cervello.
La versione aggiornata di inattivo
è il seguente (tutti gli altri stati saranno implementati in seguito):
class Athlete // (...) private function idle (): void var aPuck: Puck = getPuck (); stopAndlookAt (aPuck); // Questo è un trucco per aiutare a testare l'intelligenza artificiale. se (mStandStill) ritorno; // Il disco ha un proprietario? if (getPuckOwner ()! = null) // Sì, lo è. mBrain.popState (); if (doesMyTeamHaveThePuck ()) // La mia squadra ha appena ottenuto il disco, è tempo di attacco! mBrain.pushState (attacco); else // La squadra avversaria ha preso il disco, proviamo a rubarlo. mBrain.pushState (stealPuck); else if (distance (this, 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
Procediamo con l'implementazione degli altri stati.
Ora che l'atleta ha acquisito una certa percezione sull'ambiente ed è in grado di passare da inattivo
a qualsiasi stato, concentriamoci sul perseguimento del disco quando non ha un proprietario.
Un atleta passerà a pursuePuck
immediatamente dopo l'inizio della partita, perché il disco sarà posizionato al centro della pista senza proprietario. Il pursuePuck
lo stato ha tre transizioni:
La prima transizione è il disco è troppo lontano
, e cerca di simulare ciò che accade in un vero gioco per quanto riguarda l'inseguimento del disco. Per ragioni strategiche, di solito l'atleta più vicino al disco è quello che cerca di prenderlo, mentre gli altri aspettano o cercano di aiutarlo.
Senza passare a inattivo
quando il disco è distante, ogni atleta controllato dall'IA insegue il disco allo stesso tempo, anche se è lontano da esso. Controllando la distanza tra l'atleta e il disco, pursuePuck
si apre dal cervello e spinge inattivo
quando il disco è troppo distante, il che significa che l'atleta ha appena "rinunciato" a inseguire il disco:
class Athlete // (...) private function pursuePuck (): void var aPuck: Puck = getPuck (); if (distance (this, aPuck)> 150) // Puck è troppo lontano dalla nostra posizione attuale, quindi abbandoniamo // inseguendo il disco e speriamo che qualcuno sia più vicino per ottenere il disco // per noi. mBrain.popState (); mBrain.pushState (idle); else // Il disco è vicino, proviamo ad afferrarlo. // (...)
Quando il disco è vicino, l'atleta deve seguirlo, che può essere facilmente raggiunto con il comportamento di ricerca. Usando la posizione del puck come destinazione di ricerca, l'atleta inseguirà con grazia il disco e regolerà la sua traiettoria mentre il disco si sposta:
class Athlete // (...) private function pursuePuck (): void var aPuck: Puck = getPuck (); mBoid.steering = mBoid.steering + mBoid.separation (); if (distance (this, aPuck)> 150) // Puck è troppo lontano dalla nostra posizione attuale, quindi abbandoniamo // inseguendo il disco e speriamo che qualcuno sia più vicino per ottenere il disco // per noi. mBrain.popState (); mBrain.pushState (idle); else // Il disco è vicino, proviamo ad afferrarlo. if (aPuck.owner == null) // Nessuno ha il disco, è la nostra occasione per cercare e ottenerlo! mBoid.steering = mBoid.steering + mBoid.seek (aPuck.position); else // Qualcuno ha appena preso il disco. Se il nuovo proprietario del puck appartiene al mio team, // dovremmo passare a "attack", altrimenti dovrei passare a "stealPuck" // e provare a riavere il disco. mBrain.popState (); mBrain.pushState (doesMyTeamHaveThePuck ()? attack: stealPuck);
Le restanti due transizioni nel pursuePuck
stato, la squadra ha il disco
e l'avversario ha il disco
, sono legati al disco che viene catturato durante il processo di ricerca. Se qualcuno prende il disco, l'atleta deve scoppiare pursuePuck
affermalo e spingine uno nuovo nel cervello.
Lo stato da spingere dipende dalla proprietà del disco. Se la chiamata a doesMyTeamHaveThePuck ()
ritorna vero
, significa che un compagno di squadra ha ottenuto il disco, quindi l'atleta deve spingere attacco
, il che significa che è ora di smettere di inseguire il disco e iniziare a muoversi verso la porta dell'avversario. Se un avversario ha il disco, l'atleta deve spingere stealPuck
, che farà sì che la squadra tenti di recuperare il disco.
Come un piccolo miglioramento, gli atleti non dovrebbero rimanere troppo vicini gli uni dagli altri durante il pursuePuck
stato, perché un movimento di inseguimento "affollato" è innaturale. Aggiungere separazione alla forza di governo dello stato (linea 6
nel codice sopra) assicura che gli atleti mantengano una distanza minima tra loro.
Il risultato è una squadra in grado di perseguire il disco. Per motivi di prova, in questa demo, il disco viene posizionato al centro della pista ogni pochi secondi, per far muovere gli atleti continuamente:
Dopo aver ottenuto il disco, un atleta e la sua squadra devono spostarsi verso la porta avversaria per segnare. Questo è lo scopo del attacco
stato:
Il attacco
stato ha solo due transizioni: l'avversario ha il disco
e puck non ha proprietario
. Dato che lo stato è progettato esclusivamente per far muovere gli atleti verso la porta avversaria, non ha senso rimanere attaccante se il disco non è più in possesso della squadra.
Per quanto riguarda il movimento verso l'obiettivo avversario: l'atleta che porta il disco (leader) ei compagni di squadra che lo aiutano dovrebbero comportarsi diversamente. Il leader deve raggiungere l'obiettivo dell'avversario, e i compagni di squadra dovrebbero aiutarlo lungo la strada.
Questo può essere implementato controllando se l'atleta che esegue il codice ha il disco:
class Athlete // (...) private function attack (): void var aPuckOwner: Athlete = getPuckOwner (); // Il disco ha un proprietario? if (aPuckOwner! = null) // Sì, lo è. Scopriamo se il proprietario appartiene alla squadra avversaria. if (doesMyTeamHaveThePuck ()) if (amIThePuckOwner ()) // La mia squadra ha il disco e io sono colui che ce l'ha! Spostiamoci // verso l'obiettivo dell'avversario. mBoid.steering = mBoid.steering + mBoid.seek (getOpponentGoalPosition ()); else // La mia squadra ha il disco, ma un compagno di squadra ce l'ha. Seguiamolo // per dare un po 'di supporto durante l'attacco. mBoid.steering = mBoid.steering + mBoid.followLeader (aPuckOwner.boid); mBoid.steering = mBoid.steering + mBoid.separation (); else // L'avversario ha il disco! Fermare l'attacco // e provare a rubarlo. mBrain.popState (); mBrain.pushState (stealPuck); else // Puck non ha proprietario, quindi non ha senso mantenere // attaccando. È tempo di riorganizzarsi e iniziare a cercare il disco. mBrain.popState (); mBrain.pushState (pursuePuck);
Se amIThePuckOwner ()
ritorna vero
(linea 10), l'atleta che esegue il codice ha il disco. In tal caso, cercherà solo la posizione obiettivo dell'avversario. È praticamente la stessa logica usata per perseguire il disco nel pursuePuck
stato.
Se amIThePuckOwner ()
ritorna falso
, l'atleta non ha il disco, quindi deve aiutare il leader. Aiutare il leader è un compito complicato, quindi lo semplificeremo. Un atleta assisterà il leader semplicemente cercando una posizione davanti a lui:
Mentre il leader si muove, sarà circondato dai compagni di squadra mentre seguono il avanti
punto. Questo dà al leader alcune opzioni per passare il disco se c'è qualche problema. Come in un vero gioco, i compagni di squadra circostanti dovrebbero anche stare fuori dalla via del leader.
Questo schema di assistenza può essere ottenuto aggiungendo una versione leggermente modificata del comportamento seguente (riga 18). L'unica differenza è che gli atleti seguiranno un punto avanti del leader, invece di uno dietro di lui come originariamente implementato in quel comportamento.
Gli atleti che assistono il leader dovrebbero anche mantenere una distanza minima tra loro. Ciò viene implementato aggiungendo una forza di separazione (riga 19).
Il risultato è una squadra in grado di muoversi verso la porta avversaria, senza affollamento e simulando un movimento di attacco assistito:
L'attuale implementazione del attacco
lo stato è abbastanza buono per alcune situazioni, ma ha un difetto. Quando qualcuno prende il disco, diventa il leader ed è immediatamente seguito dai compagni di squadra.
Cosa succede se il leader si sta muovendo verso il proprio obiettivo quando prende il disco? Dai un'occhiata più da vicino alla demo qui sopra e nota lo schema innaturale quando i compagni di squadra iniziano a seguire il leader.
Quando il leader prende il disco, il comportamento di ricerca richiede un po 'di tempo per correggere la traiettoria del leader e farlo effettivamente muovere verso la porta dell'avversario. Anche quando il leader sta "manovrando", i compagni cercheranno di cercarlo avanti
punto, il che significa che si muoveranno verso il loro obiettivo (o il luogo in cui il leader sta fissando).
Quando il leader è finalmente in posizione e pronto a muoversi verso la porta avversaria, i compagni di squadra saranno "in manovra" per seguire il leader. Il leader si muoverà quindi senza il supporto del compagno di squadra fino a quando gli altri stanno aggiustando le loro traiettorie.
Questo difetto può essere risolto controllando se il compagno di squadra è in testa al leader quando la squadra recupera il disco. Qui, la condizione "avanti" significa "più vicino alla porta dell'avversario":
class Athlete // (...) 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);
Se il leader (che è il proprietario del disco) è davanti all'atleta che esegue il codice, allora l'atleta dovrebbe seguire il leader proprio come faceva prima (righe 27 e 28). Se il leader è dietro di lui, l'atleta dovrebbe mantenere la sua posizione corrente, mantenendo una distanza minima tra gli altri (linea 33).
Il risultato è un po 'più convincente rispetto all'inizio attacco
implementazione:
Mancia: Modificando i calcoli della distanza e i confronti nel isAheadOfMe ()
metodo, è possibile modificare il modo in cui gli atleti mantengono le loro posizioni attuali.
Lo stato finale nel processo di attacco è stealPuck
, che diventa attivo quando la squadra avversaria ha il disco. Lo scopo principale del stealPuck
lo stato è rubare il disco dall'avversario che lo trasporta, in modo che la squadra possa ricominciare ad attaccare:
Dal momento che l'idea alla base di questo stato è di rubare il disco all'avversario, se il disco viene recuperato dalla squadra o se diventa libero (cioè non ha proprietario), stealPuck
uscirà dal cervello e spingerà lo stato giusto per affrontare la nuova situazione:
class Athlete // (...) private function stealPuck (): void // Il disco ha qualche proprietario? if (getPuckOwner ()! = null) // Sì, lo ha, ma chi ce l'ha? if (doesMyTeamHaveThePuck ()) // La mia squadra ha il disco, quindi è ora di smettere di cercare di rubare // il disco e iniziare ad attaccare. mBrain.popState (); mBrain.pushState (attacco); else // Un avversario ha il disco. var aOpponentLeader: Athlete = getPuckOwner (); // Cerchiamo di perseguirlo mentre mantieni una certa separazione dagli altri per evitare che tutti occupino la stessa posizione nell'inseguimento. mBoid.steering = mBoid.steering + mBoid.pursuit (aOpponentLeader.boid); mBoid.steering = mBoid.steering + mBoid.separation (); else // Il disco non ha proprietario, probabilmente sta scorrendo liberamente nella pista. // Non ha senso continuare a cercare di rubarlo, quindi finiamo lo stato 'stealPuck' // e passiamo a 'pursuePuck'. mBrain.popState (); mBrain.pushState (pursuePuck);
Se il disco ha un proprietario e appartiene alla squadra avversaria, l'atleta deve inseguire il leader avversario e cercare di rubare il disco. Al fine di perseguire il leader avversario, un atleta deve predire dove sarà nel prossimo futuro, in modo che possa essere intercettato nella sua traiettoria. È diverso dal cercare il leader avversario.
Fortunatamente, questo può essere facilmente raggiunto con il comportamento perseguito (riga 19). Usando una forza di inseguimento nel stealPuck
stato, gli atleti proveranno a intercettare il capo dell'avversario, invece di seguirlo:
L'attuale implementazione di stealPuck
funziona, ma in un gioco reale solo uno o due atleti si avvicinano al leader avversario per rubare il disco. Il resto della squadra rimane nelle aree circostanti cercando di aiutare, il che impedisce un modello di furto affollato.
Può essere aggiustato aggiungendo un controllo di distanza (linea 17) prima della ricerca del leader avversario:
class Athlete // (...) private function stealPuck (): void // Il disco ha qualche proprietario? if (getPuckOwner ()! = null) // Sì, lo ha, ma chi ce l'ha? if (doesMyTeamHaveThePuck ()) // La mia squadra ha il disco, quindi è ora di smettere di cercare di rubare // il disco e iniziare ad attaccare. mBrain.popState (); mBrain.pushState (attacco); else // Un avversario ha il disco. var aOpponentLeader: Athlete = getPuckOwner (); // L'avversario con il disco è vicino a me? if (distance (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);
Invece di inseguire ciecamente il leader avversario, un atleta controllerà se la distanza tra lui e il leader avversario è inferiore, per esempio, 150
. Se questo è vero
, la ricerca avviene normalmente, ma se la distanza è maggiore di 150
, significa che l'atleta è troppo lontano dal leader avversario.
Se ciò accade, non ha senso continuare a cercare di rubare il disco, poiché è troppo lontano e probabilmente ci sono già compagni di squadra che stanno cercando di fare lo stesso. L'opzione migliore è pop stealPuck
dal cervello e spingere il difesa
stato (che sarà spiegato nel prossimo tutorial). Per ora, un atleta manterrà la sua posizione attuale se il leader avversario è troppo lontano.
Il risultato è un modello più convincente e naturale di furto (nessun affollamento):
C'è un ultimo trucco che gli atleti devono imparare per attaccare efficacemente. In questo momento, si muovono verso la porta avversaria senza considerare gli avversari lungo la strada. Un avversario deve essere visto come una minaccia e dovrebbe essere evitato.
Usando il comportamento di evitare le collisioni, gli atleti possono schivare gli avversari mentre si muovono:
Comportamento di evitare le collisioni usato per evitare gli avversari.Gli avversari saranno visti come ostacoli circolari. A causa della natura dinamica dei comportamenti di guida, che vengono aggiornati in ogni ciclo di gioco, il modello di evitamento agirà in modo aggraziato e agevole per superare ostacoli (come nel caso qui).
Per fare in modo che gli atleti evitino gli avversari (ostacoli), una riga singola deve essere aggiunta allo stato di attacco (riga 14):
class Athlete // (...) private function attack (): void var aPuckOwner: Athlete = getPuckOwner (); // Il disco ha un proprietario? if (aPuckOwner! = null) // Sì, lo è. Scopriamo se il proprietario appartiene alla squadra avversaria. if (doesMyTeamHaveThePuck ()) if (amIThePuckOwner ()) // La mia squadra ha il disco e io sono colui che ce l'ha! Spostiamoci // verso l'obiettivo dell'avversario, evitando qualsiasi avversario lungo il percorso. mBoid.steering = mBoid.steering + mBoid.seek (getOpponentGoalPosition ()); mBoid.steering = mBoid.steering + mBoid.collisionAvoidance (getOpponentTeam (). members); else // La mia squadra ha il disco, ma un compagno di squadra ce l'ha. È davanti a me? if (isAheadOfMe (aPuckOwner.boid)) // Sì, è davanti a me. Seguiamolo per dare supporto // durante l'attacco. mBoid.steering = mBoid.steering + mBoid.followLeader (aPuckOwner.boid); mBoid.steering = mBoid.steering + mBoid.separation (); else // No, il compagno di squadra con il disco è dietro di me. In tal caso // teniamo la nostra posizione attuale con una separazione dall'altra, quindi evitiamo l'affollamento. mBoid.steering = mBoid.steering + mBoid.separation (); else // L'avversario ha il disco! Fermare l'attacco // e provare a rubarlo. mBrain.popState (); mBrain.pushState (stealPuck); else // Puck non ha proprietario, quindi non ha senso mantenere // attaccando. È tempo di riorganizzarsi e iniziare a cercare il disco. mBrain.popState (); mBrain.pushState (pursuePuck);
Questa linea aggiungerà all'atleta una forza di evitamento di collisione, che sarà combinata con le forze che già esistono. Di conseguenza, l'atleta eviterà gli ostacoli contemporaneamente a cercare l'obiettivo dell'avversario.
Di seguito è una dimostrazione di un atleta che esegue il attacco
stato. Gli avversari sono immobili per evidenziare il comportamento di evitare le collisioni:
Questo tutorial ha spiegato l'implementazione del pattern di attacco usato dagli atleti per rubare e portare il disco verso la porta avversaria. Usando una combinazione di comportamenti di guida, gli atleti sono ora in grado di eseguire schemi di movimento complessi, come seguire un leader o inseguire l'avversario con il disco.
Come discusso in precedenza, l'implementazione dell'attacco mira a simulare ciò che gli umani fare, quindi il risultato è un'approssimazione di un vero gioco. Modificando individualmente gli stati che compongono l'attacco, è possibile produrre una simulazione migliore o adatta alle proprie esigenze.
Nel prossimo tutorial, imparerai come difendere gli atleti. L'IA diventerà una funzionalità completa, in grado di attaccare e difendersi, risultando in una partita con squadre controllate al 100% dall'IA che giocano l'una contro l'altra.