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.KINEMATIC body. 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_transform and

meth:

~simvx.core.physics.world.PhysicsWorld.sweep_body for the move, plus

meth:

~simvx.core.physics.world.PhysicsWorld.body_mass and

meth:

~simvx.core.physics.world.PhysicsWorld.apply_impulse for 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.collisions whatever 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

MoveResult

Outcome of one 3D collide-and-slide move.

MoveResult2D

Outcome of one 2D collide-and-slide move.

Functions

move_and_slide

Collide-and-slide a kinematic body by velocity * dt against the world.

move_and_slide_2d

Collide-and-slide a kinematic 2D body by velocity * dt.

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 +up when 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 not push_factor did 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:collisions apply-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 * dt against 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 to max_slides times. A wall hit with step_height > 0 gets one up / forward / drop step probe, which classifies what it lands on against the same slope_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_3d states 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 (0 disables). 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 of push_factor * (mass * struck_mass / (mass + struck_mass)) *         approach_speed N*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.0 is the arrest impulse of a perfectly inelastic collision, so the struck body leaves at no more than the character’s own approach speed; 0.0 imparts nothing at all and the branch body never runs. It defaults to 1.0 here 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 is 1.0 rather 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 what push_factor means. Rotation is a scalar in radians and is carried through unchanged.