I comportamenti di sterzata sono ottimi per creare schemi di movimento realistici, ma sono ancora più grandi se puoi controllarli, usarli e combinarli facilmente. In questo tutorial, discuterò e illustrerò l'implementazione di un gestore di movimento per tutti i nostri comportamenti precedentemente discussi.
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. Devi avere una conoscenza di base dei vettori di matematica.
Come discusso in precedenza, ogni comportamento dello sterzo produce una forza risultante (chiamata "forza di governo") che viene aggiunta al vettore di velocità. La direzione e l'entità di quella forza guideranno il personaggio, facendolo muovere secondo uno schema (cercare, fuggire, vagare e così via). Il calcolo generale è:
steering = seek (); // questo può essere qualsiasi comportamento di governo = troncato (sterzo, max_force) sterzo = sterzo / velocità di massa = troncata (velocità + sterzata, max_speed) posizione = posizione + velocità
Poiché la forza di governo è un vettore, può essere aggiunta a qualsiasi altro vettore (proprio come la velocità). Tuttavia, la vera "magia" sta nel fatto che è possibile aggiungere diverse forze di guida insieme: è semplice come:
steering = nothing (); // il vettore nullo, che significa sterzata "forza zero" = sterzo + ricerca (); sterzo = sterzo + fuggi (); (...) sterzo = troncato (sterzo, max_force) sterzata = sterzo / velocità di massa = troncata (velocità + sterzata, max_speed) posizione = posizione + velocità
Le forze di guida combinate produrranno un vettore che rappresenta tutti quelle forze. Nel frammento di codice sopra, la forza di sterzo risultante farà sì che il personaggio cerchi qualcosa mentre allo stesso tempo fuggirà qualcosa altro.
Controlla qui sotto alcuni esempi di forze di guida combinate per produrre un'unica forza di sterzata:
La combinazione di forze di governo produrrà schemi di movimento estremamente complessi senza sforzo. Immagina quanto sarebbe difficile scrivere codice per fare in modo che un personaggio cerchi qualcosa, ma allo stesso tempo evitare un'area specifica, senza usare vettori e forze?
Ciò richiederebbe il calcolo di distanze, aree, percorsi, grafici e simili. Se le cose si stanno spostando, tutti questi calcoli devono essere ripetuti ogni tanto, perché l'ambiente cambia costantemente.
Con i comportamenti di guida, tutte le forze lo sono dinamico. Devono essere calcolati per ogni aggiornamento di gioco, quindi reagiranno in modo naturale e senza problemi ai cambiamenti dell'ambiente.
La demo qui sotto mostra le navi che cercheranno il cursore del mouse, ma fuggiranno dal centro dello schermo, contemporaneamente allo stesso tempo:
Al fine di utilizzare più comportamenti di guida allo stesso tempo in modo semplice e facile, a responsabile del movimento torna utile. L'idea è di creare una "scatola nera" che possa essere collegata a qualsiasi entità esistente, rendendola in grado di eseguire tali comportamenti.
Il manager ha un riferimento all'entità a cui è collegato ("host"). Il gestore fornirà all'host una serie di metodi, come ad esempio cercare()
e fuggire()
. Ogni volta che tali metodi vengono richiamati, il gestore aggiorna le sue proprietà interne per produrre un vettore di forza di governo.
Dopo che il manager elabora tutte le invocazioni, aggiungerà la forza di guida risultante al vettore di velocità dell'host. Ciò cambierà la magnitudine e la direzione del vettore di velocità dell'host in base ai comportamenti attivi.
La figura seguente mostra l'architettura:
Il manager ha una serie di metodi, ognuno dei quali rappresenta un comportamento distinto. Ogni comportamento deve essere fornito con diversi pezzi di informazioni esterne per funzionare.
Il comportamento di ricerca, ad esempio, ha bisogno di un punto nello spazio utilizzato per calcolare la forza di governo verso quel luogo; perseguire ha bisogno di diverse informazioni dal suo obiettivo, come la posizione corrente e la velocità. Un punto nello spazio può essere espresso come un'istanza di Punto
o Vector2D
, entrambe le classi piuttosto comuni in qualsiasi quadro.
L'obiettivo utilizzato nel comportamento di perseguire, tuttavia, può essere qualsiasi cosa. Per rendere il gestore del movimento abbastanza generico, ha bisogno di ricevere un obiettivo che, indipendentemente dal suo tipo, è in grado di rispondere ad alcune "domande", come ad esempio "Qual è la tua velocità attuale?"Utilizzando alcuni principi di programmazione orientata agli oggetti, può essere raggiunto con interfacce.
Supponendo l'interfaccia IBoid
descrive un'entità che può essere gestita dal responsabile del movimento, qualsiasi classe nel gioco può utilizzare i comportamenti di governo, purché implementa IBoid
. Quell'interfaccia ha la seguente struttura:
interfaccia pubblica IBoid function getVelocity (): Vector3D; function getMaxVelocity (): Number; function getPosition (): Vector3D; function getMass (): Number;
Ora che il manager può interagire con tutte le entità di gioco in modo generico, è possibile creare la sua struttura di base. Il gestore è composto da due proprietà (la forza guida risultante e il riferimento host) e una serie di metodi pubblici, uno per ciascun comportamento:
SteeringManager di classe pubblica public var steering: Vector3D; host var pubblico: IBoid; // La funzione pubblica costruttore SteeringManager (host: IBoid) this.host = host; this.steering = new Vector3D (0, 0); // L'API pubblica (un metodo per ogni comportamento) cerca funzione pubblica (target: Vector3D, slowingRadius: Number = 20): void public fugge (target: Vector3D): void public function wander (): void public evade (target: IBoid): void public pursuit (target: IBoid): void // Il metodo di aggiornamento. // Dovrebbe essere chiamato dopo che tutti i comportamenti sono stati invocati public function update (): void // Reimposta la forza di pilotaggio interna. public function reset (): void // La funzione privata API interna doSeek (target: Vector3D, slowingRadius: Number = 0): Vector3D funzione privata doFlee (target: Vector3D): Vector3D funzione privata doWander (): Vector3D funzione privata doEvade (target: IBoid): Vector3D funzione privata doPursuit (target: IBoid): Vector3D
Quando il gestore viene istanziato, deve ricevere un riferimento all'host su cui è collegato. Permetterà al gestore di modificare il vettore di velocità dell'host in base ai comportamenti attivi.
Ogni comportamento è rappresentato da due metodi, uno pubblico e uno privato. Usando cerca come esempio:
ricerca di funzioni pubbliche (target: Vector3D, slowingRadius: Number = 20): void funzione privata doSeek (target: Vector3D, slowingRadius: Number = 0): Vector3D
Il pubblico cercare()
sarà invocato per dire al manager di applicare quel comportamento specifico. Il metodo non ha valore di ritorno e i suoi parametri sono correlati al comportamento stesso, come ad esempio un punto nello spazio. Sotto il cappuccio il metodo privato doSeek ()
sarà invocato e il suo valore di ritorno, la forza guida calcolata per quel determinato comportamento, sarà aggiunto al manager timone
proprietà.
Il seguente codice dimostra l'implementazione di seek:
// Il metodo di pubblicazione. // Riceve un bersaglio da cercare e un rallentamento del flusso (utilizzato per eseguire l'arrivo). ricerca di funzioni pubbliche (obiettivo: Vector3D, slowingRadius: Number = 20): void steering.incrementBy (doSeek (target, slowingRadius)); // La vera implementazione di seek (con codice di arrivo incluso) funzione privata doSeek (target: Vector3D, slowingRadius: Number = 0): Vector3D var force: Vector3D; var distance: Number; desiderato = target.subtract (host.getPosition ()); distanza = lunghezza desiderata; desired.normalize (); se (distanza <= slowingRadius) desired.scaleBy(host.getMaxVelocity() * distance/slowingRadius); else desired.scaleBy(host.getMaxVelocity()); force = desired.subtract(host.getVelocity()); return force;
Tutti gli altri metodi di comportamento sono implementati in modo molto simile. Il ricerca ()
il metodo, ad esempio, sarà simile a questo:
ricerca pubblica (obiettivo: IBoid): void steering.incrementBy (doPursuit (target)); private function doPursuit (target: IBoid): Vector3D distance = target.getPosition (). sottrarre (host.getPosition ()); var updatesNeeded: Number = distance.length / host.getMaxVelocity (); var tv: Vector3D = target.getVelocity (). clone (); tv.scaleBy (updatesNeeded); targetFuturePosition = target.getPosition (). clone (). add (tv); return doSeek (targetFuturePosition);
Usando il codice delle esercitazioni precedenti, tutto ciò che devi fare è adattarle sotto forma di comportamento()
e doBehavior ()
, in modo che possano essere aggiunti al gestore del movimento.
Ogni volta che viene invocato il metodo di un comportamento, la forza risultante prodotta viene aggiunta al gestore timone
proprietà. Di conseguenza, la proprietà accumulerà tutte le forze di governo.
Quando tutti i comportamenti sono stati invocati, il gestore deve applicare la forza di governo attuale alla velocità dell'host, quindi si muoverà in base ai comportamenti attivi. È eseguito nel aggiornare()
metodo del gestore del movimento:
public function update (): void var velocity: Vector3D = host.getVelocity (); posizione var: Vector3D = host.getPosition (); troncare (sterzare, MAX_FORCE); steering.scaleBy (1 / host.getMass ()); velocity.incrementBy (sterzo); truncate (velocity, host.getMaxVelocity ()); position.incrementBy (velocità);
Il metodo sopra deve essere invocato dall'host (o da qualsiasi altra entità di gioco) dopo che sono stati richiamati tutti i comportamenti, altrimenti l'host non cambierà mai il suo vettore di velocità per adattarsi ai comportamenti attivi.
Assumiamo una classe di nome Preda
dovrebbe muoversi usando il comportamento di guida, ma al momento non ha codice di guida né il gestore del movimento. La sua struttura sarà simile a questa:
public class Prey public var position: Vector3D; velocità var pubblica: Vector3D; massa pubblica: numero; funzione pubblica Prey (posX: Number, posY: Number, totalMass: Number) position = new Vector3D (posX, posY); velocity = new Vector3D (-1, -2); massa = totale; x = position.x; y = position.y; public function update (): void velocity.normalize (); velocity.scaleBy (MAX_VELOCITY); velocity.scaleBy (1 / massa); troncare (velocità, MAX_VELOCITY); position = position.add (velocity); x = position.x; y = position.y;
Usando questa struttura, le istanze di classe possono muoversi usando l'integrazione di Eulero, proprio come la prima demo del tutorial di ricerca. Al fine di renderlo in grado di utilizzare il gestore, ha bisogno di una proprietà che fa riferimento al gestore del movimento e deve implementare il IBoid
interfaccia:
public class Prey implementa IBoid public var position: Vector3D; velocità var pubblica: Vector3D; massa pubblica: numero; public var steering: SteeringManager; funzione pubblica Prey (posX: Number, posY: Number, totalMass: Number) position = new Vector3D (posX, posY); velocity = new Vector3D (-1, -2); massa = totale; steering = new SteeringManager (questo); x = position.x; y = position.y; public function update (): void velocity.normalize (); velocity.scaleBy (MAX_VELOCITY); velocity.scaleBy (1 / massa); troncare (velocità, MAX_VELOCITY); position = position.add (velocity); x = position.x; y = position.y; // Di seguito sono riportati i metodi richiesti dall'interfaccia IBoid. funzione pubblica getVelocity (): Vector3D return velocity; public function getMaxVelocity (): Number return 3; public function getPosition (): Vector3D return position; funzione pubblica getMass (): Number return mass;
Il aggiornare()
il metodo deve essere modificato di conseguenza in modo che anche il gestore possa essere aggiornato:
aggiornamento della funzione pubblica (): void // Fai in modo che la preda vada in giro ... steering.wander (); // Aggiorna il gestore in modo che cambi il vettore di velocità delle prede. // Il gestore eseguirà anche l'integrazione di Eulero, cambiando // il vettore di "posizione". steering.update (); // Dopo che il gestore ha aggiornato le sue strutture interne, tutto ciò che dobbiamo // fare è aggiornare la nostra posizione in base al vettore "posizione". x = position.x; y = position.y;
Tutti i comportamenti possono essere utilizzati contemporaneamente, purché tutte le chiamate di metodo vengano eseguite prima del gestore aggiornare()
invocazione, che applica la forza di governo accumulata al vettore di velocità dell'ospite.
Il codice seguente mostra un'altra versione di Prey aggiornare()
metodo, ma questa volta cercherà una posizione sulla mappa e eludere un altro personaggio (entrambi allo stesso tempo):
public function update (): void var destination: Vector3D = getDestination (); // il posto dove cercare var hunter: IBoid = getHunter (); // prendi l'entità che ci sta dando la caccia // Cerca la destinazione ed evadi il cacciatore (allo stesso tempo!) steering.seek (destinazione); steering.evade (cacciatore); // Aggiorna il gestore in modo che cambi il vettore di velocità delle prede. // Il gestore eseguirà anche l'integrazione di Eulero, cambiando // il vettore di "posizione". steering.update (); // Dopo che il gestore ha aggiornato le sue strutture interne, tutto ciò che dobbiamo // fare è aggiornare la nostra posizione in base al vettore "posizione". x = position.x; y = position.y;
La demo di seguito mostra un modello di movimento complesso in cui sono combinati diversi comportamenti. Ci sono due tipi di personaggi nella scena: il Cacciatore e il Preda.
Il cacciatore lo farà perseguire una preda se si avvicina abbastanza; durerà fino a quando dura la riserva di resistenza; quando finisce la resistenza, l'inseguimento viene interrotto e il cacciatore lo farà vagare fino a quando non recupera i suoi livelli di resistenza.
Ecco il cacciatore aggiornare()
metodo:
public function update (): void if (resting && stamina ++> = MAX_STAMINA) resting = false; if (preda! = null &&! resting) steering.pursuit (preda); resistenza - = 2; se (resistenza <= 0) prey = null; resting = true; else steering.wander(); prey = getClosestPrey(position); steering.update(); x = position.x; y = position.y;
La preda lo farà vagare indefinitamente. Se il cacciatore si avvicina troppo, lo farà eludere. Se il cursore del mouse è vicino e non c'è cacciatore intorno, la preda lo farà cercare il cursore del mouse.
Ecco le prede aggiornare()
metodo:
public function update (): void var distance: Number = Vector3D.distance (position, Game.mouse); hunter = getHunterWithinRange (posizione); if (hunter! = null) steering.evade (hunter); if (distance <= 300 && hunter == null) steering.seek(Game.mouse, 30); else if(hunter == null) steering.wander(); steering.update(); x = position.x; y = position.y;
Il risultato finale (il grigio è vagare, il verde è cercare, l'arancione è perseguire, il rosso è eludere):
Un gestore di movimento è molto utile per controllare diversi comportamenti di guida allo stesso tempo. La combinazione di tali comportamenti può produrre schemi di movimento molto complessi, permettendo a un'entità di gioco di cercare una cosa nello stesso momento in cui sfugge a un'altra.
Spero ti sia piaciuto il sistema di gestione discusso e implementato in questo tutorial e utilizzarlo nei tuoi giochi. Grazie per aver letto! Non dimenticare di tenerti aggiornato seguendoci su Twitter, Facebook o Google+.