Bullet hell

Screenshot

Screenshot

It is slightly unfair that our poor circle isn’t able to defend itself against the terrifying squares. So it’s time to implement the ability for the circle to shoot bullets.

Implementation

Dead or alive?

To keep track of which squares have been hit by bullets we add the field collided of the type bool to the struct Shape.

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

Keeping track

We need another vector to keep track of all the bullets. For simplicity’s sake we’ll call it bullets. Add it after the squares vector. Here we’ll also set the type of the elements to ensure that the Rust compiler knows what type it is before we have added anything to it. We’ll use the struct Shape for the bullets as well.

    let mut bullets: Vec<Shape> = vec![];

Shoot bullets

After the circle has moved we’ll add a check if the player has pressed the space key and add a bullet to the bullets vector. The x and y coordinates of the bullet are set to the same values as for the circle, and the speed is set to twice that of the circle.

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

Note

Note that we’re using the function is_key_pressed() which only returns true during the frame when the key was first pressed.

Since we added a new field to the Shape struct we’ll need to set it when we create a square.

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

Move bullets

We don’t want the bullets to be stationary mines, so we’ll have to loop over the bullets vector and move them in the y direction. Add the following code after the code that moves the squares.

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

Remove bullets and squares

Make sure to remove the bullets that have exited the screen in the same way that the squares are removed.

            bullets.retain(|bullet| bullet.y > 0.0 - bullet.size / 2.0);

Now it is time to remove all the squares and bullets that have collided. It can be done with the retain method on the vectors which takes a predicate that should return true if the element should be kept. We’ll just check whether the collided field on the struct is false. Do the same thing for both the squares and the bullets vectors.

            squares.retain(|square| !square.collided);
            bullets.retain(|bullet| !bullet.collided);

Collision

After the check if the circle has collided with a square we’ll add another check if any of the squares have been hit by a bullet. We’ll set the field collided to true for both the square and the bullet so that they can be removed.

        for square in squares.iter_mut() {
            for bullet in bullets.iter_mut() {
                if bullet.collides_with(square) {
                    bullet.collided = true;
                    square.collided = true;
                }
            }
        }

Clear bullets

When the game is over we also have to clear the bullets vector so that all the bullets are removed when a new game is started.

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

Draw bullets

Before the circle is drawn we’ll draw all the bullets that the player has shot. This ensures that they are drawn behind all the other shapes.

        for bullet in &bullets {
            draw_circle(bullet.x, bullet.y, bullet.size / 2.0, RED);
        }

Info

The is another function called draw_circle_lines() that can be used to draw a circle with just the outline.

This is all the code that is needed for the circle to be able to shoot down all the fearsome squares.

Challenge

To increase the difficulty it’s possible to add a minimum time for reloading between each shot. Try using the function get_time() to save when the last shot was fired and compare it with the current time. Only add a bullet if the difference is above a certain threshold.

Another possibility is to only allow a specific number of bullets on the screen at the same time.

Full source code

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

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;

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

        // Check for collisions
        if squares.iter().any(|square| circle.collides_with(square)) {
            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;
                }
            }
        }

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

        // 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,
            );
        }
        if gameover {
            let text = "GAME OVER!";
            let text_dimensions = measure_text(text, None, 60, 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