Falling squares

Screenshot

Screenshot

To make sure there is something happening in our game, it’s time to create some action. Since the hero in our game is a brave circle, our opponents will be squares falling down from the top of the window.

Implementation

Struct for shapes

To keep track of our circle and all the squares, we’ll create a struct that we can name Shape, which will contain the size and speed, as well as x and y coordinates.

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

Initialize random number generator

We’ll use a random number generator to determine when new squares should appear on the screen, how big they should be and how fast they will move. Therefore, we need to seed the random generator so that it doesn’t produce the same random numbers every time. This is done at the beginning of the main function using the rand::srand() method, to which we pass the current time as the seed.

    rand::srand(miniquad::date::now() as u64);

Note

We are using the function miniquad::date::now() from the graphics library Miniquad to get the current time.

Vector of squares

At the beginning of the main function we create a vector called squares that will contain all the squares to be displayed on the screen. The new variable circle will represent our hero, the amazing circle. The speed uses the constant MOVEMENT_SPEED, and the x and y fields are set to the center of the screen.

    let mut squares = vec![];
    let mut circle = Shape {
        size: 32.0,
        speed: MOVEMENT_SPEED,
        x: screen_width() / 2.0,
        y: screen_height() / 2.0,
    };

Start by modifying the program so that circle is used instead of the variables x and y and confirm that everything works as it did before adding the enemy squares.

Note

The Rust compiler might warn about “type annotations needed” on the Vector. Once we add an enemy square in the next section that warning should disappear.

Add enemy squares

It’s time to start the invasion of evil squares. Here, just like before, we split updating the movement and drawing the squares. This way, the movement does not depend on the screen’s refresh rate, ensuring that all changes are done before we start drawing anything to the screen.

First, we use the function rand::gen_range() to determine whether to add a new square. It takes two arguments, a minimum value and a maximum value, and returns a random number between those two values. We generate a random number between 0 and 99, and if the value is 95 or higher, a new Shape is created and added to the squares vector. To add some variation, we also use rand::gen_range() to get different size, speed, and starting position of every 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,
            });
        }

Note

Rectangles are drawn starting from their upper left corner. Therefore, we subtract half of the square’s size when calculating the x position. The y position starts at a negative value of the square’s size, so it starts completely outside the screen.

Update square positions

Now we can iterate through the vector using a for loop and update the Y position using the square’s speed and the variable delta_time. This will make the squares move downwards across the screen.

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

Remove invisible squares

Next, we need to clean up all the squares that have moved off the bottom of the screen since it’s unnecessary to draw things that are not visible. We’ll use the retain() method on the vector, which takes a function that determines whether elements should be kept. We’ll check if the square’s y value is still less than the height of the window plus the size of the square.

        squares.retain(|square| square.y < screen_height() + square.size);

Draw the squares

Finally, we add a for loop that iterates over the squares vector and uses the function draw_rectangle() to draw a rectangle at the updated position and with the correct size. Since rectangles are drawn with x and y from the top-left corner and our coordinates are based on the center of the square, we use some mathematics to calculate where they should be placed. The size is used twice, once for the width of the square and once for the height. We set the color to GREEN so that all squares will have a green color.

Note

It’s also possible to use the function draw_rectangle_ex() that uses the struct DrawTextureParams instead of a color. In addition to setting color, it can be used to set rotation and offset of the rectangle.

        for square in &squares {
            draw_rectangle(
                square.x - square.size / 2.0,
                square.y - square.size / 2.0,
                square.size,
                square.size,
                GREEN,
            );
        }

Challenge

Try setting a different color for each square by using the method choose() on vectors from Macroquad’s ChooseRandom trait, which returns a random element from the vector.

Full source code

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

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

#[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,
    };

    loop {
        clear_background(DARKPURPLE);

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

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

        next_frame().await
    }
}

Quiz

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

Agical