1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
//! Physics and collision detection.
use crate::prelude::*;
pub use collisions::{
Actor, Collider, ColliderShape, CollisionWorld, PhysicsParams, RapierContext, RapierUserData,
Solid, TileCollisionKind,
};
use super::utils::Rect;
pub use rapier2d::prelude as rapier;
pub mod collisions;
pub mod dynamic_body;
pub use dynamic_body::*;
/// For now kinematic mode is globally position based.
pub static KINEMATIC_MODE: rapier::RigidBodyType = rapier::RigidBodyType::KinematicPositionBased;
#[derive(Debug, Clone, Copy)]
enum PhysicsStage {
Update,
}
impl StageLabel for PhysicsStage {
fn name(&self) -> String {
format!("{self:?}")
}
fn id(&self) -> Ulid {
match self {
PhysicsStage::Update => Ulid(2026032502318358068964002697585621138),
}
}
}
pub fn install(session: &mut Session) {
KinematicBody::register_schema();
ColliderShape::register_schema();
session
.stages
// TODO: Think again about exactly how to organize the physics sync systems. At the time of
// writing, we have to do an extra `collision_world.step()` when debug rendering to make
// sure everything is synced. It'd be good to avoid this and maybe take inspiration from
// bevy_rapier.
.insert_stage_after(
CoreStage::PostUpdate,
SimpleSystemStage::new(PhysicsStage::Update),
)
.add_system_to_stage(PhysicsStage::Update, hydrate_physics_bodies)
.add_system_to_stage(PhysicsStage::Update, update_kinematic_bodies);
}
/// A kinematic physics body
///
/// Used primarily for players and things that need to walk around, detect what kind of platform
/// they are standing on, etc.
///
/// For now, all kinematic bodies have axis-aligned, rectangular colliders. This may or may not
/// change in the future.
#[derive(Default, Debug, Clone, Copy, HasSchema)]
#[repr(C)]
pub struct KinematicBody {
pub velocity: Vec2,
pub shape: ColliderShape,
/// Angular velocity in degrees per second
pub angular_velocity: f32,
pub gravity: f32,
pub bounciness: f32,
/// Sets a 1 frame override for the body friction. It will be re-set to `None` every frame so if
/// you wish to apply a continuous friction change, you must re-set it every frame.
///
/// This is useful for things like slippery blocks or other things that want to modify a body's
/// friction while it is on the block.
#[schema(opaque)]
pub frame_friction_override: Option<f32>,
pub is_on_ground: bool,
pub was_on_ground: bool,
/// Will be `true` if the body is currently on top of a platform/jumpthrough tile
pub is_on_platform: bool,
/// If this is `true` the body will be affected by gravity
pub has_mass: bool,
pub has_friction: bool,
pub can_rotate: bool,
/// Whether or not physics has been disabled for this body.
pub is_deactivated: bool,
/// Whether or not the body should fall through jump_through platforms
pub fall_through: bool,
/// Indicates that we should reset the collider like it was just added to the world.
///
/// This is important to make sure that it falls through JumpThrough platforms if it happens to
/// spawn inside of one.
pub is_spawning: bool,
/// If body is controlled by player or some system simulating input.
/// Allows us to make safe optimizations of non-controlled kinematics that have not moved.
pub is_controlled: bool,
/// Position cached from last kinematic body update, used to determine if object is "sleeping"
/// (is not moving) to avoid collision detection / resolution against static objects.
pub last_update_position: Vec2,
/// See comment for `last_update_position`, this tracks previous rotation to detect if object has moved.
pub last_update_rotation: f32,
}
impl KinematicBody {
/// Get the body'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 body's global transform you will get the world-space bounding box.
pub fn bounding_box(&self, transform: Transform) -> Rect {
let aabb = self.shape.compute_aabb(transform);
Rect {
min: vec2(aabb.mins.x, aabb.mins.y),
max: vec2(aabb.maxs.x, aabb.maxs.y),
}
}
}
/// Hydrate newly added [`KinematicBody`]s.
fn hydrate_physics_bodies(
entities: Res<Entities>,
bodies: Comp<KinematicBody>,
mut collision_world: CollisionWorld,
) {
let mut needs_hydrate = collision_world.colliders.bitset().clone();
needs_hydrate.bit_not();
needs_hydrate.bit_and(bodies.bitset());
for entity in entities.iter_with_bitset(&needs_hydrate) {
let body = bodies.get(entity).unwrap();
collision_world.colliders.insert(
entity,
Collider {
shape: body.shape,
..default()
},
);
collision_world.actors.insert(entity, Actor);
collision_world.handle_teleport(entity);
}
}
/// Update physics for kinematic bodies.
fn update_kinematic_bodies(
meta: Root<GameMeta>,
entities: Res<Entities>,
mut bodies: CompMut<KinematicBody>,
mut dynamic_bodies: CompMut<DynamicBody>,
mut collision_world: CollisionWorld,
mut transforms: CompMut<Transform>,
time: Res<Time>,
) {
puffin::profile_function!();
let time_factor = time.delta().as_secs_f32();
let global_gravity = meta.core.physics.gravity;
collision_world.update(
time_factor,
PhysicsParams {
gravity: global_gravity,
terminal_velocity: Some(meta.core.physics.terminal_velocity),
},
&mut transforms,
&mut dynamic_bodies,
);
for (entity, (body, dynamic_body)) in
entities.iter_with((&mut bodies, &mut OptionalMut(&mut dynamic_bodies)))
{
if body.is_deactivated {
collision_world.colliders.get_mut(entity).unwrap().disabled = true;
continue;
} else {
collision_world.colliders.get_mut(entity).unwrap().disabled = false;
}
if let Some(dynamic_body) = dynamic_body {
if dynamic_body.is_dynamic {
continue;
}
}
// has the body moved since last call to update_kinematic_bodies?
let has_moved = {
let transform = transforms.get(entity).copied().unwrap();
let rotation = transform.rotation.to_euler(EulerRot::XYZ).2;
body.last_update_position != transform.translation.xy()
|| body.last_update_rotation != rotation
|| body.is_spawning // Don't consider new objects
};
let transform = transforms.get_mut(entity).unwrap();
body.last_update_position = transform.translation.xy();
body.last_update_rotation = transform.rotation.to_euler(EulerRot::XYZ).2;
if body.has_mass && has_moved {
puffin::profile_scope!("Shove objects out of walls");
// Shove objects out of walls
loop {
let mut transform = transforms.get(entity).copied().unwrap();
if collision_world.tile_collision(transform, body.shape) != TileCollisionKind::Solid
{
break;
}
let rect = body.bounding_box(transform);
// We add a small border, because rapier will consider the collision box colliding
// if it is perfectly lined up along the edge of a tile, and `solid_at` won't.
let border = 0.1;
let collisions = (
collision_world.solid_at(vec2(rect.min.x - border, rect.max.y + border)), // Top left
collision_world.solid_at(vec2(rect.max.x + border, rect.max.y + border)), // Top right
collision_world.solid_at(vec2(rect.max.x + border, rect.min.y - border)), // Bottom right
collision_world.solid_at(vec2(rect.min.x - border, rect.min.y - border)), // Bottom left
);
match collisions {
// If we have no solid collisions at any corner.
(false, false, false, false) => {
// For some reason the `tile_collision` test did detect a collision, but
// `solid_at` did not detect a collision at any of the corners of the aabb.
warn!(
"Collision test error resulting in physics \
body stuck in wall at {rect:?}",
);
break;
}
// Check for collisions on each side of the rectangle
(false, false, _, _) => transform.translation.y += 1.0,
(_, false, false, _) => transform.translation.x += 1.0,
(_, _, false, false) => transform.translation.y -= 1.0,
(false, _, _, false) => transform.translation.x -= 1.0,
// If none of the sides of the rectangle are un-collided, then we don't know
// which direction to move to get out of the wall, and we just give up.
_ => {
break;
}
}
*transforms.get_mut(entity).unwrap() = transform;
}
}
// Sync body attributes with collider
{
let collider = collision_world.colliders.get_mut(entity).unwrap();
collider.shape = body.shape;
}
if body.is_spawning {
collision_world.handle_teleport(entity);
body.is_spawning = false;
}
if body.fall_through {
collision_world.descent(entity);
}
{
puffin::profile_scope!("move body");
if collision_world.move_vertical(&mut transforms, entity, body.velocity.y * time_factor)
{
body.velocity.y *= -body.bounciness;
}
// NOTE: It's important that we move horizontally after we move vertically, or else the
// horizontal movement will clear our `descent` and `seen_wood` flags and we may not go
// through drop through platforms while moving horizontally.
if collision_world.move_horizontal(
&mut transforms,
entity,
body.velocity.x * time_factor,
) {
body.velocity.x *= -body.bounciness;
}
}
// Check ground collision
{
let mut transform = transforms.get(entity).copied().unwrap();
// If not moving, this collision test should give the same result, and will not change the value of fall_through.
// for controlled bodies, fall_through may be modified based on inputs, and we may actually want this to be set again here,
// so only skip if not moving for bodies that are not controlled.
if has_moved || !body.is_controlled {
puffin::profile_scope!("fall through check");
// Don't get stuck floating in fall-through platforms
if body.velocity == Vec2::ZERO
&& collision_world.tile_collision_filtered(transform, body.shape, |ent| {
collision_world
.tile_collision_kinds
.get(ent)
.map(|x| *x == TileCollisionKind::JumpThrough)
.unwrap_or(false)
}) == TileCollisionKind::JumpThrough
{
body.fall_through = true;
}
}
// Move transform check down 1 slightly
transform.translation.y -= 0.1;
body.was_on_ground = body.is_on_ground;
let collider = collision_world.get_collider(entity);
let tile = collision_world.tile_collision_filtered(transform, body.shape, |ent| {
if collider.seen_wood {
collision_world
.tile_collision_kinds
.get(ent)
.map(|x| *x != TileCollisionKind::JumpThrough)
.unwrap_or(false)
} else {
true
}
});
let on_jump_through_tile = tile == TileCollisionKind::JumpThrough;
body.is_on_ground =
tile != TileCollisionKind::Empty && !(on_jump_through_tile && body.fall_through);
body.is_on_platform = body.is_on_ground && on_jump_through_tile;
}
if body.is_on_ground {
if body.has_friction {
body.velocity.x *= if let Some(friction) = body.frame_friction_override {
friction
} else {
meta.core.physics.friction_lerp
};
body.frame_friction_override = None;
if body.velocity.x.abs() <= meta.core.physics.stop_threshold {
body.velocity.x = 0.0;
}
body.velocity.y *= meta.core.physics.friction_lerp;
}
if body.velocity.y <= body.gravity * time_factor {
body.velocity.y = 0.0;
}
}
if !body.is_on_ground && body.has_mass {
body.velocity.y -= body.gravity * time_factor;
if body.velocity.y < -meta.core.physics.terminal_velocity {
body.velocity.y = -meta.core.physics.terminal_velocity;
}
}
if body.can_rotate {
apply_rotation(
time_factor,
transforms.get_mut(entity).unwrap(),
body.velocity,
body.angular_velocity,
body.is_on_ground,
body.shape,
);
}
}
}
/// Helper function to apply rotation to a kinematic body.
fn apply_rotation(
delta_time: f32,
transform: &mut Transform,
velocity: Vec2,
angular_velocity: f32,
is_on_ground: bool,
collider_shape: ColliderShape,
) {
puffin::profile_function!();
let mut angle = transform.rotation.to_euler(EulerRot::XYZ).2;
if is_on_ground {
if matches!(collider_shape, ColliderShape::Circle { .. }) {
angle += velocity.x.abs() * angular_velocity;
}
} else {
angle += (angular_velocity * delta_time).to_radians();
}
transform.rotation = Quat::from_rotation_z(angle);
}