Usando Torque e Thrusters per muovere e ruotare un'astronave progettata dal giocatore

Mentre sto lavorando a un gioco in cui le astronavi sono progettate dai giocatori e possono essere parzialmente distrutte, ho riscontrato un problema interessante: spostare una nave usando i propulsori non è un compito facile. Si potrebbe semplicemente spostare e ruotare la nave come se fosse una macchina, ma se si desidera che il progetto della nave e il danno strutturale influiscano sul movimento delle navi in ​​modo credibile, la simulazione dei propulsori potrebbe essere un approccio migliore. In questo tutorial, ti mostrerò come farlo.

Supponendo che una nave possa avere più propulsori in varie configurazioni e che la forma e le proprietà fisiche della nave possano cambiare (ad esempio, parti della nave potrebbero essere distrutte), è necessario determinare quale propulsori a fuoco per muovere e ruotare la nave. Questa è la sfida principale che dobbiamo affrontare qui.

La demo è scritta in Haxe, ma la soluzione può essere facilmente implementata in qualsiasi lingua. Si presume un motore fisico simile a Box2D o Nape, ma qualsiasi motore che fornisce i mezzi per applicare forze e impulsi e interrogare le proprietà fisiche dei corpi farà.

Prova la demo

Fare clic sul file SWF per metterlo a fuoco, quindi utilizzare i tasti freccia e i tasti Q e W per attivare diversi propulsori. Puoi passare a diversi progetti di astronavi usando i tasti numerici 1-4 e puoi fare clic su qualsiasi blocco o propulsore per rimuoverlo dalla nave.


Rappresentare la nave

Questo diagramma mostra le classi che rappresentano la nave e come si relazionano tra loro:

BodySprite è una classe che rappresenta un corpo fisico con una rappresentazione grafica. Consente agli oggetti di visualizzazione di essere attaccati alle forme e fa in modo che si muovano e ruotino correttamente con il corpo.

Il Nave la classe è un contenitore di moduli. Gestisce la struttura della nave e si occupa di collegare e scollegare i moduli. Contiene un singolo ModuleManager esempio.

L'associazione di un modulo allega la sua forma e visualizza l'oggetto sul sottostante BodySprite, ma rimuovere un modulo richiede un po 'più di lavoro. Prima la forma e l'oggetto di visualizzazione del modulo vengono rimossi dal BodySprite, e quindi la struttura della nave viene controllata in modo che tutti i moduli non collegati al nucleo (il modulo con il cerchio rosso) siano staccati. Questo viene fatto usando un algoritmo simile al fill flood che tiene conto del modo in cui ogni modulo può connettersi ad altri moduli (ad esempio, i thrusters possono connettersi solo da un lato, a seconda del loro orientamento).

Il distacco dei moduli è un po 'diverso: la loro forma e l'oggetto di visualizzazione sono ancora rimossi dal BodySprite, ma sono quindi collegati a un'istanza di ShipDebris.

Questo modo di rappresentare la nave non è il più semplice, ma ho trovato che funziona molto bene. L'alternativa sarebbe quella di rappresentare ciascun modulo come un corpo separato e "incollarli" insieme con un giunto di saldatura. Mentre ciò renderebbe molto più semplice rompere la nave, farebbe sì che la nave si senta gommosa ed elastica se avesse un gran numero di moduli.

Il ModuleManager è un contenitore che mantiene i moduli di una nave sia in una lista (permettendo una facile iterazione) sia in una mappa hash (che consente un facile accesso tramite coordinate locali).

Il ShipModule la classe rappresenta ovviamente un modulo nave. È una classe astratta che definisce alcuni metodi e attributi di convenienza che ogni modulo ha. Ogni sottoclasse di moduli è responsabile della costruzione del proprio oggetto di visualizzazione e della sua forma e, se necessario, dell'aggiornamento stesso. I moduli vengono aggiornati anche quando sono collegati ShipDebris, ma in tal caso il attachedToShip flag è impostato su falso.

Quindi una nave è in realtà solo una collezione di moduli funzionali: elementi costitutivi il cui posizionamento e tipo definisce il comportamento della nave. Certamente, avere una bella nave che galleggia come una pila di mattoni sarebbe un gioco noioso, quindi abbiamo bisogno di capire come farlo muovere in un modo che sia divertente da giocare e tuttavia in modo convincente realistico.


Semplificare il problema

Girare e muovere una nave tirando selettivamente i propulsori, variando la loro spinta regolando l'acceleratore o accendendoli e spegnendoli in rapida successione, è un problema difficile. Fortunatamente, è anche inutile.

Ad esempio, se si desidera ruotare una nave esattamente intorno a un punto, è possibile farlo semplicemente dicendo al proprio motore fisico di ruotare l'intero corpo. In questo caso, tuttavia, stavo cercando una soluzione semplice che non fosse perfetta, ma è divertente da giocare. Per semplificare il problema, introdurrò un vincolo:

I propulsori possono essere attivati ​​o disattivati ​​e non possono variare la loro spinta.

Ora che abbiamo abbandonato la perfezione e la complessità, il problema è molto più semplice. Dobbiamo determinare, per ciascun propulsore, se debba essere acceso o spento, a seconda della sua posizione sulla nave e dell'input del giocatore. Potremmo assegnare una chiave diversa per ogni propulsore, ma finiremmo con un QWOP interstellare, quindi useremo i tasti freccia per girare e spostare, e Q e W per mitragliare.


Il caso semplice: spostare la nave avanti e indietro

Il primo ordine del giorno è spostare la nave avanti e indietro, poiché questo è il caso più semplice possibile. Per spostare la nave, faremo semplicemente fuoco ai propulsori rivolti nella direzione opposta a quella che vogliamo percorrere. Ad esempio, se volessimo andare avanti, spareremmo tutti i propulsori che si trovano ad affrontare all'indietro.

 // Aggiorna il thruster, una volta per frame sovrascrive la funzione public update (): Void if (attachedToShip) // Spostamento avanti e indietro if ((Input.check (Key.UP) && orientation == ShipModule.SOUTH) || (Input.check (Key.DOWN) && orientation == ShipModule.NORTH)) fire (thrustImpulse);  // Strafing else if ((Input.check (Key.Q) && orientation == ShipModule.EAST) || (Input.check (Key.W) && orientation == ShipModule.WEST)) fire (thrustImpulse); 

Ovviamente, questo non produrrà sempre l'effetto desiderato. A causa del vincolo sopra riportato, se i propulsori non sono posizionati in modo uniforme, lo spostamento della nave potrebbe causarne la rotazione. Inoltre, non è sempre possibile scegliere la giusta combinazione di propulsori per spostare una nave secondo necessità. A volte, nessuna combinazione di propulsori sposta la nave nel modo desiderato. Questo è un effetto desiderabile nel mio gioco, in quanto rende molto evidente il danneggiamento della nave e il cattivo design della nave.


Una configurazione di nave che non può muoversi all'indietro

Rotating the Ship

In questo esempio, è ovvio che i propulsori A, D ed E spingeranno la nave a ruotare in senso orario (e anche alla deriva leggermente, ma questo è un problema completamente diverso). La rotazione della nave si riduce a sapere in che modo un propulsore contribuisce alla rotazione della nave.

Si scopre che quello che stiamo cercando qui è l'equazione di momento torcente - in particolare il segno e l'entità della coppia.

Diamo un'occhiata a quale coppia è. La coppia è definita come una misura di quanto una forza che agisce su un oggetto fa ruotare quell'oggetto:

Poiché vogliamo ruotare la nave attorno al suo centro di massa, il nostro [latex] r [/ latex] è il vettore di distanza dalla posizione del nostro propulsore al centro di massa dell'intera nave. Il centro di rotazione potrebbe essere qualsiasi punto, ma il centro di massa è probabilmente quello che un giocatore si aspetterebbe.

Il vettore di forza [latex] F [/ latex] è un vettore di direzione unitaria che descrive l'orientamento del nostro propulsore. In questo caso non ci interessa la coppia attuale, solo il suo segno, quindi va bene usare solo il vettore di direzione.

Poiché il prodotto incrociato non è definito per vettori bidimensionali, lavoreremo semplicemente con vettori tridimensionali e imposteremo il componente [latex] z [/ latex] su 0, rendere la matematica semplificare magnificamente:

[Lattice]
\ tau = r \ volte F \\
\ tau = (r_x, \ quad r_y, \ quad 0) \ times (F_x, \ quad F_y, \ quad 0) \\
\ tau = (-0 \ cdot F_y + r_y \ cdot 0, \ quad 0 \ cdot F_x - r_x \ cdot 0, \ quad -r_y \ cdot F_x + r_x \ cdot F_y) \\
\ tau = (0, \ quad 0, \ quad -r_y \ cdot F_x + r_x \ cdot F_y) \\
\ tau_z = r_x \ cdot F_y - r_y \ cdot F_x \\
[/ Lattice]


I cerchi colorati descrivono come il propulsore influisce sulla nave: il verde indica che il propulsore fa ruotare la nave in senso orario, il rosso indica che fa ruotare la nave in senso antiorario. La dimensione di ciascun cerchio indica quanto quel propulsore influisce sulla rotazione della nave.

Con questo, possiamo calcolare il modo in cui ciascun propulsore influisce sulla nave individualmente. Un valore di ritorno positivo indica che il propulsore farà ruotare la nave in senso orario e viceversa. L'implementazione di questo codice è molto semplice:

 // Calcola la not-quite-torque usando l'equazione sopra la funzione privata calculateTorque (): Float var distToCOM = shape.localCOM.mul (-1.0); return distToCOM.x * thrustDir.y - distToCOM.y * thrustDir.x;  // L'aggiornamento del thruster sovrascrive l'aggiornamento della funzione pubblica (): Void if (attachedToShip) // Se il thruster è collegato a una nave, elaboriamo l'input // del player e spariamo al thruster quando necessario. var torque = calculateTorque (); if ((Input.check (Key.UP) && orientation == ShipModule.SOUTH) || (Input.check (Key.DOWN) && orientation == ShipModule.NORTH)) fire (thrustImpulse);  else if ((Input.check (Key.Q) && orientation == ShipModule.EAST) || (Input.check (Key.W) && orientation == ShipModule.WEST)) fire (thrustImpulse);  else if ((Input.check (Key.LEFT) && coppia < -torqueThreshold) || (Input.check(Key.RIGHT) && torque > torqueThreshold)) fire (thrustImpulse);  else thrusterOn = false;  else // Se il propulsore non è attaccato a una nave, allora è allegato // a un pezzo di detriti. Se il propulsore sparava quando era // staccato, continuerà a sparare per un po '. // detachedThrustTimer è una variabile utilizzata come semplice timer, // ed è impostata quando il propulsore si stacca da una nave. if (detachedThrustTimer> 0) detachedThrustTimer - = NapeWorld.currentWorld.deltaTime; il fuoco (thrustImpulse);  else thrusterOn = false;  animate ();  // Spara il propulsore applicando un impulso al corpo genitore, // con la direzione opposta alla direzione del propulsore e // magnitudine passata come parametro. // Il flag thrusterOn è utilizzato per l'animazione. fuoco di funzione pubblica (importo: Float): Void var thrustVec = thrustDir.mul (- amount); var impulseVec = thrustVec.rotate (parent.body.rotation); parent.body.applyWorldImpulse (impulseVec, getWorldPos ()); thrusterOn = true; 

Conclusione

La soluzione dimostrata è facile da implementare e funziona bene per un gioco di questo tipo. Certo, c'è spazio per miglioramenti: questo tutorial e la demo non prendono in considerazione il fatto che una nave possa essere pilotata da qualcosa di diverso da un giocatore umano, e implementare un pilota di IA che possa effettivamente pilotare una nave semidistrutta sarebbe una sfida molto interessante (una che dovrò affrontare ad un certo punto, comunque).