Mens jeg jobber med et spill der romskipene er designet av spillerne og kan bli delvis ødelagt, opplevde jeg et interessant problem: å flytte et skip rundt med thrusters er ikke en lett oppgave. Du kan bare flytte og rotere skipet som en bil, men hvis du vil ha skipsdesign og strukturelle skader som påvirker skipets bevegelse på en troverdig måte, kan det faktisk være en bedre tilnærming å simulere thrusters. I denne veiledningen vil jeg vise deg hvordan du gjør dette.
Forutsatt at et skip kan ha flere thrustere i forskjellige konfigurasjoner, og at skipets form og fysiske egenskaper kan forandres (for eksempel deler av skipet kunne bli ødelagt), er det nødvendig å bestemme hvilken thrusters å brenne for å flytte og rotere skipet. Det er den viktigste utfordringen vi må takle her.
Demoen er skrevet i Haxe, men løsningen kan enkelt implementeres på alle språk. En fysikkmotor som ligner Box2D eller Nape antas, men en hvilken som helst motor som gir midler til å bruke krefter og impulser og spørre kroppens fysiske egenskaper, vil gjøre.Klikk på SWF for å gi det fokus, bruk piltastene og Q og W-tastene for å aktivere forskjellige thrusters. Du kan bytte til forskjellige romskipdesigner med 1-4 talltastene, og du kan klikke på en hvilken som helst blokk eller thruster for å fjerne den fra skipet.
Dette diagrammet viser klassene som representerer skipet, og hvordan de forholder seg til hverandre:
BodySprite
er en klasse som representerer en fysisk kropp med en grafisk representasjon. Det gjør at visningsobjekter kan festes til figurer, og sørger for at de beveger seg og roterer riktig med kroppen.
De Skip
klassen er en beholder med moduler. Den styrer skipets struktur og omhandler vedlegg og løsriving av moduler. Den inneholder en singel ModuleManager
forekomst.
Ved å legge til en modul festes form og visningsobjekt til underliggende BodySprite
, men å fjerne en modul krever litt mer arbeid. Først blir modulens form og skjermobjekt fjernet fra BodySprite
, og så kontrolleres skipets struktur slik at moduler som ikke er koblet til kjernen (modulen med den røde sirkelen) er løsnet. Dette gjøres ved hjelp av en algoritme som lik flomfylling som tar hensyn til måten hver modul kan koble til andre moduler (for eksempel kan thrusters bare koble fra den ene siden, avhengig av deres orientering).
Avmontering av moduler er noe annerledes: deres form og skjermobjekt er fortsatt fjernet fra BodySprite
, men er da knyttet til en forekomst av ShipDebris
.
Denne måten å representere skipet på er ikke det enkleste, men jeg fant det å fungere veldig bra. Alternativet ville være å representere hver modul som en egen kropp og "lim" dem sammen med en sveiseled. Mens dette ville gjøre at skipet skiller seg fra hverandre mye lettere, ville det også føre til at skipet skulle føles gummiaktig og elastisk hvis det hadde et stort antall moduler.
De ModuleManager
er en beholder som holder modulene til et skip i både en liste (tillater enkel iterasjon) og et hash kart (som gir enkel tilgang via lokale koordinater).
De ShipModule
klassen representerer åpenbart en skipsmodul. Det er en abstrakt klasse som definerer noen bekvemmelighetsmetoder og attributter som hver modul har. Hver underklasse av moduler er ansvarlig for å bygge sitt eget skjermobjekt og -form, og for å oppdatere seg selv om nødvendig. Modulene oppdateres også når de er festet til ShipDebris
, men i så fall attachedToShip
flagget er satt til falsk
.
Så et skip er egentlig bare en samling funksjonelle moduler: byggeklosser hvis plassering og type definerer oppførselen til skipet. Selvfølgelig, hvis du har et vakkert skip som bare svinger rundt som en haug med murstein, vil det være et kjedelig spill, så vi må finne ut hvordan du får det til å bevege seg på en måte som er morsomt å spille og likevel overbevisende realistisk.
Rotere og flytte et skip ved å selektivt skyte thrustere, variere deres trykk enten ved å justere gasspjeldet eller ved å slå dem på og av i rask rekkefølge, er et vanskelig problem. Heldigvis er det også en unødvendig.
Hvis du ønsker å rotere et skip nøyaktig rundt et punkt, kan du for eksempel gjøre det ved å fortelle din fysikkmotor for å rotere hele kroppen. I dette tilfellet var jeg imidlertid på jakt etter en enkel løsning som ikke er perfekt, men det er morsomt å spille. For å gjøre problemet enklere, introduserer jeg en begrensning:
Thrusters kan bare være på eller av, og de kan ikke variere deres trykk.
Nå som vi har forlatt perfeksjon og kompleksitet, er problemet mye enklere. Vi må bestemme, for hver thruster, om den skal være av eller på, avhengig av posisjonen på skipet og spillerens innspill. Vi kunne tildele en annen nøkkel for hver thruster, men vi ville ende opp med en interstellar QWOP, så vi vil bruke piltastene for å snu og flytte, og Q og W for strafing.
Den første rekkefølgen er å flytte skipet fremover og bakover, da dette er det enkleste mulige tilfellet. For å flytte skipet, vil vi bare skyte thrusters i retning motsatt den vi vil gå. For eksempel, hvis vi ønsket å gå videre, ville vi brenne alle thrusters som vender bakover.
// Oppdaterer thrusteren, en gang per ramme overstyre offentlig funksjonoppdatering (): Gyldig if (attachedToShip) // Flytter fremover og bakover hvis ((Input.check (Key.UP) && orientation == ShipModule.SOUTH) || (Input.check (Key.DOWN) && orientation == ShipModule.NORTH)) brann (thrustImpulse); // Strafing annet hvis ((Input.check (Key.Q) && orientation == ShipModule.EAST) || (Input.check (Key.W) && orientation == ShipModule.WEST)) brann (thrustImpulse);
Tydeligvis vil dette ikke alltid gi den ønskede effekten. På grunn av den ovennevnte begrensningen, hvis de ikke plasseres jevnt, kan skipet føre til at det roterer. Dessuten er det ikke alltid mulig å velge riktig kombinasjon av thrustere for å flytte et skip etter behov. Noen ganger vil ingen kombinasjon av thrusters flytte skipet slik vi vil. Dette er en ønskelig effekt i mitt spill, da det gjør skipsskader og dårlig skipsdesign veldig tydelig.
I dette eksemplet er det åpenbart at skyttere A, D og E vil føre til at skipet roterer med urviseren (og driver også noe, men det er et annet problem helt). Roterende skipet koker ned for å vite på hvilken måte en thruster bidrar til rotasjonen av skipet.
Det viser seg at det vi leter etter her er ligningen til moment - spesifikt tegnet og størrelsen på dreiemomentet.
Så la oss se på hvilket moment som er. Dreiemoment er definert som et mål på hvor mye en kraft som virker på en gjenstand, får objektet til å rotere:
Fordi vi ønsker å rotere skipet rundt sitt massesenter, er vår [latex] r [/ latex] avstandsvektoren fra posisjonen til våre thruster til midten av hele skipets masse. Rotasjonssenteret kan være noe poeng, men massesenteret er sannsynligvis det en spiller forventer.
Kraftvektoren [latex] F [/ latex] er en retningsvektor som beskriver orienteringen til våre thruster. I dette tilfellet bryr vi oss ikke om det aktuelle dreiemomentet, bare dets tegn, så det er greit å bare bruke retningsvektoren.
Siden kryssprodukt ikke er definert for todimensjonale vektorer, jobber vi enkelt med tredimensjonale vektorer og setter komponenten [latex] z [/ latex] til 0
, gjør matematikken forenklet vakkert:
[Latex]
\ tau = r \ ganger 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 \\
[/ Latex]
Med dette på plass kan vi beregne hvordan hver thruster påvirker skipet individuelt. En positiv returverdi indikerer at thrusteren vil føre til at skipet roterer med klokken og omvendt. Implementering av dette i kode er veldig grei:
// Beregner ikke-vridningsmomentet ved hjelp av ligningen over privat funksjon calculateTorque (): Float var distToCOM = shape.localCOM.mul (-1.0); returner distToCOM.x * thrustDir.y - distToCOM.y * thrustDir.x; // Thruster-oppdatering overstyrer oppdatering av offentlig funksjon (): Opphev if (attachedToShip) // Hvis thrusteren er festet til et skip, behandler vi spilleren // -inngangen og brenner thrusteren når det trengs. var dreiemoment = calculateTorque (); hvis ((Input.check (Key.UP) && orientation == ShipModule.SOUTH) || (Input.check (Key.DOWN) && orientation == ShipModule.NORTH)) brann (thrustImpulse); ellers hvis ((Input.check (Key.Q) && orientation == ShipModule.EAST) || (Input.check (Key.W) && orientation == ShipModule.WEST)) brann (thrustImpulse); annet hvis ((Input.check (Key.LEFT) && moment < -torqueThreshold) || (Input.check(Key.RIGHT) && torque > torqueThreshold)) brann (thrustImpulse); ellers thrusterOn = false; ellers // Hvis propelleren ikke er festet til et skip, så er det festet // til et stykke rusk. Hvis thrusteren ble avbrutt da den var // frittstående, vil den fortsette å skyte for en stund. // frittståendeThrustTimer er en variabel som brukes som en enkel timer, // og er satt når propelleren løsner fra et skip. hvis (eneboligThrustTimer> 0) detachedThrustTimer - = NapeWorld.currentWorld.deltaTime; brann (thrustImpulse); ellers thrusterOn = false; animere (); // Brenner thrusteren ved å bruke en impuls til overordnet kropp, // med retningen motsatt thrusterretningen og // størrelsesorden passert som parameter. // Den thrusterEn flagg brukes til animasjon. offentlig funksjon brann (mengde: Float): Feid var thrustVec = thrustDir.mul (- amount); var impulseVec = thrustVec.rotate (parent.body.rotation); parent.body.applyWorldImpulse (impulseVec, getWorldPos ()); thrusterOn = true;
Den demonstrerte løsningen er enkel å implementere og fungerer bra for et spill av denne typen. Selvfølgelig er det rom for forbedring: denne opplæringen og demoen tar ikke hensyn til at et skip kan bli pilotert av noe annet enn en menneskelig spiller, og implementering av en AI-pilot som faktisk kan fly et halvt ødelagt skip ville være en veldig interessant utfordring (en jeg må møte på et tidspunkt, uansett).