Spaceship Racer: Part 1 - Intro, Setup, & Movement

::Intro⌗
I absolutely love Redout. Ever since playing the Wipeout series at friends’ houses when younger, I’ve really enjoyed the futuristic design and speedy handling of anti-gravity (AG) racers. When talking with a friend recently about bite-sized game ideas that could be prototyped quickly and result in something fun, I thought it’d be fun to try recreating the fluid but responsive handling of Redout (and similar contemporary love letters to the genre like BallisticNG) in the world of 2D where the vector math would be a bit easier to debug.
2D Spaceship Racer is the result of my first pass at this using Godot. I’d still consider myself pretty fresh to Godot outside of some graphics experiments using its rendering engine, so it was a good opportunity to really build some intuition for how the engine works, get to know some quirks of GDScript (I’ve run into suprisingly few!), and take a concept from idea to prototype.
This article will be a mix of chronological series going through a few of the major iterations of the project, and a documentation of the how and why.

::Setting the Scene⌗
The whole project takes place in just one scene, with a pretty simple structure:
- player
- player_sprite
- player_collision
- player_debug_text
- camera
- star_sprite
- starfield
Our player
represents the spaceship and is a Kinematic2D object, and player_collision
is a CollisionShape2D oval that isn’t currently used, but might be used for barrier / checkpoint intersection in the future. player_debug_text
is a RichTextLabel
used for conveniently printing debug information. camera
is a Camera2D
that represents the game’s viewport and is pinned to the player to make sure that the ship is always centered and followed.
The remaining two items, star_sprite
and starfield
are used to create a dynamic starfield that makes up a “scrolling” background for the game. This isn’t actually scrolling, but rather dynamically removing off-screen stars and introducing new ones! star_sprite
is the base Sprite
that is duplicated and placed as children of starfield, which is originally an empty Node2D
.

::Player Movement⌗
The real focus of this project is in the movement; I wanted to get a sense of speed that was in control, fluid turning, and braking that felt much more like aerodynamically-influenced air braking than an arbitrary slow-down or grippy road braking.
::Getting inputs⌗
Before getting to those details, let’s take a quick look at the the function to get inputs and set them to instance variables defined on the player
class:
func get_input():
# get inputs from set project-level input maps
drift_input = Input.get_axis("move_left","move_right")
rotation_input += Input.get_axis("turn_left","turn_right")
thrust_input = Input.get_action_strength("accelerate")
brake_input = braking_coeff*Input.get_action_strength("brake")
(Note: unfortunately, my site theme doesn’t yet have support for GDScript highlighting, so I’m going to be using Python syntax highlihgting for these blocks.)
This function gets mappings for the named inputs from the project’s Input Map (Project --> Project Settings... --> Input Map
) and assigns them to the instance variables each time it’s called. A bit boring, but worth calling out since this greatly simplifies a process of writing code to listen for specific keys and writing a bunch of boilerplate for supporting key mappings from one or more devices. Godot’s documentation on 2D movement covers this really well.
::Calculating Forces⌗
There are a handful of forces that should be acting on the spaceship at any point in time:
- Thrust (forward)
- Turning (rotational)
- Drag (countering motion)
- Braking (reverse)
Thrust⌗
Let’s start with thrust: in a “real” spaceship, this is the propulsion force from the rear-facing thrusters, and the amount of thrust input will determine the force. Because force and acceleration are linearly proportional, without the other factors, this should translate directly to acceleration. The function looks like:
func apply_thrust():
var scaled_thrust_input = thrust_coeff*thrust_input
velocity += Vector2(0,-scaled_thrust_input).rotated(rotation)
return
thrust_coeff
, as with other coeffs seen in the code, is a scaling factor to make the overall “feel” of the motion seem right. Because this project has no scale, there is no accurate value for anything like the forces the thrusters would produce or masss of spaceship.
To apply the thrust, a vector with the thrust input pointing up created (y increases going down in screen space), and then rotated to match the direction of the player.
Turning⌗

On wheeled vehicles, turning is a pretty complicated process; you need contact with the road, friction from that contact, and the geometry of what is making contact with the road and how determines the arc of the vehicle. This is easy to get wrong and make a car that feels like a yacht to drive in-game.
Fortunately, for a spaceship racer, we don’t have to worry about this at all. Realistically, smaller thrusters positioned along different angles of on the ship can create some torque on the ship and create angular moment. To keep things simple, and since the ship is a compact one, this is just applied as a simple rotation around the center axis of the ship. By itself, it’s an easy one-liner:
rotation += rotation_input * rotation_coeff * delta
where rotation_input is the strength of rotational input between [-1,1]
, rotation_coeff
a coefficient for how quickly the ship is allowed to rotate, and delta
is the built-in variable passed to every call of _physics_process
to keep motion smooth invariant of frame rate hiccups.
The ability to turn around the ship’s own axis regardless of where the ship’s headed and how fast gives it a really “spacy” feel that I like and probably won’t mess with until taking this 3D.
Drag⌗
Drag is something that really doesn’t exist in space, but we want to model to make the ship feel tighter to control and less “floaty” (as things are in a gravity-less vacuum). In an atmosphere, drag is a force proportional to air density, the surface area / shape of the object, and speed. Without modeling these forces carefully (maybe a fun idea for a 3D game in the future), we can approximate this force in one line applied at each _physics_process
function call: velocity *= 0.98
. No matter where it’s headed, the velocity will scale to 98% of what it was the previous physics frame each time. This is an area for improvement, where the geometry of whether the ship is facing the direction it’s going (where it’s more aerodynamic) or perpendicular (where it’s not) would have an effect on this.
Braking⌗
This is where some of the vector math starts to shine! A staple of the AG racing is responsive air braking, where braking force is higher the faster the vehicle is going. I wanted to replicate this, but with the specific condition that the air brakes on the spaceship only work when they ship is facing the direction it’s going. This deserves its own post and will be covered in Part 2.