Skip to content

Commit 461b21a

Browse files
authored
feat: non-tile solids (#903)
Implement collisions for non-tile solids and a way to define them for elements in a map's YAML. This makes the `CollisionWorld` account for entities with the `Solid` component. I started out trying to implement a door for #43 but quickly realized I couldn't make a closed door solid. This is its own PR because I want to make sure I'm going in the right direction here and discuss the implementation. https://github.com/fishfolk/jumpy/assets/25290530/3851d2b3-8f58-41c0-8462-9b641334d63a ## Changes ### Defining solids Elements can be defined in each layer of a map's YAML file. Solids can now be defined on an element with a `solid` object. Example element in a map layer: ```yaml - pos: [630.0, 365.0] element: core:/elements/environment/demo_solid_box/demo_solid_box.element.yaml ``` Now, with a solid: ```yaml - pos: [630.0, 365.0] element: core:/elements/environment/demo_solid_box/demo_solid_box.element.yaml solid: enabled: true pos: [630.0, 365.0] size: [16.0, 16.0] ``` The solid has its own position and size since a collider may need to be separate from the sprite. For example, the door sprite will need to be very wide for when it's open but the collider for a closed door will be very thin. This was done by adding the new struct `ElementSolidMeta` to `ElementSpawn`. I wanted it to be a `Maybe<ElementSolidMeta>` but got an error from bones. My next best option was letting it be default-initialized for all elements but provide an `enabled` boolean that must be `true` for the solid to be created. Not ideal but it works. Another downside is that the size of the solid collider has to be defined *in the map*, whereas the size of the `KinematicBody` is often defined in the element's `.yaml` data file and the size of the sprite is defined in the element's `.atlast.yaml`. Maybe this can eventually be moved into an element's asset configs. ### Collision detection Solids get a `Collider` component like kinematic bodies, which are also synced to the rapier colliders. This allows game code to easily control the collider. e.g. the door will want to disable the collider when it's open, which can be done by setting `Collider.disabled` to `true`. I'm not convinced my changes to `CollisionWorld::move_{horizontal,vertical}` are correct, but this seems to mostly work. Since the `CollisionWorld::tile_collision{,_filtered}` methods now detect *any* collision they should be renamed to `CollisionWorld::collision{,_filtered}`. If this is looking good so far I will make that change. ### Game behavior I added a few demo boxes to dev level 1 for testing. It works great from the testing I did except for a couple minor bugs. If you slide into the box to the left of where you spawn you stop short of the box. But if you walk up to the box you collide with it as expected. Sliding into the one in the far bottom right of the map behaves like you would expect. This is all shown in the video. Additionally, critters cause a lot of collision warnings: > 2024-01-16T22:02:52.902798Z WARN jumpy::core::physics: Collision test error resulting in physics body stuck in wall at Rect { min: Vec2(712.0, 96.1), max: Vec2(722.0, 106.1) } Snails walk straight through solids, causing these warnings. Crabs may attempt and fail to surface under a solid after burrowing, also causing these warnings. I'm not sure what to do about this yet.
1 parent 3e99ea2 commit 461b21a

4 files changed

Lines changed: 112 additions & 15 deletions

File tree

src/core/elements.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ pub struct ElementMeta {
4545
pub plugin: Handle<LuaPlugin>,
4646
}
4747

48+
#[derive(HasSchema, Default, Debug, Clone, Copy)]
49+
#[type_data(metadata_asset("solid"))]
50+
#[repr(C)]
51+
pub struct ElementSolidMeta {
52+
pub disabled: bool,
53+
pub offset: Vec2,
54+
pub size: Vec2,
55+
}
56+
4857
#[derive(HasSchema, Deserialize, Clone, Debug)]
4958
#[repr(C)]
5059
pub struct ElementEditorMeta {
@@ -85,6 +94,10 @@ pub struct DehydrateOutOfBounds(pub Entity);
8594
#[repr(C)]
8695
pub struct ElementHandle(pub Handle<ElementMeta>);
8796

97+
#[derive(Clone, HasSchema, Default, Deref, DerefMut)]
98+
#[repr(C)]
99+
pub struct ElementSolid(pub Entity);
100+
88101
#[derive(Clone, HasSchema)]
89102
#[schema(no_default)]
90103
pub struct ElementKillCallback {

src/core/physics.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use crate::prelude::*;
44

55
pub use collisions::{
6-
Actor, Collider, ColliderShape, CollisionWorld, RapierContext, RapierUserData,
6+
Actor, Collider, ColliderShape, CollisionWorld, RapierContext, RapierUserData, Solid,
77
TileCollisionKind,
88
};
99

src/core/physics/collisions.rs

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,9 @@ impl_system_param! {
172172
actors: CompMut<'a, Actor>,
173173
/// Solids are things like walls and platforms, that aren't tiles, that have solid
174174
/// collisions.
175-
///
176-
/// > **⚠️ Warning:** Solids have not been fully implemented yet and may not work. They were
177-
/// > from the old physics system and haven't been fully ported.
178175
solids: CompMut<'a, Solid>,
179176
/// A collider is anything that can detect collisions in the world other than tiles, and
180-
/// must either be an [`Actor`] or `Solid`] to participate in collision detection.
177+
/// must either be an [`Actor`] or [`Solid`] to participate in collision detection.
181178
colliders: CompMut<'a, Collider>,
182179
/// Contains the rapier collider handles for each map tile.
183180
tile_rapier_handles: CompMut<'a, TileRapierHandle>,
@@ -196,11 +193,17 @@ pub struct Actor;
196193
/// A solid in the physics simulation.
197194
#[derive(Default, Clone, Copy, Debug, HasSchema)]
198195
#[repr(C)]
199-
pub struct Solid;
196+
pub struct Solid {
197+
pub disabled: bool,
198+
pub pos: Vec2,
199+
pub size: Vec2,
200+
#[schema(opaque)]
201+
pub rapier_handle: Option<rapier::RigidBodyHandle>,
202+
}
200203

201204
/// A collider body in the physics simulation.
202205
///
203-
/// This is used for actors and solids in the simulation, not for tiles.
206+
/// This is only used for actors in the simulation, not for tiles or solids.
204207
#[derive(Default, Clone, Debug, HasSchema)]
205208
#[repr(C)]
206209
pub struct Collider {
@@ -398,6 +401,36 @@ impl<'a> CollisionWorld<'a> {
398401
rapier_collider.set_enabled(!collider.disabled);
399402
rapier_collider.set_position_wrt_parent(rapier::Isometry::new(default(), 0.0));
400403
}
404+
405+
for (solid_ent, solid) in self.entities.iter_with(&mut self.solids) {
406+
let bones_shape = ColliderShape::Rectangle { size: solid.size };
407+
let shared_shape = collider_shape_cache.shared_shape(bones_shape);
408+
409+
// Get or create a collider for the solid
410+
let handle = solid.rapier_handle.get_or_insert_with(|| {
411+
let body_handle = rigid_body_set.insert(
412+
rapier::RigidBodyBuilder::fixed().user_data(RapierUserData::from(solid_ent)),
413+
);
414+
collider_set.insert_with_parent(
415+
rapier::ColliderBuilder::new(shared_shape.clone())
416+
.active_events(rapier::ActiveEvents::COLLISION_EVENTS)
417+
.active_collision_types(rapier::ActiveCollisionTypes::all())
418+
.user_data(RapierUserData::from(solid_ent)),
419+
body_handle,
420+
rigid_body_set,
421+
);
422+
body_handle
423+
});
424+
let solid_body = rigid_body_set.get_mut(*handle).unwrap();
425+
426+
// Update the solid position
427+
solid_body.set_translation(rapier::Vector::new(solid.pos.x, solid.pos.y), false);
428+
429+
let rapier_collider = collider_set.get_mut(solid_body.colliders()[0]).unwrap();
430+
rapier_collider.set_enabled(!solid.disabled);
431+
rapier_collider.set_position_wrt_parent(rapier::Isometry::new(default(), 0.0));
432+
rapier_collider.set_shape(shared_shape.clone());
433+
}
401434
}
402435

403436
/// Update all of the map tile collisions.
@@ -430,11 +463,9 @@ impl<'a> CollisionWorld<'a> {
430463
.entities
431464
.iter_with((&self.tile_layers, &self.spawned_map_layer_metas))
432465
{
433-
let bones_shape = ColliderShape::Rectangle {
466+
let tile_shared_shape = collider_shape_cache.shared_shape(ColliderShape::Rectangle {
434467
size: layer.tile_size,
435-
};
436-
let shared_shape = collider_shape_cache.shared_shape(bones_shape);
437-
468+
});
438469
for x in 0..layer.grid_size.x {
439470
for y in 0..layer.grid_size.y {
440471
let pos = uvec2(x, y);
@@ -459,7 +490,7 @@ impl<'a> CollisionWorld<'a> {
459490
.user_data(RapierUserData::from(tile_ent)),
460491
);
461492
collider_set.insert_with_parent(
462-
rapier::ColliderBuilder::new(shared_shape.clone())
493+
rapier::ColliderBuilder::new(tile_shared_shape.clone())
463494
.active_events(rapier::ActiveEvents::COLLISION_EVENTS)
464495
.active_collision_types(rapier::ActiveCollisionTypes::all())
465496
.user_data(RapierUserData::from(tile_ent)),
@@ -594,6 +625,11 @@ impl<'a> CollisionWorld<'a> {
594625
rapier::QueryFilter::new().predicate(&|_handle, rapier_collider| {
595626
let ent = RapierUserData::entity(rapier_collider.user_data);
596627

628+
if self.solids.contains(ent) {
629+
// Include all solid collisions
630+
return true;
631+
}
632+
597633
let Some(tile_kind) = self.tile_collision_kinds.get(ent) else {
598634
// Ignore non-tile collisions
599635
return false;
@@ -615,6 +651,10 @@ impl<'a> CollisionWorld<'a> {
615651
// Subtract from the remaining attempted movement
616652
dy -= diff;
617653

654+
if self.solids.contains(ent) {
655+
break true;
656+
}
657+
618658
let tile_kind = *self.tile_collision_kinds.get(ent).unwrap();
619659

620660
// collider wants to go down and collided with jumpthrough tile
@@ -725,6 +765,11 @@ impl<'a> CollisionWorld<'a> {
725765
rapier::QueryFilter::new().predicate(&|_handle, rapier_collider| {
726766
let ent = RapierUserData::entity(rapier_collider.user_data);
727767

768+
if self.solids.contains(ent) {
769+
// Include all solid collisions
770+
return true;
771+
}
772+
728773
let Some(tile_kind) = self.tile_collision_kinds.get(ent) else {
729774
// Ignore non-tile collisions
730775
return false;
@@ -747,6 +792,10 @@ impl<'a> CollisionWorld<'a> {
747792
// Subtract from the remaining attempted movement
748793
dx -= diff;
749794

795+
if self.solids.contains(ent) {
796+
break true;
797+
}
798+
750799
let tile_kind = *self.tile_collision_kinds.get(ent).unwrap();
751800

752801
// If we ran into a jump-through tile, go through it and continue casting
@@ -810,7 +859,21 @@ impl<'a> CollisionWorld<'a> {
810859
/// > perfectly lined up along the edge of a tile, but `tile_collision_point` won't.
811860
#[allow(unused)]
812861
pub fn solid_at(&self, pos: Vec2) -> bool {
813-
self.tile_collision_point(pos) == TileCollisionKind::Solid
862+
self.solid_collision_point(pos)
863+
|| self.tile_collision_point(pos) == TileCollisionKind::Solid
864+
}
865+
866+
pub fn solid_collision_point(&self, pos: Vec2) -> bool {
867+
for (_, (solid, collider)) in self.entities.iter_with((&self.solids, &self.colliders)) {
868+
let bbox = collider
869+
.shape
870+
.bounding_box(Transform::from_translation(solid.pos.extend(0.0)));
871+
if bbox.contains(pos) {
872+
return true;
873+
}
874+
}
875+
876+
false
814877
}
815878

816879
/// Returns the tile collision at the given point.
@@ -865,11 +928,17 @@ impl<'a> CollisionWorld<'a> {
865928
&*shape.shared_shape(),
866929
rapier::QueryFilter::new().predicate(&|_handle, collider| {
867930
let ent = RapierUserData::entity(collider.user_data);
868-
self.tile_collision_kinds.contains(ent) && filter(ent)
931+
(self.solids.contains(ent) || self.tile_collision_kinds.contains(ent))
932+
&& filter(ent)
869933
}),
870934
)
871935
.map(|x| RapierUserData::entity(self.ctx.collider_set.get(x).unwrap().user_data))
872-
.and_then(|e| self.tile_collision_kinds.get(e).copied())
936+
.and_then(|ent| {
937+
if self.solids.contains(ent) {
938+
return Some(TileCollisionKind::Solid);
939+
}
940+
self.tile_collision_kinds.get(ent).copied()
941+
})
873942
.unwrap_or_default()
874943
}
875944

src/core/physics/collisions/shape.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ impl ColliderShape {
3838
}
3939
}
4040

41+
/// Get the shape's axis-aligned-bounding-box ( AABB ).
42+
///
43+
/// An AABB is the smallest non-rotated rectangle that completely contains the the collision
44+
/// shape.
45+
///
46+
/// By passing in the shape's global transform you will get the world-space bounding box.
47+
pub fn bounding_box(self, transform: Transform) -> Rect {
48+
let aabb = self.compute_aabb(transform);
49+
50+
Rect {
51+
min: vec2(aabb.mins.x, aabb.mins.y),
52+
max: vec2(aabb.maxs.x, aabb.maxs.y),
53+
}
54+
}
55+
4156
pub fn shared_shape(&self) -> rapier::SharedShape {
4257
match self {
4358
ColliderShape::Circle { diameter } => rapier::SharedShape::ball(*diameter / 2.0),

0 commit comments

Comments
 (0)