Folosind cuplul și propulsoarele pentru a deplasa și roti un Spaceship proiectat de jucător

În timp ce lucram la un joc în care navele spațiale sunt proiectate de jucători și pot fi distruse parțial, am întâmpinat o problemă interesantă: mișcarea unei nave în jurul mașinilor de împingere nu este o sarcină ușoară. Puteți pur și simplu să vă mișcați și să rotiți nava ca o mașină, dar dacă doriți ca proiectarea navei și daunele structurale să afecteze mișcarea navelor într-un mod credibil, simularea efectivelor ar putea fi o abordare mai bună. În acest tutorial, vă voi arăta cum să faceți acest lucru.

Presupunând că o navă poate avea propulsoare multiple în diverse configurații și că forma și proprietățile fizice ale navei se pot schimba (de exemplu, părți ale navei ar putea fi distruse), este necesar să se determine care pompele de pompare pentru a muta și a roti nava. Aceasta este principala provocare pe care trebuie să o abordăm aici.

Demo-ul este scris în Haxe, dar soluția poate fi ușor implementată în orice limbă. Un motor fizic similar cu Box2D sau Nape este asumat, dar orice motor care furnizează mijloacele de a aplica forțe și impulsuri și de a interoga proprietățile fizice ale corpurilor va face.

Încercați demonstrația

Faceți clic pe SWF pentru ao focaliza, apoi utilizați tastele săgeți și tastele Q și W pentru a activa propulsoarele diferite. Puteți trece la diferite modele de nave spațiale folosind tastele numerice 1-4 și puteți face clic pe orice bloc sau propulsor pentru al scoate de pe navă.


Reprezentarea navei

Această diagramă arată clasele care reprezintă nava și modul în care se raportează reciproc:

BodySprite este o clasă care reprezintă un corp fizic cu o reprezentare grafică. Acesta permite ca obiectele de afisaj să fie atașate la forme și să se asigure că se mișcă și se rotesc corect cu corpul.

Navă clasa este un container de module. Administrează structura navei și se ocupă cu atașarea și detașarea modulelor. Acesta conține un singur ModuleManager instanță.

Atașarea unui modul îi atașează forma și afișează obiectul la baza BodySprite, dar eliminarea unui modul necesită un pic mai mult de lucru. Mai întâi, forma și afișarea obiectului modulului sunt eliminate din BodySprite, și apoi structura navei este verificată astfel încât toate modulele care nu sunt conectate la miez (modulul cu cercul roșu) să fie desprinse. Acest lucru se realizează utilizând un algoritm similar plinului de umplere care ia în considerare modul în care fiecare modul se poate conecta la alte module (de exemplu, propulsoarele se pot conecta numai dintr-o parte, în funcție de orientarea lor).

Modulele de detașare sunt oarecum diferite: forma și obiectul afișajului sunt încă eliminate din BodySprite, dar sunt apoi atașate la un exemplu de ShipDebris.

Acest mod de reprezentare a navei nu este cel mai simplu, dar mi sa părut că funcționează foarte bine. Alternativa ar fi reprezentarea fiecărui modul ca un corp separat și "lipirea" acestora împreună cu o îmbinare de sudură. În timp ce acest lucru ar face ca ruperea navei să fie mult mai ușoară, aceasta ar determina, de asemenea, nava să se simtă cauciucată și elastică dacă ar avea un număr mare de module.

ModuleManager este un container care păstrează modulele unei nave în ambele o listă (care permite iterația ușoară) și o hartă hash (care permite accesul facil prin coordonatele locale).

ShipModule clasa reprezintă în mod evident un modul de navă. Este o clasă abstractă care definește câteva metode și atribute de conveniență pe care fiecare modul le are. Fiecare subclasă a modulului este responsabilă de construirea propriului obiect și formă de afișare și de actualizarea însăși, dacă este necesar. Modulele sunt, de asemenea, actualizate atunci când sunt atașate ShipDebris, dar în acest caz attachedToShip steagul este setat la fals.

Deci, o navă este într-adevăr doar o colecție de module funcționale: blocuri de construcție a căror plasare și tip definește comportamentul navei. Bineînțeles, o navă destul de plutitoare ca o grămadă de cărămizi ar face un joc plictisitor, așa că trebuie să ne dăm seama cum să-l facem să se deplaseze într-un mod care este distractiv de jucat și totuși convingător de realist.


Simplificarea problemei

Rotirea și deplasarea unei nave prin tragerea selectivă a propulsoarelor, variind forța lor prin ajustarea clapetei de accelerație sau prin rotirea și oprirea acestora în succesiune rapidă, este o problemă dificilă. Din fericire, este de asemenea inutil.

Dacă doriți să rotiți o navă exact în jurul unui punct, de exemplu, ați putea face acest lucru pur și simplu spunând motorului fizicii dvs. să rotească întregul corp. În acest caz, totuși, căutam o soluție simplă, care nu este perfectă, dar este distractivă să joace. Pentru a simplifica problema, voi introduce o constrângere:

Propulsoarele pot fi pornite sau oprite și nu pot schimba direcția.

Acum că am abandonat perfecțiunea și complexitatea, problema este mult mai simplă. Trebuie să determinăm, pentru fiecare propulsor, dacă ar trebui să fie pornit sau oprit, în funcție de poziția sa pe navă și de intrarea jucătorului. Am putea atribui o cheie diferită pentru fiecare propulsor, dar am terminat cu un QWOP interstelar, așa că vom folosi tastele săgeată pentru a ne întoarce și a ne mișca, iar Q și W pentru strafing.


Cazul simplu: deplasarea înainte și înapoi a navei

Prima ordine de afaceri este de a muta nava înainte și înapoi, deoarece acesta este cel mai simplu caz posibil. Pentru a mișca nava, pur și simplu vom trage bara de alergare îndreptată spre direcția opusă celei pe care dorim să o ducem. De exemplu, dacă vrem să mergem mai departe, ar fi declanșat toate propulsoarele care se îndreaptă spre spate.

 // Actualizează propulsorul, o dată pe cadru suprascrie actualizarea funcției publice (): Void if (attachedToShip) // Deplasarea înainte și înapoi dacă ((Input.check (Key.UP) && orientation == ShipModule.SOUTH) || (Input.check (Key.DOWN) && orientare == ShipModule.NORTH)) incendiu (thrustImpulse);  // Strafing altfel dacă ((Input.check (Key.Q) && orientation == ShipModule.EAST) || (Input.check (Key.W) && orientare == ShipModule.WEST)) foc (thrustImpulse); 

Evident, acest lucru nu va produce întotdeauna efectul dorit. Datorită constrângerii de mai sus, dacă propulsoarele nu sunt așezate uniform, deplasarea navei ar putea cauza o rotire. În plus, nu este întotdeauna posibil să alegeți combinația potrivită de propulsoare pentru a muta o navă după cum este necesar. Uneori, nici o combinație de propulsoare nu va mișca nava așa cum vrem. Acest lucru este un efect de dorit în jocul meu, deoarece face daune navei și proiectarea rău navei foarte evident.


O configurație a navei care nu se poate mișca înapoi

Rotirea navei

În acest exemplu, este evident că lansatoarele de ardere A, D și E vor face nava să se rotească în sensul acelor de ceasornic (și, de asemenea, să se rănească oarecum, dar asta este o problemă diferită altfel). Rotirea navei se reduce la cunoașterea modului în care un propulsor contribuie la rotația navei.

Se pare că ceea ce căutăm aici este ecuația lui Cuplu - în special semnul și amploarea cuplului.

Deci, haideți să aruncăm o privire asupra cuplului. Cuplul este definit ca o măsură a cât de mult o forță care acționează asupra unui obiect determină rotirea obiectului:

Pentru că vrem să rotim nava în jurul centrului său de masă, latexul nostru este vectorul distanței de la poziția propulsorului nostru până la centrul de masă al întregii nave. Centrul de rotație ar putea fi orice punct, dar centrul de masă este probabil cel pe care un jucător îl așteaptă.

Vectorul de forță [latex] F [/ latex] este un vector de direcție unitară care descrie orientarea propulsorului nostru. În acest caz, nu ne pasă de cuplul real, doar semnul său, deci este bine să folosiți doar vectorul de direcție.

Întrucât produsul încrucișat nu este definit pentru vectorii cu două dimensiuni, vom lucra pur și simplu cu vectori tridimensionali și vom seta componenta [latex] z [/ latex] la 0, făcând matematica să se simplifice frumos:

[Latex]
\ tau = r \ ori F \\
\ tau = (r_x, \ quad r_y, \ quad 0) \ ori (F_x, \ quad F_y, \ quad 0) \\
\ tdot = (-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 \\
[/ Latex]


Cercurile colorate descriu modul în care propulsorul afectează nava: verde indică că propulsorul face ca nava să se rotească în sensul acelor de ceasornic, roșu indică faptul că provoacă nava să se rotească în sens invers acelor de ceasornic. Mărimea fiecărui cerc indică cât de mult acel propulsor afectează rotația navei.

Cu acest lucru, putem calcula modul în care fiecare propulsor afectează individual nava. O valoare de întoarcere pozitivă indică faptul că propulsorul va face nava să se rotească în sensul acelor de ceasornic și invers. Implementarea codului este foarte simplă:

 // Calculează cuplul care nu este destul, folosind ecuația de mai sus funcția privată calculateTorque (): Float var distToCOM = shape.localCOM.mul (-1.0); întoarcere distToCOM.x * thrustDir.y - distToCOM.y * thrustDir.x;  // Update update update (): Void if (attachToShip) // Dacă propulsorul este atașat la o navă, procesăm intrarea player-ului și tragem propulsorul atunci când este necesar. var torque = calculTorque (); dacă ((Input.check (Key.UP) && orientare == ShipModule.SOUTH) || (Input.check (Key.DOWN) && orientare == ShipModule.NORTH)) foc (thrustImpulse);  altfel dacă ((Input.check (Key.Q) && orientation == ShipModule.EAST) || (Input.check (Key.W) && orientare == ShipModule.WEST)) foc (thrustImpulse);  altfel dacă ((Input.check (Key.LEFT) && cuplu < -torqueThreshold) || (Input.check(Key.RIGHT) && torque > torqueThreshold)) foc (împingere);  altfel thrusterOn = false;  altceva // Dacă propulsorul nu este atașat unei nave, atunci este atașat // unei bucăți de resturi. Dacă propulsorul ardea atunci când a fost detașat, va continua să tragă o vreme. // detachedThrustTimer este o variabilă folosită ca un cronometru simplu, // și este setată atunci când propulsorul se detașează de o navă. dacă (detachedThrustTimer> 0) detachedThrustTimer - = NapeWorld.currentWorld.deltaTime; foc (thrustImpulse);  altfel thrusterOn = false;  animate ();  / / Impinge propulsorul prin aplicarea unui impuls corpului părinte, // cu direcția opusă direcției propulsorului și // magnitudinea trecută ca parametru. / / Drapelul thrusterOn este folosit pentru animație. funcția publică (suma: Float): Void var thrustVec = thrustDir.mul (- suma); var impulseVec = thrustVec.rotate (parent.body.ro); părinte.body.applyWorldImpulse (impulseVec, getWorldPos ()); thrusterOn = adevărat; 

Concluzie

Soluția demonstrată este ușor de implementat și funcționează bine pentru un joc de acest tip. Desigur, există loc de îmbunătățire: acest tutorial și demo-ul nu iau în considerare faptul că o navă ar putea fi pilotată de altceva decât un jucător uman, iar implementarea unui pilot AI care poate zbura de fapt o navă pe jumătate distrusă ar fi o provocare foarte interesantă (una trebuie să mă confrunt la un moment dat, oricum).