Points

Screenshot

Screenshot

What is a game without points and a high score? Now that the circle can shoot down the squares it is time to add some points. Every square that is shot down will add to the score, where bigger squares will be worth more points. The current score will be shown on the screen, as well as the highest score achieved.

Is bigger better?

Bigger squares could be worth more because they contain more resources. It is also possible to make bigger circles harder to kill by requiring multiple hits to destroy.

If the current score is the highest score when the game is over, it will be written to a file on disk so that it can be read each time the game is started. This will only work if the game is played on desktop as the WebAssembly version doesn’t have access to the file system. It would be possible to store the high score in the browser storage, but that won’t be covered here to keep the implementation simple.

Implementation

Import module

To be able to read and write files we need to import std::fs modul from the Rust standard library. Add this line directly below the line to import Macroquad at the top of the file.

use std::fs;

New variables

We will need two new variables, score and high_score, to keep track of the player’s points as well as the highest score ever achieved. We’ll use the function fs::read_to_string() to read the file highscore.dat from disk. The points stored in the file need to be converted to u32 with i.parse::<u32>(). If anything goes wrong, if the file doesn’t exist or it contains something other than a number, the number 0 will be returned instead.

    let mut score: u32 = 0;
    let mut high_score: u32 = fs::read_to_string("highscore.dat")
        .map_or(Ok(0), |i| i.parse::<u32>())
        .unwrap_or(0);

Writing to disk

We’re writing the points directly to the computers hard drive, which will not work if the game has been compiled to WebAssembly and is run on a web page. This will be treated as if the file doesn’t exist.

It could be possible to use the browser’s storage, or sending the score to a web server, but that is not covered by this guide.

Updating the high score

If the circle collides with a square we’ll check if the current score is higher than the high score. If it is higher, we’ll update the high score and store the new high score to the file highscore.dat.

        if squares.iter().any(|square| circle.collides_with(square)) {
            if score == high_score {
                fs::write("highscore.dat", high_score.to_string()).ok();
            }
            gameover = true;
        }

Reading files on web

Macroquad supports reading files when the game is run on a web page. We could use the function load_string() to load the high score instead. But since it isn’t possible to save the file, this isn’t particularly useful in this case.

Increasing the score

When a bullet hits a square, we’ll increase the current score based on the size of the square. After that we’ll update the high_score if the current score is higher.

                if bullet.collides_with(square) {
                    bullet.collided = true;
                    square.collided = true;
                    score += square.size.round() as u32;
                    high_score = high_score.max(score);
                }

Resetting the score

When a new game is started, we need to set the score variable to 0.

        if gameover && is_key_pressed(KeyCode::Space) {
            squares.clear();
            bullets.clear();
            circle.x = screen_width() / 2.0;
            circle.y = screen_height() / 2.0;
            score = 0;
            gameover = false;
        }

Displaying scores

Finally, we’ll display the score and high_score on the screen. We’ll display the score in the top left corner of the screen. To be able to display the high score in the top right corner we’ll use the function measure_text() to calculate how far from the right edge of the screen the text should be displayed.

To ensure that the dimensions are correct we must use the same arguments for both measure_text() and draw_text(). The arguments for these functions are text, font, font_size and font_scale. Since we aren’t setting any specific font or scaling the size of the text, we’ll use None as the value for font, and 1.0 as font_scale. The font_size can be set to 25.0.

        draw_text(
            format!("Score: {}", score).as_str(),
            10.0,
            35.0,
            25.0,
            WHITE,
        );
        let highscore_text = format!("High score: {}", high_score);
        let text_dimensions = measure_text(highscore_text.as_str(), None, 25, 1.0);
        draw_text(
            highscore_text.as_str(),
            screen_width() - text_dimensions.width - 10.0,
            35.0,
            25.0,
            WHITE,
        );

Measure text

The function measure_text() returns the struct TextDimensions which contains the fields width, height, and offset_y.

Run the game and try to get a high score!

Challenge: High score message

Try writing a congratulations message below the “GAME OVER” text if the player reached a high score.

Full source code

Click to show the the full source code
use macroquad::prelude::*;

use std::fs;

struct Shape {
    size: f32,
    speed: f32,
    x: f32,
    y: f32,
    collided: bool,
}

impl Shape {
    fn collides_with(&self, other: &Self) -> bool {
        self.rect().overlaps(&other.rect())
    }

    fn rect(&self) -> Rect {
        Rect {
            x: self.x - self.size / 2.0,
            y: self.y - self.size / 2.0,
            w: self.size,
            h: self.size,
        }
    }
}

#[macroquad::main("My game")]
async fn main() {
    const MOVEMENT_SPEED: f32 = 200.0;

    rand::srand(miniquad::date::now() as u64);
    let mut squares = vec![];
    let mut bullets: Vec<Shape> = vec![];
    let mut circle = Shape {
        size: 32.0,
        speed: MOVEMENT_SPEED,
        x: screen_width() / 2.0,
        y: screen_height() / 2.0,
        collided: false,
    };
    let mut gameover = false;
    let mut score: u32 = 0;
    let mut high_score: u32 = fs::read_to_string("highscore.dat")
        .map_or(Ok(0), |i| i.parse::<u32>())
        .unwrap_or(0);

    loop {
        clear_background(DARKPURPLE);

        if !gameover {
            let delta_time = get_frame_time();
            if is_key_down(KeyCode::Right) {
                circle.x += MOVEMENT_SPEED * delta_time;
            }
            if is_key_down(KeyCode::Left) {
                circle.x -= MOVEMENT_SPEED * delta_time;
            }
            if is_key_down(KeyCode::Down) {
                circle.y += MOVEMENT_SPEED * delta_time;
            }
            if is_key_down(KeyCode::Up) {
                circle.y -= MOVEMENT_SPEED * delta_time;
            }
            if is_key_pressed(KeyCode::Space) {
                bullets.push(Shape {
                    x: circle.x,
                    y: circle.y,
                    speed: circle.speed * 2.0,
                    size: 5.0,
                    collided: false,
                });
            }

            // Clamp X and Y to be within the screen
            circle.x = clamp(circle.x, 0.0, screen_width());
            circle.y = clamp(circle.y, 0.0, screen_height());

            // Generate a new square
            if rand::gen_range(0, 99) >= 95 {
                let size = rand::gen_range(16.0, 64.0);
                squares.push(Shape {
                    size,
                    speed: rand::gen_range(50.0, 150.0),
                    x: rand::gen_range(size / 2.0, screen_width() - size / 2.0),
                    y: -size,
                    collided: false,
                });
            }

            // Movement
            for square in &mut squares {
                square.y += square.speed * delta_time;
            }
            for bullet in &mut bullets {
                bullet.y -= bullet.speed * delta_time;
            }

            // Remove shapes outside of screen
            squares.retain(|square| square.y < screen_height() + square.size);
            bullets.retain(|bullet| bullet.y > 0.0 - bullet.size / 2.0);

            // Remove collided shapes
            squares.retain(|square| !square.collided);
            bullets.retain(|bullet| !bullet.collided);
        }

        if gameover && is_key_pressed(KeyCode::Space) {
            squares.clear();
            bullets.clear();
            circle.x = screen_width() / 2.0;
            circle.y = screen_height() / 2.0;
            score = 0;
            gameover = false;
        }

        // Check for collisions
        if squares.iter().any(|square| circle.collides_with(square)) {
            if score == high_score {
                fs::write("highscore.dat", high_score.to_string()).ok();
            }
            gameover = true;
        }
        for square in squares.iter_mut() {
            for bullet in bullets.iter_mut() {
                if bullet.collides_with(square) {
                    bullet.collided = true;
                    square.collided = true;
                    score += square.size.round() as u32;
                    high_score = high_score.max(score);
                }
            }
        }

        // Draw everything
        for bullet in &bullets {
            draw_circle(bullet.x, bullet.y, bullet.size / 2.0, RED);
        }
        draw_circle(circle.x, circle.y, circle.size / 2.0, YELLOW);
        for square in &squares {
            draw_rectangle(
                square.x - square.size / 2.0,
                square.y - square.size / 2.0,
                square.size,
                square.size,
                GREEN,
            );
        }
        draw_text(
            format!("Score: {}", score).as_str(),
            10.0,
            35.0,
            25.0,
            WHITE,
        );
        let highscore_text = format!("High score: {}", high_score);
        let text_dimensions = measure_text(highscore_text.as_str(), None, 25, 1.0);
        draw_text(
            highscore_text.as_str(),
            screen_width() - text_dimensions.width - 10.0,
            35.0,
            25.0,
            WHITE,
        );
        if gameover {
            let text = "GAME OVER!";
            let text_dimensions = measure_text(text, None, 50, 1.0);
            draw_text(
                text,
                screen_width() / 2.0 - text_dimensions.width / 2.0,
                screen_height() / 2.0,
                50.0,
                RED,
            );
        }

        next_frame().await
    }
}

Quiz

Try your knowledge by answering the following quiz before you move on to the next chapter.

Agical