Rust & Wasm: Build Your Own Breakout Game I

Nikhil Gupta
4 min readDec 21, 2022

--

In this article, we will use Bevy game engine in our React application from our Rust WASM library to build the popular breakout game from scratch.

We will use the same setup as previous articles but for a quick recap:

Setup [Quick Recap]

Create a new react app

npx create-react-app rust-wasm-demo --template typescript
cd rust-wasm-demo

Create a new wasm library

cargo new rust-wasm-lib --lib
cd rust-wasm-lib

Add wasm-bindgen and bevy

cargo add wasm-bindgen
cargo add bevy

Build the wasm

wasm-pack build --target web
cd ..

Install the wasm and run the app

npm i ./rust-wasm-lib/pkg
npm run start

Create a Bevy App

Now, let’s create a run_bevy_app function in our lib.rs that will initialize the Bevy app, setup the canvas size and add a startup function.

use wasm_bindgen::prelude::*;
use bevy::{
prelude::*,
}

#[wasm_bindgen]
pub fn run_bevy_app() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor {
title: "Let's Play!".to_string(),
width: 900.,
height: 600.,
..default()
},
..default()
}))
.add_startup_system(setup) // Defined Below
.run();
}

Define the setup function

Next, let’s define the setup function. This will add the required game entities to the world.

fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
asset_server: Res<AssetServer>,
) {
// ...
}

Setup the camera

fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
asset_server: Res<AssetServer>,
) {
// Use the default 2D Camera
commands.spawn(Camera2dBundle::default());
}

Add the paddle

Declare a component and some constants

#[derive(Component)]
struct Paddle;

const BOTTOM_WALL: f32 = -300.;
const GAP_BETWEEN_PADDLE_AND_FLOOR: f32 = 60.0;
const PADDLE_SIZE: Vec3 = Vec3::new(120.0, 20.0, 0.0);
const PADDLE_COLOR: Color = Color::rgb(0.3, 0.3, 0.7);

Please note #[derive(Component)] that is used for Bevy's ECS (Entity Component System) paradigm.

Render a SpritBundle

fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
asset_server: Res<AssetServer>,
) {
// ...
let paddle_y = BOTTOM_WALL + GAP_BETWEEN_PADDLE_AND_FLOOR;
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
));
}

Build the new wasm library

Let’s run wasm-pack again to build the updated library

wasm-pack build --target web

Call the function from the demo app

Similar to previous articles, let’s add a button to load the wasm and call run_bevy_app function from our App.ts file like so:

// App.ts
import init, { run_bevy_app } from "rust-wasm-lib";
import './App.css';

function App() {
const runBevyApp = async () => {
await init();
run_bevy_app();
};

return (
<div className="App">
<button onClick={runBevyApp}>Run Bevy App</button>
</div>
);
}

export default App;

Now, if you run the updated app and click on Run Bevy App, you should see a paddle on the canvas!

Add the ball

Declare a component and define constants

#[derive(Component)]
struct Ball;

const BALL_STARTING_POSITION: Vec3 = Vec3::new(0.0, -50.0, 1.0);
const BALL_SIZE: Vec3 = Vec3::new(30.0, 30.0, 0.0);
const BALL_COLOR: Color = Color::rgb(1.0, 0.5, 0.5);

Render a MaterialMesh2dBundle

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

Now, if you run the updated app and click on Run Bevy App, you should see a paddle and a ball on the canvas!

Move the paddle

Time to add some movement to our game!

Define the function and constants

The function below checks the keyboard input and updates the position of the paddle accordingly.

const TIME_STEP: f32 = 1.0 / 60.0;
const PADDLE_SPEED: f32 = 500.0;

fn move_paddle(
keyboard_input: Res<Input<KeyCode>>,
mut query: Query<&mut Transform, With<Paddle>>,
) {
let mut paddle_transform = query.single_mut();
let mut direction = 0.0;

if keyboard_input.pressed(KeyCode::Left) {
direction -= 1.0;
}

if keyboard_input.pressed(KeyCode::Right) {
direction += 1.0;
}

let new_paddle_position = paddle_transform.translation.x + direction * PADDLE_SPEED * TIME_STEP;

paddle_transform.translation.x = new_paddle_position;
}

Add it to the startup system

#[wasm_bindgen]
pub fn run_bevy_app() {
App::new()
...
.add_startup_system(setup)
.add_system(move_paddle)
.run();
}

If you run the updated app now, you will be able to move the paddle based on the key press!

However, the paddle can currently go out of the screen as there are no constraints. Also, the ball is currently just hanging in mid-air. 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:

--

--

No responses yet