bevy-collision-plugin/src/lib.rs
2ndbeam 9d0ae295ee feat!: Got rid of UpdateShapes component
- update_collider_shapes now filters by Changed<Transform>
2026-02-15 18:05:27 +03:00

177 lines
5.9 KiB
Rust

#![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::{Aabb2d, BoundingCircle}, prelude::*};
#[derive(Component, PartialEq, Debug)]
/// Enum of available shapes
pub enum CollisionShape {
/// Rectangle shape
Aabb {
/// The shape itself, tied to [`Transform`]
shape: Aabb2d,
/// Start offset, used to calculate current position inside shape.
offset: Vec2,
/// Size of the shape, used to calculate global position.
size: Vec2,
},
/// Circle shape
Circle {
/// The shape itself, tied to [`Transform`]
shape: BoundingCircle,
/// Start offset, used to calculate current position inside shape.
offset: Vec2,
},
}
impl CollisionShape {
/// Returns true if shapes intersect
pub fn intersects(&self, other: &CollisionShape) -> bool {
match self {
Self::Aabb { shape: aabb, .. } => {
match other {
CollisionShape::Aabb { shape: other_aabb, .. } => aabb.intersects(other_aabb),
CollisionShape::Circle { shape: circle, .. } => aabb.intersects(circle),
}
},
Self::Circle{ shape: circle, .. } => {
match other {
CollisionShape::Aabb { shape: aabb, .. } => circle.intersects(aabb),
CollisionShape::Circle { shape: other_circle, .. } => circle.intersects(other_circle),
}
},
}
}
/// 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 }
}
/// 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)]
/// Component, handling multiple collision shapes for single entity
pub struct Collider {
/// Shapes of the collider
pub shapes: Vec<CollisionShape>,
/// Bitmask of collider groups.
/// Setting this to 0 makes collider invisible for others
pub mask: usize,
/// Bitmask of groups which will be checked for collision.
/// Setting this to 0 makes collider ignorant to others
pub check_mask: usize,
}
impl Collider {
#[inline]
/// Returns true if any of self shapes intersect other's shapes, ignoring groups
pub fn intersects_unchecked(&self, other: &Collider) -> bool {
for shape in self.shapes.iter() {
for other_shape in other.shapes.iter() {
if shape.intersects(other_shape) {
return true;
}
}
}
false
}
/// Returns true if any of self shapes intersect other's shapes, respecting groups.
/// Wrapper around [intersects_unchecked](Self::intersects_unchecked)
pub fn intersects(&self, other: &Collider) -> bool {
if self.check_mask & other.mask == 0 {
return false;
}
self.intersects_unchecked(other)
}
/// Update shapes' current position
pub fn update_shapes_position(&mut self, new_position: &Vec2) {
for shape in self.shapes.iter_mut() {
match shape {
CollisionShape::Aabb { shape, offset, size } => {
shape.min.x = new_position.x + offset.x - size.x / 2.;
shape.min.y = new_position.y + offset.y - size.y / 2.;
shape.max.x = shape.min.x + size.x;
shape.max.y = shape.min.y + size.y;
},
CollisionShape::Circle { shape, offset } => {
shape.center.x = new_position.x + offset.x;
shape.center.y = new_position.y + offset.y;
},
}
}
}
}
/// Update collider shapes to match new [`Transform`]
pub fn update_collider_shapes(
colliders: Query<(&mut Collider, &Transform), Changed<Transform>>,
) {
for (mut collider, transform) in colliders {
collider.update_shapes_position(&transform.translation.xy());
}
}
#[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,
}
fn collider_filter(checker: &&Collider, checkable: &&Collider) -> bool {
checker != checkable && checker.check_mask != 0
}
/// 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)>,
) {
for (collider, entity) in colliders.iter() {
for (other_collider, other_entity) in colliders.iter().filter(|(c, _)| collider_filter(&collider, c)) {
if collider.intersects(other_collider) {
commands.trigger(CollisionEvent { entity, detected: other_entity });
}
}
}
}
/// Plugin that adds collision systems
pub struct CollisionPlugin;
impl Plugin for CollisionPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PostUpdate, (update_collider_shapes, check_for_collisions).chain());
}
}