generated from 2ndbeam/bevy-template
feat!: Implemented CollisionEvent trigger
- Added integration tests - Replaced GlobalTransform with Transform in collision math - Fixed doc lint - collider_detects_different_groups test does not pass
This commit is contained in:
parent
53778f0b33
commit
63b30f4a9f
2 changed files with 208 additions and 22 deletions
100
src/lib.rs
100
src/lib.rs
|
|
@ -1,29 +1,29 @@
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
// TODO: add more precise crate documentation
|
||||||
|
//! Simple 2D collision system, inspired by Godot Engine.
|
||||||
|
|
||||||
use bevy::math::bounding::IntersectsVolume;
|
use bevy::math::bounding::IntersectsVolume;
|
||||||
#[warn(missing_docs)]
|
|
||||||
|
|
||||||
use bevy::{math::bounding::{Aabb2d, BoundingCircle}, prelude::*};
|
use bevy::{math::bounding::{Aabb2d, BoundingCircle}, prelude::*};
|
||||||
|
|
||||||
// TODO: add more precise crate documentation
|
#[derive(Component, PartialEq, Debug)]
|
||||||
// TODO: implement event emitting on collision
|
|
||||||
/// Simple 2D collision system, inspired by Godot Engine.
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
/// Enum of available shapes
|
/// Enum of available shapes
|
||||||
pub enum CollisionShape {
|
pub enum CollisionShape {
|
||||||
/// Rectangle shape
|
/// Rectangle shape
|
||||||
Aabb {
|
Aabb {
|
||||||
/// The shape itself, tied to **global** position
|
/// The shape itself, tied to [`Transform`]
|
||||||
shape: Aabb2d,
|
shape: Aabb2d,
|
||||||
/// Start offset, used to calculate global position.
|
/// Start offset, used to calculate current position inside shape.
|
||||||
offset: Vec2,
|
offset: Vec2,
|
||||||
/// Size of the shape, used to calculate global position.
|
/// Size of the shape, used to calculate global position.
|
||||||
size: Vec2,
|
size: Vec2,
|
||||||
},
|
},
|
||||||
/// Circle shape
|
/// Circle shape
|
||||||
Circle {
|
Circle {
|
||||||
/// The shape itself, tied to **global** position
|
/// The shape itself, tied to [`Transform`]
|
||||||
shape: BoundingCircle,
|
shape: BoundingCircle,
|
||||||
/// Start offset, used to calculate global position
|
/// Start offset, used to calculate current position inside shape.
|
||||||
offset: Vec2,
|
offset: Vec2,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -46,9 +46,35 @@ impl CollisionShape {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create [`CollisionShape::Aabb`]
|
||||||
|
pub fn new_aabb(offset: Vec2, size: Vec2) -> Self {
|
||||||
|
let half_size = Vec2 { x: size.x / 2f32, y: size.y / 2f32 };
|
||||||
|
Self::Aabb { shape: Aabb2d::new(offset, half_size), offset, size }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
/// Create [`CollisionShape::Circle`]
|
||||||
|
pub fn new_circle(offset: Vec2, radius: f32) -> Self {
|
||||||
|
Self::Circle { shape: BoundingCircle::new(offset, radius), offset }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns current centered offset for the shape
|
||||||
|
pub fn current_offset(&self) -> Vec2 {
|
||||||
|
match self {
|
||||||
|
Self::Aabb { shape, .. } => {
|
||||||
|
Vec2::new(
|
||||||
|
(shape.min.x + shape.max.x) / 2.,
|
||||||
|
(shape.min.y + shape.max.y) / 2.,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
Self::Circle { shape, .. } => {
|
||||||
|
shape.center
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, PartialEq, Debug)]
|
||||||
#[require(Transform)]
|
#[require(Transform)]
|
||||||
/// Component, handling multiple collision shapes for single entity
|
/// Component, handling multiple collision shapes for single entity
|
||||||
pub struct Collider {
|
pub struct Collider {
|
||||||
|
|
@ -79,50 +105,80 @@ impl Collider {
|
||||||
/// Returns true if any of self shapes intersect other's shapes, respecting groups.
|
/// Returns true if any of self shapes intersect other's shapes, respecting groups.
|
||||||
/// Wrapper around [intersects_unchecked](Self::intersects_unchecked)
|
/// Wrapper around [intersects_unchecked](Self::intersects_unchecked)
|
||||||
pub fn intersects(&self, other: &Collider) -> bool {
|
pub fn intersects(&self, other: &Collider) -> bool {
|
||||||
|
println!("groups: {}", self.check_mask & other.mask );
|
||||||
if self.check_mask & other.mask == 0 {
|
if self.check_mask & other.mask == 0 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
self.intersects_unchecked(other)
|
self.intersects_unchecked(other)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update shapes global position
|
/// Update shapes' current position
|
||||||
pub fn update_shapes_position(&mut self, global_position: &Vec2) {
|
pub fn update_shapes_position(&mut self, new_position: &Vec2) {
|
||||||
for shape in self.shapes.iter_mut() {
|
for shape in self.shapes.iter_mut() {
|
||||||
match shape {
|
match shape {
|
||||||
CollisionShape::Aabb { shape, offset, size } => {
|
CollisionShape::Aabb { shape, offset, size } => {
|
||||||
shape.min.x = global_position.x + offset.x;
|
shape.min.x = new_position.x + offset.x - size.x / 2.;
|
||||||
shape.min.y = global_position.y + offset.y;
|
shape.min.y = new_position.y + offset.y - size.y / 2.;
|
||||||
shape.max.x = shape.min.x + size.x;
|
shape.max.x = shape.min.x + size.x;
|
||||||
shape.max.y = shape.min.y + size.y;
|
shape.max.y = shape.min.y + size.y;
|
||||||
},
|
},
|
||||||
CollisionShape::Circle { shape, offset } => {
|
CollisionShape::Circle { shape, offset } => {
|
||||||
shape.center.x = global_position.x + offset.x;
|
shape.center.x = new_position.x + offset.x;
|
||||||
shape.center.y = global_position.y + offset.y;
|
shape.center.y = new_position.y + offset.y;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component, Debug)]
|
||||||
|
/// Add this component on entities, which have their [`Transform`] updated.
|
||||||
|
/// Used for [`update_collider_shapes`] system
|
||||||
pub struct UpdateShapes;
|
pub struct UpdateShapes;
|
||||||
|
|
||||||
/// Update collider shapes to match new global transform
|
/// Update collider shapes to match new [`Transform`]
|
||||||
pub fn update_collider_shapes(
|
pub fn update_collider_shapes(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
colliders: Query<(&mut Collider, &GlobalTransform, Entity), With<UpdateShapes>>,
|
colliders: Query<(&mut Collider, &Transform, Entity), With<UpdateShapes>>,
|
||||||
) {
|
) {
|
||||||
for (mut collider, transform, entity) in colliders {
|
for (mut collider, transform, entity) in colliders {
|
||||||
collider.update_shapes_position(&transform.translation().xy());
|
collider.update_shapes_position(&transform.translation.xy());
|
||||||
commands.entity(entity).remove::<UpdateShapes>();
|
commands.entity(entity).remove::<UpdateShapes>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(EntityEvent, Debug)]
|
||||||
|
/// Emitted when colliders intersect in [`check_for_collisions`] system
|
||||||
|
pub struct CollisionEvent {
|
||||||
|
/// Entity, whose [`Collider`] detected another [`Collider`]
|
||||||
|
pub entity: Entity,
|
||||||
|
/// Entity, whose [`Collider`] was detected as intersecting
|
||||||
|
pub detected: Entity,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check each [`Collider`] for collisions with another colliders.
|
||||||
|
/// Triggers [`CollisionEvent`] on collider intersection
|
||||||
|
pub fn check_for_collisions(
|
||||||
|
mut commands: Commands,
|
||||||
|
colliders: Query<(&Collider, Entity)>,
|
||||||
|
) {
|
||||||
|
let mut visited_colliders: Vec<&Collider> = Vec::new();
|
||||||
|
for (collider, entity) in colliders.iter() {
|
||||||
|
for (other_collider, other_entity) in colliders.iter().filter(|(c, _)| !visited_colliders.contains(c)) {
|
||||||
|
if collider.intersects(other_collider) {
|
||||||
|
commands.trigger(CollisionEvent { entity, detected: other_entity });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visited_colliders.push(collider);
|
||||||
|
println!("{visited_colliders:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Plugin that adds collision systems
|
/// Plugin that adds collision systems
|
||||||
pub struct CollisionPlugin;
|
pub struct CollisionPlugin;
|
||||||
|
|
||||||
impl Plugin for CollisionPlugin {
|
impl Plugin for CollisionPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_systems(Update, update_collider_shapes);
|
app.add_systems(PostUpdate, (update_collider_shapes, check_for_collisions).chain());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
130
tests/collider_position.rs
Normal file
130
tests/collider_position.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_collision_plugin::{Collider, CollisionEvent, CollisionPlugin, CollisionShape, UpdateShapes};
|
||||||
|
|
||||||
|
const CHECKED_GROUPS: usize = 1 >> 0 | 1 >> 1;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct MovingCollider;
|
||||||
|
|
||||||
|
fn moving_collider_bundle() -> impl Bundle {
|
||||||
|
(
|
||||||
|
Collider {
|
||||||
|
shapes: vec![
|
||||||
|
CollisionShape::new_aabb(Vec2::new(0., 0.), Vec2::new(10., 10.)),
|
||||||
|
CollisionShape::new_circle(Vec2::new(0., 15.), 5.),
|
||||||
|
],
|
||||||
|
mask: 0,
|
||||||
|
check_mask: CHECKED_GROUPS,
|
||||||
|
},
|
||||||
|
Transform::from_translation(Vec3::new(0., 0., 0.)),
|
||||||
|
MovingCollider,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn zerogroup_collider_bundle() -> impl Bundle {
|
||||||
|
(
|
||||||
|
Collider {
|
||||||
|
shapes: vec![
|
||||||
|
CollisionShape::new_aabb(Vec2::new(0., 0.), Vec2::new(10., 10.)),
|
||||||
|
],
|
||||||
|
mask: 1 >> 0,
|
||||||
|
check_mask: 0,
|
||||||
|
},
|
||||||
|
Transform::from_translation(Vec3::new(53., 5., 0.)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn firstgroup_collider_bundle() -> impl Bundle {
|
||||||
|
(
|
||||||
|
Collider {
|
||||||
|
shapes: vec![
|
||||||
|
CollisionShape::new_circle(Vec2::new(0., 0.), 5.),
|
||||||
|
],
|
||||||
|
mask: 1 >> 1,
|
||||||
|
check_mask: 0,
|
||||||
|
},
|
||||||
|
Transform::from_translation(Vec3::new(53., 18., 0.)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_collider(
|
||||||
|
mut commands: Commands,
|
||||||
|
query: Query<(Entity, &mut Transform), With<MovingCollider>>,
|
||||||
|
) {
|
||||||
|
for (entity, mut transform) in query {
|
||||||
|
transform.translation.x += 50.;
|
||||||
|
commands.entity(entity)
|
||||||
|
.insert(UpdateShapes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup(
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
commands.spawn(moving_collider_bundle());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configured_app() -> App {
|
||||||
|
let mut app = App::new();
|
||||||
|
|
||||||
|
app.add_plugins(CollisionPlugin)
|
||||||
|
.add_systems(Startup, setup)
|
||||||
|
.add_systems(Update, move_collider);
|
||||||
|
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collider_shapes_update_properly() {
|
||||||
|
let mut app = configured_app();
|
||||||
|
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
let mut query = app.world_mut().query::<&Collider>();
|
||||||
|
for collider in query.iter(app.world()) {
|
||||||
|
let aabb_offset = collider.shapes[0].current_offset();
|
||||||
|
let circle_offset = collider.shapes[1].current_offset();
|
||||||
|
|
||||||
|
let expected_aabb = Vec2::new(50., 0.);
|
||||||
|
let expected_circle = Vec2::new(50., 15.);
|
||||||
|
|
||||||
|
assert_eq!(aabb_offset, expected_aabb);
|
||||||
|
assert_eq!(circle_offset, expected_circle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct CollisionList(Vec<Entity>);
|
||||||
|
|
||||||
|
fn setup_others(
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
commands.spawn(zerogroup_collider_bundle());
|
||||||
|
commands.spawn(firstgroup_collider_bundle());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_collision_event(
|
||||||
|
event: On<CollisionEvent>,
|
||||||
|
mut res: ResMut<CollisionList>,
|
||||||
|
) {
|
||||||
|
if !res.0.contains(&event.detected) {
|
||||||
|
res.0.push(event.detected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collider_detects_different_groups() {
|
||||||
|
let mut app = configured_app();
|
||||||
|
|
||||||
|
app.insert_resource(CollisionList(vec![]))
|
||||||
|
.add_systems(Startup, setup_others)
|
||||||
|
.add_observer(on_collision_event);
|
||||||
|
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
let collisions = app.world().get_resource::<CollisionList>().unwrap();
|
||||||
|
let got_collisions = collisions.0.len();
|
||||||
|
let expected = 2;
|
||||||
|
|
||||||
|
assert_eq!(got_collisions, expected);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue