simvx.core.physics.slide¶
Collide-and-slide movement policy for kinematic character bodies.
This is the ONE thing that distinguishes a character body from any other
- attr:
~simvx.core.physics.world.BodyMode.KINEMATICbody. It is a pair of free functions, not a backend method, written purely against five world primitives:- meth:
~simvx.core.physics.world.PhysicsWorld.body_transform,- meth:
~simvx.core.physics.world.PhysicsWorld.set_body_transformand- meth:
~simvx.core.physics.world.PhysicsWorld.sweep_bodyfor the move, plus- meth:
~simvx.core.physics.world.PhysicsWorld.body_massand- meth:
~simvx.core.physics.world.PhysicsWorld.apply_impulsefor the push. Being a free function means no backend can override it, every backend gets bit-identical policy, and it is unit-testable against a stub world with no physics backend at all.
The policy reads the start pose once, tracks it in plain Python floats, passes
every sweep an explicit from_transform, and writes the pose back exactly once
at the end. Nothing is written to the backend during the loop, so an abandoned
step-up leg simply discards a tracked pose: there is no revert to perform.
The final :meth:set_body_transform is UNCONDITIONAL and load-bearing. Some
backends only search colliding pairs over bodies they consider active, and
re-posing a kinematic body is what keeps it active and therefore visible to
areas and sensors. A “pose unchanged, skip the write” guard would look like a
free saving (one fewer FFI crossing per idle character on the web backend) and
would silently make every stationary character invisible to triggers. Do not add
one.
The policy never integrates gravity (the caller supplies the velocity) and never
writes the body’s simulated velocity: like every kinematic body driven by
move_and_collide, a character is position-driven and its stored linear
velocity stays zero. A character never CARRIES a body resting on it, but it does
SHOVE what it walks into, by default: push_factor is 1.0, and 0.0 is
the opt-out that restores the pure block and costs one float comparison per
blocking contact. Unreal is the only other engine shipping a built-in push and it
too is on by default. The blocking contacts the move resolved are handed back in
- attr:
MoveResult.collisionswhatever the factor, so game code that wants different physics (carrying, one-way pushes, damage on impact) still has everything it needs to do its own thing.
The push is one impulse per blocking contact the character did not land ON (see
the floor exemption below), along the contact normal, of magnitude
push_factor * (M * m / (M + m)) * approach_speed – the character’s mass
M, the struck body’s mass m, and the component of the character’s
velocity into that surface at the moment it was blocked. Scaling by the approach
speed rather than using a flat impulse is what stops a walking character and a
sprinting one shoving a crate identically; Unreal scales its own push force the
same way, under a flag it turns on by default (bScalePushForceToVelocity).
M * m / (M + m) is the reduced mass of the pair, and multiplied by the
approach speed it is exactly the impulse of a perfectly inelastic collision: the
one that leaves both bodies moving together. That is what makes 1.0 a
meaningful default rather than a hazard. ONE contact changes the struck body’s
velocity by impulse / m, which comes out as
push_factor * M / (M + m) * approach, so at 1.0 a single contact adds at
most the character’s own approach speed and never more – measured on both 3D
backends, a 70 kg character walking at 4 m/s hands a 1 kg crate 3.9437 m/s, an
equal 70 kg crate 2.0000 m/s and a 1000 kg crate 0.2617 m/s, each matching the
formula to four decimals. A character that keeps walking catches a light body up
and hits it again, and those kicks do stack: this is a bound per contact, not a
speed limit on the body. The unbounded alternative (M alone, no m) would
have handed that 1 kg crate 280 m/s from one contact,
which is why the knob could not previously ship on. Above 1.0 the impulse
stops being physical and becomes a shove-harder dial; below it, a character that
leans on things gently.
The impulse goes through the CENTRE OF MASS, not through the contact point, so a
shove never tumbles what it hits. That is a deliberate departure from the more
physical alternative, and the reason is cross-backend: the angular response to an
off-centre impulse is the one part of the seam a backend is allowed to
approximate, and the built-in tiers do (they use inverse_mass as an inverse-
inertia stand-in). Measured in one scene – a 70 kg character walking into a
settled 1 kg crate for two seconds at push_factor 1 – applying at the contact
point tilted the crate 43 degrees on builtin against 90 on Jolt and sent it 6.98 m
against 10.14; through the centre of mass there is no tilt at all and the two
travel 6.98 m and 7.27 m. A game wanting the crate to topple has the contact point
in :attr:MoveResult.collisions and can add the torque itself.
The one thing the push reads about the body it hit is that body’s effective mass,
through :meth:~simvx.core.physics.world.PhysicsWorld.body_mass. An immovable
body answers inf: it has no reduced mass to share, and the impulse would have
been inert on it anyway, so the branch skips it outright and a wall or a parked
platform absorbs the walk exactly as before.
The push also reads the contact’s own classification, and a contact this move
called FLOOR is exempt. A character standing on something is pressing into it
every step at whatever speed gravity has just given it, so pushing floor contacts
would turn a character’s weight into a downward impulse on whatever it stands on:
measured with the exemption removed and the character falling at 18 m/s^2, a 70 kg
character dropped 8 m onto a 1 kg crate at push_factor 1 drove it 0.2286 m into
the static floor on the built-in solver – a crate whose resting centre is 0.35 m
up, so two thirds of the way down to it – and left it 0.0589 m low once everything
settled, against 0.0005 m on Jolt. With the exemption both leave the crate exactly
where it was resting. It is the same exemption Unity’s stock controller recipe
makes by filtering downward hits, and it leaves the standing case to the solver,
which is what actually holds a character up. Unreal instead applies a separate,
explicitly scaled downward force for it (StandingDownwardForceScale); there
is no such knob here, so standing imparts nothing. Walls, ceilings and slopes too
steep to walk on are all pushed.
The push is applied while the velocity still has its component into that surface,
not afterwards over the collected hits: the slide deflects the velocity out of
each normal as it goes, so a pass over :attr:MoveResult.collisions at the end
would measure every primary contact as approaching at zero.
Internals are deliberately scalar (plain floats plus math.sqrt / math.hypot)
rather than numpy: the vector types are constructed only at the sweep_body and
set_body_transform boundaries, which measures materially cheaper per character
per step than the same policy expressed in Vec3 arithmetic.
Module Contents¶
Classes¶
Outcome of one 3D collide-and-slide move. |
|
Outcome of one 2D collide-and-slide move. |
Functions¶
Collide-and-slide a kinematic body by |
|
Collide-and-slide a kinematic 2D body by |
Data¶
API¶
- simvx.core.physics.slide.__all__¶
[‘MoveResult’, ‘MoveResult2D’, ‘move_and_slide’, ‘move_and_slide_2d’]
- class simvx.core.physics.slide.MoveResult[source]¶
Outcome of one 3D collide-and-slide move.
Attributes: velocity: Post-slide velocity (deflected out of every contact normal); the caller writes this back as its new velocity. on_floor: True if a contact this move classified as floor, or if the ground probe found walkable ground under the feet. on_wall: True if a contact this move classified as wall. on_ceiling: True if a contact this move classified as ceiling. floor_normal: Normal of the floor contact this move (unit
Vec3), or+upwhen there was no floor contact. position: The final pose position the policy wrote, so the caller needs no read-back call after the move. collisions: The blocking :class:~simvx.core.physics.world.SweepHit\ s the policy resolved, in slide order. Handed back whether or notpush_factordid anything with them, so game code can apply physics of its own, e.g.world.apply_impulse(c.body, impulse, at=c.point)for each entry. Empty when the move was unobstructed.- velocity: simvx.core.math.Vec3¶
None
- on_floor: bool¶
None
- on_wall: bool¶
None
- on_ceiling: bool¶
None
- floor_normal: simvx.core.math.Vec3¶
None
- position: simvx.core.math.Vec3¶
None
- collisions: tuple[simvx.core.physics.world.SweepHit, ...]¶
None
- class simvx.core.physics.slide.MoveResult2D[source]¶
Outcome of one 2D collide-and-slide move.
2D sibling of :class:
MoveResult; see it for the field semantics, including the :attr:collisionsapply-your-own-impulse contract.- velocity: simvx.core.math.Vec2¶
None
- on_floor: bool¶
None
- on_wall: bool¶
None
- on_ceiling: bool¶
None
- floor_normal: simvx.core.math.Vec2¶
None
- position: simvx.core.math.Vec2¶
None
- collisions: tuple[simvx.core.physics.world2d.SweepHit2D, ...]¶
None
- simvx.core.physics.slide.move_and_slide(world: simvx.core.physics.world.PhysicsWorld, handle: simvx.core.physics.world.BodyHandle, velocity: simvx.core.math.Vec3, dt: float, *, up: simvx.core.math.Vec3, slope_limit: float, step_height: float, skin_width: float, max_slides: int, push_factor: float = 1.0, mass: float = 1.0) simvx.core.physics.slide.MoveResult[source]¶
Collide-and-slide a kinematic body by
velocity * dtagainst the world.Sweeps along the remaining motion; on each blocking contact it advances to exactly the reported clear distance, classifies the normal against
up, deflects both the remaining motion and the velocity out of the surface, and repeats up tomax_slidestimes. A wall hit withstep_height > 0gets one up / forward / drop step probe, which classifies what it lands on against the sameslope_limit. A landing whose own normal is not walkable is accepted only when walkable ground continues ahead of it, so a ledge a rounded character can only perch on the edge of is a step and a foothold up the flank of a ball is not; :func:_step_up_3dstates the rule and its window. A final downward probe establishes floor state without moving the body.Args: world: Any :class:
~simvx.core.physics.world.PhysicsWorld. handle: Handle of the KINEMATIC body to move. velocity: Desired world-space velocity (Vec3), units/s. dt: Timestep in seconds. up: World up vector (Vec3); normalised here. slope_limit: Maximum walkable slope, in RADIANS. step_height: Maximum step-up height in world units (0disables). skin_width: Contact clearance requested from each sweep; also scales the ground probe’s lift and reach. max_slides: Maximum collide-and-slide iterations. push_factor: Dimensionless multiplier on how hard to shove what the character walks into. Every blocking contact EXCEPT one classified as floor, and except an immovable body, receives an impulse ofpush_factor * (mass * struck_mass / (mass + struck_mass)) * approach_speedN*s along the contact normal, where the approach speed is the component of the character’s velocity INTO that surface; standing on a body imparts nothing to it.1.0is the arrest impulse of a perfectly inelastic collision, so the struck body leaves at no more than the character’s own approach speed;0.0imparts nothing at all and the branch body never runs. It defaults to1.0here and at the node-level knob, :attr:~simvx.core.physics.nodes.CharacterBody3D.push_factor, alike. mass: The character’s mass in kg, the dimensioned half of the impulse. The default here is1.0rather than the 70 kg of :attr:~simvx.core.physics.nodes.CharacterBody3D.mass: this function is the raw policy, called with a stub world in tests and with explicit numbers by anything driving it directly, so its default is the neutral one and the node supplies a human-shaped mass instead.Returns: A :class:
MoveResult, including the final pose and the blocking contacts.
- simvx.core.physics.slide.move_and_slide_2d(world: simvx.core.physics.world2d.Physics2DWorld, handle: simvx.core.physics.world.BodyHandle, velocity: simvx.core.math.Vec2, dt: float, *, up: simvx.core.math.Vec2, slope_limit: float, step_height: float, skin_width: float, max_slides: int, push_factor: float = 1.0, mass: float = 1.0) simvx.core.physics.slide.MoveResult2D[source]¶
Collide-and-slide a kinematic 2D body by
velocity * dt.2D sibling of :func:
move_and_slide; see it for the policy, the ground-probe rule and the argument semantics, including whatpush_factormeans. Rotation is a scalar in radians and is carried through unchanged.