While working on a game in which the spaceships are designed by players and can be partially destroyed, I encountered an interesting problem: moving a ship around using thrusters is not an easy task. You could simply move and rotate the ship around like a car, but if you want ship design and structural damage to affect ships’ movement in a believable way, actually simulating thrusters could be a better approach. In this tutorial, I’ll show you how to do this.
Assuming a ship can have multiple thrusters in various configurations, and that the ship’s shape and physical properties can change (for example, parts of the ship could be destroyed), it is necessary to determine which thrusters to fire in order to move and rotate the ship. That’s the main challenge we need to tackle here.
The demo is written in Haxe, but the solution can easily be implemented in any language. A physics engine similar to Box2D or Nape is assumed, but any engine that provides the means to apply forces and impulses and query the physical properties of bodies will do.Try the Demo
Click the SWF to give it focus, then use the arrow keys and the Q and W keys to activate different thrusters. You can switch to different spaceship designs using the 1-4 number keys, and you can click any block or thruster to remove it from the ship.
Representing the Ship
This diagram shows the classes that represent the ship, and how they relate to each other:
BodySprite
is a class that represents a physical body with a graphical representation. It allows display objects to be attached to shapes, and makes sure that they move and rotate correctly with the body.
The Ship
class is a container of modules. It manages the structure of the ship and deals with attaching and detaching modules. It contains a single ModuleManager
instance.
Attaching a module attaches its shape and display object to the underlying BodySprite
, but removing a module requires a bit more work. First the module’s shape and display object are removed from the BodySprite
, and then the structure of the ship is checked so that any modules not connected to the core (the module with the red circle) are detached. This is done using an algorithm similar to flood fill that takes into account the way each module can connect to other modules (for example, thrusters can only connect from one side, depending on their orientation).
Detaching modules is somewhat different: their shape and display object are still removed from the BodySprite
, but are then attached to an instance of ShipDebris
.
This way of representing the ship is not the simplest, but I found it to work very well. The alternative would be to represent each module as a separate body and “glue” them together with a weld joint. While this would make breaking the ship apart much easier, it would also cause the ship to feel rubbery and elastic if it had a large number of modules.
The ModuleManager
is a container that keeps the modules of a ship in both a list (allowing easy iteration) and a hash map (allowing easy access via local coordinates).
The ShipModule
class obviously represents a ship module. It’s an abstract class that defines some convenience methods and attributes that each module has. Each module subclass is responsible for constructing its own display object and shape, and for updating itself if needed. Modules are also updated when they’re attached to ShipDebris
, but in that case the attachedToShip
flag is set to false
.
So a ship is really just a collection of functional modules: building blocks whose placement and type defines the behavior of the ship. Of course, having a pretty ship just floating around like a pile of bricks would make for a boring game, so we need to figure out how to make it move around in a way that is fun to play and yet convincingly realistic.
Simplifying the Problem
Rotating and moving a ship by selectively firing thrusters, varying their thrust either by adjusting throttle or by turning them on and off in quick succession, is a difficult problem. Fortunately, it is also an unnecessary one.
If you wanted to rotate a ship precisely around a point, for example, you could do that simply by telling your physics engine to rotate the whole body. In this case, however, I was looking for a simple solution that isn’t perfect, but is fun to play. To make the problem simpler, I’ll introduce a constraint:
Thrusters can only be on or off and they can’t vary their thrust.
Now that we’ve abandoned perfection and complexity, the problem is a lot simpler. We need to determine, for each thruster, whether it should be on or off, depending on its position on the ship and the player’s input. We could assign a different key for each thruster, but we’d end up with an interstellar QWOP, so we’ll use the arrow keys for turning and moving, and Q and W for strafing.
The Simple Case: Moving the Ship Forwards and Backwards
The first order of business is to move the ship forwards and backwards, as this is the simplest possible case. To move the ship, we’ll simply fire the thrusters facing in the direction opposite to the one we want to go. For example, if we wanted to go forward, we’d fire all the thrusters that face backwards.
// Updates the thruster, once per frame override public function update():Void { if (attachedToShip) { // Moving forwards and backwards 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); } } }
Obviously, this will not always produce the desired effect. Due to the above constraint, if the thrusters aren’t placed evenly, moving the ship could cause it to rotate. On top of that, it is not always possible to choose the right combination of thrusters to move a ship as needed. Sometimes, no combination of thrusters will move the ship the way we want. This is a desirable effect in my game, as it makes ship damage and bad ship design very obvious.
A ship configuration that can’t move backwards
Rotating the Ship
In this example, it is obvious that firing thrusters A, D and E will cause the ship to rotate clockwise (and also drift somewhat, but that’s a different problem altogether). Rotating the ship boils down to knowing in what way a thruster contributes to the rotation of the ship.
It turns out that what we’re looking for here is the equation of torque – specifically the sign and magnitude of torque.
So let’s take a look at what torque is. Torque is defined as a measure of how much a force acting on an object causes that object to rotate:
Because we want to rotate the ship around its center of mass, our \(r\) is the distance vector from the position of our thruster to the center of mass of the whole ship. The center of rotation could be any point, but the center of mass is probably the one a player would expect.
The force vector \(F\) is a unit direction vector that describes the orientation of our thruster. In this case we don’t care about the actual torque, only its sign, so it’s okay to use just the direction vector.
Since cross product isn’t defined for two dimensional vectors, we’ll simply work with three dimensional vectors and set the \(z\) component to 0
, making the math simplify beautifully:
\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 \\
\)
The colored circles describe how the thruster affects the ship: green indicates the thruster causes the ship to rotate clockwise, red indicates it causes the ship to rotate counter-clockwise. The size of each circle indicates how much that thruster affects the ship’s rotation.
With this in place, we can calculate how each thruster affects the ship individually. A positive return value indicates that the thruster will cause the ship to rotate clockwise, and vice-versa. Implementing this in code is very straightforward:
// Calculates the not-quite-torque using the equation above private function calculateTorque(): Float { var distToCOM = shape.localCOM.mul( -1.0); return distToCOM.x * thrustDir.y - distToCOM.y * thrustDir.x; } // Thruster update override public function update():Void { if (attachedToShip) { // If the thruster is attached to a ship, we process the player // input and fire the thruster when needed. 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) && torque < -torqueThreshold) || (Input.check(Key.RIGHT) && torque > torqueThreshold)) { fire(thrustImpulse); } else { thrusterOn = false; } } else { // If the thruster isn't attached to a ship, then it is attached // to a piece of debris. If the thruster was firing when it was // detached, it will continue firing for a while. // detachedThrustTimer is a variable used as a simple timer, // and is set when the thruster detaches from a ship. if (detachedThrustTimer > 0) { detachedThrustTimer -= NapeWorld.currentWorld.deltaTime; fire(thrustImpulse); } else { thrusterOn = false; } } animate(); } // Fires the thruster by applying an impulse to the parent body, // with the direction opposite to the thruster direction and // magnitude passed as parameter. // The thrusterOn flag is used for animation. public function fire(amount: Float): Void { var thrustVec = thrustDir.mul(- amount); var impulseVec = thrustVec.rotate(parent.body.rotation); parent.body.applyWorldImpulse(impulseVec, getWorldPos()); thrusterOn = true; }
Conclusion
The demonstrated solution is easy to implement and works well for a game of this type. Of course, there is room for improvement: this tutorial and the demo don’t take into consideration that a ship might be piloted by something other than a human player, and implementing an AI pilot that can actually fly a half-destroyed ship would be a very interesting challenge (one I’ll have to face at some point, anyway).