Kollisionskurs

Screenshot

Våra ovänner fyrkanterna är ännu inte så farliga, så får att öka spänningen är det dags att skapa konflikt. Om vår vän cirkeln kolliderar med en fyrkant så är spelet över och måste startas om.

Efter att vi har ritat upp alla cirklar och fyrkanter så lägger vi till en kontroll som ser om någon fyrkant rör vid cirkeln. Om den gör det så visar vi texten Game Over och väntar på att spelaren trycker på space-tangenten. När spelaren trycker på space så nollställs vektorn med fyrkanter och cirkeln flyttas tillbaka till mitten av skärmen.

Implementering

Kollisionsmetod

Vi utökar structen Shape med en implementation som innehåller metoden collides_with() som kollar om den kolliderar med en annan Shape. Denna använder sig av Macroquads Rect struct som har hjälpmetoden overlaps(). Vi skapar även en egen hjälpmetod rect() som skapar en Rect från vår Shape.

Info

Det finns många hjälpmetoder på Rect för göra beräkningar på rektanglar, som contains(), intersect(), scale(), combine_with() och move_to().

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

Notera

Macroquads Rect utgår också från övre vänstra hörnet, därför måste vi även här subtrahera halva storleken från både X och Y.

Är det game over?

Vi behöver en ny boolesk variabel gameover som håller reda på om spelaren har dött som vi lägger in före huvudloopen.

    let mut gameover = false;

För att cirkeln och fyrkanterna inte ska röra sig medan det är game over så görs all kod för förflyttning enbart om variabeln gameover är false.

        if !gameover {
            ...
        }

Kollidering

Efter förflyttningskoden lägger vi till en kontroll om någon fyrkant kolliderar med cirkeln. Vi använder metoden any() på iteratorn för vektorn squares och kollar om någon fyrkant kolliderar med vår hjälte cirkeln. Om det har skett en kollision sätter vi variabeln gameover till true.

        if squares.iter().any(|square| circle.collides_with(square)) {
            gameover = true;
        }

Utmaning

Kollisionskoden utgår från att cirkeln är en fyrkant. Prova att skriva kod som tar hänsyn till att cirkeln inte fyller ut hela fyrkanten.

Återställning

Om gameover-variabeln är true och spelaren precis har tryckt på mellanslagstangenten så tömmer vi vektorn squares med metoden clear() och återställer cirkelns x och y-koordinater till mitten av skärmen. Sen sätter vi variabeln gameover till false så att spelet kan börja igen.

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

Info

Skillnaden mellan is_key_down() och is_key_pressed() är att den senare bara kollar om tangenten trycktes ned under den aktuella bildrutan, medan den tidigare returnerar sant för alla bildrutor från att knappen trycktes ned och sedan hålls nedtryckt. Ett experiment du kan göra är att använda is_key_pressed() för att styra cirkeln.

Det finns även is_key_released() som kollar om tangenten släpptes under den aktuella bildrutan.

Skriv ut Game Over

Slutligen ritar vi ut texten “Game Over!” i mitten av skärmen efter cirkeln och fyrkanterna har ritats ut, men bara om variabeln gameover är true.

Info

Det går också att använda funktionen draw_text_ex() som tar en DrawTextParams struct istället för font_size och color. Med den kan man ange fler parameterar som font, font_scale, font_scale_aspect och rotation.

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

Utmaning

Eftersom draw_text() utgår från textens baslinje så kommer texten inte visas exakt i mitten av skärmen. Prova att använda fälten offset_y och height från text_dimensions för att räkna ut textens mittpunkt. Macroquads exempel text measures kan ge tips till hur det fungerar.

Kompletta källkoden

Klicka för att visa hela källkoden
use macroquad::prelude::*;

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

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 circle = Shape {
        size: 32.0,
        speed: MOVEMENT_SPEED,
        x: screen_width() / 2.0,
        y: screen_height() / 2.0,
    };
    let mut gameover = false;

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

            // 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,
                });
            }

            // Move squares
            for square in &mut squares {
                square.y += square.speed * delta_time;
            }

            // Remove squares below bottom of screen
            squares.retain(|square| square.y < screen_height() + square.size);
        }

        // Check for collisions
        if squares.iter().any(|square| circle.collides_with(square)) {
            gameover = true;
        }

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

        // Draw everything
        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,
            );
        }
        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