Oort is programming game, where you write Rust code to control spaceships in a battle. Your program will control the ships movement, weapons, radar, and communications. Oort uses newtonian physics, a radar system, and a basic radio system. It has a series of tutorials that slowly introduce you to the complexity of the system. Oort also has several more complex scenarios, that make use of all of the features. This culmalates in regular tournaments that the discord community runs, which battle each others bots to see who’s is the best.
I started going through the Oort tutorials as part of my journey to learn Rust (and because it looks fun). I am trying to work my way through them without looking up too much the “proper” way to do it. Just relying on what I know so far of Rust, and what physics I can remember from collage. Later I will try to make a full bot, using better algorithms and hopefully more standard Rust practices. My code I use for Oort can be found in this repository, which I will be continuing to update as I progress.
These first few tutorials show you some of the more commonly used functions for Oort. They mostly just have you uncomment code. The one thing I do want to note is that to write a bot for Oort, you do so by implementing the Ship struct.
// loads the standard oort apis
use oort_api::prelude::*;
pub struct Ship {}
impl Ship {
pub fn new() -> Ship {
Ship {}
}
pub fn tick(&mut self) {
// This function is called every game tick
// This is where you will put all of your logic for your ship
}
}
Tutorial 5 & 6 start teaching about how to aim and fire your ship. Oort takes place in two dimensions and therefore position, velocity, and acceleration, are all 2d vectors. Oorts comes with a struct Vec2 to help with this.
Tutorial 5 has you firing at a target that is moving with a constant velocity(v). The bullets our ship shoots travel at a speed of 1,000 meters/second. So we have to determine where the enemy will be once our bullets reach it. To do this we can use the following formula to determine the where the target will be(p1) after a certain amount of time (Δt): p1 = p0 + v * Δt
The code would look something like:
let p0 = target();
let v = target_velocity();
let d = target() - position(); // distance to target
let t = d.length() / BULLET_SPEED; // how long before our bullets reach the target
let p1 = p0 + v * t;
Now there is one problem with this, the time for our bullets to reach p0 is different then the time to reach p1. This can cause us the target entirely. My approach to fixing this was to recalculate d and t to match our calculation for p1, and repeat this multiple times.
let mut p1 = p0 + v * t;
for _ in 0..100 {
let d = p1 - position();
let t = d.length() / BULLET_SPEED;
p1 = p0 + v * t;
}
The tick function for our ship will then look like:
let p1 = self.calculate_p1();
let p1_angle = angle_diff(heading(), p1.angle());
// draws a green line from our ship to the target ship
// this is useful to visualize what is happening
draw_line(position(), target(), 0x00ff00);
// draws a cyan line to p1 of the target
// this is where we should be aiming
draw_line(position(), p1, 0x47cbe6);
// Only fire if we are facing p1
// Note: everything is in floats, so p1_angle will never be exactly 0
if p1_angle.abs() < degree_to_radian(0.05) {
fire(0); // this tell the ship to fire weapon number '0'
}
// Turn to face the target
turn(p1_angle * 100.0);
In this one, the target is accelerating(a) in a random direction. So we need to change our formula. It should now be: p1 = p0 + v * t + 1/2 * a * t2
In rust we will first need to calculate a, which is the change in v over time. We will first add a new field to Ship, this will be used to save the v from the previous tick.
pub struct Ship {
prev_v: Vec2, // target v from previous tick
}
Now we can update calculate_p1:
fn calculate_p1(&mut self) -> Vec2 {
let p0 = target();
let v = target_velocity();
let d = target() - position();
let t = d.length() / BULLET_SPEED;
// calculate the acceleration.
// Note: TICK_LENGTH is the amount of seconds of a single tick
// Since we are checking 'v' every tick, then this is the amount of time since the last time we updated 'v'
let a = (v - self.prev_v) / TICK_LENGTH;
// Now we can include 'a' into our p1 calculation
let mut p1 = p0 + v * t + 0.5 * a * t.powi(2);
for _ in 0..100 {
let d = p1 - position();
let t = d.length() / BULLET_SPEED;
p1 = p0 + v * t + 0.5 * a * t.powi(2);
}
// update prev_v, so we can use it next tick
self.prev_v = v;
p1
}
That’s all the change we need now, but there is a few things to keep in mind. The formula we are using assumes a constant a, however the target can and will make changes to their a before our bullets get there. That will cause us to sometimes miss. Thankfully assuming constant a works well enough for now, in the future we may have to come up with something else.
Overall we learned about the basic structure of Oort, and did some physics so we could shoot the target. In my next post we’ll take a look at the radar, and perhaps the radio.