Skip to content

Commit

Permalink
feat: non-tile solids (#903)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nelson137 authored Jan 25, 2024
1 parent 3e99ea2 commit 461b21a
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 15 deletions.
13 changes: 13 additions & 0 deletions src/core/elements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ pub struct ElementMeta {
pub plugin: Handle<LuaPlugin>,
}

#[derive(HasSchema, Default, Debug, Clone, Copy)]
#[type_data(metadata_asset("solid"))]
#[repr(C)]
pub struct ElementSolidMeta {
pub disabled: bool,
pub offset: Vec2,
pub size: Vec2,
}

#[derive(HasSchema, Deserialize, Clone, Debug)]
#[repr(C)]
pub struct ElementEditorMeta {
Expand Down Expand Up @@ -85,6 +94,10 @@ pub struct DehydrateOutOfBounds(pub Entity);
#[repr(C)]
pub struct ElementHandle(pub Handle<ElementMeta>);

#[derive(Clone, HasSchema, Default, Deref, DerefMut)]
#[repr(C)]
pub struct ElementSolid(pub Entity);

#[derive(Clone, HasSchema)]
#[schema(no_default)]
pub struct ElementKillCallback {
Expand Down
2 changes: 1 addition & 1 deletion src/core/physics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use crate::prelude::*;

pub use collisions::{
Actor, Collider, ColliderShape, CollisionWorld, RapierContext, RapierUserData,
Actor, Collider, ColliderShape, CollisionWorld, RapierContext, RapierUserData, Solid,
TileCollisionKind,
};

Expand Down
97 changes: 83 additions & 14 deletions src/core/physics/collisions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,9 @@ impl_system_param! {
actors: CompMut<'a, Actor>,
/// Solids are things like walls and platforms, that aren't tiles, that have solid
/// collisions.
///
/// > **⚠️ Warning:** Solids have not been fully implemented yet and may not work. They were
/// > from the old physics system and haven't been fully ported.
solids: CompMut<'a, Solid>,
/// A collider is anything that can detect collisions in the world other than tiles, and
/// must either be an [`Actor`] or `Solid`] to participate in collision detection.
/// must either be an [`Actor`] or [`Solid`] to participate in collision detection.
colliders: CompMut<'a, Collider>,
/// Contains the rapier collider handles for each map tile.
tile_rapier_handles: CompMut<'a, TileRapierHandle>,
Expand All @@ -196,11 +193,17 @@ pub struct Actor;
/// A solid in the physics simulation.
#[derive(Default, Clone, Copy, Debug, HasSchema)]
#[repr(C)]
pub struct Solid;
pub struct Solid {
pub disabled: bool,
pub pos: Vec2,
pub size: Vec2,
#[schema(opaque)]
pub rapier_handle: Option<rapier::RigidBodyHandle>,
}

/// A collider body in the physics simulation.
///
/// This is used for actors and solids in the simulation, not for tiles.
/// This is only used for actors in the simulation, not for tiles or solids.
#[derive(Default, Clone, Debug, HasSchema)]
#[repr(C)]
pub struct Collider {
Expand Down Expand Up @@ -398,6 +401,36 @@ impl<'a> CollisionWorld<'a> {
rapier_collider.set_enabled(!collider.disabled);
rapier_collider.set_position_wrt_parent(rapier::Isometry::new(default(), 0.0));
}

for (solid_ent, solid) in self.entities.iter_with(&mut self.solids) {
let bones_shape = ColliderShape::Rectangle { size: solid.size };
let shared_shape = collider_shape_cache.shared_shape(bones_shape);

// Get or create a collider for the solid
let handle = solid.rapier_handle.get_or_insert_with(|| {
let body_handle = rigid_body_set.insert(
rapier::RigidBodyBuilder::fixed().user_data(RapierUserData::from(solid_ent)),
);
collider_set.insert_with_parent(
rapier::ColliderBuilder::new(shared_shape.clone())
.active_events(rapier::ActiveEvents::COLLISION_EVENTS)
.active_collision_types(rapier::ActiveCollisionTypes::all())
.user_data(RapierUserData::from(solid_ent)),
body_handle,
rigid_body_set,
);
body_handle
});
let solid_body = rigid_body_set.get_mut(*handle).unwrap();

// Update the solid position
solid_body.set_translation(rapier::Vector::new(solid.pos.x, solid.pos.y), false);

let rapier_collider = collider_set.get_mut(solid_body.colliders()[0]).unwrap();
rapier_collider.set_enabled(!solid.disabled);
rapier_collider.set_position_wrt_parent(rapier::Isometry::new(default(), 0.0));
rapier_collider.set_shape(shared_shape.clone());
}
}

/// Update all of the map tile collisions.
Expand Down Expand Up @@ -430,11 +463,9 @@ impl<'a> CollisionWorld<'a> {
.entities
.iter_with((&self.tile_layers, &self.spawned_map_layer_metas))
{
let bones_shape = ColliderShape::Rectangle {
let tile_shared_shape = collider_shape_cache.shared_shape(ColliderShape::Rectangle {
size: layer.tile_size,
};
let shared_shape = collider_shape_cache.shared_shape(bones_shape);

});
for x in 0..layer.grid_size.x {
for y in 0..layer.grid_size.y {
let pos = uvec2(x, y);
Expand All @@ -459,7 +490,7 @@ impl<'a> CollisionWorld<'a> {
.user_data(RapierUserData::from(tile_ent)),
);
collider_set.insert_with_parent(
rapier::ColliderBuilder::new(shared_shape.clone())
rapier::ColliderBuilder::new(tile_shared_shape.clone())
.active_events(rapier::ActiveEvents::COLLISION_EVENTS)
.active_collision_types(rapier::ActiveCollisionTypes::all())
.user_data(RapierUserData::from(tile_ent)),
Expand Down Expand Up @@ -594,6 +625,11 @@ impl<'a> CollisionWorld<'a> {
rapier::QueryFilter::new().predicate(&|_handle, rapier_collider| {
let ent = RapierUserData::entity(rapier_collider.user_data);

if self.solids.contains(ent) {
// Include all solid collisions
return true;
}

let Some(tile_kind) = self.tile_collision_kinds.get(ent) else {
// Ignore non-tile collisions
return false;
Expand All @@ -615,6 +651,10 @@ impl<'a> CollisionWorld<'a> {
// Subtract from the remaining attempted movement
dy -= diff;

if self.solids.contains(ent) {
break true;
}

let tile_kind = *self.tile_collision_kinds.get(ent).unwrap();

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

if self.solids.contains(ent) {
// Include all solid collisions
return true;
}

let Some(tile_kind) = self.tile_collision_kinds.get(ent) else {
// Ignore non-tile collisions
return false;
Expand All @@ -747,6 +792,10 @@ impl<'a> CollisionWorld<'a> {
// Subtract from the remaining attempted movement
dx -= diff;

if self.solids.contains(ent) {
break true;
}

let tile_kind = *self.tile_collision_kinds.get(ent).unwrap();

// If we ran into a jump-through tile, go through it and continue casting
Expand Down Expand Up @@ -810,7 +859,21 @@ impl<'a> CollisionWorld<'a> {
/// > perfectly lined up along the edge of a tile, but `tile_collision_point` won't.
#[allow(unused)]
pub fn solid_at(&self, pos: Vec2) -> bool {
self.tile_collision_point(pos) == TileCollisionKind::Solid
self.solid_collision_point(pos)
|| self.tile_collision_point(pos) == TileCollisionKind::Solid
}

pub fn solid_collision_point(&self, pos: Vec2) -> bool {
for (_, (solid, collider)) in self.entities.iter_with((&self.solids, &self.colliders)) {
let bbox = collider
.shape
.bounding_box(Transform::from_translation(solid.pos.extend(0.0)));
if bbox.contains(pos) {
return true;
}
}

false
}

/// Returns the tile collision at the given point.
Expand Down Expand Up @@ -865,11 +928,17 @@ impl<'a> CollisionWorld<'a> {
&*shape.shared_shape(),
rapier::QueryFilter::new().predicate(&|_handle, collider| {
let ent = RapierUserData::entity(collider.user_data);
self.tile_collision_kinds.contains(ent) && filter(ent)
(self.solids.contains(ent) || self.tile_collision_kinds.contains(ent))
&& filter(ent)
}),
)
.map(|x| RapierUserData::entity(self.ctx.collider_set.get(x).unwrap().user_data))
.and_then(|e| self.tile_collision_kinds.get(e).copied())
.and_then(|ent| {
if self.solids.contains(ent) {
return Some(TileCollisionKind::Solid);
}
self.tile_collision_kinds.get(ent).copied()
})
.unwrap_or_default()
}

Expand Down
15 changes: 15 additions & 0 deletions src/core/physics/collisions/shape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ impl ColliderShape {
}
}

/// Get the shape's axis-aligned-bounding-box ( AABB ).
///
/// An AABB is the smallest non-rotated rectangle that completely contains the the collision
/// shape.
///
/// By passing in the shape's global transform you will get the world-space bounding box.
pub fn bounding_box(self, transform: Transform) -> Rect {
let aabb = self.compute_aabb(transform);

Rect {
min: vec2(aabb.mins.x, aabb.mins.y),
max: vec2(aabb.maxs.x, aabb.maxs.y),
}
}

pub fn shared_shape(&self) -> rapier::SharedShape {
match self {
ColliderShape::Circle { diameter } => rapier::SharedShape::ball(*diameter / 2.0),
Expand Down

0 comments on commit 461b21a

Please sign in to comment.