Samtidigt som jag arbetar med ett spel där rymdskepparna är utformade av spelarna och kan bli delvis förstörda, mötte jag ett intressant problem: att flytta ett skepp runt med hjälp av thrusters är inte en lätt uppgift. Du kan helt enkelt flytta och rotera skeppet runt som en bil, men om du vill ha skeppsdesign och strukturskador som påverkar skeppsrörelsen på ett trovärdigt sätt, kan det faktiskt simulera thrusters vara ett bättre tillvägagångssätt. I den här handledningen visar jag dig hur du gör det här.
Förutsatt att ett fartyg kan ha flera thrusters i olika konfigurationer, och att fartygets form och fysiska egenskaper kan förändras (till exempel delar av fartyget kan förstöras) är det nödvändigt att bestämma som thrusters att elda för att flytta och rotera skeppet. Det är den viktigaste utmaningen vi behöver ta itu med här.
Demon är skrivet i Haxe, men lösningen kan enkelt implementeras på vilket språk som helst. En fysikmotor som liknar Box2D eller Nape antas, men en motor som ger medel att tillämpa krafter och impulser och fråga fysiska egenskaper hos kroppar kommer att göra.Klicka på SWF för att fokusera den, använd sedan piltangenterna och Q och W-tangenterna för att aktivera olika thrusters. Du kan byta till olika rymdskeppsdesign med 1-4 sifferknappar, och du kan klicka på ett block eller en thruster för att ta bort den från skeppet.
Detta diagram visar de klasser som representerar skeppet och hur de relaterar till varandra:
BodySprite
är en klass som representerar en fysisk kropp med en grafisk representation. Det gör att visningsobjekt kan fästas på former och säkerställer att de rör sig och roterar korrekt med kroppen.
De Fartyg
klassen är en behållare med moduler. Den hanterar fartygets struktur och behandlar monterings- och avmonteringsmoduler. Den innehåller en singel ModuleManager
exempel.
Bifoga en modul fäster sin form och visningsobjekt mot det underliggande BodySprite
, men att ta bort en modul kräver lite mer arbete. Först tas modulens form och visningsobjekt bort från BodySprite
, och sedan kontrolleras fartygets struktur så att alla moduler som inte är anslutna till kärnan (modulen med den röda cirkeln) är lossna. Detta görs med en algoritm som liknar översvämningsfyllning som tar hänsyn till hur varje modul kan ansluta till andra moduler (till exempel kan thrusters endast anslutas från en sida, beroende på deras orientering).
Avlägsnande av moduler är något annorlunda: deras form och visningsobjekt avlägsnas fortfarande från BodySprite
, men är sedan kopplade till en instans av ShipDebris
.
Det här sättet att representera skeppet är inte det enklaste, men jag tyckte att det fungerade mycket bra. Alternativet skulle vara att representera varje modul som en separat kropp och "limma" dem tillsammans med en svetsfog. Medan detta skulle göra att fartyget sönderdelas mycket lättare, skulle det också leda till att fartyget känner gummi och elastik om det hade ett stort antal moduler.
De ModuleManager
är en behållare som håller modulerna i ett fartyg i både en lista (tillåter lätt iteration) och en hash-karta (som tillåter enkel åtkomst via lokala koordinater).
De ShipModule
klassen representerar självklart en skeppsmodul. Det är en abstrakt klass som definierar några bekvämlighetsmetoder och attribut som varje modul har. Varje modulunderklass är ansvarig för att konstruera sitt eget visningsobjekt och -form och för att uppdatera sig om det behövs. Moduler uppdateras också när de är fästade ShipDebris
, men i så fall attachedToShip
flaggan är inställd på falsk
.
Så ett skepp är egentligen bara en samling funktionella moduler: byggstenar vars placering och typ definierar skeppets beteende. Att ha ett fint skepp som bara svävar runt som en hög med tegel skulle naturligtvis göra ett tråkigt spel, så vi behöver ju räkna ut hur man får det att röra sig på ett sätt som är roligt att spela och ändå övertygande realistiskt.
Att rotera och flytta ett fartyg genom att selektivt skjuta tryckkrafterna, varierar deras tryck antingen genom att justera gasreglaget eller genom att snabbt slå dem på och av, är ett svårt problem. Lyckligtvis är det också en onödig.
Om du vill rotera ett skepp exakt runt en punkt, kan du till exempel göra det genom att berätta för din fysikmotor att rotera hela kroppen. I det här fallet letade jag dock efter en enkel lösning som inte är perfekt, men det är kul att spela. För att göra problemet enklare, introducerar jag en begränsning:
Thrusters kan bara vara på eller av och de kan inte variera deras dragkraft.
Nu när vi har övergivit perfektion och komplexitet är problemet mycket enklare. Vi måste bestämma, för varje propeller, huruvida den ska vara på eller av, beroende på dess position på fartyget och spelarens inmatning. Vi kunde tilldela en ny nyckel för varje propeller, men vi skulle sluta med en interstellär QWOP, så vi använder piltangenterna för att vrida och flytta, och Q och W för strafing.
Den första verksamheten är att flytta skeppet framåt och bakåt, eftersom det här är det enklaste möjliga fallet. För att flytta skeppet, ska vi helt enkelt skjuta de thrusters som står inför i motsatt riktning mot den vi vill åka. Till exempel, om vi ville gå framåt, skulle vi släcka alla thrusters som vänder mot bakåt.
// Uppdaterar thrusteren, en gång per ram överstyr public function update (): Radera if (attachedToShip) // Flytta framåt och bakåt om ((Input.check (Key.UP) && orientation == ShipModule.SOUTH) || (Input.check (Key.DOWN) && orientering == ShipModule.NORTH)) eld (thrustImpulse); // Strafing annars om ((Input.check (Key.Q) && orientering == ShipModule.EAST) || (Input.check (Key.W) && orientation == ShipModule.WEST)) eld (thrustImpulse);
Självklart kommer detta inte alltid att ge den önskade effekten. På grund av ovanstående begränsning, om de inte placeras jämnt, kan fartyget få det att rotera. Utöver det är det inte alltid möjligt att välja rätt kombination av thrusters för att flytta ett skepp efter behov. Ibland kommer ingen kombination av thrusters att flytta skeppet som vi vill. Detta är en önskvärd effekt i mitt spel, eftersom det gör fartygsskador och dåligt skeppsdesign väldigt uppenbart.
I det här exemplet är det uppenbart att skjutreglage A, D och E kommer att orsaka att fartyget roterar medurs (och driver också något, men det är ett annat problem helt och hållet). Att rotera fartyget kollar ner för att veta hur en propeller bidrar till fartygets rotation.
Det visar sig att det vi söker här är ekvationen för vridmoment - specifikt tecknet och magneten av vridmomentet.
Så låt oss ta en titt på vad vridmomentet är. Vridmoment definieras som ett mått på hur mycket en kraft som verkar på ett objekt gör att objektet roterar:
Eftersom vi vill rotera skeppet runt dess masscentrum är vår [latex] r [/ latex] avståndsvektorn från positionen av vår propeller till mitten av hela skeppets massa. Rotationscentrumet kan vara någon punkt, men masscentrumet är förmodligen det en spelare förväntar sig.
Kraftvektorn [latex] F [/ latex] är en enhetsriktningsvektor som beskriver orienteringen av vår propeller. I det här fallet bryr vi oss inte om själva vridmomentet, bara dess tecken, så det är okej att bara använda riktningsvektorn.
Eftersom korsprodukt inte definieras för tvådimensionella vektorer, arbetar vi helt enkelt med tredimensionella vektorer och sätter komponenten [latex] z [/ latex] till 0
, göra matematiken förenkla vackert:
[latex]
\ tau = r \ gånger F \\
\ tau = (r_x, \ quad r_y, \ quad 0) \ gånger (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 detta på plats kan vi beräkna hur varje thruster påverkar skeppet individuellt. Ett positivt returvärde indikerar att propelleren kommer att orsaka att fartyget roterar medurs och vice versa. Genomförandet av detta i kod är mycket enkelt:
// Beräknar icke-vridmomentet med ekvationen ovanför den privata funktionen beräknaTorque (): Float var distToCOM = shape.localCOM.mul (-1.0); returnera distToCOM.x * thrustDir.y - distToCOM.y * thrustDir.x; // Thruster-uppdatering åsidosätta uppdateringen av den offentliga funktionen (): Radera if (attachedToShip) // Om propelleren är ansluten till ett fartyg, behandlar vi spelaren // ingången och bränner propelleren vid behov. var vridmoment = beräknaTorque (); ((Input.check (Key.UP) annars om ((Input.check (Key.Q) && orientation == ShipModule.EAST) || (Input.check (Key.W) && orientation == ShipModule.WEST)) eld (thrustImpulse); annars om ((Input.check (Key.LEFT) && vridmoment < -torqueThreshold) || (Input.check(Key.RIGHT) && torque > vridmomentThreshold)) eld (thrustImpulse); annars thrusterOn = false; else // Om propelleren inte är fäst vid ett fartyg, är det fästat // till en skräp. Om bromsen brände när den var // fristående, fortsätter den att skjuta på ett tag. // friståendeThrustTimer är en variabel som används som en enkel timer, // och är inställd när bromsen lossnar från ett fartyg. om (friståendeThrustTimer> 0) detachedThrustTimer - = NapeWorld.currentWorld.deltaTime; brand (thrustImpulse); annars thrusterOn = false; animera (); // Brännar propelleren genom att impulsera förälderkroppen, // med motsatt riktning mot propellerriktningen och // magnitud som passerar som parameter. // ThrusterEn flagga används för animering. allmän funktion brand (mängd: Float): Void var thrustVec = thrustDir.mul (- mängd); var impulsVec = thrustVec.rotate (parent.body.rotation); parent.body.applyWorldImpulse (impulseVec, getWorldPos ()); thrusterOn = true;
Den demonstrerade lösningen är lätt att implementera och fungerar bra för ett spel av denna typ. Självklart finns det utrymme för förbättringar. Denna handledning och demo tar inte hänsyn till att ett fartyg kan bli pilotat av något annat än en mänsklig spelare och genomföra en AI-pilot som faktiskt kan flyga ett halvt förstört skepp skulle vara en mycket intressant utmaning (en jag måste möta någon gång, ändå).