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..fd37f142be4db 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, BoundingCircle, BoundingVolume, IntersectsVolume}, prelude::*, - sprite::collide_aabb::{collide, Collision}, 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,16 +351,17 @@ 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( - ball_transform.translation, - ball_size, - transform.translation, - transform.scale.truncate(), + let collision = collide_with_side( + BoundingCircle::new(ball_transform.translation.truncate(), BALL_DIAMETER / 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,35 @@ fn play_collision_sound( }); } } + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum Collision { + Left, + Right, + Top, + Bottom, +} + +// 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 = wall.closest_point(ball.center()); + 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 offset.y > 0. { + Collision::Top + } else { + Collision::Bottom + }; + + Some(side) +}