Tijdens het werken aan een spel waarin de ruimteschepen door spelers zijn ontworpen en gedeeltelijk kunnen worden vernietigd, stuitte ik op een interessant probleem: het verplaatsen van een schip met boegschroeven is geen gemakkelijke taak. Je kunt het schip eenvoudig verplaatsen en draaien als een auto, maar als je wilt dat schipontwerp en structurele schade de bewegingen van schepen op een geloofwaardige manier beïnvloeden, kan het simuleren van boegschroeven een betere aanpak zijn. In deze zelfstudie laat ik u zien hoe u dit kunt doen.
Ervan uitgaande dat een schip meerdere boegschroeven in verschillende configuraties kan hebben en dat de vorm en fysieke eigenschappen van het schip kunnen veranderen (bijvoorbeeld delen van het schip kunnen worden vernietigd), is het noodzakelijk om te bepalen welke stuwraketten om te vuren om het schip te verplaatsen en draaien. Dat is de belangrijkste uitdaging die we hier moeten aanpakken.
De demo is geschreven in Haxe, maar de oplossing kan eenvoudig in elke taal worden geïmplementeerd. Er wordt uitgegaan van een fysica-engine vergelijkbaar met Box2D of Nape, maar elke engine die de middelen biedt om krachten en impulsen toe te passen en de fysieke eigenschappen van lichamen te onderzoeken, zal doen.Klik op de SWF om het focus te geven en gebruik vervolgens de pijltjestoetsen en de Q- en W-toetsen om verschillende thrusters te activeren. Je kunt overschakelen naar verschillende ruimteschipontwerpen met de 1-4-cijfertoetsen en je kunt op elk blok of een boegschroef klikken om het van het schip te verwijderen.
Dit diagram toont de klassen die het schip vertegenwoordigen en hoe ze zich tot elkaar verhouden:
BodySprite
is een klasse die een fysiek lichaam vertegenwoordigt met een grafische weergave. Hiermee kunnen weergaveobjecten aan vormen worden bevestigd en zorgt ervoor dat ze correct met het lichaam worden verplaatst en geroteerd.
De Schip
klasse is een container met modules. Het beheert de structuur van het schip en gaat over het bevestigen en losmaken van modules. Het bevat een single ModuleManager
aanleg.
Door een module te bevestigen, worden de vorm en het weergaveobject aan het onderliggende gekoppeld BodySprite
, maar het verwijderen van een module vereist wat meer werk. Eerst worden de vorm en het weergaveobject van de module verwijderd uit de BodySprite
, en dan wordt de structuur van het schip gecontroleerd, zodat alle modules die niet zijn verbonden met de kern (de module met de rode cirkel) worden losgemaakt. Dit gebeurt met behulp van een algoritme dat lijkt op het vullen met een overstroming, waarbij rekening wordt gehouden met de manier waarop elke module verbinding kan maken met andere modules (stuwraketten kunnen bijvoorbeeld alleen vanaf één kant worden aangesloten, afhankelijk van hun oriëntatie).
Het losmaken van modules is enigszins anders: hun vorm en weergaveobject zijn nog steeds verwijderd van de BodySprite
, maar zijn dan gekoppeld aan een instantie van ShipDebris
.
Deze manier om het schip te vertegenwoordigen is niet de eenvoudigste, maar ik vond dat het heel goed werkte. Het alternatief zou zijn om elke module als een afzonderlijk lichaam weer te geven en deze samen met een lasverbinding te "lijmen". Hoewel dit het breken van het schip veel gemakkelijker zou maken, zou het schip ook rubberachtig en elastisch aanvoelen als het een groot aantal modules had.
De ModuleManager
is een container die de modules van een schip in zowel een lijst (waardoor eenvoudige iteratie) als een hash-kaart (waardoor gemakkelijke toegang via lokale coördinaten).
De ShipModule
klasse is duidelijk een scheepsmodule. Het is een abstracte klasse die enkele gemaksmethoden en attributen definieert die elke module heeft. Elke modulesubklasse is verantwoordelijk voor het construeren van zijn eigen weergaveobject en -vorm, en voor het bijwerken van zichzelf indien nodig. Modules worden ook bijgewerkt wanneer ze zijn bijgevoegd ShipDebris
, maar in dat geval de attachedToShip
vlag is ingesteld op vals
.
Een schip is dus eigenlijk maar een verzameling functionele modules: bouwstenen waarvan de plaatsing en het type het gedrag van het schip bepalen. Natuurlijk zou het hebben van een mooi schip dat alleen maar ronddrijft als een stapel stenen een saai spel kunnen maken, dus we moeten erachter komen hoe we het kunnen laten bewegen op een manier die leuk is om te spelen en toch overtuigend realistisch is.
Het draaien en verplaatsen van een schip door selectief stuwraketten af te vuren, hun slag te variëren door gas in te stellen of door ze snel achter elkaar in en uit te schakelen, is een moeilijk probleem. Gelukkig is het ook onnodig.
Als je bijvoorbeeld een schip rond een punt zou willen draaien, zou je dat eenvoudig kunnen doen door je physics engine te vertellen dat hij het hele lichaam moet roteren. In dit geval was ik echter op zoek naar een eenvoudige oplossing die niet perfect is, maar wel leuk om te spelen. Om het probleem eenvoudiger te maken, introduceer ik een beperking:
Boegschroeven kunnen alleen aan of uit zijn en ze kunnen hun stuwkracht niet variëren.
Nu we de perfectie en complexiteit hebben verlaten, is het probleem een stuk eenvoudiger. We moeten voor elke boegschroef bepalen of deze aan of uit moet zijn, afhankelijk van de positie op het schip en de input van de speler. We zouden voor elke boegschroef een andere sleutel kunnen toewijzen, maar we zouden eindigen met een interstellaire QWOP, dus we gebruiken de pijltjestoetsen om te draaien en te bewegen, en Q en W voor beschieting.
De eerste opdracht is om het schip naar voren en naar achteren te verplaatsen, omdat dit het eenvoudigste geval is. Om het schip te verplaatsen, schieten we eenvoudig de boegschroeven in de richting tegenovergesteld aan die we willen gaan. Als we bijvoorbeeld vooruit willen gaan, vuren we alle stuwraketten af die naar achteren wijzen.
// Updates van de boegschroef, eenmaal per frame override publieke functie update (): Void if (attachedToShip) // Voorwaarts en achterwaarts verplaatsen als ((Input.check (Key.UP) && orientation == ShipModule.SOUTH) || (Input.check (Key.DOWN) && orientation == ShipModule.NORTH)) fire (thrustImpulse); // Anders opstellen if ((Input.check (Key.Q) && orientation == ShipModule.EAST) || (Input.check (Key.W) && orientation == ShipModule.WEST)) fire (thrustImpulse);
Uiteraard zal dit niet altijd het gewenste effect hebben. Vanwege de bovenstaande beperking, als de boegschroeven niet gelijkmatig worden geplaatst, kan het verplaatsen van het schip ertoe leiden dat het draait. Bovendien is het niet altijd mogelijk om de juiste combinatie van stuwraketten te kiezen om een schip naar behoefte te verplaatsen. Soms zal een combinatie van stuwraketten het schip niet verplaatsen zoals we willen. Dit is een wenselijk effect in mijn spel, omdat het schipschade en slecht scheepsontwerp heel duidelijk maakt.
In dit voorbeeld is het duidelijk dat het afvuren van stuwraketten A, D en E ervoor zorgt dat het schip met de klok mee roteert (en ook wat afwijkt, maar dat is een ander probleem). Het draaien van het schip komt neer op het weten op welke manier een boegschroef bijdraagt aan de rotatie van het schip.
Het lijkt erop dat we hier kijken naar de vergelijking van torque - specifiek het teken en de grootte van het koppel.
Laten we dus eens kijken naar wat het koppel is. Koppel wordt gedefinieerd als een maat voor hoeveel een kracht die op een voorwerp inwerkt ertoe leidt dat dat object roteert:
Omdat we het schip rond zijn zwaartepunt willen draaien, is onze [latex] r [/ latex] de afstandsvector van de positie van onze boegschroef tot het zwaartepunt van het hele schip. Het rotatiecentrum kan elk punt zijn, maar het zwaartepunt is waarschijnlijk het punt dat een speler zou verwachten.
De krachtvector [latex] F [/ latex] is een eenheidsrichtingvector die de oriëntatie van onze boegschroef beschrijft. In dit geval geven we niet om het werkelijke koppel, alleen om het teken, dus het is prima om alleen de richtingsvector te gebruiken.
Aangezien crossproduct niet is gedefinieerd voor tweedimensionale vectoren, werken we eenvoudig met driedimensionale vectoren en wordt de component [latex] z [/ latex] ingesteld op 0
, het maken van de wiskunde prachtig vereenvoudigen:
[latex]
\ tau = r \ keer F \\
\ tau = (r_x, \ quad r_y, \ quad 0) \ tijden (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]
Op deze manier kunnen we berekenen hoe elke boegschroef het schip individueel beïnvloedt. Een positieve retourwaarde geeft aan dat de boegschroef ervoor zorgt dat het schip met de klok mee roteert en vice versa. Het implementeren van deze code is heel eenvoudig:
// Berekent het niet-vrij-koppel met behulp van de vergelijking bovenstaande persoonlijke functie calculationTorque (): Float var distToCOM = shape.localCOM.mul (-1.0); return distToCOM.x * thrustDir.y - distToCOM.y * thrustDir.x; // Thruster-update negeren openbare functie-update (): Void if (attachedToShip) // Als de boegschroef is gekoppeld aan een schip, verwerken we de speler // invoer en vuren de boegschroef af wanneer dat nodig is. var koppel = calculationTorque (); 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) && torque < -torqueThreshold) || (Input.check(Key.RIGHT) && torque > torqueThreshold)) fire (thrustImpulse); else thrusterOn = false; else // Als de boegschroef niet aan een schip is bevestigd, dan is het bevestigd // aan een stuk puin. Als de boegschroef aan het vuren was toen het // los was, zal het een tijdje blijven vuren. // detachedThrustTimer is een variabele die wordt gebruikt als een eenvoudige timer, // en wordt ingesteld wanneer de boegschroef loskomt van een schip. if (detachedThrustTimer> 0) detachedThrustTimer - = NapeWorld.currentWorld.deltaTime; brand (thrustImpulse); else thrusterOn = false; anim (); // Blus de boegschroef door een impuls uit te oefenen op het moederlichaam, // met de richting tegengesteld aan de boegschroefrichting en // magnitude gepasseerd als parameter. // De thrusterOn-vlag wordt gebruikt voor animatie. openbare functie fire (amount: Float): Void var thrustVec = thrustDir.mul (- amount); var impulseVec = thrustVec.rotate (parent.body.rotation); parent.body.applyWorldImpulse (impulseVec, getWorldPos ()); thrusterOn = true;
De gedemonstreerde oplossing is eenvoudig te implementeren en werkt goed voor een spel van dit type. Natuurlijk is er ruimte voor verbetering: deze tutorial en de demo houden geen rekening met het feit dat een schip bestuurd kan worden door iets anders dan een menselijke speler, en het implementeren van een AI-piloot die daadwerkelijk een halfvernield schip kan besturen zou een zeer interessante uitdaging (waar ik op een bepaald moment wel mee geconfronteerd zal worden).