feat(rapier-cluster): per-entity body kind / material / filtering / sensor hooks (#120)#125
Merged
martinjms merged 1 commit intofeat/rapier-clusterfrom May 4, 2026
Merged
Conversation
…ensor hooks (#120) Add four spawn-time customization hooks to RapierClusterSimulation: body_kind_for, material_for, collision_groups_for, is_sensor. Closes the gap between "Rapier integration exists" and "Rapier integration can model the entities a real game has." New public types (all #[non_exhaustive]): - RapierBodyKind { Dynamic | KinematicPositionBased | KinematicVelocityBased | Fixed } - RapierMaterial { friction, restitution, density } + ::new() constructor - RapierCollisionGroups { memberships, filter } + ::new() constructor Re-exports rapier3d::geometry::Group from arcane_infra root so users can construct collision groups without depending on rapier3d directly. All five hooks (these four plus the existing collider_for) are called exactly once per entity at first-sight spawn; subsequent return-value changes are ignored for already-spawned bodies. Scope-split: introducing the Fixed body-kind variant only changes physics-side behavior (solver-skipped, AABB-tracked). Until the clustering-binding epic lands, Fixed entities still migrate by PGP affinity — they are not yet pinned to chunk ownership. Tests (8 new): - fixed_body_does_not_move_under_gravity - kinematic_position_based_ignores_gravity - material_for_is_honored_on_collider (structural) - high_restitution_bounces_higher_than_zero_restitution (dynamic) - density_changes_body_mass - non_overlapping_collision_groups_filter_contacts - sensor_fires_event_without_pushback - all_hooks_called_exactly_once_per_entity Verification: build clean, 46/46 rapier_cluster tests pass, doc examples compile, clippy clean both feature configurations, fmt clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Author
Self-review against #120 specArchitecture fit ✅
Spec compliance ✅
Test coverage ✅
Minor observations (not blocking):
CI: ✅ Verdict: clean implementation, fully spec-compliant. Approving and squash-merging into |
This was referenced May 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Quick Summary
RapierClusterSimulation:body_kind_for,material_for,collision_groups_for,is_sensor.Dynamicbody.RapierBodyKind,RapierMaterial,RapierCollisionGroups. Re-exportsrapier3d::geometry::Groupso users don't need a direct rapier3d dep.Change Type
Impact
RapierClusterSimulationcan now declare per-entity body kind (Dynamic/Kinematic*/Fixed), material (friction/restitution/density), collision filtering (memberships/filter bitsets), and sensor flag. Default trait methods preserve the current "everything is Dynamic with default material" behavior — no breaking change for existing impls.rapier_cluster; no hot-path or replication changes. Five spawn-time hooks behind default methods with unchanged invocation lifecycle.Verification
cargo build -p arcane-infraand--features rapier-cluster)cargo test -p arcane-infra --features rapier-cluster --lib— 76 passing including 8 new Rapier physics: per-entity body kind + material + collision filtering + sensor hooks #120 tests)cargo test --doc -p arcane-infra --features rapier-cluster)cargo clippy --all-targets -- -D warnings)cargo fmt --all -- --check)Decisions made
Bundle 5 spawn params into internal
SpawnParamsstruct. Considered: 8-argspawn()+#[allow(clippy::too_many_arguments)].Reason: Cleaner call site, gives one place to extend for future hooks (CCD toggle, etc.); clippy-clean without lint suppression.
pub const fn new(...)constructors onRapierMaterialandRapierCollisionGroups. Considered: builder pattern; struct-update with..Default::default().Reason:
#[non_exhaustive]blocks struct-literal construction outside the defining crate (E0639).newis the simplest external constructor and staysconst. Doc example wouldn't compile without this.Re-export
rapier3d::geometry::Group. Considered: leave it; users add rapier3d as a direct dep.Reason: Constructing
RapierCollisionGroupsrequiresGroup::GROUP_1etc.; re-exporting keeps the rapier3d version pinning underarcane-infra's control and matches the existing pattern of hiding the rapier3d types behind crate-root re-exports.RapierBodyKindderivesDefault. Considered: manual impl matching the issue spec wording.Reason: clippy
derivable-implsflagged the manual version; the derive form (with#[default]onDynamic) is idiomatic and shorter.Restitution test tracks peak position only after tick 20. Considered: track peak across all ticks (initial draft).
Reason: Pre-impact peak is just the drop position (
y=3), so both elastic and inelastic runs returned ≈3 and the test was a tautology. Skipping the pre-impact window measures the actual rebound.Single parameterized
HookSimtest fixture vs N narrow ones. Considered: per-test fixtures matching the existingContactRecorderpattern.Reason: The 8 Rapier physics: per-entity body kind + material + collision filtering + sensor hooks #120 tests need heterogeneous per-entity configs (collision groups, sensor, body kind) plus per-(hook, entity) call-counting for the once-per-entity invariant. One fixture with
EntitySpecoverrides covers all of them; N narrow fixtures duplicate machinery.Test gravity setup pattern. Considered: setting gravity globally for the test module.
Reason: Most existing tests rely on the default zero-gravity for benchmark parity; selectively overriding gravity per-test (with the documented
RapierConfig { gravity: [0, -9.81, 0], ..Default::default() }snippet from the issue body) keeps the rest of the test suite unchanged.Implementation notes
Architecture fit
RapierClusterSimulationonly — users on the plainClusterSimulationpath get all defaults (matchescollider_for's existing behavior).SpawnParamsstruct ispub(crate)(private to the module). Public users still see five named hook methods, not a struct.RapierBodyKind,RapierMaterial,RapierCollisionGroupsare all#[non_exhaustive]for SemVer flexibility (per existing convention withRapierConfig/RapierColliderShape).Scope-split call-out
Introducing
Fixedhere only changes physics-side behavior (solver-skipped, only AABB tracked in broadphase). Until the (unfiled) clustering-binding epic lands,Fixedentities still migrate by PGP affinity — they are not yet pinned to chunk ownership. The clustering layer is unchanged in this PR.Module docs
# Per-entity spawn-time hookssection enumerating the four hooks and their defaults.## Subclass-style vs property-value-stylesub-section perentity-model.md§5.# Examplewith a property-value-styleMyGameimpl that uses every hook.Tests
8 new tests appended to
mod tests. Shared fixtureHookSimlets each test set per-entity overrides viaEntitySpecand inspect contact events / hook call counts.Reference
feat/rapier-clusterdocs/architecture/entity-model.md— body kinds (§4), subclass vs property-value (§5), spatial-bound vs affinity-bound (§6)