Ci sono diversi modi per fare un gioco particolare. Di solito, uno sviluppatore sceglie qualcosa che si adatta alle sue capacità, utilizzando le tecniche che già conosce per produrre il miglior risultato possibile. A volte, le persone non sanno ancora che hanno bisogno di una certa tecnica - forse anche più semplice e migliore - semplicemente perché già conoscono un modo per creare quel gioco.
In questa serie di tutorial, imparerai come creare l'intelligenza artificiale per un gioco di hockey utilizzando una combinazione di tecniche, come i comportamenti di guida, che ho precedentemente spiegato come concetti.
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.
L'hockey è uno sport divertente e popolare e, come videogioco, incorpora molti argomenti di gamedev, come schemi di movimento, lavoro di squadra (attacco, difesa), intelligenza artificiale e tattica. Un gioco di hockey giocabile è la soluzione ideale per dimostrare la combinazione di alcune tecniche utili.
Simulare il meccanico dell'hockey, con gli atleti che corrono e si muovono, è una sfida. Se i modelli di movimento sono predefiniti, anche con percorsi diversi, il gioco diventa prevedibile (e noioso). Come possiamo implementare un ambiente così dinamico mantenendo il controllo su ciò che sta accadendo? La risposta è: usando i comportamenti di guida.
I comportamenti dello sterzo mirano a creare modelli di movimento realistici con la navigazione improvvisativa. Si basano su semplici forze che vengono combinate ad ogni aggiornamento del gioco, quindi sono estremamente dinamiche per natura. Questo li rende la scelta perfetta per implementare qualcosa di così complesso e dinamico come un hockey o una partita di calcio.
Per motivi di tempo e insegnamento, riduciamo un po 'la portata del gioco. Il nostro gioco di hockey seguirà solo un piccolo insieme delle regole originali dello sport: nel nostro gioco non ci saranno penalità e nessun portiere, quindi ogni atleta può muoversi intorno alla pista:
Gioco di hockey con regole semplificate.Ogni obiettivo sarà sostituito da un piccolo "muro" senza rete. Al fine di segnare, una squadra deve spostare il disco (il disco) per farlo toccare qualsiasi lato della porta avversaria. Quando qualcuno segna, entrambe le squadre si riorganizzano, e il disco sarà piazzato al centro; la partita riprenderà dopo pochi secondi.
Per quanto riguarda la gestione del puck: se un atleta, ad esempio A, ha il disco, ed è toccato da un avversario, dire B, allora B ottiene il disco e A diventa immobile per alcuni secondi. Se il disco lascia la pista, verrà immediatamente posizionato al centro della pista.
Userò il motore di gioco Flixel per occuparmi della parte grafica del codice. Tuttavia, il codice del motore sarà semplificato o omesso negli esempi, per mantenere l'attenzione sul gioco stesso.
Iniziamo con l'ambiente di gioco, che è composto da una pista, un numero di atleti e due obiettivi. La pista è composta da quattro rettangoli disposti attorno all'area del ghiaccio; questi rettangoli si scontreranno con tutto ciò che li tocca, quindi nulla lascerà l'area ghiacciata.
Un atleta sarà descritto dal Atleta
classe:
Atleta di classe pubblica private var mBoid: Boid; // controlla il comportamento dello sterzo roba privata var mId: int; // un identificatore univoco per la funzione pubblica athelete Atleta (thePosX: Number, thePosY: Number, theTotalMass: Number) mBoid = new Boid (thePosX, thePosY, theTotalMass); public function update (): void // Cancella tutte le forze di governo mBoid.steering = null; // Esplora WanderInTheRink (); // Aggiorna tutte le cose di governo mBoid.update (); private function wanderInTheRink (): void var aRinkCenter: Vector3D = getRinkCenter (); // Se la distanza dal centro è maggiore di 80, // torna al centro, altrimenti continua a vagare. if (Utils.distance (this, aRinkCenter)> = 80) mBoid.steering = mBoid.steering + mBoid.seek (aRinkCenter); else mBoid.steering = mBoid.steering + mBoid.wander ();
La proprietà mBoid
è un'istanza di Boid
classe, un incapsulamento della logica matematica utilizzata nella serie dei comportamenti di guida. Il mBoid
l'istanza ha, tra gli altri elementi, i vettori matematici che descrivono la direzione corrente, la forza di governo e la posizione dell'entità.
Il aggiornare()
metodo nel Atleta
la classe sarà invocata ogni volta che il gioco si aggiorna. Per ora, cancella solo qualsiasi forza di sterzata attiva, aggiunge una forza vagante e infine chiama mBoid.update ()
. Il comando precedente aggiorna tutta la logica di comportamento dello sterzo incapsulata all'interno mBoid
, fare muovere l'atleta (usando l'integrazione di Eulero).
La classe di gioco, che è responsabile del ciclo di gioco, sarà chiamata visualizzarloState
. Ha la pista, due gruppi di atleti (un gruppo per ogni squadra) e due obiettivi:
public class PlayState private var mAthletes: FlxGroup; private var mRightGoal: obiettivo; private var mLeftGoal: obiettivo; public function create (): void // Qui tutto viene creato e aggiunto allo schermo. override public function update (): void // Fa in modo che la pista si scontrino con gli atleti in collisione (mRink, mAthletes); // Assicurati che tutti gli atleti rimangano all'interno della pista. applyRinkContraints (); funzione privata applyRinkContraints (): void // controlla se gli atleti si trovano all'interno dei confini // della pista.
Supponendo che un singolo atleta sia stato aggiunto alla partita, qui sotto è il risultato di tutto finora:
L'atleta deve seguire il cursore del mouse, in modo che il giocatore possa effettivamente controllare qualcosa. Poiché il cursore del mouse ha una posizione sullo schermo, può essere utilizzato come destinazione per il comportamento di arrivo.
Il comportamento di arrivo farà sì che un atleta cerchi la posizione del cursore, rallenti gradualmente la velocità mentre si avvicina al cursore, e alla fine si ferma lì.
Nel Atleta
classe, sostituiamo il metodo errante con il comportamento di arrivo:
public class Athlete // (...) public function update (): void // Cancella tutte le forze di governo mBoid.steering = null; // L'atleta è controllato dal giocatore, // quindi basta seguire il cursore del mouse. followMouseCursor (); // Aggiorna tutte le cose di governo mBoid.update (); private function followMouseCursor (): void var aMouse: Vector3D = getMouseCursorPosition (); mBoid.steering = mBoid.steering + mBoid.arrive (aMouse, 50);
Il risultato è un atleta che può posizionare il cursore del mouse. Poiché la logica del movimento si basa su comportamenti di guida, gli atleti navigano sulla pista in modo convincente e scorrevole.
Usa il cursore del mouse per guidare l'atleta nella demo qui sotto:
Il disco sarà rappresentato dalla classe Disco
. Le parti più importanti sono le aggiornare()
metodo e il mOwner
proprietà:
public class Puck public var velocity: Vector3D; posizione var pubblica: Vector3D; private var mOwner: Atleta; // l'atleta che attualmente porta il disco. public function setOwner (theOwner: Athlete): void if (mOwner! = theOwner) mOwner = theOwner; velocity = null; public function update (): void public function get owner (): Athlete return mOwner;
Seguendo la stessa logica dell'atleta, quella del puck aggiornare()
il metodo sarà invocato ogni volta che il gioco si aggiorna. Il mOwner
la proprietà determina se il disco è in possesso di qualsiasi atleta. Se mOwner
è nullo
, significa che il disco è "libero" e si muoverà, rimbalzando alla fine della pista.
Se mOwner
non è nullo
, significa che il disco viene portato da un atleta. In questo caso, ignorerà eventuali controlli di collisione e sarà posizionato con forza davanti all'atleta. Questo può essere ottenuto usando l'atleta velocità
vettore, che corrisponde anche alla direzione dell'atleta:
Il avanti
il vettore è una copia di quella dell'atleta velocità
vettore, quindi indicano nella stessa direzione. Dopo avanti
è normalizzato, può essere ridimensionato di qualsiasi valore, per esempio, 30
-per controllare fino a che punto il disco sarà posizionato davanti all'atleta.
Finalmente, il disco posizione
riceve l'atleta posizione
aggiunto a avanti
, posizionando il disco nella posizione desiderata.
Di seguito è riportato il codice per tutto ciò:
public class Puck // (...) private function placeAheadOfOwner (): void var ahead: Vector3D = mOwner.boid.velocity.clone (); avanti = normalizza (avanti) * 30; position = mOwner.boid.position + ahead; override public function update (): void if (mOwner! = null) placeAheadOfOwner (); // (...)
Nel visualizzarloState
classe, c'è un test di collisione per verificare se il disco si sovrappone a qualsiasi atleta. Se lo fa, l'atleta che ha appena toccato il disco diventa il suo nuovo proprietario. Il risultato è un disco che "si attacca" all'atleta. Nella demo sottostante, guida l'atleta a toccare il disco al centro della pista per vederlo in azione:
È ora di muovere il disco a causa del colpo di bastone. Indipendentemente dall'atleta che porta il disco, tutto ciò che è necessario per simulare un colpo con il bastone è calcolare un nuovo vettore di velocità. Quella nuova velocità sposterà il disco verso la destinazione desiderata.
Un vettore di velocità può essere generato da un vettore di posizione da un altro; il vettore appena generato passerà quindi da una posizione all'altra. Questo è esattamente ciò che è necessario per calcolare il nuovo vettore di velocità del disco dopo un hit:
Calcolo della nuova velocità del disco dopo un colpo dal bastone.Nell'immagine sopra, il punto di destinazione è il cursore del mouse. La posizione corrente del disco può essere utilizzata come punto di partenza, mentre il punto in cui il disco dovrebbe essere dopo che è stato colpito dal bastone può essere usato come punto finale.
Lo pseudo-codice qui sotto mostra l'implementazione di goFromStickHit ()
, un metodo nel Disco
classe che implementa la logica illustrata nell'immagine sopra:
public class Puck // (...) public function goFromStickHit (theAthlete: Athlete, theDestination: Vector3D, theSpeed: Number = 160): void // Posiziona il puck davanti al proprietario per impedire traiettorie inattese // (ad es. atleta che lo ha appena colpito) placeAheadOfOwner (); // Contrassegna il disco come libero (nessun proprietario) setOwner (null); // Calcola la nuova velocità del puck var new_velocity: Vector3D = theDestination - position; velocity = normalize (new_velocity) * theSpeed;
Il new_velocity
il vettore passa dalla posizione corrente del disco al bersaglio (la destinazione
). Dopo questo, è normalizzato e ridimensionato da la velocità
, che definisce la grandezza (lunghezza) di new_velocity
. Questa operazione, in altre parole, definisce la velocità con cui il disco si sposta dalla posizione corrente alla destinazione. Finalmente, il disco velocità
il vettore è sostituito da new_velocity
.
Nel visualizzarloState
classe, il goFromStichHit ()
il metodo viene invocato ogni volta che il giocatore fa clic sullo schermo. Quando accade, il cursore del mouse viene utilizzato come destinazione per il colpo. Il risultato è visto in questa demo:
Finora, abbiamo avuto un solo atleta che si muoveva attorno alla pista. Con l'aggiunta di più atleti, l'IA deve essere implementata per far sembrare tutti questi atleti come se fossero vivi e pensanti.
Per riuscirci, utilizzeremo una macchina a stati finiti basata sullo stack (FSM stack-based, in breve). Come precedentemente descritto, le FSM sono versatili e utili per implementare l'intelligenza artificiale nei giochi.
Per la nostra partita di hockey, una proprietà chiamata mBrain
sarà aggiunto al Atleta
classe:
Atleta di classe pubblica // (...) private var mBrain: StackFSM; // controlla la funzione pubblica di roba di IA Atleta (thePosX: Number, thePosY: Number, theTotalMass: Number) // (...) mBrain = new StackFSM (); // (...)
Questa proprietà è un'istanza di StackFSM
, una classe precedentemente utilizzata nell'esercitazione FSM. Usa una pila per controllare gli stati IA di un'entità. Ogni stato è descritto come un metodo; quando uno stato viene inserito nello stack, diventa il attivo metodo e viene chiamato durante ogni aggiornamento di gioco.
Ogni stato eseguirà un compito specifico, come spostare l'atleta verso il disco. Ogni stato è responsabile della fine di se stesso, il che significa che è responsabile di scappare dalla pila.
L'atleta può essere controllato dal giocatore o dall'IA ora, quindi aggiornare()
metodo nel Atleta
la classe deve essere modificata per verificare la situazione:
public class Athlete // (...) public function update (): void // Cancella tutte le forze di governo mBoid.steering = null; if (mControlledByAI) // L'atleta è controllato dall'IA. Aggiorna il cervello (FSM) e // stai lontano dalle pareti della pista. mBrain.update (); else // L'atleta è controllato dal giocatore, quindi segui // il cursore del mouse. followMouseCursor (); // Aggiorna tutte le cose di governo mBoid.update ();
Se l'intelligenza artificiale è attiva, mBrain
viene aggiornato, che richiama il metodo di stato attualmente attivo, facendo in modo che l'atleta si comporti di conseguenza. Se il giocatore ha il controllo, mBrain
viene ignorato tutti insieme e l'atleta si muove guidato dal giocatore.
Riguardo agli stati per spingere nel cervello: per ora implementiamo solo due di essi. Uno stato permetterà a un atleta di prepararsi per una partita; quando si prepara per la partita, un atleta si sposta nella sua posizione nella pista e si ferma, fissando il disco. L'altro stato farà semplicemente stare fermo l'atleta e fisserà il disco.
Nelle prossime sezioni, implementeremo questi stati.
Se l'atleta è nel inattivo
stato, smetterà di muoversi e fisserà il disco. Questo stato viene usato quando l'atleta è già in posizione nella pista ed è in attesa che qualcosa accada, come all'inizio della partita.
Lo stato sarà codificato nel Atleta
classe, sotto il inattivo()
metodo:
public class Athlete // (...) public function Athlete (thePosX: Number, thePosY: Number, theTotalMass: Number, theTeam: FlxGroup) // (...) // Indica al cervello che lo stato corrente è 'idle' mBrain.pushState (inattivo); private function idle (): void var aPuck: Puck = getPuck (); stopAndlookAt (aPuck.position); funzione privata stopAndlookAt (thePoint: Vector3D): void mBoid.velocity = thePoint - mBoid.position; mBoid.velocity = normalize (mBoid.velocity) * 0.01;
Poiché questo metodo non esce dallo stack, rimarrà attivo per sempre. In futuro, questo stato si aprirà automaticamente per fare spazio ad altri stati, come ad esempio attacco, ma per ora fa il trucco.
Il stopAndStareAt ()
il metodo segue lo stesso principio usato per calcolare la velocità del disco dopo un colpo. Un vettore dalla posizione dell'atleta alla posizione del disco è calcolato da thePoint - mBoid.position
e usato come nuovo vettore di velocità dell'atleta.
Quel nuovo vettore di velocità sposterà l'atleta verso il disco. Per garantire che l'atleta non si muova, il vettore viene ridimensionato 0.01
, "restringendo" la sua lunghezza quasi a zero. Rende l'atleta smesso di muoversi, ma lo tiene a fissare il disco.
Se l'atleta è nel prepareForMatch
stato, si muoverà verso la sua posizione iniziale, fermandosi tranquillamente lì. La posizione iniziale è dove l'atleta dovrebbe essere giusto prima dell'inizio della partita. Poiché l'atleta dovrebbe fermarsi a destinazione, il comportamento di arrivo può essere riutilizzato:
public class Athlete // (...) private var mInitialPosition: Vector3D; // la posizione nella pista in cui l'atleta deve essere posizionato funzione pubblica Atleta (thePosX: Number, thePosY: Number, theTotalMass: Number, theTeam: FlxGroup) // (...) mInitialPosition = new Vector3D (thePosX, thePosY); // Informa il cervello che lo stato corrente è "inattivo" mBrain.pushState (inattivo); private function prepareForMatch (): void mBoid.steering = mBoid.steering + mBoid.arrive (mInitialPosition, 80); // Sono nella posizione iniziale? if (distance (mBoid.position, mInitialPosition) <= 5) // I'm in position, time to stare at the puck. mBrain.popState(); mBrain.pushState(idle); // (… )
Lo stato usa il comportamento di arrivo per spostare l'atleta verso la posizione iniziale. Se la distanza tra l'atleta e la sua posizione iniziale è inferiore a 5
, significa che l'atleta è arrivato nel luogo desiderato. Quando questo accade, prepareForMatch
si apre dallo stack e spinge inattivo
, rendendolo il nuovo stato attivo.
Di seguito è riportato il risultato dell'utilizzo di un FSM stack-based per controllare diversi atleti. stampa sol
metterli in posizioni casuali nella pista, spingendo il prepareForMatch
stato:
Questo tutorial ha presentato le basi per implementare un gioco di hockey usando i comportamenti di governo e le macchine a stati finiti basati sullo stack. Usando una combinazione di questi concetti, un atleta è in grado di muoversi nella pista, seguendo il cursore del mouse. L'atleta può anche colpire il disco verso una destinazione.
Usando due stati e un FSM stack-based, gli atleti possono riorganizzarsi e passare alla loro posizione nella pista, preparandosi per la partita.
Nel prossimo tutorial imparerai come attaccare gli atleti, portando il disco verso l'obiettivo evitando gli avversari.