From a41ff9d269abde5ea155de7d20821b154ca3b23b Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Tue, 23 Jan 2024 15:52:03 -0700 Subject: [PATCH 1/5] Use Aabb2d::intersects for breakout example collisions --- crates/bevy_sprite/src/collide_aabb.rs | 297 ------------------------- crates/bevy_sprite/src/lib.rs | 2 - examples/games/breakout.rs | 51 ++++- 3 files changed, 44 insertions(+), 306 deletions(-) delete mode 100644 crates/bevy_sprite/src/collide_aabb.rs diff --git a/crates/bevy_sprite/src/collide_aabb.rs b/crates/bevy_sprite/src/collide_aabb.rs deleted file mode 100644 index cc9c7c6c4bf41..0000000000000 --- a/crates/bevy_sprite/src/collide_aabb.rs +++ /dev/null @@ -1,297 +0,0 @@ -//! Utilities for detecting if and on which side two axis-aligned bounding boxes (AABB) collide. - -use bevy_math::{Vec2, Vec3}; - -/// The side where a collision occurred, as returned by [`collide`]. -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub enum Collision { - Left, - Right, - Top, - Bottom, - Inside, -} - -struct CollisionBox { - pub top: f32, - pub bottom: f32, - pub left: f32, - pub right: f32, -} - -impl CollisionBox { - pub fn new(pos: Vec3, size: Vec2) -> Self { - Self { - top: pos.y + size.y / 2., - bottom: pos.y - size.y / 2., - left: pos.x - size.x / 2., - right: pos.x + size.x / 2., - } - } -} - -// TODO: ideally we can remove this once bevy gets a physics system -/// Axis-aligned bounding box collision with "side" detection. -/// -/// The [Collision], in case it occurred, is the side of `b` where `a` hit. -/// -/// * `a_pos` and `b_pos` are the center positions of the rectangles, typically obtained by -/// extracting the `translation` field from a [`Transform`](bevy_transform::components::Transform) component -/// * `a_size` and `b_size` are the dimensions (width and height) of the rectangles. -/// -/// The return value is the side of `B` that `A` has collided with. [`Collision::Left`] means that -/// `A` collided with `B`'s left side. [`Collision::Top`] means that `A` collided with `B`'s top side. -/// If the collision occurs on multiple sides, the side with the shallowest penetration is returned. -/// If all sides are involved, [`Collision::Inside`] is returned. -pub fn collide(a_pos: Vec3, a_size: Vec2, b_pos: Vec3, b_size: Vec2) -> Option { - let a = CollisionBox::new(a_pos, a_size); - let b = CollisionBox::new(b_pos, b_size); - - // check to see if the two rectangles are intersecting - if a.left < b.right && a.right > b.left && a.bottom < b.top && a.top > b.bottom { - // check to see if we hit on the left or right side - let (x_collision, x_depth) = if a.left < b.left && a.right > b.left && a.right < b.right { - (Collision::Left, b.left - a.right) - } else if a.left > b.left && a.left < b.right && a.right > b.right { - (Collision::Right, a.left - b.right) - } else { - (Collision::Inside, -f32::INFINITY) - }; - - // check to see if we hit on the top or bottom side - let (y_collision, y_depth) = if a.bottom < b.bottom && a.top > b.bottom && a.top < b.top { - (Collision::Bottom, b.bottom - a.top) - } else if a.bottom > b.bottom && a.bottom < b.top && a.top > b.top { - (Collision::Top, a.bottom - b.top) - } else { - (Collision::Inside, -f32::INFINITY) - }; - - // if we had an "x" and a "y" collision, pick the "primary" side using penetration depth - if y_depth.abs() < x_depth.abs() { - Some(y_collision) - } else { - Some(x_collision) - } - } else { - None - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// - /// - /// ______ - /// | | - /// | A | - /// ----|----|---- - /// | |____| | - /// | B | - /// |____________| - /// - #[test] - fn top_collision() { - let a = Vec3::new(0., 30., 0.); - let b = Vec3::new(0., 0., 0.); - - check(a, b, Some(Collision::Top)); - } - - /// - /// - /// -------------- - /// | B | - /// | _____ | - /// |___|____|___| - /// | | - /// | A | - /// | | - /// ----- - #[test] - fn bottom_collision() { - let a = Vec3::new(0., -30., 0.); - let b = Vec3::new(0., 0., 0.); - - check(a, b, Some(Collision::Bottom)); - } - - /// - /// ______ - /// | --|----------- - /// | | | | - /// | A | | B | - /// | |_|__________| - /// |_____| - /// - #[test] - fn left_collision() { - let a = Vec3::new(0., 0., 0.); - let b = Vec3::new(30., 0., 0.); - - check(a, b, Some(Collision::Left)); - } - - /// - /// ______ - /// -----------|-- | - /// | B | | | - /// | | | A | - /// |__________|_| | - /// |_____| - #[test] - fn right_collision() { - let a = Vec3::new(0., 0., 0.); - let b = Vec3::new(-30., 0., 0.); - - check(a, b, Some(Collision::Right)); - } - - /// - /// ______ - /// ----|----|---- - /// | | | | - /// | | | B | - /// |___|____|___| - /// | A | - /// |____| - #[test] - fn without_corners_on_intersection_area() { - let a = Vec3::new(0., 0., 0.); - let b = Vec3::new(0., 0., 0.); - - check(a, b, Some(Collision::Inside)); - } - - fn check(a: Vec3, b: Vec3, expected: Option) { - let a_size = Vec2::new(30., 50.); - let b_size = Vec2::new(50., 30.); - assert_eq!(collide(a, a_size, b, b_size), expected); - } - - fn collide_two_rectangles( - // (x, y, size x, size y) - a: (f32, f32, f32, f32), - b: (f32, f32, f32, f32), - ) -> Option { - collide( - Vec3::new(a.0, a.1, 0.), - Vec2::new(a.2, a.3), - Vec3::new(b.0, b.1, 0.), - Vec2::new(b.2, b.3), - ) - } - - #[test] - fn inside_collision() { - // Identical - #[rustfmt::skip] - let res = collide_two_rectangles( - (1., 1., 1., 1.), - (1., 1., 1., 1.), - ); - assert_eq!(res, Some(Collision::Inside)); - // B inside A - #[rustfmt::skip] - let res = collide_two_rectangles( - (2., 2., 2., 2.), - (2., 2., 1., 1.), - ); - assert_eq!(res, Some(Collision::Inside)); - // A inside B - #[rustfmt::skip] - let res = collide_two_rectangles( - (2., 2., 1., 1.), - (2., 2., 2., 2.), - ); - assert_eq!(res, Some(Collision::Inside)); - } - - #[test] - fn collision_based_on_b() { - // Right of B - #[rustfmt::skip] - let res = collide_two_rectangles( - (3., 2., 2., 2.), - (2., 2., 2., 2.), - ); - assert_eq!(res, Some(Collision::Right)); - // Left of B - #[rustfmt::skip] - let res = collide_two_rectangles( - (1., 2., 2., 2.), - (2., 2., 2., 2.), - ); - assert_eq!(res, Some(Collision::Left)); - // Top of B - #[rustfmt::skip] - let res = collide_two_rectangles( - (2., 3., 2., 2.), - (2., 2., 2., 2.), - ); - assert_eq!(res, Some(Collision::Top)); - // Bottom of B - #[rustfmt::skip] - let res = collide_two_rectangles( - (2., 1., 2., 2.), - (2., 2., 2., 2.), - ); - assert_eq!(res, Some(Collision::Bottom)); - } - - // In case the X-collision depth is equal to the Y-collision depth, always - // prefer X-collision, meaning, `Left` or `Right` over `Top` and `Bottom`. - #[test] - fn prefer_x_collision() { - // Bottom-left collision - #[rustfmt::skip] - let res = collide_two_rectangles( - (1., 1., 2., 2.), - (2., 2., 2., 2.), - ); - assert_eq!(res, Some(Collision::Left)); - // Top-left collision - #[rustfmt::skip] - let res = collide_two_rectangles( - (1., 3., 2., 2.), - (2., 2., 2., 2.), - ); - assert_eq!(res, Some(Collision::Left)); - // Bottom-right collision - #[rustfmt::skip] - let res = collide_two_rectangles( - (3., 1., 2., 2.), - (2., 2., 2., 2.), - ); - assert_eq!(res, Some(Collision::Right)); - // Top-right collision - #[rustfmt::skip] - let res = collide_two_rectangles( - (3., 3., 2., 2.), - (2., 2., 2., 2.), - ); - assert_eq!(res, Some(Collision::Right)); - } - - // If the collision intersection area stretches more along the Y-axis then - // return `Top` or `Bottom`. Otherwise, `Left` or `Right`. - #[test] - fn collision_depth_wins() { - // Top-right collision - #[rustfmt::skip] - let res = collide_two_rectangles( - (3., 3., 2., 2.), - (2.5, 2.,2., 2.), - ); - assert_eq!(res, Some(Collision::Top)); - // Top-right collision - #[rustfmt::skip] - let res = collide_two_rectangles( - (3., 3., 2., 2.), - (2., 2.5, 2., 2.), - ); - assert_eq!(res, Some(Collision::Right)); - } -} diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 3ca92858c314c..9c085149e610e 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -8,8 +8,6 @@ mod texture_atlas; mod texture_atlas_builder; mod texture_slice; -pub mod collide_aabb; - pub mod prelude { #[doc(hidden)] pub use crate::{ diff --git a/examples/games/breakout.rs b/examples/games/breakout.rs index c5b103e97adfe..7e972b902525a 100644 --- a/examples/games/breakout.rs +++ b/examples/games/breakout.rs @@ -1,8 +1,8 @@ //! A simplified implementation of the classic game "Breakout". use bevy::{ + math::bounding::{Aabb2d, BoundingVolume, IntersectsVolume}, prelude::*, - sprite::collide_aabb::{collide, Collision}, sprite::MaterialMesh2dBundle, }; @@ -354,12 +354,14 @@ fn check_for_collisions( // check collision with walls for (collider_entity, transform, maybe_brick) in &collider_query { - let collision = collide( - ball_transform.translation, - ball_size, - transform.translation, - transform.scale.truncate(), + let collision = collide_with_side( + Aabb2d::new(ball_transform.translation.truncate(), ball_size / 2.), + Aabb2d::new( + transform.translation.truncate(), + transform.scale.truncate() / 2., + ), ); + if let Some(collision) = collision { // Sends a collision event so that other systems can react to the collision collision_events.send_default(); @@ -381,7 +383,6 @@ fn check_for_collisions( 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 */ } } // reflect velocity on the x-axis if we hit something on the x-axis @@ -413,3 +414,39 @@ fn play_collision_sound( }); } } + +// The side of `b` that `a` hit. +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum Collision { + Left, + Right, + Top, + Bottom, +} + +// Returns `Some` if `a` collides with `b`. +fn collide_with_side(a: Aabb2d, b: Aabb2d) -> Option { + if !a.intersects(&b) { + return None; + } + + let closest = b.closest_point(a.center()); + let relative = closest - b.center(); + let normalized = relative / b.half_size(); + + let side = if normalized.x.abs() > normalized.y.abs() { + if normalized.x < 0. { + Collision::Left + } else { + Collision::Right + } + } else { + if normalized.y > 0. { + Collision::Top + } else { + Collision::Bottom + } + }; + + Some(side) +} From 754824c4bd97a7a82826758f644babb589ae037f Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Tue, 23 Jan 2024 21:18:13 -0700 Subject: [PATCH 2/5] Clippy prefers this --- examples/games/breakout.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/games/breakout.rs b/examples/games/breakout.rs index 7e972b902525a..32919e069d437 100644 --- a/examples/games/breakout.rs +++ b/examples/games/breakout.rs @@ -440,12 +440,10 @@ fn collide_with_side(a: Aabb2d, b: Aabb2d) -> Option { } else { Collision::Right } + } else if normalized.y > 0. { + Collision::Top } else { - if normalized.y > 0. { - Collision::Top - } else { - Collision::Bottom - } + Collision::Bottom }; Some(side) From cbb9f4bc6ca6bcdf54362fb551b89c0d2dc2f51e Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Wed, 24 Jan 2024 07:10:13 -0700 Subject: [PATCH 3/5] Ball is a circle --- examples/games/breakout.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/games/breakout.rs b/examples/games/breakout.rs index 32919e069d437..57f592d36aaf5 100644 --- a/examples/games/breakout.rs +++ b/examples/games/breakout.rs @@ -1,7 +1,7 @@ //! A simplified implementation of the classic game "Breakout". use bevy::{ - math::bounding::{Aabb2d, BoundingVolume, IntersectsVolume}, + math::bounding::{Aabb2d, BoundingCircle, BoundingVolume, IntersectsVolume}, prelude::*, sprite::MaterialMesh2dBundle, }; @@ -16,7 +16,7 @@ const PADDLE_PADDING: f32 = 10.0; // We set the z-value of the ball to 1 so it renders on top in the case of overlapping sprites. 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_DIAMETER: f32 = 30.; const BALL_SPEED: f32 = 400.0; const INITIAL_BALL_DIRECTION: Vec2 = Vec2::new(0.5, -0.5); @@ -209,7 +209,8 @@ fn setup( MaterialMesh2dBundle { mesh: meshes.add(shape::Circle::default()).into(), material: materials.add(BALL_COLOR), - transform: Transform::from_translation(BALL_STARTING_POSITION).with_scale(BALL_SIZE), + transform: Transform::from_translation(BALL_STARTING_POSITION) + .with_scale(Vec2::splat(BALL_DIAMETER).extend(1.)), ..default() }, Ball, @@ -350,12 +351,11 @@ fn check_for_collisions( mut collision_events: EventWriter, ) { let (mut ball_velocity, ball_transform) = ball_query.single_mut(); - let ball_size = ball_transform.scale.truncate(); // check collision with walls for (collider_entity, transform, maybe_brick) in &collider_query { let collision = collide_with_side( - Aabb2d::new(ball_transform.translation.truncate(), ball_size / 2.), + BoundingCircle::new(ball_transform.translation.truncate(), BALL_DIAMETER / 2.), Aabb2d::new( transform.translation.truncate(), transform.scale.truncate() / 2., @@ -415,7 +415,6 @@ fn play_collision_sound( } } -// The side of `b` that `a` hit. #[derive(Debug, PartialEq, Eq, Copy, Clone)] enum Collision { Left, @@ -424,15 +423,16 @@ enum Collision { Bottom, } -// Returns `Some` if `a` collides with `b`. -fn collide_with_side(a: Aabb2d, b: Aabb2d) -> Option { - if !a.intersects(&b) { +// Returns `Some` if `ball` collides with `wall`. The returned `Collision` is the +// side of `wall` that `ball`` hit. +fn collide_with_side(ball: BoundingCircle, wall: Aabb2d) -> Option { + if !ball.intersects(&wall) { return None; } - let closest = b.closest_point(a.center()); - let relative = closest - b.center(); - let normalized = relative / b.half_size(); + let closest = wall.closest_point(ball.center()); + let relative = closest - wall.center(); + let normalized = relative / wall.half_size(); let side = if normalized.x.abs() > normalized.y.abs() { if normalized.x < 0. { From a0acee4434d575204b15b61ab929ca1432d817fc Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Wed, 24 Jan 2024 07:55:52 -0700 Subject: [PATCH 4/5] Update examples/games/breakout.rs Co-authored-by: IQuick 143 --- examples/games/breakout.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/games/breakout.rs b/examples/games/breakout.rs index 57f592d36aaf5..60579ba97ebf1 100644 --- a/examples/games/breakout.rs +++ b/examples/games/breakout.rs @@ -424,7 +424,7 @@ enum Collision { } // Returns `Some` if `ball` collides with `wall`. The returned `Collision` is the -// side of `wall` that `ball`` hit. +// side of `wall` that `ball` hit. fn collide_with_side(ball: BoundingCircle, wall: Aabb2d) -> Option { if !ball.intersects(&wall) { return None; From 1e19f30491db73265b72d816187cf38a8c6a15e4 Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Wed, 24 Jan 2024 08:10:17 -0700 Subject: [PATCH 5/5] Use ball instead of wall position when determining collision side Co-authored-by: IQuick 143 --- examples/games/breakout.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/examples/games/breakout.rs b/examples/games/breakout.rs index 60579ba97ebf1..fd37f142be4db 100644 --- a/examples/games/breakout.rs +++ b/examples/games/breakout.rs @@ -431,16 +431,14 @@ fn collide_with_side(ball: BoundingCircle, wall: Aabb2d) -> Option { } let closest = wall.closest_point(ball.center()); - let relative = closest - wall.center(); - let normalized = relative / wall.half_size(); - - let side = if normalized.x.abs() > normalized.y.abs() { - if normalized.x < 0. { + let offset = ball.center() - closest; + let side = if offset.x.abs() > offset.y.abs() { + if offset.x < 0. { Collision::Left } else { Collision::Right } - } else if normalized.y > 0. { + } else if offset.y > 0. { Collision::Top } else { Collision::Bottom