The original WipEout cover art for Playstation. Image source: mobygames.com

::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.

the game in action
The demo in action. The main goal of this project was in building and iterating on the ship's handling and feel when playing on a gamepad. I plan to post the project source on Github eventually. PS: Those stars about represent the extent of my pixel art skills. The space ship sprite asset is by Guardian5 on itch.io.

::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.

the basic space scene
Okay, so not exactly Wipeout material here, but we've got everything needed to start tuning those ship controls!

::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:

  1. Thrust (forward)
  2. Turning (rotational)
  3. Drag (countering motion)
  4. 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
ackermann steering on a car
How a typical road car's steering works. The tie rod keeps the two wheels on the same angle to the axle, and this creates a center of rotation on the inside of the car. For the spaceship in this prototype, the ICR will be right in the center of the sgement B. (Source)

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.