rust breakout part 2 – player, blocks and ball, oh my!

(All the code from this lesson is found here on Github)

Welcome back to the second installment of our Rust game development journey! In the previous post, we met Macroquad and set up a simple window. But let’s face it: that’s a little boring!

It’s now time to take that window and set up the foundation of a real game, bringing in the player paddle, the ball, and the blocks that we’re going to want to smash. Let’s get started!

Getting the Player Paddle on Screen

First, we need a paddle for the player to control. Picture this: a sleek rectangle, 150 by 40 pixels, zipping across the screen at 700 pixels per second. It’ll start smack in the middle, sliding left or right with the arrow keys, and we’ll keep it from escaping the screen with some handy boundary checks.

Create a new file called player.rs in your src directory, and add this code:

use macroquad::prelude::*;

const PLAYER_SIZE: Vec2 = Vec2::from_array([100.0, 40.0]);

const PLAYER_SPEED: f32 = 700.0;

pub struct Player {
    pub rect: Rect,
}

impl Player {
    pub fn new() -> Self {
        Self {
            rect: Rect::new(
                screen_width()  0.5 - PLAYER_SIZE.x  * 0.5, // Center horizontally
                screen_height() - 100.0,                     // Near the bottom
                PLAYER_SIZE.x,
                PLAYER_SIZE.y,
            ),
        }
    }

    pub fn update(&mut self, dt: f32) {
        let x_move = match (is_key_down(KeyCode::Left), is_key_down(KeyCode::Right) {
            (true, false) => -1.0,  // Move left

            (false, true) => 1.0,   // Move right

            _ => 0.0,               // Stay still
        };

        self.rect.x += x_move * dt * PLAYER_SPEED;

        // Keep the paddle in bounds

        if self.rect.x < 0.0 {
            self.rect.x = 0.0;
        }

        if self.rect.x > screen_width() - self.rect.w {
            self.rect.x = screen_width() - self.rect.w;
        }
    }

    pub fn draw(&self) {
        draw_rectangle(self.rect.x, self.rect.y, self.rect.w, self.rect.h, BLUE);
    }
}

This code defines a Player struct with a rectangle (rect) for its position and size. The new function spawns it centered horizontally and 100 pixels from the bottom. update handles movement (using dt for smooth, frame-rate-independent speed), and draw paints it blue. Simple, yet effective!

Drawing the Blocks

Next up: the blocks our ball will smash. Each block is a 150-by-40-pixel rectangle, placed wherever we specify, and drawn in a stylish gray. Create a block.rs file in src and add:

use macroquad::prelude::*;

pub const BLOCK_SIZE: Vec2 = Vec2::from_array([150.0, 40.0]);

pub struct Block {
    pub rect: Rect,
}

impl Block {
    pub fn new(pos: Vec2) -> Self {
        Self {
            rect: Rect::new(pos.x, pos.y, BLOCK_SIZE.x, BLOCK_SIZE.y),
        }
    }

    pub fn draw(&self) {
        draw_rectangle(self.rect.x, self.rect.y, self.rect.w, self.rect.h, GRAY);
    }
}

This is straightforward: a Block struct with a positionable rectangle and a draw method. We’ll arrange these into a grid later.

Adding the Bouncing Ball

Now, the star of the show: the ball! It’s a 50-by-50-pixel ball (well… a square!), moving at 400 pixels per second. It bounces off screen edges and starts with a random horizontal direction but always heads downward initially. Add this to a new ball.rs file:

use macroquad::prelude::*;

pub const BALL_SIZE: f32 = 50.0;

pub const BALL_SPEED: f32 = 400.0;

pub struct Ball {

    pub rect: Rect,

    pub vel: Vec2, // Velocity for direction

}

impl Ball {
    pub fn new(pos: Vec2) -> Self {
        Self {

            rect: Rect::new(pos.x, pos.y, BALL_SIZE, BALL_SIZE),

            vel: vec2(rand::gen_range(-1.0, 1.0), 1.0).normalize(), // Random x, downward y
        }
    }

    pub fn update(&mut self, dt: f32) {
        self.rect.x += self.vel.x * dt *  BALL_SPEED;
        self.rect.y += self.vel.y  * dt *  BALL_SPEED;

        // Bounce off edges
        if self.rect.x < 0.0 {
            self.vel.x = 1.0;
        }

        if self.rect.x > screen_width() - self.rect.w {
            self.vel.x = -1.0;
        }

        if self.rect.y < 0.0 {
            self.vel.y = 1.0;
        }

        if self.rect.y > screen_height() - self.rect.h {
            self.vel.y = -1.0;
        }
    }

    pub fn draw(&self) {
        draw_rectangle(self.rect.x, self.rect.y, self.rect.w, self.rect.h, GRAY);
    }
}

The vel field (a Vec2) controls direction, normalized to ensure consistent speed. The update method makes it bounce by flipping velocity when it hits an edge. For now, it’s gray like the blocks—maybe we’ll jazz it up later!

Putting It All Together

Time to tie everything into main.rs. We’ll set up the player, a grid of blocks, and the ball, then run a game loop to update and draw them. Replace your main.rs with:

use macroquad::prelude::*;

mod player;
mod block;
mod ball;
use player::Player;
use block::{Block, BLOCK_SIZE};
use ball::{Ball, BALL_SIZE};

#[macroquad::main("Breakout")]
async fn main() {

    let mut player = Player::new();
    let mut blocks = Vec::new();
    let mut balls = Vec::new();

    // Create a 6x6 grid of blocks
    let (width, height) = (6, 6);

    let padding = 5.0;

    let total_block_size = BLOCK_SIZE + vec2(padding, padding);

    let block_start_pos = vec2(
        (screen_width() - (total_block_size.x  width as f32))  0.5, // Center horizontally
        50.0,                                                         // Start 50px from top
    );

    for i in 0..width * height {
        let block_x = (i % width) as f32 * total_block_size.x;
        let block_y = (i / width) as f32 * total_block_size.y;
        blocks.push(Block::new(block_start_pos + vec2(block_x, block_y)));
    }

    // Spawn the ball in the middle
    balls.push(Ball::new(vec2(screen_width()  0.5, screen_height()  0.5)));

    loop {
        // Update

        player.update(get_frame_time());

        for ball in balls.iter_mut() {
            ball.update(get_frame_time());
        }

        // Draw
        clear_background(WHITE);

        player.draw();

        for block in blocks.iter() {
            block.draw();
        }

        for ball in balls.iter() {
            ball.draw();
        }

        next_frame().await;
    }
}

This sets up a 6×6 grid of blocks with padding, centers everything nicely, and runs a loop to keep the game alive. The get_frame_time() function ensures smooth movement across different frame rates.

Build and run it, and you should see this:

We’re on the way!

Much better than a simple “Hello Macroquad” window, I’m sure you’ll agree!

Conclusion

Voilà! We’ve got all the elements of a basic Breakout game. The paddle slides, the ball bounces, and the blocks sit there, begging to be smashed. It’s a solid foundation — the next steps will be to add block destruction and collision detection to our game to make it work as we expect. See you next time!

Leave a comment