Le macchine a stati finiti e i comportamenti di guida sono una combinazione perfetta: la loro natura dinamica consente la combinazione di stati e forze semplici per creare modelli comportamentali complessi. In questo tutorial, imparerai come codificare a squadra schema usando una macchina a stati finiti a pila combinata con comportamenti di governo.
Tutte le icone FSM realizzate da Lorc e disponibili su http://game-icons.net. Asset nella demo: Top / Down Spara il foglio di spunta di Takomogames e Alien Breed (esque) Tilesheet top-down di SpicyPixel.
Nota: Sebbene questo tutorial sia scritto usando AS3 e Flash, dovresti essere in grado di utilizzare le stesse tecniche e concetti in quasi tutti gli ambienti di sviluppo di giochi.
Dopo aver completato questo tutorial, sarai in grado di implementare uno schema di squadra in cui un gruppo di soldati seguirà il leader, dando la caccia ai nemici e saccheggiando oggetti:
Il precedente tutorial sulle macchine a stati finiti descriveva quanto fossero utili per implementare la logica dell'intelligenza artificiale: invece di scrivere una pila molto complessa di codice AI, la logica può essere distribuita attraverso una serie di stati semplici, ognuno con compiti molto specifici, come scappando da un nemico.
La combinazione di stati risulta in un'IA sofisticata, ma facile da capire, modificare e mantenere. Quella struttura è anche uno dei pilastri dietro i comportamenti di guida: la combinazione di forze semplici per creare schemi complessi.
Ecco perché gli FSM e i comportamenti di guida sono un'ottima combinazione. Gli stati possono essere usati per controllare quali forze agiranno su un personaggio, migliorando il già potente insieme di schemi che possono essere creati usando i comportamenti di governo.
Al fine di organizzare tutti i comportamenti, saranno distribuiti sugli stati. Ogni stato genererà una forza di comportamento specifica, o un insieme di essi, come la ricerca, la fuga e l'evitamento della collisione.
Quando uno stato particolare è attivo, solo la forza risultante sarà applicata al personaggio, facendolo comportare di conseguenza. Ad esempio, se lo stato attualmente attivo è scappa
e le sue forze sono una combinazione di fuggire
e evitare le collisioni
, il personaggio fuggirà da un luogo evitando qualsiasi ostacolo.
Le forze di direzione vengono calcolate ogni aggiornamento di gioco e quindi aggiunte al vettore di velocità del personaggio. Di conseguenza, quando lo stato attivo cambia (e con esso il pattern di movimento), il personaggio passerà gradualmente al nuovo pattern man mano che le nuove forze vengono aggiunte dopo ogni aggiornamento.
La natura dinamica dei comportamenti di guida assicura questa transizione fluida; gli stati coordinano solo quali forze di guida sono attive in un dato momento.
La struttura per implementare un modello di squadra incapsulerà gli FSM e i comportamenti di guida all'interno delle proprietà di una classe. Qualsiasi classe che rappresenta un'entità che si muove o è influenzata in altro modo dalle forze di governo avrà una proprietà chiamata uccelloide
, che è un'istanza del Boid
classe:
public class Boid posizione var pubblica: Vector3D; velocità var pubblica: Vector3D; public var steering: Vector3D; massa pubblica: numero; ricerca funzione pubblica (target: Vector3D, slowingRadius: Number = 0): Vector3D (...) funzione pubblica fuggire (posizione: Vector3D): Vector3D (...) public function update (): void (...) (... )
Il Boid
classe è stata utilizzata nella serie di comportamento dello sterzo e fornisce proprietà come velocità
e posizione
(entrambi i vettori di matematica), insieme ai metodi per aggiungere forze di governo, come ad esempio cercare()
, fuggire()
, eccetera.
Un'entità che utilizza un FSM basato sullo stack avrà la stessa struttura del file Formica
classe dal tutorial FSM precedente: l'FSM stack-based è gestito da cervello
proprietà e ogni stato è implementato come metodo.
Di seguito è il Soldato
classe, che ha comportamento di guida e capacità di gestione delle risorse umane:
soldato di classe pubblica private var brain: StackFSM; // Controlla la roba privata var di FSM: Boid; // Controlla i comportamenti di governo funzione pubblica Soldato (posX: numero, posY: numero, totale: numero) (...) brain = new StackFSM (); // Spingi lo stato "follow" in modo che il soldato segua il leader brain.pushState (segue); public function update (): void // Aggiorna il cervello. Avvia la funzione di stato corrente. brain.update (); // Aggiorna i comportamenti di governo boid.update ();
Il modello di squadra verrà implementato utilizzando una macchina a stati finiti basata sullo stack. I soldati, che sono membri della squadra, seguiranno il leader (controllato dal giocatore), dando la caccia ai nemici vicini.
Quando un nemico muore, potrebbe cadere un oggetto che può essere buono o cattivo (a medikit o a badkit, rispettivamente). Un soldato spezzerà la formazione della squadra e raccoglierà oggetti buoni nelle vicinanze, o evaderà il posto per evitare eventuali cattivi oggetti.
Di seguito una rappresentazione grafica dell'FSM stack-based che controlla il "cervello" del soldato:
Le prossime sezioni presentano l'implementazione di ogni stato. Tutti gli snippet di codice in questo tutorial descrivono l'idea principale di ogni passaggio, omettendo tutte le specifiche relative al motore di gioco utilizzato (Flixel, in questo caso).
Il primo stato da implementare è quello che resterà attivo quasi tutto il tempo: Segui il leader. La parte del looting verrà implementata in seguito, quindi per ora il Seguire
lo stato farà sì che il soldato segua il leader, cambiando lo stato corrente in caccia
se c'è un nemico nelle vicinanze:
public function follow (): void var aLeader: Boid = Game.instance.boids [0]; // ottiene un puntatore al leader addSteeringForce (boid.followLeader (aLeader)); // segui il leader // C'è un mostro nelle vicinanze? if (getNearestEnemy ()! = null) // Sì, c'è! Scoprilo! // Premere lo stato "hunt". Farà smettere al soldato di seguire il capo e // iniziare a dare la caccia al mostro. brain.pushState (caccia); funzione privata getNearestEnemy (): Monster // qui va l'implementazione per ottenere il nemico più vicino
Nonostante la presenza di nemici, mentre lo stato è attivo genererà sempre una forza per seguire il leader, usando il comportamento del leader seguente.
Se getNearestEnemy ()
restituisce qualcosa, significa che c'è un nemico in giro. In tal caso, il caccia
lo stato viene inserito nello stack attraverso la chiamata brain.pushState (caccia)
, facendo in modo che il soldato smetta di seguire il leader e inizi a cacciare i nemici.
Per ora, l'implementazione del caccia()
lo stato può solo saltar fuori dallo stack, in questo modo i soldati non saranno bloccati nello stato di caccia:
public function hunt (): void // Per ora, inseriamo lo stato di caccia () dal cervello. brain.popState ();
Si noti che nessuna informazione è passata al caccia
stato, come chi è il nemico più vicino. Tale informazione deve essere raccolta dal caccia
stato stesso, perché determina se il caccia
dovrebbe rimanere attivo o uscire dallo stack (restituendo il controllo al file Seguire
stato).
Il risultato finora è una squadra di soldati che segue il leader (si noti che i soldati non cacciano perché il caccia()
il metodo si apre solo):
Mancia: ogni stato dovrebbe essere responsabile della fine della sua esistenza spuntando dallo stack.
Il prossimo stato da attuare è caccia
, che farà sì che i soldati cacciano i nemici vicini. Il codice per caccia()
è:
public function hunt (): void var aNearestEnemy: Monster = getNearestEnemy (); // Abbiamo un mostro nelle vicinanze? if (aNearestEnemy! = null) // Sì, lo facciamo. Calcoliamo quanto è distante. var aDistance: Number = calculateDistance (aNearestEnemy, this); // Il mostro è abbastanza vicino da sparare? se (aDistanza <= 80) // Yes, so let's face it! faceEnemyStandingStill(aNearestEnemy); // Fire away! Take that, monster! shoot(); else // No, the monster is far away. Seek it until it gets close enough. addSteeringForce(boid.seek(aNearestEnemy.boid.position)); // Avoid crowding while seeking the target… addSteeringForce(boid.separation()); else // No, there is no monster nearby. Maybe it was killed or ran away. Let's pop the "hunt" // state and come back doing what we were doing before the hunting. brain.popState();
Lo stato inizia assegnando aNearestEnemy
con il nemico più vicino. Se aNearestEnemy
è nullo
significa che non c'è nessun nemico intorno, quindi lo stato deve finire. La chiamata brain.popState ()
apre il caccia
stato, commutando il soldato allo stato successivo nella pila.
Se aNearestEnemy
non è nullo
, significa che c'è un nemico da cacciare e lo stato dovrebbe rimanere attivo. L'algoritmo di caccia si basa sulla distanza tra il soldato e il nemico: se la distanza è maggiore di 80, il soldato cercherà la posizione del nemico; se la distanza è inferiore a 80, il soldato dovrà affrontare il nemico e sparare stando fermo.
Da caccia()
sarà invocato ogni aggiornamento di gioco, se un nemico è nei dintorni, il soldato cercherà o sparerà a quel nemico. La decisione di muovere o sparare è controllata dinamicamente dalla distanza tra il soldato e il nemico.
Il risultato è una squadra di soldati in grado di seguire il leader e dare la caccia ai nemici vicini:
Ogni volta che un nemico viene ucciso, potrebbe far cadere un oggetto. Il soldato deve ritirare l'oggetto se è buono o fuggire dall'oggetto se è cattivo. Questo comportamento è rappresentato da due stati nella FSM precedentemente descritta:
collectItem
e scappa
stati. Il collectItem
lo stato farà arrivare un soldato all'oggetto caduto, mentre il scappa
stato farà il soldato fuggire dalla posizione del cattivo oggetto. Entrambi gli stati sono quasi identici, l'unica differenza è l'arrivo o la forza di fuga:
public function runAway (): void var aItem: Item = getNearestItem (); if (aItem! = null && aItem.alive && aItem.type == Item.BADKIT) var aItemPos: Vector3D = new Vector3D (); aItemPos.x = aItem.x; aItemPos.y = aItem.y; addSteeringForce (boid.flee (aItemPos)); else brain.popState (); public function collectItem (): void var aItem: Item = getNearestItem (); if (aItem! = null && aItem.alive && aItem.type == Item.MEDKIT) var aItemPos: Vector3D = new Vector3D (); aItemPos.x = aItem.x; aItemPos.y = aItem.y; addSteeringForce (boid.arrive (aItemPos, 50)); else brain.popState (); funzione privata getNearestItem (): Item // qui va il codice per ottenere l'elemento più vicino
Qui è utile un'ottimizzazione delle transizioni. Il codice per la transizione dal Seguire
stato al collectItem
o il scappa
gli stati sono gli stessi: controlla se c'è un oggetto nelle vicinanze, quindi premi il nuovo stato.
Lo stato da spingere dipende dal tipo di oggetto. Di conseguenza la transizione a collectItem
o scappa
può essere implementato come un unico metodo, denominato checkItemsNearby ()
:
private function checkItemsNearby (): void var aItem: Item = getNearestItem (); if (aItem! = null) brain.pushState (aItem.type == Item.BADKIT? runAway: collectItem);
Questo metodo controlla l'elemento più vicino. Se è buono, il collectItem
lo stato è spinto nel cervello; se è cattivo, il scappa
stato è spinto. Se non ci sono oggetti da raccogliere, il metodo non fa nulla.
Questa ottimizzazione consente l'uso di checkItemsNearby ()
per controllare la transizione da qualsiasi stato a collectItem
o scappa
. Secondo il soldato FSM, quella transizione esiste in due stati: Seguire
e caccia
.
La loro implementazione può essere leggermente modificata per adattarsi a questa nuova transizione:
public function follow (): void var aLeader: Boid = Game.instance.boids [0]; // ottiene un puntatore al leader addSteeringForce (boid.followLeader (aLeader)); // segue il leader // Controlla se c'è un oggetto da collezionare (o scappa da) checkItemsNearby (); // C'è un mostro nelle vicinanze? if (getNearestEnemy ()! = null) // Sì, c'è! Scoprilo! // Premere lo stato "hunt". Farà smettere al soldato di seguire il capo e // iniziare a dare la caccia al mostro. brain.pushState (caccia); public function hunt (): void var aNearestEnemy: Monster = getNearestEnemy (); // Controlla se c'è un oggetto da collezionare (o scappa da) checkItemsNearby (); // Abbiamo un mostro nelle vicinanze? if (aNearestEnemy! = null) // Sì, lo facciamo. Calcoliamo quanto è distante. var aDistance: Number = calculateDistance (aNearestEnemy, this); // Il mostro è abbastanza vicino da sparare? se (aDistanza <= 80) // Yes, so let's face it! faceEnemyStandingStill(aNearestEnemy); // Fire away! Take that, monster! shoot(); else // No, the monster is far away. Seek it until it gets close enough. addSteeringForce(boid.seek(aNearestEnemy.boid.position)); // Avoid crowding while seeking the target… addSteeringForce(boid.separation()); else // No, there is no monster nearby. Maybe it was killed or ran away. Let's pop the "hunt" // state and come back doing what we were doing before the hunting. brain.popState();
Mentre segue il leader, un soldato controllerà gli oggetti vicini. Quando caccia un nemico, un soldato controlla anche gli oggetti vicini.
Il risultato è la demo qui sotto. Nota che un soldato proverà a raccogliere o eludere un oggetto ogni volta che ce n'è uno nelle vicinanze, anche se ci sono nemici da cacciare e il leader da seguire.
Un aspetto importante per quanto riguarda stati e transizioni è il priorità tra loro. A seconda della linea in cui viene inserita una transizione all'interno dell'implementazione di uno stato, la priorità di tale transizione cambia.
Usando il Seguire
stato e la transizione fatta da checkItemsNearby ()
ad esempio, dai un'occhiata alla seguente implementazione:
public function follow (): void var aLeader: Boid = Game.instance.boids [0]; // ottiene un puntatore al leader addSteeringForce (boid.followLeader (aLeader)); // segue il leader // Controlla se c'è un oggetto da collezionare (o scappa da) checkItemsNearby (); // C'è un mostro nelle vicinanze? if (getNearestEnemy ()! = null) // Sì, c'è! Scoprilo! // Premere lo stato "hunt". Farà smettere al soldato di seguire il capo e // iniziare a dare la caccia al mostro. brain.pushState (caccia);
Quella versione di Seguire()
farà passare un soldato collectItem
o scappa
prima controllare se c'è un nemico in giro. Di conseguenza, il soldato raccoglierà (o fuggirà da) un oggetto anche quando ci sono nemici in giro che dovrebbero essere cacciati dal caccia
stato.
Ecco un'altra implementazione:
public function follow (): void var aLeader: Boid = Game.instance.boids [0]; // ottiene un puntatore al leader addSteeringForce (boid.followLeader (aLeader)); // segui il leader // C'è un mostro nelle vicinanze? if (getNearestEnemy ()! = null) // Sì, c'è! Scoprilo! // Premere lo stato "hunt". Farà smettere al soldato di seguire il capo e // iniziare a dare la caccia al mostro. brain.pushState (caccia); else // Controlla se c'è un oggetto da collezionare (o scappa da) checkItemsNearby ();
Quella versione di Seguire()
farà passare un soldato collectItem
o scappa
solo dopo scopre che non ci sono nemici da uccidere.
L'attuale implementazione di Seguire()
, caccia()
e collectItem ()
soffre di problemi prioritari. Il soldato proverà a raccogliere un oggetto anche quando ci sono cose più importanti da fare. Per sistemarlo, sono necessari alcuni aggiustamenti.
Per quanto riguarda la Seguire
stato, il codice può essere aggiornato a:
(seguire () con priorità)
public function follow (): void var aLeader: Boid = Game.instance.boids [0]; // ottiene un puntatore al leader addSteeringForce (boid.followLeader (aLeader)); // segui il leader // C'è un mostro nelle vicinanze? if (getNearestEnemy ()! = null) // Sì, c'è! Scoprilo! // Premere lo stato "hunt". Farà smettere al soldato di seguire il capo e // iniziare a dare la caccia al mostro. brain.pushState (caccia); else // Controlla se c'è un oggetto da collezionare (o scappa da) checkItemsNearby ();
Il caccia
lo stato deve essere cambiato in:
public function hunt (): void var aNearestEnemy: Monster = getNearestEnemy (); // Abbiamo un mostro nelle vicinanze? if (aNearestEnemy! = null) // Sì, lo facciamo. Calcoliamo quanto è distante. var aDistance: Number = calculateDistance (aNearestEnemy, this); // Il mostro è abbastanza vicino da sparare? se (aDistanza <= 80) // Yes, so let's face it! faceEnemyStandingStill(aNearestEnemy); // Fire away! Take that, monster! shoot(); else // No, the monster is far away. Seek it until it gets close enough. addSteeringForce(boid.seek(aNearestEnemy.boid.position)); // Avoid crowding while seeking the target… addSteeringForce(boid.separation()); else // No, there is no monster nearby. Maybe it was killed or ran away. Let's pop the "hunt" // state and come back doing what we were doing before the hunting. brain.popState(); // Check if there is an item to collect (or run away from) checkItemsNearby();
Finalmente, il collectItem
lo stato deve essere modificato per interrompere qualsiasi saccheggio se c'è un nemico intorno:
public function collectItem (): void var aItem: Item = getNearestItem (); var aMonsterNearby: Boolean = getNearestEnemy ()! = null; if (! aMonsterNearby && aItem! = null && aItem.alive && aItem.type == Item.MEDKIT) var aItemPos: Vector3D = new Vector3D (); aItemPos.x = aItem.x; aItemPos.y = aItem.y; addSteeringForce (boid.arrive (aItemPos, 50)); else brain.popState ();
Il risultato di tutte queste modifiche è la demo dall'inizio del tutorial:
In questo tutorial, hai imparato a codificare un pattern di squadra in cui un gruppo di soldati seguirà un leader, dando la caccia e saccheggiando i nemici vicini. L'intelligenza artificiale è implementata usando un FSM stack-based combinato con diversi comportamenti di governo.
Come dimostrato, le macchine a stati finiti e i comportamenti di guida sono una combinazione potente e una grande combinazione. Spargendo la logica sugli stati FSM, è possibile selezionare dinamicamente quali forze di guida agiranno su un personaggio, consentendo la creazione di complessi pattern AI.
Combina i comportamenti di guida che già conosci con gli FSM e crea modelli nuovi e straordinari!