use crate::physics::*;
use bevy::prelude::*;
use bevy_prototype_debug_lines::*;
#[derive(Component, Default)]
pub struct CollisionBox(pub f32, pub f32);
#[derive(Component, Default)]
pub struct CollisionLayers {
pub layer_id: usize,
pub collides_with_ids: Vec<usize>,
}
impl CollisionLayers {
fn can_collide(&self, col_candidate: &Self) -> bool {
self.collides_with_ids.contains(&col_candidate.layer_id)
}
}
#[derive(Component, Default, Debug)]
pub struct CollisionData {
pub allowed_left: Option<f32>,
pub allowed_right: Option<f32>,
pub allowed_top: Option<f32>,
pub allowed_bottom: Option<f32>,
pub entity_left: Option<Entity>,
pub entity_right: Option<Entity>,
pub entity_top: Option<Entity>,
pub entity_bottom: Option<Entity>,
}
fn is_collision(allowed_value: Option<f32>) -> bool {
match allowed_value {
Some(dist) => dist <= 0.0,
None => false,
}
}
impl CollisionData {
pub fn is_grounded(&self) -> bool {
is_collision(self.allowed_bottom)
}
pub fn collides_left(&self) -> bool {
is_collision(self.allowed_left)
}
pub fn collides_right(&self) -> bool {
is_collision(self.allowed_right)
}
pub fn collides_top(&self) -> bool {
is_collision(self.allowed_top)
}
fn update_data(&mut self, data: Option<(f32, Entity)>, direction: &str) {
let (allowed, closest_entity) = match direction {
"top" => (&mut self.allowed_top, &mut self.entity_top),
"bottom" => (&mut self.allowed_bottom, &mut self.entity_bottom),
"left" => (&mut self.allowed_left, &mut self.entity_left),
"right" => (&mut self.allowed_right, &mut self.entity_right),
dir => panic!("Invalid direction {}", dir),
};
match data {
Some((dist, entity)) => {
*allowed = Some(dist);
*closest_entity = Some(entity);
}
None => {
*allowed = None;
*closest_entity = None
}
}
}
}
#[derive(Bundle, Default)]
pub struct CollisionBundle {
pub collision_box: CollisionBox,
pub collision_data: CollisionData,
pub collision_layers: CollisionLayers,
}
pub fn overlap(x1: f32, x2: f32, y1: f32, y2: f32) -> bool {
if x1 < x2 {
x1 + y1 > x2 - y2
} else {
x2 + y2 > x1 - y1
}
}
fn update_space(
entity: &Entity,
space: &mut std::collections::HashMap<Entity, Option<(f32, Entity)>>,
current_space: f32,
colliding_entity: &Entity,
) {
match space.get(entity) {
Some(Some((min_space, _))) => {
if *min_space > current_space {
space.insert(*entity, Some((current_space, *colliding_entity)));
}
}
Some(None) | None => {
space.insert(*entity, Some((current_space, *colliding_entity)));
}
}
}
pub fn update_collision_data(
mut query: Query<(
Entity,
&Transform,
&CollisionBox,
&CollisionLayers,
&RigidBody,
)>,
mut query_collision_data: Query<&mut CollisionData>,
) {
let mut closest_top: std::collections::HashMap<Entity, Option<(f32, Entity)>> =
std::collections::HashMap::new();
let mut closest_bottom: std::collections::HashMap<Entity, Option<(f32, Entity)>> =
std::collections::HashMap::new();
let mut closest_left: std::collections::HashMap<Entity, Option<(f32, Entity)>> =
std::collections::HashMap::new();
let mut closest_right: std::collections::HashMap<Entity, Option<(f32, Entity)>> =
std::collections::HashMap::new();
for (entity, _, _, _, _) in query.iter() {
closest_top.insert(entity, None);
closest_bottom.insert(entity, None);
closest_left.insert(entity, None);
closest_right.insert(entity, None);
}
let mut iter = query.iter_combinations_mut();
while let Some(
[(entity1, transform1, collision_box1, collision_layers1, rigidbody1), (entity2, transform2, collision_box2, collision_layers2, rigidbody2)],
) = iter.fetch_next()
{
if !collision_layers1.can_collide(collision_layers2)
|| !collision_layers2.can_collide(collision_layers1)
{
continue;
}
if let (RigidBody::Static, RigidBody::Static) = (rigidbody1, rigidbody2) {
continue;
}
let (col_width1, col_height1) = (collision_box1.0, collision_box1.1);
let (col_width2, col_height2) = (collision_box2.0, collision_box2.1);
if overlap(
transform1.translation.x,
transform2.translation.x,
col_width1,
col_width2,
) {
if transform1.translation.y < transform2.translation.y {
let current_space_top = (transform2.translation.y - col_height2)
- (transform1.translation.y + col_height1);
update_space(&entity1, &mut closest_top, current_space_top, &entity2);
update_space(&entity2, &mut closest_bottom, current_space_top, &entity1);
} else {
let current_space_bottom = (transform1.translation.y - col_height1)
- (transform2.translation.y + col_height2);
update_space(
&entity1,
&mut closest_bottom,
current_space_bottom,
&entity2,
);
update_space(&entity2, &mut closest_top, current_space_bottom, &entity1);
}
}
if overlap(
transform1.translation.y,
transform2.translation.y,
col_height1,
col_height2,
) {
if transform1.translation.x < transform2.translation.x {
let current_space_right = (transform2.translation.x - col_width2)
- (transform1.translation.x + col_width1);
update_space(&entity1, &mut closest_right, current_space_right, &entity2);
update_space(&entity2, &mut closest_left, current_space_right, &entity1);
} else {
let current_space_left = (transform1.translation.x - col_width1)
- (transform2.translation.x + col_width2);
update_space(&entity1, &mut closest_left, current_space_left, &entity2);
update_space(&entity2, &mut closest_right, current_space_left, &entity1);
}
}
}
for (entity, data) in closest_top {
if let Ok(mut collision_data) = query_collision_data.get_mut(entity) {
collision_data.update_data(data, "top")
}
}
for (entity, data) in closest_bottom {
if let Ok(mut collision_data) = query_collision_data.get_mut(entity) {
collision_data.update_data(data, "bottom")
}
}
for (entity, data) in closest_left {
if let Ok(mut collision_data) = query_collision_data.get_mut(entity) {
collision_data.update_data(data, "left")
}
}
for (entity, data) in closest_right {
if let Ok(mut collision_data) = query_collision_data.get_mut(entity) {
collision_data.update_data(data, "right")
}
}
}
#[allow(clippy::type_complexity)]
pub fn calculate_movement(
mut query: Query<(&mut Velocity, &CollisionData), (With<CollisionBox>, With<RigidBody>)>,
time: Res<Time>,
physics_config: Res<PhysicsConfiguration>,
) {
if !physics_config.paused {
let dt = time.delta_seconds();
for (mut v, collision_data) in query.iter_mut() {
let to_move_x = v.x() * dt;
let to_move_y = v.y() * dt;
if let Some(allowed_right) = collision_data.allowed_right {
if to_move_x > 0.0 && to_move_x > allowed_right {
v.set_x(allowed_right / dt);
}
}
if let Some(allowed_left) = collision_data.allowed_left {
if to_move_x < 0.0 && -to_move_x > allowed_left {
v.set_x(-allowed_left / dt);
}
}
if let Some(allowed_top) = collision_data.allowed_top {
if to_move_y > 0.0 && to_move_y > allowed_top {
v.set_y(allowed_top / dt);
}
}
if let Some(allowed_bottom) = collision_data.allowed_bottom {
if to_move_y < 0.0 && -to_move_y > allowed_bottom {
v.set_y(-allowed_bottom / dt);
}
}
}
}
}
pub fn draw_collision_boxes(
query: Query<(&Transform, &CollisionBox)>,
mut lines: ResMut<DebugLines>,
physics_configuration: Res<PhysicsConfiguration>,
) {
if physics_configuration.show_collision_boxes {
for (transform, collision_box) in query.iter() {
let duration = 0.0;
let top_left_corner =
transform.translation + Vec3::new(-collision_box.0, collision_box.1, 0.0);
let bottom_left_corner = top_left_corner + Vec3::new(0.0, -collision_box.1 * 2.0, 0.0);
let bottom_right_corner =
bottom_left_corner + Vec3::new(collision_box.0 * 2.0, 0.0, 0.0);
let top_right_corner = bottom_right_corner + Vec3::new(0.0, collision_box.1 * 2.0, 0.0);
lines.line(bottom_left_corner, bottom_right_corner, duration);
lines.line(bottom_right_corner, top_right_corner, duration);
lines.line(top_right_corner, top_left_corner, duration);
lines.line(top_left_corner, bottom_left_corner, duration);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{tests::create_physics_app, PhysicsBundle};
#[test]
fn overlap_works() {
assert!(overlap(10.0, 12.0, 4.0, 10.0));
assert!(overlap(0.0, 12.0, 14.0, 10.0));
assert!(overlap(-1.0, 6.0, 12.0, 2.0));
assert!(overlap(0.0, 0.0, 16.0, 16.0));
assert!(overlap(0.0, 4.0, 3.0, 3.0));
assert!(overlap(16.0, 10.0, 4.0, 3.0));
}
#[test]
fn updates_space_correctly() {
let entity1 = Entity::from_raw(1);
let entity2 = Entity::from_raw(2);
let dist_to_closest = 10.0;
let mut data: std::collections::HashMap<Entity, Option<(f32, Entity)>> =
std::collections::HashMap::new();
update_space(&entity1, &mut data, dist_to_closest, &entity2);
assert_eq!(data[&entity1], Some((dist_to_closest, entity2)));
let new_dist_to_closest = dist_to_closest + 10.0;
update_space(&entity1, &mut data, new_dist_to_closest, &entity2);
assert_eq!(data[&entity1], Some((dist_to_closest, entity2)));
let new_dist_to_closest = dist_to_closest / 2.0;
update_space(&entity1, &mut data, new_dist_to_closest, &entity2);
assert_eq!(data[&entity1], Some((new_dist_to_closest, entity2)));
}
#[test]
fn collision_works_simple() {
let mut app = create_physics_app();
let collision_id = app
.world
.spawn_empty()
.insert(PhysicsBundle {
rigidbody: RigidBody::Dynamic,
..default()
})
.insert(CollisionBundle {
collision_box: CollisionBox(8.0, 8.0),
collision_layers: CollisionLayers {
layer_id: 0,
collides_with_ids: vec![0, 1],
},
..default()
})
.insert(Transform::from_xyz(0.0, 0.0, 0.0))
.id();
let to_collide_id = app
.world
.spawn_empty()
.insert(PhysicsBundle {
rigidbody: RigidBody::Dynamic,
..default()
})
.insert(CollisionBundle {
collision_box: CollisionBox(8.0, 8.0),
collision_layers: CollisionLayers {
layer_id: 0,
collides_with_ids: vec![0, 1],
},
..default()
})
.insert(Transform::from_xyz(20.0, 0.0, 0.0))
.id();
app.update();
let col_data1 = app.world.get::<CollisionData>(collision_id).unwrap();
let col_data2 = app.world.get::<CollisionData>(to_collide_id).unwrap();
assert_eq!(col_data1.allowed_right, Some(4.0));
assert_eq!(col_data2.allowed_left, Some(4.0));
assert_eq!(col_data1.allowed_top, None);
assert_eq!(col_data1.allowed_bottom, None);
assert_eq!(col_data1.allowed_left, None);
assert_eq!(col_data1.entity_right, Some(to_collide_id));
assert_eq!(col_data2.entity_left, Some(collision_id));
assert_eq!(col_data1.entity_top, None);
assert_eq!(col_data1.entity_bottom, None);
assert_eq!(col_data1.entity_left, None);
}
#[test]
fn collision_works_advanced() {
let mut app = create_physics_app();
let collision_id = app
.world
.spawn_empty()
.insert(PhysicsBundle {
rigidbody: RigidBody::Dynamic,
..default()
})
.insert(CollisionBundle {
collision_box: CollisionBox(8.0, 8.0),
collision_layers: CollisionLayers {
layer_id: 0,
collides_with_ids: vec![0, 1, 2],
},
..default()
})
.insert(Transform::from_xyz(0.0, 0.0, 0.0))
.id();
let to_collide_1_id = app
.world
.spawn_empty()
.insert(PhysicsBundle {
rigidbody: RigidBody::Dynamic,
..default()
})
.insert(CollisionBundle {
collision_box: CollisionBox(8.0, 8.0),
collision_layers: CollisionLayers {
layer_id: 2,
collides_with_ids: vec![0, 2],
},
..default()
})
.insert(Transform::from_xyz(-10.0, -24.0, 0.0))
.id();
let to_collide_2_id = app
.world
.spawn_empty()
.insert(PhysicsBundle {
rigidbody: RigidBody::Dynamic,
..default()
})
.insert(CollisionBundle {
collision_box: CollisionBox(8.0, 8.0),
collision_layers: CollisionLayers {
layer_id: 1,
collides_with_ids: vec![0, 1, 2],
},
..default()
})
.insert(Transform::from_xyz(10.0, -24.0, 0.0))
.id();
app.update();
let col_data1 = app.world.get::<CollisionData>(collision_id).unwrap();
let col_data2 = app.world.get::<CollisionData>(to_collide_1_id).unwrap();
let col_data3 = app.world.get::<CollisionData>(to_collide_2_id).unwrap();
assert_eq!(col_data1.allowed_bottom, Some(8.0));
assert_eq!(col_data1.allowed_top, None);
assert_eq!(col_data2.allowed_top, Some(8.0));
assert_eq!(col_data3.allowed_top, Some(8.0));
assert_eq!(col_data2.allowed_right, None);
assert_eq!(col_data3.allowed_left, None);
}
#[test]
fn perfect_overlap_collision_collides() {
let mut app = create_physics_app();
let collision_id = app
.world
.spawn_empty()
.insert(PhysicsBundle {
rigidbody: RigidBody::Dynamic,
..default()
})
.insert(CollisionBundle {
collision_box: CollisionBox(8.0, 8.0),
collision_layers: CollisionLayers {
layer_id: 0,
collides_with_ids: vec![0, 1],
},
..default()
})
.insert(Transform::from_xyz(0.0, 16.0, 0.0))
.id();
let to_collide_id = app
.world
.spawn_empty()
.insert(PhysicsBundle {
rigidbody: RigidBody::Dynamic,
..default()
})
.insert(CollisionBundle {
collision_box: CollisionBox(8.0, 8.0),
collision_layers: CollisionLayers {
layer_id: 1,
collides_with_ids: vec![0, 1, 2],
},
..default()
})
.insert(Transform::from_xyz(0.0, 0.0, 0.0))
.id();
app.update();
let col_data1 = app.world.get::<CollisionData>(collision_id).unwrap();
let col_data2 = app.world.get::<CollisionData>(to_collide_id).unwrap();
assert_eq!(col_data1.allowed_bottom, Some(0.0));
assert_eq!(col_data2.allowed_top, Some(0.0));
}
}