Model
saddle-physics-object-interaction splits the problem into two roles:
- an
ObjectInteractorentity that owns candidate scoring, command intake, and at most one active hold - an
InteractableBodyentity that stays fully simulated as an Avian dynamic rigid body
The interactor never teleports the prop directly. Instead, the physics step computes a spring/damper force and an alignment torque toward a desired hold point in front of the interactor.
Data Flow
1. Read commands
The runtime consumes message inputs:
- acquire
- explicit target selection
- drop
- throw
- hold-distance adjustment
- held-object rotation
- surface-placement toggle
- target cycling
These messages update per-interactor command state. Input helpers, AI controllers, replay tools, and E2E scenarios all drive the same surface.
2. Refresh candidates
Candidate refresh runs on the variable-rate update schedule:
- build an interactor-space origin from
Transform+InteractionAnchor - optionally perform a direct ray hit
- gather overlap candidates with a forgiving sphere
- reject bodies that are disabled, non-dynamic, already held, too far, too heavy, or blocked by line of sight
- store survivors as collected candidates
- optionally rerank them through a
SelectionScorerProvider- the shipped
DefaultSelectionScoreruses distance, view alignment, prop priority, sticky-target bonus, and direct-hit bonus
- the shipped
- keep the previous target when a newcomer only wins by less than
target_switch_hysteresis - write the sorted result into
InteractionCandidatesand the selected entry intoInteractionTarget
The selected target is just data. Acquisition is still a separate phase.
3. Acquire
When an interactor has a pending acquire command:
- validate the chosen prop again
- resolve the local anchor point
- center of mass
- remembered hit point
- per-prop custom local anchor
- choose the orientation policy
- preserve world rotation
- align to interactor
- custom prop-local rotation
- optionally swap collision layers for the held phase
- claim the prop immediately inside the acquire pass so two interactors cannot successfully grab the same body in the same frame
- insert
Holdingon the interactor andHeldBy+HeldRuntimeon the prop
This phase emits ObjectAcquired.
4. Maintain hold
Hold maintenance runs on the physics schedule so it stays aligned with Avian’s fixed-step world:
- optionally ease a fresh pickup through
pull_to_handbefore it settles at steady hold distance - optionally replace the forward hold point with a traced wall/shelf target when
SurfacePlacementModeis enabled - compute the desired hold point from interactor transform, anchor offset, and
HoldDistance - derive the held body’s world-space anchor position from its physics state
- use a spring force toward the target point
- use an angular spring toward the desired orientation
- clamp both force and torque
- track instability and occlusion timers
- emit
HeldObjectBecameUnstablewhen the grace window is exceeded
The prop remains dynamic the whole time. It can collide, swing, scrape, and be blocked by walls.
The important consequence is that surface placement is still physics-driven rather than a teleport. The hold target moves onto the traced surface, but the body gets there through the same spring/damper model as a normal carry state.
5. Release and throw
The physics step also resolves pending release requests:
Droppedjust clears held state and restores collision policyThrownbuilds a throw-intent context from the actor, request scales, config, and per-prop overrides- an optional
ThrowProfileProvidermaps that intent to arbitrary linear/angular impulses- the shipped
DefaultThrowProfilepreserves the previous forward-plus-up lob and right-axis spin
- the shipped
- if no throw profile is installed, the runtime treats throw as a release with no extra impulse
- forced releases use explicit reasons such as
DistanceExceeded,Occluded,Unstable, orTargetInvalid
This phase emits ObjectReleased and, for throws, ObjectThrown.
Schedule Reasoning
The plugin exposes two important execution lanes:
update_scheduleReadCommandsRefreshCandidatesAcquireTargetsPresentation
physics_scheduleMaintainHoldReleaseAndThrow
That split is intentional:
- command latency stays low because input and AI messages are consumed on the normal update loop
- hold forces remain stable because the solver runs inside the fixed-step physics schedule
- output/debug state can still be refreshed once per rendered frame
Why Force-Based Movement Instead of Teleportation
Teleporting the held body to the hold point every frame looks acceptable in a toy demo, but it breaks the goals of a reusable physics-handle crate:
- collisions become unreliable or explosive
- heavy and light props feel identical
- stacked bodies do not react naturally
- throws inherit less believable motion
- networking and replay integration lose a clean physics story
The spring-damper model keeps the object simulated while still letting the consumer tune how “tight” or “loose” the handle feels.
Collision Policy
Held objects often fight with the holder or camera rig. The crate keeps that concern generic through InteractionCollisionPolicy:
PreserveIgnoreInteractorLayerDisableAllCustomLayers(...)
The runtime snapshots the previous collision-layer state and restores it on release.
Per-Prop Overrides
Important feel changes belong on the prop, not in controller-specific glue:
PreferredHoldDistanceInteractionMassLimitOverrideHoldPointOverrideHoldOrientationOverrideInteractionCollisionPolicyThrowResponseOverride
Placement state is intentionally actor-side (SurfacePlacementMode) rather than prop-side. That keeps the crate composable: the same held object can be freely carried by one actor and placement-snapped by another without mutating prop authoring data.
This lets a crate, tool, orb, or saw blade each behave differently without changing the plugin API.
The same principle now applies to higher-level feel policy: candidate filtering stays in the runtime, but ranking and throw shaping are intentionally pluggable so different games can keep the same acquisition/hold/release core while swapping their own target heuristics or launch profiles.