"Thrust reversers deployed on the CFM56 engine of an Airbus A321". Image and caption from Wikipedia.

This is a continuation of Part 1 of the Spaceship Racer series. If you haven’t checked that out and want to read about project setup, thrust, turning, and drag forces, feel free to read that first. I’ll be going into a bit of a discussion of air and thrust-based braking to kick things off, then get to the implementation.

::Primer on aircraft braking forces

In Part 1, I mentioned how turning for the spaceship can feasibly take place around its central axis because there’s no application of force coming from the wheels, but rather an arbitrary balance of space thrusters. Similarly, for braking forces, we don’t have friction on brake pads that then take advantage of grip against the ground surface to slow the vehicle down. Rather, we’ve got aerodynamics and thrust vectoring in play. Unlike the turning, I decided to give this a bit more of a proper treatment in the game. Before going any further, I feel the need to disclaim that I’m neither an aerospace engineer nor aerodynamicist. I just like things that go fast and handle nimbly. :)

Because there is nothing to “grip” against in the air or space, braking is done by redirecting forces in a new direction, ideally counter to the direction of travel.

In real-world aircraft, this is done by opening parts of the engine body outward. This does two things:

  1. Increases aerodynamic drag. By creating additional surface area in the direction of travel, more force is applied to the aircraft from the front, and leads to some slowing down. This is a type of “passive” braking in that the brake force is affected by the shape of the changing surface, but otherwise is dependent only on speed & direction.

  2. Creates additional channels behind the turbine to redirect air flow through and back out facing ahead. This is very similar in principle to (1), but has the added benefit of being able to redirect forces from the thrusters ahead rather than behind and actively push the vehicle back with air.

Both of these processes work in an atmosphere, and for a air jet engine, neither will work in the absence of air.

However, if the thrust mechanism uses some other form of propulsion, whether real (solid propulsion) or sci-fi (Mega Ion Hyperdrive), then redirecting these forces is possible. Although the ship in this demo is in space, anti-gravity (AG) racing games tend to have a more air-braking style braking response, so I’ve modeled a basic version of that.

Anakin's podracer from Star Wars
Podracers from Star Wars appear to use some combination of air braking and reverse thrusting. Note the flaps on the sides of the engines on Anakin's podracer. Image source.

::Implementation

Projections

My first idea for the braking force was to create a braking vector pointing behind the ship with magnitude modulated by braking force, then project it onto the line of velocity vector. The idea here is that the projected vector magnitude (the scalar projection) will be the component of the braking that applies to the ship’s heading.

Intuiviely, this means that braking when facing toward the direction of travel maximizes braking force, and braking when perpendicular to travel makes the brakes completely ineffective.

My first pass at a function to apply brakes looked something like this:

func apply_brakes():
	braking_vector = Vector2(0,brake_input).rotated(rotation)

	braking_projected_on_velocity = velocity.normalized() * braking_vector.dot(velocity.normalized())
	
    print(rad2deg(velocity.angle_to(braking_vector)))
	
    # if braking vector is less than 90* from velocity (they are aligned),
    # it has no effect (without this, braking can be used to 
    # accelerate facing backward)
	if rad2deg(abs(velocity.angle_to(braking_vector))) < 90:
		braking_projected_on_velocity = Vector2(0,0)
	
	velocity += braking_projected_on_velocity

    """
	2 problems with this:
		1. braking is way too powerful, and controlling that is convoluted due to dot product
		2. we check the angle anyway, but then also do dot product, why don't we use dot to begin with
	"""

This worked, but had some key problems. Before we get to that, let’s break down the steps.

braking_vector = Vector2(0,brake_input).rotated(rotation) will take a vector with only a y component, and rotate it to face the direction of the space ship. Note that this is facing forward, which is a bit confusing, but ensures that the projection will be a positive scalar.

From there, braking_projected_on_velocity = velocity.normalized() * braking_vector.dot(velocity.normalized()) calculates the projection based on the dot product of the direction the ship is facing and the direction it’s traveling. The dot product results in a scalar, which is multiplied with a unit vector of the original velocity to produced a scaled, new velocity.

if rad2deg(abs(velocity.angle_to(braking_vector))) < 90:
		braking_projected_on_velocity = Vector2(0,0)

This snippet ensures that if the angle between the two vectors is acute, then the braking force is zero. Without this, facing backward would produce an increase in velocity rather than braking.

This solution did provide braking, but it was difficult to tune the amount of braking force given it was the product of so many different values in a non-physically-scaled context. Also, this method checks for the angle between the vectors anyway. If that is being calculated, then we might as well just get that and calculate the cosine.

Soh-Cah-Toa

(section header explanation)

Here’s what that refactor looks like.

var braking_angle_to_velocity = braking_vector.angle_to(velocity)

# [-1,1]
var braking_effectiveness = cos(braking_angle_to_velocity)
# [0,1]
braking_effectiveness = max(0,braking_effectiveness)

braking_vector = braking_effectiveness*braking_vector
print(braking_vector)

# prevent "over-braking" for negative velocity
if braking_vector.length() > velocity.length():
    velocity = Vector2(0,0)
else:
    velocity += braking_vector

return

Now this is cleaner. Instead of dealing with projections and angle checkng, we get the angle once, and get both the magnitude and sign of the result in one line. A quick max can make sure the effectiveness is never less than zero (which would result in acceleration). A quick conditional check to make sure that the braking effect is not enough to reverse direction is the last step before adjusting the velocity.

(Braking with thrust redirection could actually cause a reversal in direction, but that’s something I don’t want players to deal with here.)

Can we go simpler?

Before writing this post, I gave the above solution another look, and wondered if the angle calculations were unnecessary. Here’s what I came up with:

braking_vector = Vector2(0,braking_force*braking_coeff).rotated(rotation)

# convert velocity to un-rotated space
# (rel x pointing sideways, rel y pointing up/down)
var rel_velocity = velocity.rotated(-rotation)

var braking_amt_x = braking_force*0.3
var braking_amt_y = braking_force*0.7

#add the velocity component-wise
var braking_force_factor = 0.01
rel_velocity.x -= rel_velocity.x*braking_amt_x*braking_force_factor
rel_velocity.y -= rel_velocity.y*braking_amt_y*braking_force_factor
print(rel_velocity)

# reverse back to global space
velocity = rel_velocity.rotated(rotation)

return

So what’s going on here? Up until now, I was rotating the braking vector to match the ship’s coordinate system. Well, what if the ship’s velocity were transformed instead? This is done by rotating the velocity by -rotation, and aligns the forward-back component to the y-axis and left-right component to the x-axis. Now, the braking force can be split into x and y components and applied separately. This likely is computationally less efficient than the vector math above, but does allow for finer control over how the braking force is applied and scales, and I can tune it to “feel right”. You miht notice that I add a smaller dampening to the horizontal direction anyway. This is kind of like baking in extra air resistance, and helps prevent awkward handling. I might someday write a more physically-based model, but that day isn’t today!

::Up next: a starfield to fly in

All this speed and handling is hard to appreciate against a plain background. When first prototyping the motion, I just created a background with procedural noise to test against, but I thought it’d be fun to add some stars in there. I drew up a quick star sprite in LibreSprite and wrote a script to procedurally spawn, randomly place and scale, and despawn the stars as the ship travels. More on this in part 3!