Skip to content

Instantly share code, notes, and snippets.

@mikechambers
Created October 25, 2022 03:04
Show Gist options
  • Save mikechambers/955ed0ae4e604b7102d1eafd494adc80 to your computer and use it in GitHub Desktop.
Save mikechambers/955ed0ae4e604b7102d1eafd494adc80 to your computer and use it in GitHub Desktop.
Bevy Rust Game Framework Breakout Example with comments
//original at:
//https://github.com/bevyengine/bevy/blob/main/examples/games/breakout.rs
use bevy::{
prelude::*,
sprite::collide_aabb::{collide, Collision},
time::FixedTimestep,
};
//basically our FPS - 60FPS
const TIME_STEP: f32 = 1.0 / 60.0;
//paddle size x, y, z (x is width, y is height)
const PADDLE_SIZE: Vec3 = Vec3::new(120.0, 20.0, 0.0);
const GAP_BETWEEN_PADDLE_AND_FLOOR : f32 = 60.0;
const PADDLE_SPEED : f32 = 500.0;
const PADDLE_PADDING: f32 = 10.0;
//starting position. set z to 1 so it renders above other items
const BALL_STARTING_POSITION: Vec3 = Vec3::new(0.0, -50.0, 1.0);
//x = width, y = height
const BALL_SIZE: Vec3 = Vec3::new(30.0, 30.0, 0.0);
const BALL_SPEED :f32 = 400.0;
//x,y - down to the right
const INITIAL_BALL_DIRECTION : Vec2 = Vec2::new(0.5, -0.5);
const WALL_THICKNESS: f32 = 10.0;
//relative to 2d origin, which is center screen
const LEFT_WALL:f32 = -450.0;
const RIGHT_WALL:f32 = 450.0;
const BOTTOM_WALL:f32 = -300.0;
const TOP_WALL:f32 = 300.0;
//width / height
const BRICK_SIZE: Vec2 = Vec2::new(100.0, 30.0);
const GAP_BETWEEN_PADDLE_AND_BRICKS: f32 = 270.0;
const GAP_BETWEEN_BRICKS: f32 = 5.0;
const GAP_BETWEEN_BRICKS_AND_CEILING:f32 = 20.0;
const GAP_BETWEEN_BRICK_AND_SIDE:f32 = 20.0;
const SCOREBOARD_FONT_SIZE: f32 = 40.0;
//Val describes possible types of values in a flexbox layout (px or percent)
const SCOREBOARD_TEXT_PADDING: Val = Val::Px(5.0);
//Color has rgb, rgba and hex methods
const BACKGROUND_COLOR: Color = Color::rgb(0.9, 0.9, 0.9);
const PADDLE_COLOR: Color = Color::rgb(0.3, 0.3, 0.7);
const BALL_COLOR: Color = Color::rgb(1.0, 0.5, 0.5);
const BRICK_COLOR: Color = Color::rgb(0.5, 0.5, 1.0);
const WALL_COLOR: Color = Color::rgb(0.8, 0.8, 0.8);
const TEXT_COLOR: Color = Color::rgb(0.5, 0.5, 1.0);
const SCORE_COLOR: Color = Color::rgb(1.0, 0.5, 0.5);
//main entry point for rust program
fn main() {
App::new()
//add default plugins, such as Windowing, etc...
.add_plugins(DefaultPlugins)
//Resources represent globally unique data
.insert_resource(Scoreboard {score: 0})
//ClearColor defines the color used to clear the screen, default
//for all cameras, and basically the background color
.insert_resource(ClearColor(BACKGROUND_COLOR))
//startup system is called once when app starts
.add_startup_system(setup)
//Listen for CollisionEvents, We use this to send an event when there is
//a collision, then listen for it to play a sound
.add_event::<CollisionEvent>()
//Note that the systems below run in order they are defined
//
//system sets allows you to group systems
//https://github.com/bevyengine/bevy/blob/main/examples/ecs/system_sets.rs
.add_system_set(
SystemSet::new()
.with_run_criteria(FixedTimestep::step(TIME_STEP as f64))
.with_system(check_for_collisions)
.with_system(move_paddle.before(check_for_collisions))
.with_system(apply_velocity.before(check_for_collisions))
.with_system(player_collision_sound.after(check_for_collisions)),
)
.add_system(update_scoreboard)
.add_system(bevy::window::close_on_esc)
.run();
}
//Components are basically Data types that entities can have
#[derive(Component)]
struct Paddle;
#[derive(Component)]
struct Ball;
//I think Deref and DerefMut means we may want to uniquely access the actual item
#[derive(Component, Deref, DerefMut)]
struct Velocity(Vec2);
#[derive(Component)]
struct Collider;
//default impliments default values for simple resource
#[derive(Default)]
struct CollisionEvent;
#[derive(Component)]
struct Brick;
//Handle contains a unique id that corresponds to a specific item in the
//assets collection
struct CollisionSound(Handle<AudioSource>);
//A bundle is a typed collection of components.
//in this case, a SpriteBundle, and a collider
#[derive(Bundle)]
struct WallBundle {
#[bundle]
sprite_bundle:SpriteBundle,
collider: Collider,
}
enum WallLocation {
Left,
Right,
Bottom,
Top,
}
impl WallLocation {
fn position(&self) -> Vec2 {
match self {
WallLocation::Left => Vec2::new(LEFT_WALL, 0.),
WallLocation::Right => Vec2::new(RIGHT_WALL, 0.),
WallLocation::Bottom => Vec2::new(0., BOTTOM_WALL),
WallLocation::Top => Vec2::new(0., TOP_WALL),
}
}
fn size(&self) -> Vec2 {
let arena_height = TOP_WALL - BOTTOM_WALL;
let arena_width = RIGHT_WALL - LEFT_WALL;
assert!(arena_height > 0.0);
assert!(arena_width > 0.0);
match self {
WallLocation::Left | WallLocation::Right => {
Vec2::new(WALL_THICKNESS, arena_height + WALL_THICKNESS)
},
WallLocation::Bottom | WallLocation::Top => {
Vec2::new(arena_width + WALL_THICKNESS, WALL_THICKNESS)
}
}
}
}
impl WallBundle {
fn new(location:WallLocation) -> WallBundle {
WallBundle {
sprite_bundle: SpriteBundle {
transform: Transform {
translation: location.position().extend(0.0),
scale: location.size().extend(1.0),
..default()
},
sprite : Sprite {
color: WALL_COLOR,
..default()
},
..default()
},
collider: Collider,
}
}
}
struct Scoreboard {
score: usize,
}
//one time setup function
//commands are a queue of commands to run on the world. Can use to
//spawn / despawn entities, add / remove components and manage resource
//Command are queued to be performed later when it is safe to do so.
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>
) {
commands.spawn_bundle(Camera2dBundle::default());
//AssetServer is used to load assets from the file system. Note that calling
//load() does not necessarily load the asset right away. Instead is returned
//a handle to that asset which can be used. (and then when the asset loads
//it will be avaliable)
let ball_collision_sound = asset_server.load("sounds/breakout_collision.ogg");
commands.insert_resource(CollisionSound(ball_collision_sound));
let paddle_y = BOTTOM_WALL + GAP_BETWEEN_PADDLE_AND_FLOOR;
//calling spawn spawns a single entity
commands
.spawn()
.insert(Paddle)
.insert_bundle(
SpriteBundle {
transform: Transform {
translation: Vec3::new(0.0, paddle_y, 0.0),
scale: PADDLE_SIZE,
..default()
},
sprite : Sprite {
color: PADDLE_COLOR,
..default()
},
..default()
}
)
.insert(Collider);
commands
.spawn()
.insert(Ball)
.insert_bundle(
SpriteBundle {
transform: Transform {
scale: BALL_SIZE,
translation: BALL_STARTING_POSITION,
..default()
},
sprite : Sprite {
color: BALL_COLOR,
..default()
},
..default()
}
)
.insert(Velocity(INITIAL_BALL_DIRECTION.normalize() * BALL_SPEED));
//Text items that make up the scoreboard
commands.spawn_bundle(
TextBundle::from_sections([
TextSection::new(
"Score: ",
TextStyle {
font:asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: SCOREBOARD_FONT_SIZE,
color: TEXT_COLOR
},
),
TextSection::from_style(
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: SCOREBOARD_FONT_SIZE,
color: SCORE_COLOR
}
),
])
//Position is absolute, offset from top and left
.with_style(
Style {
position_type: PositionType::Absolute,
position: UiRect {
top: SCOREBOARD_TEXT_PADDING,
left: SCOREBOARD_TEXT_PADDING,
..default()
},
..default()
}
)
);
//Walls
commands.spawn_bundle(WallBundle::new(WallLocation::Left));
commands.spawn_bundle(WallBundle::new(WallLocation::Right));
commands.spawn_bundle(WallBundle::new(WallLocation::Bottom));
commands.spawn_bundle(WallBundle::new(WallLocation::Top));
assert!(BRICK_SIZE.x > 0.0);
assert!(BRICK_SIZE.y > 0.0);
//note the 2. is the dot operator to autoconvert
//https://doc.rust-lang.org/nomicon/dot-operator.html
let total_width_of_bricks = (RIGHT_WALL - LEFT_WALL) - 2. * GAP_BETWEEN_BRICK_AND_SIDE;
let bottom_edge_of_bricks = paddle_y + GAP_BETWEEN_PADDLE_AND_BRICKS;
let total_height_of_bricks = TOP_WALL - bottom_edge_of_bricks -
GAP_BETWEEN_BRICKS_AND_CEILING;
assert!(total_width_of_bricks > 0.0);
assert!(total_height_of_bricks > 0.0);
let n_columns = (total_width_of_bricks / (BRICK_SIZE.x + GAP_BETWEEN_BRICKS))
.floor() as usize;
let n_rows = (total_height_of_bricks / (BRICK_SIZE.y + GAP_BETWEEN_BRICKS))
.floor() as usize;
let n_vertical_gaps = n_columns - 1;
let center_of_bricks = (LEFT_WALL + RIGHT_WALL) / 2.0;
let left_edge_of_bricks = center_of_bricks
- (n_columns as f32 / 2.0 * BRICK_SIZE.x)
- n_vertical_gaps as f32 / 2.0 * GAP_BETWEEN_BRICKS;
let offset_x = left_edge_of_bricks + BRICK_SIZE.x / 2.;
let offset_y = bottom_edge_of_bricks + BRICK_SIZE.y / 2.;
for row in 0..n_rows {
for column in 0..n_columns {
let brick_position = Vec2::new(
offset_x + column as f32 * (BRICK_SIZE.x + GAP_BETWEEN_BRICKS),
offset_y + row as f32 * (BRICK_SIZE.y + GAP_BETWEEN_BRICKS)
);
commands
.spawn()
.insert(Brick)
.insert_bundle(
SpriteBundle {
sprite: Sprite {
color: BRICK_COLOR,
..default()
},
transform: Transform {
translation: brick_position.extend(0.0),
scale: Vec3::new(BRICK_SIZE.x, BRICK_SIZE.y, 1.0),
..default()
},
..default()
}
)
.insert(Collider);
}
}
}
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;
let left_bounds = LEFT_WALL + WALL_THICKNESS / 2.0 +
PADDLE_SIZE.x / 2.0 + PADDLE_PADDING;
let right_bounds = RIGHT_WALL - WALL_THICKNESS / 2.0 -
PADDLE_SIZE.x / 2.0 - PADDLE_PADDING;
paddle_transform.translation.x = new_paddle_position.clamp(left_bounds, right_bounds);
}
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;
}
}
fn update_scoreboard(scoreboard: Res<Scoreboard>, mut query : Query<&mut Text>) {
let mut text = query.single_mut();
text.sections[1].value = scoreboard.score.to_string();
}
fn check_for_collisions(
mut commands: Commands,
mut scoreboard: ResMut<Scoreboard>,
mut ball_query: Query<(&mut Velocity, &Transform), With<Ball>>,
collider_query: Query<(Entity, &Transform, Option<&Brick>), With<Collider>>,
mut collision_events: EventWriter<CollisionEvent>
) {
let (mut ball_velocity, ball_transform) = ball_query.single_mut();
let ball_size = ball_transform.scale.truncate();
for (collider_entity, transform, maybe_brick) in &collider_query {
let collision = collide(
ball_transform.translation,
ball_size,
transform.translation,
transform.scale.truncate()
);
if let Some(collision) = collision {
collision_events.send_default();
if maybe_brick.is_some() {
scoreboard.score += 1;
commands.entity(collider_entity).despawn();
}
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 => {},
}
if reflect_x {
ball_velocity.x = -ball_velocity.x;
}
if reflect_y {
ball_velocity.y = -ball_velocity.y;
}
}
}
}
fn player_collision_sound(
collision_events: EventReader<CollisionEvent>,
audio:Res<Audio>,
sound: Res<CollisionSound>
) {
if !collision_events.is_empty() {
collision_events.clear();
audio.play(sound.0.clone());
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment