Counting Laps
The main purpose of this game is to fly the ship around the map and be faster than the AI's. The game needs to keep track of the laps so that it an tell who wins.
So how can we count laps? Well, we can check if the player crosses the start line. How do we know when this happens? Well, if we have the transform of the start line we can check which side of the start line a ship is currently on. Then we can track when it changes side.
So let's create a struct to represent the score:
#![allow(unused)] fn main() { struct Score { laps: Vec<f64>, previous_progress: f32 } }
Why is laps a vector of f64's? Surely it should be an integer of some
sort? Well, we may as well store the time in which the lap is completed.
The lap count is easily derived using score.laps.len()
and as a bonus
the time difference between players is easily calculable.
And the previous_progress is to store a value telling how far around the track the player is. Assuming we have a way to tell how far around they are, we can do:
#![allow(unused)] fn main() { /// Checks if the player crosses the start/finish line and updates /// the score to match pub fn update(&self, map: &Map, ship: &Ship, time: f64) { let current_progress = map.calc_progress_relative_to_startline((ship.position.x, ship.position.y)); // Progress has jumped from previously being near 1.0 (nearly completed) // to being near 0.0 (just started), so they probably did a lap if self.previous_progress > 0.8 && current_progress < 0.2{ self.laps.push(time); } // Progress has jumped from previously being near 0.0 (just started) // to being close to 1.0 (nearly completed) so the player went back // across the line. if self.previous_progress < 0.2 && current_progress > 0.8{ self.laps.pop(); } self.previous_progress = current_progress } }
This uses a mythical map.calc_progress_relative_to_startline
function that takes
a position in global coordinates and returns a single floating point
number. This number should jump jump from 1.0 to 0.0 when the player crosses the
start line, and can be anything else when the player is a long way away.
You may have expected it to be zero at the start line and go positive/negative when moving each direction, but then you have to deal with a discontinuity and where the startline is. By placing the discontinuity at the start line it reduces the number of edge cases.
So how can we calc_progress_relative_to_startline
. One way would be to convert
the player position into polar coordinates and use the angle. But this wouldn't
take the "tilt" of the startline into account so would only be accurate for the middle
of the track. But if we transform the ships position into the local coordinate
system of the start line:
#![allow(unused)] fn main() { let start_position = self.get_start_position(); let start_line_direction = self.get_track_direction(start_position.angle); let start_position_cartesian = start_position.to_cartesian(); let distance_from_startline = ( position.0 - start_position_cartesian.0, position.1 - start_position_cartesian.1 ); let s = f32::sin(-start_line_direction); let c = f32::cos(-start_line_direction); let position_local = ( c*distance_from_startline.0 - s*distance_from_startline.1, s*distance_from_startline.0 + c*distance_from_startline.1 ); }
We can then check to see if the player is near the start line:
#![allow(unused)] fn main() { if f32::abs(position_local.0) > self.track_width { // Position is off to the side of the track 0.5 } else { // Offset so that start line is at progress = 1.0 let progress = position_local.1 + 1.0; if progress > 1.5 { // Position is a long way in front of the line 0.5 } else if progress < 0.5 { // Position is a long way behind the line 0.5 } else }
And force the discontinuity to be at 0.0/1.0 boundary
#![allow(unused)] fn main() { // Position is near the line. We want the returned // nunmber to be between 0.0 and 1.0 and the discontinuty // to be at the start line. Currently `progress` goes // from 0.5 to 1.5 if progress > 1.0 { progress - 1.0 } else { progress } }
ANd now we can count laps!
Display a leader board
It would be cool for the player to be able to know how far behind the leader they are (I have great faith in human's piloting ability...). In my mind the leaderboard should be structured:
LAP 2/3
<SHIP>: 00.00
<SHIP>: +03.13
<SHIP>: +07.32
<SHIP>: +23.50
The <SHIP>
is the ship glyph in the font, and the number is the number of
seconds behind the leader. If the leader is on a different lap, the leaderboard
can display:
LAP 2/3
<SHIP>: 00.00
<SHIP>: +03.13
<SHIP>: +--.--
<SHIP>: +--.--
We have all the scores in an array, so let's sort a vector of reference to them in order of lap and then timing:
#![allow(unused)] fn main() { pub fn generate_leaderboard_text(&mut self) { self.leaderboard_text.clear(); let mut ship_and_score_refs: Vec<(&Ship, &Score)> = self.ship_entities.iter().zip(self.scores.iter()).collect(); ship_and_score_refs.sort_by(|a, b| { a.1.cmp(b.1)}); }
Hmm, what's this cmp
function? Technically it should be an
implementation of the Ord
trait, but to implement Ord
you also
need to implementation Eq
and PartialOrd
and PartialEq
. So
instead of impl Ord for Score
I'm just putting the cmp function
in the imp Score
block.
When sorting scores, first we need to sort by the lap counter:
#![allow(unused)] fn main() { pub fn cmp(&self, other: &Self) -> Ordering { let a_laps = self.laps.len(); let b_laps = other.laps.len(); let a_last_lap = self.laps.last(); let b_last_lap = other.laps.last(); if a_laps > b_laps { Ordering::Less } else if a_laps < b_laps { Ordering::Greater } else { ....???? } }
If the laps are the same, we need to sort by the least time:
#![allow(unused)] fn main() { if let Some(a_last_lap) = a_last_lap { if let Some(b_last_lap) = b_last_lap { // Both scores show at least one lap, so compare times if a_last_lap > b_last_lap { // A has the longer time, so is doing worse Ordering::Greater } else { Ordering::Less } } else { // b has not done any laps Ordering::Less } } else { if b_last_lap.is_some() { // b has done some laps, a has not Ordering::Greater } else { Ordering::Equal } } }
Some of these conditions should never be hit - if
a_last_lap.is_some()
, then b_last_lap
will also be
is_some()
because we check that the number of laps is
the same. I can't think of a way to express this to the
compiler, so it'll just be a bit more verbose than it
needs to be.
Now that we can sort a list of scores we can find the winner and format the scoreboard as described above. The resulting function is:
#![allow(unused)] fn main() { pub fn generate_leaderboard_text(&mut self) { self.leaderboard_text.clear(); let mut ship_and_score_refs: Vec<(&Ship, &Score)> = self.ship_entities.iter().zip(self.scores.iter()).collect(); ship_and_score_refs.sort_by(|a, b| a.1.cmp(b.1)); let winner_score = ship_and_score_refs.first().expect("No Ships").1; self.leaderboard_text.append_string( &format!("Lap {}/{} ", winner_score.laps.len(), NUM_LAPS_TO_WIN), &[0.5, 0.5, 0.5], ); for (ship, score) in ship_and_score_refs { let color = [ship.color.0, ship.color.1, ship.color.2]; if score.laps.len() == winner_score.laps.len() { if let Some(winner_time) = winner_score.laps.last() { // Same lap - display time let time = score.laps.last().unwrap() - winner_time; let seconds = time as u32; let millis = (time.fract() * 100.0).floor() as u32; self.leaderboard_text .append_string(&format!("~ {:02}:{:02} ", seconds, millis), &color); } else { // No-one has any time yet self.leaderboard_text .append_string(&format!("~ --:-- ",), &color); } } else { // This player is at least a lap behind self.leaderboard_text .append_string(&format!("~ --:-- ",), &color); } } } }
The unwrap
should never be encountered for the same reason as
mentioned above. We only get there when
winner_score.laps.last().is_some()
and when the length of the two laps arrays are equal. If someone knows how to tell the compiler this,
I'd love to know!
Anyway, the only thing to do now is to play it and see how far ahead purple actually gets.....