Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Physics Overview

Live Demo: Physics

Physics in nightshade is Rapier3D wrapped in ECS components. The engine owns the RigidBodySet, ColliderSet, and ImpulseJointSet on a single PhysicsResources struct, and runs the simulation at a fixed timestep on the frame schedule. Components on entities (RigidBodyComponent, ColliderComponent, CharacterControllerComponent) hold the parameters and the Rapier handles. Transforms sync into Rapier before the step and back out after.

Enabling Physics

The physics feature flag pulls in Rapier3D and the components that drive it.

[dependencies]
nightshade = { git = "...", features = ["engine", "physics"] }

Physics World

The physics world is a resource on world.resources.physics. Configuration sits on the same struct.

#![allow(unused)]
fn main() {
let physics = &mut world.resources.physics;

physics.gravity = Vec3::new(0.0, -9.81, 0.0);
physics.fixed_timestep = 1.0 / 60.0;
}

Gravity is a vector, not a magnitude, so a top-down game can set it to zero and a side-scroller can rotate it. The fixed timestep is the rate the simulation actually steps at. 60 Hz is the default and what most demos assume.

Core Concepts

Rigid Bodies

A rigid body is the dynamic state Rapier integrates each step. There are three kinds.

  • Dynamic. Mass and inertia tensor, integrated under gravity and applied forces.
  • Kinematic. Position is set by code each frame, dynamic bodies see it as an immovable wall that happens to move.
  • Fixed. Infinite mass, never moves. Floors, walls, terrain.

Colliders

A collider is the shape Rapier uses for contact and intersection tests. Shapes available are Ball, Cuboid, Capsule, Cylinder, Cone, TriMesh, HeightField, and compound combinations.

Character Controllers

A character controller is a kinematic body with a small state machine on top that resolves contacts itself instead of letting the constraint solver do it. The result is movement that respects walls but does not get pushed around by stacked boxes.

Quick Start

#![allow(unused)]
fn main() {
use nightshade::ecs::physics::*;

fn initialize(&mut self, world: &mut World) {
    spawn_cube_at(world, Vec3::new(0.0, -0.5, 0.0));

    for index in 0..10 {
        spawn_cube_at(world, Vec3::new(0.0, 2.0 + index as f32 * 1.5, 0.0));
    }
}
}

The floor is one fixed cube. The stack is ten dynamic cubes spawned at increasing height. The physics schedule does the rest.

Physics Synchronization

The simulation runs at the fixed timestep, the renderer runs at the display rate, and the two are not the same. Each frame works like this:

  1. Game logic mutates entity transforms.
  2. The physics step runs zero or more times depending on how much real time has passed since the last step.
  3. Rapier writes the new positions and orientations back into the entity transforms.
  4. Interpolation between the previous and current physics positions smooths the visual.

The interpolation step is what hides the discrete simulation rate from the eye. Without it, fast-moving bodies appear to stutter at 60 Hz regardless of the frame rate.

Querying Physics

Picking (Raycasting)

Picking casts a ray from the cursor pixel into the world and returns the closest hit.

#![allow(unused)]
fn main() {
let (width, height) = world.resources.window.cached_viewport_size.unwrap_or((800, 600));
let screen_center = Vec2::new(width as f32 / 2.0, height as f32 / 2.0);

if let Some(hit) = pick_closest_entity(world, screen_center) {
    let hit_entity = hit.entity;
    let hit_distance = hit.distance;
    let hit_position = hit.world_position;
}
}

pick_closest_entity hits the bounding volume of each candidate. That is fast enough to run every frame for hover and selection, and inaccurate enough that the hit point can be off the actual surface by the radius of the bounding sphere.

For per-triangle accuracy, use the trimesh variant.

#![allow(unused)]
fn main() {
if let Some(hit) = pick_closest_entity_trimesh(world, screen_center) {
    let hit_entity = hit.entity;
    let hit_position = hit.world_position;
}
}

The trimesh path is slower per pick but lands on the actual mesh. Use it when the hit point matters, like for decal placement or precise interaction.

Physics Materials

Friction, restitution, and density live directly on ColliderComponent. There is no separate material asset.

#![allow(unused)]
fn main() {
world.core.set_collider(entity, ColliderComponent::new_cuboid(0.5, 0.5, 0.5)
    .with_friction(0.5)
    .with_restitution(0.3)
    .with_density(1.0));
}

Friction controls sliding resistance, restitution controls bounciness, density combines with shape volume to give the body its mass.

Debug Visualization

The debug draw flag dumps collider shapes, contact points, and collision normals as line geometry.

#![allow(unused)]
fn main() {
world.resources.physics.debug_draw = true;

physics_debug_draw_system(world);
}

Useful while building levels to see where collision actually is versus where the visual mesh sits. The two diverge more than you would expect.

Performance Tips

Five things move the needle, roughly in this order.

  1. Use primitive shapes (boxes, spheres, capsules) wherever possible. Trimesh collision is an order of magnitude slower per pair.
  2. Filter collision groups so unrelated objects do not even broadphase against each other.
  3. Combine shapes into compound colliders on one body instead of parenting many small bodies.
  4. Let idle bodies sleep. Rapier does this automatically when linear and angular velocity stay below a threshold for long enough.
  5. Keep the fixed timestep at 60 Hz unless something specifically needs more.

Joints

Joints constrain the relative motion of two bodies. Doors are revolute joints, drawers are prismatic, ropes are rope joints, chains are spherical. See Physics Joints for the full set.