diff --git a/Cargo.toml b/Cargo.toml index 3479df334a..376d4cef3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -139,6 +139,10 @@ path = "examples/ecs/startup_system.rs" name = "ecs_guide" path = "examples/ecs/ecs_guide.rs" +[[example]] +name = "breakout" +path = "examples/game/breakout/main.rs" + [[example]] name = "mouse_input" path = "examples/input/mouse_input.rs" diff --git a/crates/bevy_sprite/src/collide_aabb.rs b/crates/bevy_sprite/src/collide_aabb.rs new file mode 100644 index 0000000000..021f395eca --- /dev/null +++ b/crates/bevy_sprite/src/collide_aabb.rs @@ -0,0 +1,62 @@ +use glam::{Vec3, Vec2}; + +#[derive(Debug)] +pub enum Collision { + Left, + Right, + Top, + Bottom, +} + +// TODO: ideally we can remove this once bevy gets a physics system +/// Axis-aligned bounding box collision with "side" detection +pub fn collide(a_pos: Vec3, a_size: Vec2, b_pos: Vec3, b_size: Vec2) -> Option { + let a_min = a_pos.truncate() - a_size / 2.0; + let a_max = a_pos.truncate() + a_size / 2.0; + + let b_min = b_pos.truncate() - b_size / 2.0; + let b_max = b_pos.truncate() + b_size / 2.0; + + // check to see if the two rectangles are intersecting + if a_min.x() < b_max.x() + && a_max.x() > b_min.x() + && a_min.y() < b_max.y() + && a_max.y() > b_min.y() + { + // check to see if we hit on the left or right side + let (x_collision, x_depth) = + if a_min.x() < b_min.x() && a_max.x() > b_min.x() && a_max.x() < b_max.x() { + (Some(Collision::Left), b_min.x() - a_max.x()) + } else if a_min.x() > b_min.x() && a_min.x() < b_max.x() && a_max.x() > b_max.x() { + (Some(Collision::Right), a_min.x() - b_max.x()) + } else { + (None, 0.0) + }; + + // check to see if we hit on the top or bottom side + let (y_collision, y_depth) = + if a_min.y() < b_min.y() && a_max.y() > b_min.y() && a_max.y() < b_max.y() { + (Some(Collision::Bottom), b_min.y() - a_max.y()) + } else if a_min.y() > b_min.y() && a_min.y() < b_max.y() && a_max.y() > b_max.y() { + (Some(Collision::Top), a_min.y() - b_max.y()) + } else { + (None, 0.0) + }; + + // if we had an "x" and a "y" collision, pick the "primary" side using penetration depth + match (x_collision, y_collision) { + (Some(x_collision), Some(y_collision)) => { + if y_depth < x_depth { + Some(y_collision) + } else { + Some(x_collision) + } + } + (Some(x_collision), None) => Some(x_collision), + (None, Some(y_collision)) => Some(y_collision), + (None, None) => None, + } + } else { + None + } +} diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 5c795b4e9d..eccd4e247c 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -1,6 +1,7 @@ mod color_material; mod dynamic_texture_atlas_builder; pub mod entity; +pub mod collide_aabb; mod rect; mod render; mod sprite; diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index 2021244a32..4d8700ba3c 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -12,7 +12,7 @@ use glam::Vec2; #[derive(Default, RenderResources, RenderResource)] #[render_resources(from_self)] pub struct Sprite { - size: Vec2, + pub size: Vec2, } // SAFE: sprite is repr(C) and only consists of byteables diff --git a/examples/game/breakout/main.rs b/examples/game/breakout/main.rs new file mode 100644 index 0000000000..7239c4fefe --- /dev/null +++ b/examples/game/breakout/main.rs @@ -0,0 +1,231 @@ +use bevy::{ + prelude::*, + sprite::collide_aabb::{collide, Collision}, +}; + +fn main() { + App::build() + .add_default_plugins() + .add_resource(ClearColor(Color::rgb(0.7, 0.7, 0.7))) + .add_startup_system(setup.system()) + .add_system(paddle_movement_system.system()) + .add_system(ball_collision_system.system()) + .add_system(ball_movement_system.system()) + .run(); +} + +struct Paddle { + speed: f32, +} + +struct Ball { + velocity: Vec3, +} + +struct Brick; +struct Wall; + +fn setup(command_buffer: &mut CommandBuffer, mut materials: ResMut>) { + // Add the game's entities to our world + let mut builder = command_buffer.build(); + builder + // camera + .entity_with(OrthographicCameraComponents::default()) + // paddle + .entity_with(SpriteComponents { + material: materials.add(Color::rgb(0.2, 0.2, 0.8).into()), + translation: Translation(Vec3::new(0.0, -250.0, 0.0)), + sprite: Sprite { + size: Vec2::new(120.0, 30.0), + }, + ..Default::default() + }) + .with(Paddle { speed: 500.0 }) + // ball + .entity_with(SpriteComponents { + material: materials.add(Color::rgb(0.8, 0.2, 0.2).into()), + translation: Translation(Vec3::new(0.0, -100.0, 1.0)), + sprite: Sprite { + size: Vec2::new(30.0, 30.0), + }, + ..Default::default() + }) + .with(Ball { + velocity: 400.0 * Vec3::new(0.5, -0.5, 0.0).normalize(), + }); + + // Add walls + let wall_material = materials.add(Color::rgb(0.5, 0.5, 0.5).into()); + let wall_thickness = 10.0; + let bounds = Vec2::new(900.0, 600.0); + + builder + // left + .entity_with(SpriteComponents { + material: wall_material, + translation: Translation(Vec3::new(-bounds.x() / 2.0, 0.0, 0.0)), + sprite: Sprite { + size: Vec2::new(wall_thickness, bounds.y() + wall_thickness), + }, + ..Default::default() + }) + .with(Wall) + // right + .entity_with(SpriteComponents { + material: wall_material, + translation: Translation(Vec3::new(bounds.x() / 2.0, 0.0, 0.0)), + sprite: Sprite { + size: Vec2::new(wall_thickness, bounds.y() + wall_thickness), + }, + ..Default::default() + }) + .with(Wall) + // bottom + .entity_with(SpriteComponents { + material: wall_material, + translation: Translation(Vec3::new(0.0, -bounds.y() / 2.0, 0.0)), + sprite: Sprite { + size: Vec2::new(bounds.x() + wall_thickness, wall_thickness), + }, + ..Default::default() + }) + .with(Wall) + // top + .entity_with(SpriteComponents { + material: wall_material, + translation: Translation(Vec3::new(0.0, bounds.y() / 2.0, 0.0)), + sprite: Sprite { + size: Vec2::new(bounds.x() + wall_thickness, wall_thickness), + }, + ..Default::default() + }) + .with(Wall); + + // Add bricks + let brick_rows = 4; + let brick_columns = 5; + let brick_spacing = 20.0; + let brick_size = Vec2::new(150.0, 30.0); + let bricks_width = brick_columns as f32 * (brick_size.x() + brick_spacing) - brick_spacing; + // center the bricks and move them up a bit + let bricks_offset = Vec3::new(-(bricks_width - brick_size.x()) / 2.0, 100.0, 0.0); + + for row in 0..brick_rows { + let y_position = row as f32 * (brick_size.y() + brick_spacing); + for column in 0..brick_columns { + let brick_position = Vec3::new( + column as f32 * (brick_size.x() + brick_spacing), + y_position, + 0.0, + ) + bricks_offset; + builder + // brick + .entity_with(SpriteComponents { + material: materials.add(Color::rgb(0.2, 0.2, 0.8).into()), + sprite: Sprite { size: brick_size }, + translation: Translation(brick_position), + ..Default::default() + }) + .with(Brick); + } + } +} + +fn paddle_movement_system( + world: &mut SubWorld, + time: Res