Poängsystem

Screenshot

Vad vore ett spel utan poäng och high score? Det är nu dags att implementera ett poängsystem för vårt spel. Poäng kommer ges för varje fyrkant som skjuts ner, baserat på storleken. Poängen kommer visas på skärmen, såväl som den högsta poäng som har uppnåtts. Om poängen är en high score så kommer poängen skrivas ner till en fil på disk så att det kan läsas in igen nästa gång spelet startas.

Implementering

Importera modul

För att kunna läsa och skriva filer behöver vi importera Rusts std::fs modul som innehåller funktionalitet för att läsa och skriva till datorns lokala filsystem. Denna rad kan läggas in under raden som importerar Macroquad längst upp i filen.

use std::fs;

Nya variabler

Vi behöver två nya variabler, score och high_score för att hålla reda på spelarens poäng och den högsta poängen som har uppnåtts. Vi använder oss av funktionen fs::read_to_string() för att läsa in filen highscore.dat. Poängen i filen måste konverteras till en u32 vilket görs med i.parse::<u32>(). Om något går fel, som att filen inte finns eller innehåller något som inte är en siffra, så kommer siffran 0 att returneras.

    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);

Notera

Här skriver vi direkt till datorns hårddisk, vilket inte fungerar om spelet har kompilerats till WASM och körs på en webbsida.

Uppdatera high score

Om cirkeln krockar med en fyrkant så lägger vi till en kontroll om spelarens poäng är en high score. Är den det så skriver vi ner high scoren till filen 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;
        }

Notera

Macroquad har stöd för att läsa filer som fungerar även när spelet körs på en webbsida. Här skulle vi kunna använda funktionen load_string() istället, men eftersom vi inte kan skriva filen är det inte så meningsfullt.

Öka poäng

Om en kula träffar en fyrkant så ökar vi spelarens poäng baserat på storleken på fyrkanten. Sen uppdaterar vi värdet i variabeln high_score om poängen är högre än det gamla värdet.

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

Nollställ poäng

När vi startar en ny spelomgång måste vi nollställa variabeln score.

        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;
        }

Skriv ut poäng och high score

Till sist ritar vi ut poängen och high score på skärmen. Poängen skriver vi alltid ut i övre vänstra hörnet. För att kunna skriva ut high scoren i högra hörnet behöver vi använda oss av funktionen measure_text() för att räkna ut hur långt från skärmens högra sida texten ska placeras.

För att dimensionerna ska stämma måste samma värden användas som argument till measure_text() som till draw_text(). Argumenten är text, font, font_size och font_scale. Eftersom vi inte sätter någon speciell font eller skalar om texten så skickar vi in None som font, och 1.0 som font_scale. Däremot måste font_size vara samma som i anropet av draw_text() vilket i vårt fall är 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,
        );

Info

Funktionen measure_text() returnerar structen TextDimensions som innehåller fälten width, height och offset_y.

Kör igång spelet och försök få så hög poäng som möjligt!

Utmaning

Testa att skriva ut en gratulationstext på skärmen vid Game Over om spelaren uppnådde en high score.

Kompletta källkoden

Klicka för att visa hela källkoden
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

Testa dina nya kunskaper genom att svara på följande quiz innan du går vidare.

Agical