Rust & Wasm: Build Your Own Breakout Game I
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: