Rust & Wasm: Build Your Own Breakout Game II

Nikhil Gupta
5 min readDec 23, 2022

In this article, we will continue on our journey to build the popular breakout game from scratch using Rust & Wasm in a React application using the Bevy game engine.

We will build upon the previous article available here:

Restrict paddle movement

When we left off last time, we were able to move the paddle out of the screen, which is definitely not a good user experience. Let’s quickly fix this like so:

Declare some constants for the wall and padding

const WALL_THICKNESS: f32 = 10.0;

const LEFT_WALL: f32 = -450.;
const RIGHT_WALL: f32 = 450.;

const PADDLE_PADDING: f32 = 10.0;

Clamp the paddle

fn move_paddle(
keyboard_input: Res<Input<KeyCode>>,
mut query: Query<&mut Transform, With<Paddle>>,
) {
// ...
let new_paddle_position = paddle_transform.translation.x + direction * PADDLE_SPEED * TIME_STEP;

let left_bound = LEFT_WALL + WALL_THICKNESS / 2.0 + PADDLE_SIZE.x / 2.0 + PADDLE_PADDING;
let right_bound = RIGHT_WALL - WALL_THICKNESS / 2.0 - PADDLE_SIZE.x / 2.0 - PADDLE_PADDING;

paddle_transform.translation.x = new_paddle_position.clamp(left_bound, right_bound);
}

Now, if you run the updated app and click on Run Bevy App, the paddle will stay at a minimum distance of PADDLE_PADDING from either wall.

Move the ball

Last time, the ball was simply hanging in mid-air. Time to add some movement to it!

Declare a Velocity component and define constants

#[derive(Component, Deref, DerefMut)]
struct Velocity(Vec2);

const BALL_SPEED: f32 = 400.0;
const INITIAL_BALL_DIRECTION: Vec2 = Vec2::new(0.0, -0.5);

Add Velocity component to Ball bundle

fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
asset_server: Res<AssetServer>,
) {
// ...
commands.spawn((
MaterialMesh2dBundle {
mesh: meshes.add(shape::Circle::default().into()).into(),
material: materials.add(ColorMaterial::from(BALL_COLOR)),
transform: Transform::from_translation(BALL_STARTING_POSITION).with_scale(BALL_SIZE),
..default()
},
Ball,
Velocity(INITIAL_BALL_DIRECTION.normalize() * BALL_SPEED), //New
));
}

Define the transformation

fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>) {
for (mut transform, velocity) in &mut query {
transform.translation.x += velocity.x * TIME_STEP;
transform.translation.y += velocity.y * TIME_STEP;
}
}

Note that we are simply updating the translation based on the velocity of the ball at each time step.

Add to the startup system

#[wasm_bindgen]
pub fn run_bevy_app() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor {
title: "I am a window!".to_string(),
width: 900.,
height: 600.,
..default()
},
..default()
}))
.add_startup_system(setup)
.add_system(move_paddle)
.add_system(apply_velocity) // New
.run();
}

This ensures that apply_velocity is called as part of the game system.

Now, if you run the updated app and click on Run Bevy App, you should see the ball falling down. However, there is no concept of collision yet and the ball simply passes through the paddle. Let's fix this!

Add collisions

Declare a Collider component

#[derive(Component)]
struct Collider;

Add it to the Paddle bundle

fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
asset_server: Res<AssetServer>,
) {
// ...
commands.spawn((
SpriteBundle {
transform: Transform {
translation: Vec3::new(0.0, paddle_y, 0.0),
scale: PADDLE_SIZE,
..default()
},
sprite: Sprite {
color: PADDLE_COLOR,
..default()
},
..default()
},
Paddle,
Collider //New
));
// ...
}

Declare the function to check collisions

fn check_for_collisions(
mut ball_query: Query<(&mut Velocity, &Transform), With<Ball>>,
collider_query: Query<(&Transform), With<Collider>>,
) {
}

Note that it uses two queries on Bevy’s ECS system to get the ball and colliders (all entities with the Collider component). ball_query is declared as mutable, as the function will modify the velocity of the ball whenever a collision is detected.

Import Bevy’s sprite collider and check

use bevy::{
sprite::collide_aabb::{collide, Collision}
}

fn check_for_collisions(
mut ball_query: Query<(&mut Velocity, &Transform), With<Ball>>,
collider_query: Query<(&Transform), With<Collider>>,
) {
let (mut ball_velocity, ball_transform) = ball_query.single_mut();
let ball_size = ball_transform.scale.truncate();

for (transform) in &collider_query {
if let Some(collision) = collide(
ball_transform.translation,
ball_size,
transform.translation,
transform.scale.truncate(),
) {
// We will modify the ball's velocity here
}
}
}

Here, we go through all entities with the Collider component and use Bevy's collide API to check for collisions.

Reflect the ball if needed

fn check_for_collisions(
mut ball_query: Query<(&mut Velocity, &Transform), With<Ball>>,
collider_query: Query<(&Transform), With<Collider>>,
) {
if let Some(collision) = ... {
let mut reflect_x = false;
let mut reflect_y = false;

match collision {
Collision::Left => reflect_x = ball_velocity.x > 0.0,
Collision::Right => reflect_x = ball_velocity.x < 0.0,
Collision::Top => reflect_y = ball_velocity.y < 0.0,
Collision::Bottom => reflect_y = ball_velocity.y > 0.0,
Collision::Inside => { /* do nothing */ }
}
if reflect_x {
ball_velocity.x = -ball_velocity.x;
}
if reflect_y {
ball_velocity.y = -ball_velocity.y;
}
}
}

The function simply sets the flags reflect_x and reflect_y based on the collision and ball direction. If any of these flags are true, the ball's velocity is updated accordingly.

Add to the startup system

#[wasm_bindgen]
pub fn run_bevy_app() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor {
title: "I am a window!".to_string(),
width: 900.,
height: 600.,
..default()
},
..default()
}))
.add_startup_system(setup)
.add_system(check_for_collisions)
.add_system(move_paddle.before(check_for_collisions))
.add_system(apply_velocity.before(check_for_collisions))
.run();
}

Here, we simply add the function check_for_collisions to the system and make sure that it is called before updating the paddle or the ball.

If you run the updated app now, you will see the ball falling down, colliding with the paddle and then, bouncing back up!

However, there are no bricks yet for us to score points. Also, the ball can only collide with the paddle but goes out of the screen when hitting any of the walls. In the next article, we will fix both of these while continuing to build our own breakout game! Check it out here:

If you liked this article, subscribe here to get the complete code and updates for the entire collection:

--

--