Collisions

Screenshot

Screenshot

To make the game more exciting, let’s add some conflict. If our hero, the brave yellow circle, collides with a square, the game will be over and has to be restarted.

After we have drawn the circle and all squares, we’ll add a check to see if any square touches the circle. If it does, we’ll display the text “GAME OVER!” in capital letters and wait for the player to press the space key. When the player presses space, we’ll reset the vector with squares and move the circle back to the center of the screen.

Implementation

Collision function

We expand the Shape struct with an implementation that contains the method collides_with() to check if it collides with another Shape. This method uses the overlaps() helper method from Macroquad’s Rect struct. We also create a helper method called rect() that creates a Rect from our Shape.

Info

There are many methods on Rect to do calculations on rectangles, such as contains(), intersect(), scale(), combine_with() and 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,
        }
    }
}

Note

The origin of Macroquad’s Rect is also from the top left corner, so we must subtract half its size from both X and Y.

Is it game over?

Let’s add a boolean variable called gameover to the start of the main loop to keep track of whether the player has died.

    let mut gameover = false;

Since we don’t want the circle and squares to move while it’s game over, the movement code is wrapped in an if statement that checks if the gameover variable is false.

        if !gameover {
            ...
        }

Collision

After the movement code, we add a check if any square collides with the circle. We use the method any() on the iterator for the vector squares and check if any square collides with our hero circle. If a collision occurs, we set the variable gameover to true.

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

Challenge

The collision code assumes that the circle is a square. Try writing code that takes into account that the circle does not entirely fill the square.

Reset the game

If the gameover variable is true and the player has just pressed the space key, we clear the vector squares using the clear() method and reset the x and y coordinates of circle to the center of the screen. Then, we set the variable gameover to false so that the game can start over.

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

Info

The difference between the functions is_key_down() and is_key_pressed() is that the latter only checks if the key was pressed during the current frame, while the former returns true for all frames from when the button was pressed and then held down. An experiment you can do is to use is_key_pressed() to control the circle.

There’s also a function called is_key_released() which checks if the key was released during the current frame.

Display GAME OVER

Finally, we draw the text “GAME OVER!” in the middle of the screen after the circle and squares have been drawn, but only if the variable gameover is true. Macroquad does not have any feature to decide which things will be drawn on top of other things. Each thing drawn will be drawn on top of all other things drawn earlier during the the same frame.

Info

It’s also possible to use the function draw_text_ex() which takes a DrawTextParams struct instead of font_size and color. Using that struct it’s possible to set more parameters such as font, font_scale, font_scale_aspect and 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,
            );
        }

Challenge

Since draw_text() is based on the text’s baseline, the text won’t appear exactly in the center of the screen. Try using the offset_y and height fields from text_dimensions to calculate the text’s midpoint. Macroquad’s example text measures can provide tips on how it works.

Full source code

Click to show the the full source code
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

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

Agical