Ship Motion Physics

Currently the ship sprites are positioned by simple time-varying functions. We need to switch this to being controlled by some sort of physics. There are a couple parts to the physics:

  1. Motion Dynamics (drag, inertia, application of thrust)
  2. Collision Detection

This page will cover the motion dynamics.


The first part is to define our in-game ship entity. The ship entity needs to store the current position, the velocity, and the state of the engine. To make it easier to render, the ship entity also contains it's color and to allow the motion physics to be separated from the input/control logic, the application of force/thrust is also a separate member:


#![allow(unused)]
fn main() {
pub struct Ship {
    pub position: Transform2d,
    pub velocity: Transform2d,
    pub linear_thrust: f32,
    pub turning_thrust: f32,
    pub color: (f32, f32, f32, f32),
}
}

Inside the game we can now create a vector of ships, and render it with a single ship sprite:


#![allow(unused)]
fn main() {
pub struct App {
    ....
    ship_sprite: ShipSprite,
    ship_entities: Vec<Ship>,
    ....
}

<< snip >>

        // Render all the ships
        self.ship_sprite.world_to_camera = world_to_camera;
        self.ship_sprite.camera_to_clipspace = camera_to_clipspace;

        for ship in &self.ship_entities {
            self.ship_sprite.world_to_sprite = ship.position.to_mat3_array();
            self.ship_sprite.ship_color = ship.color;
            self.ship_sprite.ship_engine = ship.linear_thrust;
            self.ship_sprite.render(&self.gl);
        }
}

So now that we can see our ship entities, what does the motion physics look like?

  1. The engine should provide thrust in the direction the ship is facing
  2. There should be damping/drag to slow the ship down

Conceptually:

acceleration -= k_drag * velocity
acceleration += ship_direction * thrust * k_thrust

velocity += acceleration * delta_time
position += velocity * delta_time

Turns out that's all that's really required:


#![allow(unused)]
fn main() {
use super::transform::Transform2d;

const ENGINE_THRUST: f32 = 10.0;
const TURNING_THRUST: f32 = 40.0;
const LINEAR_DAMPING: f32 = 2.0;
const ANGULAR_DAMPING: f32 = 8.0;

pub struct Ship {
    pub position: Transform2d,
    pub velocity: Transform2d,
    pub linear_thrust: f32,
    pub angular_thrust: f32,
    pub color: (f32, f32, f32, f32),
}

impl Ship {
    pub fn new(color: (f32, f32, f32, f32), start_transform: Transform2d) -> Self {
        Ship {
            position: start_transform,
            velocity: Transform2d::new(0.0, 0.0, 0.0, 0.0),
            linear_thrust: 0.0,
            angular_thrust: 0.0,
            color: color,
        }
    }

    pub fn update(&mut self, dt: f32) {
        let angle: f32 = self.position.rot;

        let c = f32::cos(angle);
        let s = f32::sin(angle);

        let forwards = (-s, c);

        let mut acceleration = (0.0, 0.0, 0.0);
        acceleration.0 += forwards.0 * self.linear_thrust * ENGINE_THRUST;
        acceleration.1 += forwards.1 * self.linear_thrust * ENGINE_THRUST;
        acceleration.2 += self.angular_thrust * TURNING_THRUST;

        acceleration.0 -= self.velocity.x * LINEAR_DAMPING;
        acceleration.1 -= self.velocity.y * LINEAR_DAMPING;
        acceleration.2 -= self.velocity.rot * ANGULAR_DAMPING;

        self.velocity.x += acceleration.0 * dt;
        self.velocity.y += acceleration.1 * dt;
        self.velocity.rot += acceleration.2 * dt;

        // Integration
        self.position.x += self.velocity.x * dt;
        self.position.y += self.velocity.y * dt;
        self.position.rot += self.velocity.rot * dt;

        self.position.rot = wrap_angle(self.position.rot);
    }
}

fn wrap_angle(angle: f32) -> f32 {
    // Ensure a number is between pi and -pi
    // Not sure if this is the optimal way, but it works
    let angle = angle + std::f32::consts::PI; // Work between 0 and 2PI;
    let sig = f32::signum(angle);
    let mag = f32::abs(angle) % (2.0 * std::f32::consts::PI);

    return sig * (mag - std::f32::consts::PI);
}
}

Connect up some input to one of the ships:


#![allow(unused)]
fn main() {
    pub fn key_event(&mut self, event: KeyboardEvent) {

        let player_entity = &mut self.ship_entities[0];
        if event.code() == "KeyW" {
            player_entity.linear_thrust = 1.0;
        }
        if event.code() == "KeyS" {
            player_entity.linear_thrust = -1.0;
        }
        if event.code() == "KeyA" {
            player_entity.angular_thrust = 1.0;
        }
        if event.code() == "KeyD" {
            player_entity.angular_thrust = -1.0;
        }
    }
}

And we are good to go:

You'll notice that once you start turning it keeps turning, that's because we haven't yet turned the keypress events into something that cleanly signals if the player is holding the key down or not. I was also sneaky and defined the camera transform as the X/Y transform of the player.