Skip to content

Rapier physics: per-entity body kind + material + collision filtering + sensor hooks #120

@martinjms

Description

@martinjms

Quick Summary

  • 🟡 Status: ready to start, not blocked. Builds on #117 / #118. No external epic dependencies — all work is internal to arcane-infra::rapier_cluster.
  • Add per-entity spawn-time customization hooks to RapierClusterSimulation: body_kind_for, material_for, collision_groups_for, is_sensor. Currently every spawned entity is a Dynamic body with default friction / restitution / density and no filtering.
  • Closes the gap between "Rapier integration exists" and "Rapier integration can model the entities a real game has" — without these hooks, every entity is identical at the physics level.
  • Aligns with the unified-entity model documented in entity-model.md: structures = entities with body_kind = Fixed, players = Dynamic, kinematic platforms = KinematicPositionBased, etc.
  • ⚠️ Scope-split call-out: introducing the Fixed body-kind variant here does not solve the spatial-binding clustering question. Until the (unfiled) clustering-binding epic lands, Fixed entities still migrate by PGP affinity — physics-side they behave correctly (solver-skipped, only AABB tracked); clustering-side they're not yet pinned to chunk ownership. Do not bolt spatial-binding logic into this PR.

Why This Matters

The existing Rapier integration treats every entity as a default-tuned Dynamic body. For a real game, this means:

  • A wall built by a player would fall to the floor under gravity (it's Dynamic, not Fixed).
  • An ice surface and a rubber surface have the same friction / restitution.
  • A player projectile collides with the player who fired it (no collision filtering).
  • A "trigger zone" pickup volume physically pushes anything that enters it (no sensor mode).

These are not edge cases — they're the basic vocabulary of physics-based gameplay. Without the hooks, the integration is correct for tech demos and unusable for games.

Scope

  • In:
    • Trait method body_kind_for(&self, entry: &EntityStateEntry, config: &RapierConfig) -> RapierBodyKind with default Dynamic.
    • Trait method material_for(&self, entry: &EntityStateEntry, config: &RapierConfig) -> RapierMaterial with default zero-friction / zero-restitution / unit-density.
    • Trait method collision_groups_for(&self, entry: &EntityStateEntry, config: &RapierConfig) -> RapierCollisionGroups with default "everything collides with everything" — concretely RapierCollisionGroups { memberships: Group::ALL, filter: Group::ALL }, equivalent to Rapier's InteractionGroups::all().
    • Trait method is_sensor(&self, entry: &EntityStateEntry, config: &RapierConfig) -> bool with default false.
    • New public types: RapierBodyKind { Dynamic | KinematicPositionBased | KinematicVelocityBased | Fixed }, RapierMaterial { friction, restitution, density }, RapierCollisionGroups { memberships, filter }.
    • All hooks called exactly once per entity at first-sight spawn (same lifecycle contract as collider_for).
    • Tests demonstrating each hook's effect via direct Rapier-state inspection.
  • Out (separate issues):
    • In-tick imperative ops (impulses, raycasts) — separate issue (#121).
    • Cluster-binding-aware migration for Fixed entities — clustering-binding epic (not yet filed). This PR introduces the variant only; physics-side semantics work, clustering-side semantics will be wired by the binding epic.
    • Map / terrain loading — #119.

Action Items

  • Define RapierBodyKind, RapierMaterial, RapierCollisionGroups public types (with #[non_exhaustive]).
  • Add body_kind_for hook with default impl.
  • Add material_for hook with default impl.
  • Add collision_groups_for hook with default impl returning RapierCollisionGroups { memberships: Group::ALL, filter: Group::ALL }.
  • Add is_sensor hook with default impl.
  • Update RapierState::spawn to consult all four hooks at first-sight.
  • Tests:
    • Fixed entity stays put under gravity (verify body kind honored). Test must set non-default gravity, e.g. RapierConfig { gravity: [0.0, -9.81, 0.0], ..Default::default() } — the crate's default gravity is [0,0,0] for benchmark parity.
    • KinematicPositionBased entity ignores forces; user-set position takes effect.
    • High-friction material reduces sliding distance vs zero-friction.
    • Restitution = 1.0 produces near-elastic bounce; restitution = 0 produces near-inelastic. Also requires non-zero gravity in the test config (see Fixed-under-gravity note above).
    • Density change affects mass-derived collision response.
    • Collision groups: two entities in non-overlapping groups don't generate contacts.
    • Sensor collider: contact event still fires, no physical pushback.
    • All hooks called exactly once per entity (same call-count invariant as collider_for).
  • Module docs updated; # Example extended. Add a short note that per entity-model.md §5, users may organize their RapierClusterSimulation either as a property-value-style impl that matches on entry.user_data to return the right hook values, or as a subclass-style set of impls dispatched by the user's own per-entity routing. The hook signatures support both — both should be documented.
  • Re-export new public types from arcane_infra crate root (lib.rs — append to the existing rapier_cluster::{...} re-export block).
  • Clippy clean both feature configurations.

Reference

  • Parent EPIC: #8 — Cluster physics backends
  • Builds on: #117 (minimum integration), #118 (contact events + colliders)
  • Architecture doc: docs/architecture/entity-model.md — body kinds (§4), subclass vs property-value polymorphism (§5), affinity-bound vs spatial-bound binding (§6)
  • Downstream: brainy-bots/arcane-demos#6 (clustering visualization demo) needs Fixed for items, sensor for trigger volumes, collision groups for projectiles
  • Unblocks: real-game-shape entities (walls as Fixed, projectiles as Dynamic with collision filtering, trigger zones as sensors)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions