simvx.core.physics.world2d¶
World convention (state it loudly)¶
Y-up, identical to the 3D world. Gravity defaults to Vec2(0, -9.81): down
is -Y. A Y-down game (renders +Y downward on screen) simply sets
gravity=Vec2(0, 9.81); the physics world itself stays neutral and never assumes a
screen orientation. Rotation is a scalar angle in radians, positive
counter-clockwise (standard math convention).
The load-bearing part of the contract is the bulk-array transfer: per-frame
body state is exchanged as a single numpy buffer (one transfer per world per
frame), in a fixed body->row order, by filling a caller-preallocated array
in place. The transform row is (N, 4) = [px, py, cos(theta), sin(theta)]
(cos/sin rather than the bare angle so interpolation lerps the unit
vector with no +-pi wraparound, matching Transform2D and the pymunk angle
convention); the velocity row is (N, 3) = [lx, ly, omega] (linear x/y plus
scalar angular velocity in radians/s). See :meth:register_bodies,
- meth:
read_transforms, and :meth:read_velocities.
Every method is declared @abstractmethod here, so a backend has a complete
contract to implement and a partial backend fails loudly rather than silently
returning nothing.
Physics2DWorld: the 2D backend transport interface.
This module defines Physics2DWorld, the abstract interface every 2D physics
backend (BuiltinPhysics2D now, an optional PymunkPhysics2D later)
implements. It is the 2D sibling of :class:~simvx.core.physics.world.PhysicsWorld
and serves the same purpose: a transport abstraction that moves rigid-body
state across the Python boundary efficiently, not a definition of solver
behaviour.
A SEPARATE interface (rather than the 3D one constrained to a plane) is
deliberate: the bulk contract width differs ((N,4) transforms /
(N,3) velocities here vs (N,7) / (N,6) in 3D), the optional native
backend differs (pymunk / Chipmunk2D vs Jolt), and rotation is a scalar radian
angle rather than a quaternion. Dimension-agnostic pieces (:class:BodyMode,
- class:
CombineMode, :class:ContactPhase, :class:PhysicsMaterial) are REUSED by import from the 3D modules, never duplicated.
Module Contents¶
Classes¶
Result of a successful 2D raycast against the world. |
|
Result of a shape sweep stopping against a body. |
|
A node-agnostic 2D body-pair collision event emitted by a physics world. |
|
A node-agnostic 2D sensor-overlap event emitted by a physics world. |
|
Abstract 2D backend interface: one isolated simulation world. |
Functions¶
True when |
|
Validate a 2D body scale against a shape kind, as float32 |
Data¶
API¶
- simvx.core.physics.world2d.BodyHandle¶
None
- simvx.core.physics.world2d.ShapeHandle¶
None
- simvx.core.physics.world2d.JointHandle¶
None
- simvx.core.physics.world2d.is_unit_scale_2d(scale: numpy.ndarray) bool[source]¶
True when
scaleis exactly(1, 1)and so changes no geometry.
- simvx.core.physics.world2d.normalise_body_scale_2d(scale: object, kind: str) numpy.ndarray[source]¶
Validate a 2D body scale against a shape kind, as float32
(2,).The 2D twin of :func:
~simvx.core.physics.world.normalise_body_scale, with the same rule: geometry defined by a single radius (a circle, a capsule, a thick segment) cannot express a non-uniform scale and raises rather than silently substituting a component, while a rectangle, a convex polygon and a segment soup scale componentwise. Negative components mirror, and uniformity is judged on magnitudes, so thescale.x = -1sprite flip is uniform.Args: scale: A
Vec2or any 2-sequence. kind: The backend’s own name for the geometry ("circle","box","capsule","segment", …). Unknown kinds scale componentwise.Returns: The scale as a float32
(2,)array, always fresh and never a view of the caller’s, for the reason the 3D twin gives.Raises: ValueError: If either component is zero or non-finite, or if the geometry cannot represent the requested non-uniform scale.
- class simvx.core.physics.world2d.RaycastHit2D[source]¶
Result of a successful 2D raycast against the world.
Attributes: body: Handle of the body the ray hit. point: World-space contact point (
Vec2). normal: World-space surface normal at the hit (Vec2, unit length). distance: Distance from the ray origin topointalong the ray.- body: simvx.core.physics.world2d.BodyHandle¶
None
- point: simvx.core.math.Vec2¶
None
- normal: simvx.core.math.Vec2¶
None
- distance: float¶
None
- class simvx.core.physics.world2d.SweepHit2D[source]¶
Result of a shape sweep stopping against a body.
2D sibling of :class:
~simvx.core.physics.world.SweepHit: a single “other” body (like a query result); the swept body is implicit (the caller).Attributes: body: Handle of the OTHER body that was hit. point: World-space contact point (
Vec2). normal: World-space surface normal (Vec2, unit), pointing AWAY from the other body toward the moving body (the direction that separates the mover). A sweep that begins already in contact still reports the surface’s separating normal, never the cast axis. distance: A distance alongmotionat which the mover’s shape is guaranteed NOT to penetrate the blocker. An exact time-of-impact backend reportsmax(0.0, toi - skin); a substepped backend reports a bound refined by bisection to|motion| / (substeps * 2**8)and ignoresskin. It is therefore an under-estimate bounded by the backend’s sweep granularity, and may be exactly0.0for a sweep that begins in contact. Callers advance to exactlydistanceand subtract nothing further.- body: simvx.core.physics.world2d.BodyHandle¶
None
- point: simvx.core.math.Vec2¶
None
- normal: simvx.core.math.Vec2¶
None
- distance: float¶
None
- class simvx.core.physics.world2d.ContactEvent2D[source]¶
A node-agnostic 2D body-pair collision event emitted by a physics world.
2D sibling of :class:
~simvx.core.physics.world.ContactEvent. Keyed by body HANDLES only (the world never names a node); orientation is fixeda -> b. Reuses :class:ContactPhase(no second phase enum).A contact event is emitted only for pairs with at least one DYNAMIC participant. Kinematic-vs-kinematic and kinematic-vs-static pairs cannot be pushed apart, produce no solver work, and produce no
ContactEvent2Don any backend. Sensor overlap is the separate one-directional stream (see- Class:
OverlapEvent2D) and is unaffected.
Attributes: a: Handle of the first body of the pair (canonical order). b: Handle of the second body of the pair. phase: :class:
ContactPhase(ENTER/EXIT). point: World contact point (Vec2). Meaningful onENTER; degenerate (Vec2(0)) onEXIT, and on the rareENTERwhose manifold carries no point at all (:func:~simvx.core.physics.capability.contact_manifold_payload). normal: Unit contact normal orienteda -> b(Vec2). Degenerate (Vec2(0)) onEXIT. impulse: Normal impulse magnitude the solver applied to the pair this step.0.0onEXITand0.0on anENTERthe solver resolved with no impulse.Noneiff the backend does not advertise :attr:Capability.CONTACT_IMPULSE, which is a different fact from a measured0.0. impulse_estimate: The portable stand-in forimpulse, identical in meaning to :attr:~simvx.core.physics.world.ContactEvent.impulse_estimate: the impulse it would take to arrest the approach, computed the same way on every backend from the pair’s masses and the difference of the two bodies’ LINEAR velocities – not from the at-pointrel_velocitybelow (:func:~simvx.core.physics.capability.contact_impulse_estimate). Always a number,0.0onEXITand0.0for a pair that is not approaching. rel_velocity: Pre-solve velocity ofbw.r.t.aAT THE CONTACT POINT (Vec2), spin included, identical in meaning to :attr:~simvx.core.physics.world.ContactEvent.rel_velocity: each body’sv + omega * perp(r)for the offset from its centre to :attr:point, differenced. A wheel spinning against the ground reports the speed of its tread, which its centre is not moving at. Where :attr:pointis degenerate for want of a manifold point, this is the difference of the two bodies’ LINEAR velocities: there is nowhere to measure the spin at. Degenerate (Vec2(0)) onEXIT.- phase: simvx.core.physics.world.ContactPhase¶
None
- point: simvx.core.math.Vec2¶
None
- normal: simvx.core.math.Vec2¶
None
- impulse: float | None¶
None
- impulse_estimate: float¶
None
- rel_velocity: simvx.core.math.Vec2¶
None
- class simvx.core.physics.world2d.OverlapEvent2D[source]¶
A node-agnostic 2D sensor-overlap event emitted by a physics world.
2D sibling of :class:
~simvx.core.physics.world.OverlapEvent: a SECOND, independent edge-diffed stream. DIRECTEDsensor -> other(the observing sensor decides via its mask). Reuses :class:ContactPhase.Attributes: sensor: Handle of the detecting sensor body (the observer). other: Handle of the detected body (a normal body OR another sensor). phase: :class:
ContactPhase(ENTER/EXIT).- sensor: simvx.core.physics.world2d.BodyHandle¶
None
- other: simvx.core.physics.world2d.BodyHandle¶
None
- phase: simvx.core.physics.world.ContactPhase¶
None
- class simvx.core.physics.world2d.Physics2DWorld(*, gravity: simvx.core.math.Vec2)[source]¶
Bases:
abc.ABCAbstract 2D backend interface: one isolated simulation world.
A
Physics2DWorldowns a set of bodies, advances them as a unit at a fixed timestep via :meth:step, and exchanges per-frame state in bulk. Concrete backends (BuiltinPhysics2D, laterPymunkPhysics2D) implement every method.What this promises across backends
The same contract the 3D seam states, and it is stated there once for both dimensions: see :class:
~simvx.core.physics.world.PhysicsWorldfor the three promises (identical contract behaviour, numbers only within a documented tolerance, backend-dependent features and payload fields queryable as a- Class:
~simvx.core.physics.capability.Capability) and the two rules that follow from them. The seam is two disjoint ABCs because the payload types differ, not because the promise does.
Bulk-array contract (the keystone)
Call :meth:
register_bodiesonce (or whenever membership changes) to fix the body->row order used by the bulk readers.Each frame, after :meth:
step, call :meth:read_transformsand/or- meth:
read_velocities, passing a caller-preallocated, C-contiguousfloat32numpy array of the documented shape. The backend fills it in place; it must not allocate or return a new array on the hot path.
The array shapes/dtypes/contiguity are part of the contract and MUST be checked by subclasses (see
_check_transforms_out/_check_velocities_out, which raiseValueError: the buffer is caller input, so the check is not an assert). The transform row is(N, 4) = [px, py, cos(theta), sin(theta)]and the velocity row is(N, 3) = [lx, ly, omega](see the module docstring for the Y-up / radians convention).Shape contract (immutable values, owned by the resource that asked)
Identical to the 3D one stated on
- Class:
~simvx.core.physics.world.PhysicsWorld, restated here so a 2D backend author never has to read the 3D interface: a shape handle names an IMMUTABLE VALUE and everycreate_*factory mints a FRESH one. The factories do not memoise on the geometry, because the world cannot know when a caller has finished with a handle and a geometry-keyed cache would only ever grow.
Sharing and lifetime belong to the
Shape2Dresource (:class:~simvx.core.physics.shapes2d.Shape2D), which builds its handle once per world and releases it when the resource is collected. A caller working against this interface directly owns what it creates and releases it with- Meth:
destroy_shape.
There is no mutation API, and there must never be one. A hypothetical
set_shape_radiuswould silently resize every body sharing the handle. Any future edit-the-geometry surface must BUILD A NEW HANDLE and hand it to- meth:
set_body_shape, which is what changing a collider does today.
Destruction is about the handle, never about a body.
- meth:
destroy_shapereleases the world’s reference and leaves every body’s geometry alone.
Per-body SCALE belongs to the body, precisely because a shape is shared:
- Meth:
create_bodyand :meth:set_body_transformtake ascaleand the backend applies it to its own instance of the geometry. Geometries that cannot express a non-uniform scale raise rather than approximate (see- Func:
~simvx.core.physics.world2d.normalise_body_scale_2d).
Initialization
Initialise the world.
Args: gravity: World gravity acceleration vector (
Vec2), metres/s^2. Y-up:Vec2(0, -9.81)is “down”.- property gravity: simvx.core.math.Vec2[source]¶
World gravity acceleration vector (
Vec2), metres/s^2 (Y-up).Scaled per body by the
gravity_scale:meth:create_bodytakes.
- property solver_iterations: int[source]¶
Impulse-solver iterations per :meth:
step(>= 1).See :attr:
~simvx.core.physics.world.PhysicsWorld.solver_iterations; the 2D tiers honour it through the builtin velocity loop and Chipmunk’sspace.iterations.
- property position_iterations: int[source]¶
Rigid-joint position passes per :meth:
step(>= 1).See :attr:
~simvx.core.physics.world.PhysicsWorld.position_iterations. The builtin 2D solver runs this many Baumgarte passes over its pins, hinges, welds and grooves; Chipmunk has no position solver, so the pymunk lane keeps the value and reads it back without running it.
- property sleep_time_threshold: float[source]¶
Seconds of continuous sub-threshold motion before a body sleeps (
> 0).See :attr:
~simvx.core.physics.world.PhysicsWorld.sleep_time_threshold.
- property sleep_velocity_threshold: float[source]¶
Speed below which a body counts as at rest, m/s (
>= 0).See :attr:
~simvx.core.physics.world.PhysicsWorld.sleep_velocity_threshold.
- property contact_slop: float[source]¶
Contact overlap tolerated without correction, world units (
>= 0).See :attr:
~simvx.core.physics.world.PhysicsWorld.contact_slop. A pixel-scale 2D game is the case this knob most exists for: at gravity 980 and 50-unit sprites, raise it to roughly0.1-0.5.
- capabilities() frozenset[simvx.core.physics.capability.Capability][source]¶
Return the set of :class:
Capabilityfeatures this 2D backend honours.2D sibling of :meth:
~simvx.core.physics.world.PhysicsWorld.capabilities, sharing the same dimension-agnostic :class:Capabilityenum: a whole feature this interface has no methods for, a strengthened guarantee about a method every backend has, or a payload field that carries a value only where the backend can measure it. The default is the empty set, so a partially implemented backend claims nothing by accident; concrete backends override to list exactly what they honour, in either direction.
- abstractmethod create_circle(radius: float) simvx.core.physics.world2d.ShapeHandle[source]¶
Create a circle collision shape and return an opaque handle.
Args: radius: Circle radius, world units (> 0).
Returns: An opaque shape handle for use with :meth:
create_body.
- abstractmethod create_box(half_extents: simvx.core.math.Vec2) simvx.core.physics.world2d.ShapeHandle[source]¶
Create an axis-aligned (body-local) box shape, centred at the origin.
Args: half_extents: Half-sizes along x/y (
Vec2, both > 0).Returns: An opaque shape handle for use with :meth:
create_body.
- abstractmethod create_capsule(radius: float, height: float) simvx.core.physics.world2d.ShapeHandle[source]¶
Create a Y-axis capsule collision shape and return an opaque handle.
Args: radius: Capsule radius, world units (> 0). height: Total extent along Y including the two semicircular caps (> 0). The central segment half-length is
max(0, height / 2 - radius); whenheight <= 2 * radiusthe segment collapses to a point and the capsule behaves as a circle.Returns: An opaque shape handle for use with :meth:
create_body.
- abstractmethod create_segment(a: simvx.core.math.Vec2, b: simvx.core.math.Vec2, radius: float = 0.0) simvx.core.physics.world2d.ShapeHandle[source]¶
Create a line-segment collision shape (2D-only).
A thick line from
atob(a “beam”), the 2D analogue with no 3D equivalent. Useful for thin static walls / floors and one-way platforms.Args: a: Segment start, body-local (
Vec2). b: Segment end, body-local (Vec2). radius: Segment thickness radius (>= 0); 0 is an infinitely thin line.Returns: An opaque shape handle for use with :meth:
create_body.
- abstractmethod create_convex_polygon(points: numpy.ndarray) simvx.core.physics.world2d.ShapeHandle[source]¶
Create a convex polygon collision shape from CCW points.
Args: points:
(N, 2)float32 array of >= 3 points in counter-clockwise winding, defining a convex polygon (body-local).Returns: An opaque shape handle for use with :meth:
create_body.
- abstractmethod create_concave_polygon(segments: numpy.ndarray) simvx.core.physics.world2d.ShapeHandle[source]¶
Create a STATIC edge-soup collision shape (2D analogue of a mesh).
Args: segments:
(N, 2, 2)float32 array of N line segments (each an[start, end]pair ofVec2points), body-local. The 2D analogue of a static triangle mesh: STATIC-ONLY level geometry.Returns: An opaque shape handle for use with :meth:
create_body. A concave polygon is a STATIC-ONLY collider: placing it on a non-STATIC body is an error.
- abstractmethod destroy_shape(shape: simvx.core.physics.world2d.ShapeHandle) None[source]¶
Release the world’s reference to a shape handle.
2D sibling of
- Meth:
~simvx.core.physics.world.PhysicsWorld.destroy_shape, with the identical contract. Dropsshapefrom the backend’s shape table: a backend that owns a native shape object frees it here, a pure-Python one simply forgets the record. This is what a- Class:
~simvx.core.physics.shapes2d.Shape2Dresource calls for each handle it owns when it is collected, and the escape hatch for a caller working against this interface directly.
Bodies are not touched. One handle is routinely shared by many bodies, so destroying it cannot mean “take the collider away from whoever is using it”: every body created with this shape keeps its geometry and keeps simulating. It neither raises nor detaches. As in 3D, “keeps simulating” covers the full surface: the body stays editable through every
set_body_*setter and sweepable through- Meth:
sweep_body, so a backend must reach a body’s geometry through the record that body owns rather than by re-indexing its shape table.
An unknown handle is a silent no-op, so destroying twice or destroying after :meth:
clearis safe in any teardown order. The HANDLE is invalid afterwards: passing it to :meth:create_body, :meth:set_body_shape,- Meth:
shapecastor :meth:overlapraisesKeyError, and is a caller error.KeyErroron every backend and every one of those four calls, matching what a bogus BODY handle already raises.
Args: shape: A handle previously returned by one of the
create_*shape factories.
- abstractmethod create_body(shape: simvx.core.physics.world2d.ShapeHandle, body_type: simvx.core.physics.world.BodyMode, transform: Any, *, mass: float = 1.0, scale: simvx.core.math.Vec2 | None = None, can_sleep: bool = True, linear_damping: float = DEFAULT_LINEAR_DAMPING, angular_damping: float = DEFAULT_ANGULAR_DAMPING, gravity_scale: float = DEFAULT_GRAVITY_SCALE, collision_layer: int = 1, collision_mask: int = 4294967295, is_sensor: bool = False, material: simvx.core.physics.material.PhysicsMaterial | None = None, continuous: bool = False) simvx.core.physics.world2d.BodyHandle[source]¶
Create a body in the world and return its handle.
Mirrors :meth:
~simvx.core.physics.world.PhysicsWorld.create_body(same layer/mask, sensor, material, andcontinuoussemantics), with 2D transforms (positionVec2+ scalar rotation).Args: shape: An opaque shape handle from one of the
create_*shape factories. A concave polygon on a non-STATIC body is an error. body_type: One of :class:BodyMode. transform: Initial world transform, in the form everytransformargument on this interface accepts: aTransform2D(.position+.rotation), a bareVec2/ sequence (position only, zero rotation), or a(position, rotation)pair where rotation is any real scalar in radians.numpyfloats are real scalars here, since the engine’s own maths is float32; the pair must be a tuple, because a 2-element list orVec2is a bare position. mass: Body mass in kg,> 0(a non-positive or NaN mass raisesValueError, whatever the mode). Consulted while the body is DYNAMIC and RETAINED across :meth:set_body_mode, so a body created STATIC or KINEMATIC carries this mass into a later DYNAMIC flip. STATIC and KINEMATIC integrate as infinite-mass regardless of the value. scale: Per-body scale ofshape(Vec2);None(the default) means unscaled. Scale belongs to the body rather than the shape because a shape handle is shared, so one collider resource serves bodies at any number of sizes: a body atscale=4collides as the four-times-larger collider, which is what makes a scaled node’s collider match what is drawn. Geometry that cannot express a non-uniform scale RAISES rather than approximating (see :func:normalise_body_scale_2d); negative components mirror. Editable afterwards through :meth:set_body_transform. can_sleep: Whether this body is ALLOWED to fall asleep once it settles (see :meth:sleeping). Defaults True; False keeps it permanently simulated. Editable through :meth:set_body_can_sleep. linear_damping: Per-second rate at which the body sheds linear speed with nothing touching it, applied asv = v * max(0, 1 - linear_damping * dt) + a * dt(see :func:~simvx.core.physics.world.normalise_damping, which also records where Jolt’s integrator differs under sustained acceleration). Defaults to :data:~simvx.core.physics.world.DEFAULT_LINEAR_DAMPING. Editable through :meth:set_body_damping. angular_damping: The same rate for spin. Defaults to :data:~simvx.core.physics.world.DEFAULT_ANGULAR_DAMPING, and is independent oflinear_damping. gravity_scale: Multiplier on the world’s gravity for this body alone.1(the default) falls normally,0ignores gravity, and a negative value falls upward. Editable through :meth:set_body_gravity_scale. collision_layer: 32-bit layer membership; defaults to layer 1. collision_mask: 32-bit mask of layers this body scans; defaults all. is_sensor: When True, this body is a SENSOR (trigger): broadphase only, excluded from collision resolution, never blocks a shape sweep, feeds the separate overlap stream (one-directionalsensor.mask & other.layer). material: The body’s SURFACE (friction, restitution, combine modes) as one :class:PhysicsMaterialresource, shareable across bodies.None(the default) uses :data:~simvx.core.physics.material.DEFAULT_PHYSICS_MATERIAL. The resource is frozen; a live body’s surface changes by passing a different one to :meth:set_body_material. continuous: When True, use continuous collision detection (centre sweep vs STATIC, TOI clamp; basic-tier honesty, full CCD deferred to pymunk). Defaults False (discrete).Returns: An opaque body handle, stable until :meth:
destroy_body.
- abstractmethod destroy_body(handle: simvx.core.physics.world2d.BodyHandle) None[source]¶
Remove a body from the world; the handle is invalid afterwards.
Callers that use the bulk readers must re-call :meth:
register_bodiesto re-establish row order.An unknown handle is a silent no-op, the same contract
- Meth:
destroy_shapefollows, so destroying twice or destroying after- Meth:
clearis safe in any teardown order.
Destruction ENDS every contact and sensor overlap the body was in, so the event streams report one
EXITper open edge rather than dropping it: a listener is never left believing a pair is still in contact (the same rule- Meth:
set_body_filterfollows). The events surface in the first- Meth:
drain_contact_events/ :meth:drain_overlap_eventsafter the next- Meth:
step, and never twice. Their payload is the ordinary degenerateEXITpayload, and the destroyed handle they carry is an identity to match against a caller’s own bookkeeping, never an argument for a further world call.
Destruction also WAKES every body that was in contact with this one, so whatever it was holding up falls (see :meth:
sleeping).
- abstractmethod set_body_transform(handle: simvx.core.physics.world2d.BodyHandle, transform: Any, *, scale: simvx.core.math.Vec2 | None = None, wake: bool = True) None[source]¶
Teleport a body to a new world transform, optionally rescaling it.
Wakes the body, and a pose that really moves it also wakes the bodies it was in contact with, so a platform driven out from under a sleeping crate drops the crate (see :meth:
sleeping). Re-writing the pose a body already holds moves nothing and wakes nothing.Args: handle: Body handle. transform: New world transform, in the forms this interface accepts. scale: New per-body scale of the body’s shape (
Vec2), orNone(the default) to leave the scale it already has alone.Nonerather than(1, 1)is load-bearing: a character controller re-writes its body’s pose every step and knows nothing about scale. Rescaling is a geometry change, so it wakes the body’s neighbours exactly as :meth:set_body_shapedoes. wake: Whether this write may disturb sleepers (see :meth:sleeping).Falsere-poses the body without waking it or anything it was holding up: streaming a level in, or repositioning a pooled object, must not re-activate a settled pile.
- abstractmethod set_body_velocity(handle: simvx.core.physics.world2d.BodyHandle, linear: simvx.core.math.Vec2, angular: float = 0.0) None[source]¶
Set a body’s linear and (scalar) angular velocity directly.
A body the caller made
STATICis never set in motion by this call, on either backend: an immovable body stays where it was put, and the way to move one is :meth:set_body_transform. The write is still remembered and read back, on both backends. The pymunk adapter holds a STATIC sensor KINEMATIC (see- Attr:
~simvx.core.physics.capability.Capability.SENSOR_DETECTS_STATIC), which Chipmunk WOULD integrate, so for that one case the value is kept by the adapter instead of being given to the library, and becomes the live velocity if the body later leavesSTATIC. A static body’s velocity is also read as a surface velocity by the friction solve on both backends, which is how a conveyor belt is expressed.
Args: handle: Body handle. linear: Linear velocity (
Vec2), world units/s. angular: Angular velocity (scalar float), radians/s (CCW positive). Defaults to0.0.
- abstractmethod set_body_mode(handle: simvx.core.physics.world2d.BodyHandle, mode: simvx.core.physics.world.BodyMode, *, wake: bool = True) None[source]¶
Change a live body’s motion mode in place (no destroy/recreate).
Flips STATIC / KINEMATIC / DYNAMIC, updating effective (inverse) mass and moment of inertia: STATIC / KINEMATIC are infinite (inverse 0), DYNAMIC restores the mass the body was created with and recomputes the per-shape moment from it. Flips are lossless, so freezing a body to STATIC and waking it later gives back exactly its original mass and moment.
A flip to DYNAMIC wakes the bodies this one was in contact with: it can no longer hold anything up, so what rested on it must fall (see
- Meth:
sleeping). Freezing a body to STATIC takes nothing away and leaves a sleeping neighbour asleep, and re-asserting the mode a body already has does nothing at all.
wakegoverns both halves of that, and the two directions of the flip are not symmetric:LEAVING DYNAMIC ends the body’s own sleep whatever
wakesays, because an immovable body is never asleep (see :meth:sleeping). That restores the invariant rather than waking anything, so it is not suppressible.ENTERING DYNAMIC with
wake=Falsehands the body to the simulation PARKED: DYNAMIC and asleep, its velocity zeroed, costing nothing until something disturbs it (:meth:wake, a pose write, an impulse, or an awake body arriving at it). That is what streaming a section in wants, its geometry landing settled rather than paying to fall into place. The default hands the body over awake, moving from the next step.A body whose
can_sleepis False has no parked state to be handed to, so it is freed AWAKE whichever waywakepoints: forbidding sleep forbids it by this route too.Entering KINEMATIC parks nothing. A KINEMATIC body never reports as asleep, and it moves only when it is written to, which is itself a disturbance. So it stays in the pair and overlap search whichever way
wakepoints: a platform streamed in and flipped to KINEMATIC where it stands still fires the triggers it is standing in.
Parking needs a backend that advertises :attr:
Capability.SLEEP. Without it nothing can be held out of the simulation, so a body freed withwake=Falseis simulated from the very next step.Args: handle: Body handle. mode: The new :class:
BodyMode. wake: Whether this write may disturb sleepers (see :meth:sleeping), and whether a body entering DYNAMIC is handed over awake.Raises: ValueError: If the stored mass is not
> 0andmodeis DYNAMIC, or if the body’s shape kind forbidsmode(a concave polygon is STATIC-only).
- abstractmethod set_body_mass(handle: simvx.core.physics.world2d.BodyHandle, mass: float, *, wake: bool = True) None[source]¶
Set a live body’s mass, recomputing its moment from its current shape.
Velocity is preserved, not momentum. The new mass is RETAINED like the create-time one, so it survives a later :meth:
set_body_modeflip and setting it on a STATIC or KINEMATIC body takes effect when that body becomes DYNAMIC.A re-massed body is also a support whose behaviour has changed under whatever is resting on it, so the wake reaches its neighbours too.
Args: handle: Body handle. mass: New mass in kg,
> 0. wake: Whether this write may disturb sleepers (see :meth:sleeping).Raises: ValueError: If
massis not> 0(NaN included).
- abstractmethod set_body_filter(handle: simvx.core.physics.world2d.BodyHandle, collision_layer: int, collision_mask: int, *, wake: bool = True) None[source]¶
Set a live body’s 32-bit layer membership and collision mask together.
One method for the pair because the acceptance rule reads both sides of both bodies. Honoured from the next :meth:
step, and the event streams follow it: a pair that was touching and no longer matches reports an EXIT rather than vanishing silently, one that newly matches reports an ENTER, and an edit that changes no verdict reports nothing at all.Args: handle: Body handle. collision_layer: New 32-bit layer membership. collision_mask: New 32-bit mask of layers this body scans. wake: Whether this write may disturb sleepers (see :meth:
sleeping).
- abstractmethod set_body_material(handle: simvx.core.physics.world2d.BodyHandle, material: simvx.core.physics.material.PhysicsMaterial | None) None[source]¶
Replace a live body’s surface material.
The whole surface at once, because that is what a material IS. See
- Meth:
~simvx.core.physics.world.PhysicsWorld.set_body_material: the world reads the resource’s values and does not retain it, which is safe because the frozen resource has no later edit to miss.
Args: handle: Body handle. material: The new surface, or
Nonefor :data:~simvx.core.physics.material.DEFAULT_PHYSICS_MATERIAL.
- abstractmethod set_body_damping(handle: simvx.core.physics.world2d.BodyHandle, linear: float, angular: float) None[source]¶
Set a live body’s linear and angular damping together.
See :meth:
~simvx.core.physics.world.PhysicsWorld.set_body_damping.Args: handle: Body handle. linear: Per-second linear damping rate (
>= 0, finite). angular: Per-second angular damping rate (>= 0, finite).Raises: ValueError: If either rate is negative, NaN or infinite.
- abstractmethod set_body_gravity_scale(handle: simvx.core.physics.world2d.BodyHandle, scale: float) None[source]¶
Set a live body’s gravity multiplier.
Wakes the body, for the reason
- Meth:
~simvx.core.physics.world.PhysicsWorld.set_body_gravity_scalegives: gravity is applied by integration, which skips a sleeper.
Args: handle: Body handle. scale: New multiplier on the world’s gravity for this body.
Raises: ValueError: If
scaleis NaN or infinite.
- abstractmethod set_body_continuous(handle: simvx.core.physics.world2d.BodyHandle, enabled: bool) None[source]¶
Turn continuous collision detection on or off for a live body.
Honoured only where the backend advertises
- Attr:
~simvx.core.physics.capability.Capability.CONTINUOUS; a backend that does not accepts the call and does nothing, exactly as it already ignores thecontinuousargument to :meth:create_body.
Args: handle: Body handle. enabled: True for continuous (swept) integration, False for discrete.
- abstractmethod set_body_shape(handle: simvx.core.physics.world2d.BodyHandle, shape: simvx.core.physics.world2d.ShapeHandle, *, wake: bool = True) None[source]¶
Swap a live body’s collision shape, keeping everything else.
The body keeps its handle, pose, velocity, mode, mass, filter, sensor flag, material, CCD flag and one-way configuration; only the geometry changes, with the moment of inertia recomputed from the new shape at the retained mass. An overlap the new shape introduces is not resolved at swap time: the ordinary contact solve pushes it apart over the following steps, which is why the body wakes.
The event streams describe the geometry, not the swap: a pair the new shape still touches is NOT announced again, a pair it no longer reaches reports an EXIT, and one it newly reaches reports an ENTER, so replacing a collider with an identical one is silent.
The swap also vacates whatever volume the old geometry held, so the wake reaches the body’s neighbours as well: shrinking a platform under a sleeping crate drops the crate rather than leaving it on a ledge that is no longer there.
Args: handle: Body handle. shape: An opaque shape handle from one of the
create_*factories. wake: Whether this write may disturb sleepers (see :meth:sleeping).Raises: ValueError: If the new shape’s kind forbids the body’s current mode (a concave polygon is a STATIC-only collider).
- abstractmethod body_velocity(handle: simvx.core.physics.world2d.BodyHandle) tuple[simvx.core.math.Vec2, float][source]¶
Read a body’s current
(linear, angular)velocity, per-body.Cold per-body read parallel to :meth:
body_transform; the bulk- Meth:
read_velocitiesstays the hot scatter path.
Returns:
(linear, angular)velocity (Vec2, scalar float in radians/s). Returns zero velocities for an infinite-mass body that was never moved.
- abstractmethod body_transform(handle: simvx.core.physics.world2d.BodyHandle) tuple[simvx.core.math.Vec2, float][source]¶
Read a body’s current pose as
(position, rotation).Cold per-body read parallel to :meth:
body_velocity.Returns:
(position, rotation)(Vec2, scalar float radians).
- abstractmethod body_mass(handle: simvx.core.physics.world2d.BodyHandle) float[source]¶
Read a body’s EFFECTIVE mass in kg, the one an impulse divides by.
The counterpart of :meth:
set_body_mass, and deliberately not a read-back of what was set: a STATIC or KINEMATIC body is infinite-mass whatever mass it was created with, so it answersmath.infand an impulse applied to it moves nothing. Flipping the same body to DYNAMIC restores its retained mass and this returns that.Returns: Mass in kg for a DYNAMIC body,
math.inffor an immovable one.
- abstractmethod sleeping(handle: simvx.core.physics.world2d.BodyHandle) bool[source]¶
True if the body is asleep (skipped by integrate + solve until woken).
STATIC / KINEMATIC bodies are never ‘asleep’ (they were never awake): returns False for them, and a body flipped off DYNAMIC stops reporting as asleep at once, whatever
wake=the flip was given – an immovable body has no motion to resume, so clearing the flag restores this invariant rather than waking anything.A sleeper is woken by anything the solver would otherwise let it miss: a contact, an impulse or force, a write to its own pose, velocity, mode or any of the live-edit setters, AND a change to a body it was in contact with that takes support away. Destroying a body, teleporting it, changing its geometry or its mass, or flipping it to DYNAMIC therefore wakes whatever was resting on it; without that a crate whose support has gone hangs in mid-air for the life of the world, because the ordinary wake-on-contact needs a contact that still exists. Writes that change nothing (re-posing a parked platform where it already is, re-asserting the mode a body already has) wake nothing, so a game may drive both every frame.
Sleep and wake are ISLAND-ATOMIC. A pile parks as a unit: no body commits until every DYNAMIC body it touches, and everything those touch in turn, has come to rest as well. A disturbance that wakes any member of a sleeping island wakes all of it, so nothing a change took support from is left asleep however deep the pile: pull the bottom crate out of a settled stack and the whole stack falls, not just the crate that was on it. What a pile rests ON is in nobody’s island (STATIC and KINEMATIC bodies end the walk), so two piles sharing a floor sleep and wake independently.
Asleep is FROZEN. A sleeping body’s pose is the pose it fell asleep at, every step it stays asleep, so anything that reads a settled scene – a save, a placement check, a golden image – reads the same numbers until something wakes it.
That is an invariant of this interface rather than a courtesy each backend remembers: every mutator that can take support away (:meth:
set_body_transform, :meth:set_body_mode, :meth:set_body_mass,- Meth:
set_body_filter, :meth:set_body_shape, :meth:set_one_way) wakes the body and its neighbours. All but :meth:set_one_waytakewake=Falseto suppress it for the cases where a write is bookkeeping rather than a disturbance: streaming a level in, respawning a pooled object, or an editor writing a value the player cannot feel. Suppression is opt-IN because getting it wrong the other way strands a body in mid-air, which no later step can recover.- Meth:
apply_impulse, :meth:apply_force, :meth:apply_torqueand- Meth:
set_one_wayhave no such flag, because changing what the solver does with the body is the whole point of each of them; a- Meth:
set_one_waythat changes nothing is not a disturbance in the first place, and wakes nothing.
- abstractmethod wake(handle: simvx.core.physics.world2d.BodyHandle) None[source]¶
Wake a sleeping body, whatever its sleep timer had reached.
Reaches only the body it names: the island rule belongs to a change that takes support away, and this call takes nothing away. A body that is already awake, and one that is STATIC or KINEMATIC and so was never asleep, are both a no-op.
Args: handle: Body handle.
- abstractmethod sleep(handle: simvx.core.physics.world2d.BodyHandle) None[source]¶
Put a body to sleep now, without waiting for it to settle.
Freezes the body where it is: skipped by integration and the contact velocity solve until something wakes it, while staying a full collider that still reads back its (frozen) pose. Use it to park a pile a game knows is finished rather than paying for it to come to rest first.
A no-op on a body that is STATIC or KINEMATIC (never awake), and on one whose
can_sleepis False: forbidding sleep means forbidding it.Args: handle: Body handle.
- abstractmethod set_body_can_sleep(handle: simvx.core.physics.world2d.BodyHandle, enabled: bool) None[source]¶
Allow or forbid this body ever falling asleep.
The create-time
can_sleepargument, live. Forbidding it wakes the body if it was asleep; allowing it again lets the body settle from the current step, with no credit for time already spent at rest.A body that may not sleep is simulated every step for the life of the world. That is the point of it, and it is also the cost: it will show up in a profile as a body that never leaves the integrate and solve sets, which is correct rather than a defect.
Args: handle: Body handle. enabled: True to allow sleeping (the default), False to forbid it.
- abstractmethod apply_impulse(handle: simvx.core.physics.world2d.BodyHandle, impulse: simvx.core.math.Vec2, *, at: simvx.core.math.Vec2 | None = None, angular: float = 0.0) None[source]¶
Apply an instantaneous velocity change to a body NOW.
Args: handle: Body handle. impulse: Linear impulse (
Vec2), N*s. at: Optional world-space application point; the offsetr = at - positioncontributes a scalar angular impulse via the 2D cross productcross(r, impulse)scaled by the inverse moment of inertia. angular: Optional explicit scalar angular impulse (radians-equivalent), applied via the inverse moment of inertia.
- abstractmethod apply_force(handle: simvx.core.physics.world2d.BodyHandle, force: simvx.core.math.Vec2, *, at: simvx.core.math.Vec2 | None = None) None[source]¶
Accumulate a continuous force, applied during the NEXT :meth:
step.Auto-cleared at the end of the step (re-add each fixed step to sustain). Inert on non-DYNAMIC.
atadds a scalar torquecross(r, force).
- abstractmethod apply_torque(handle: simvx.core.physics.world2d.BodyHandle, torque: float) None[source]¶
Accumulate a continuous scalar torque for the NEXT :meth:
step.Auto-cleared after the step (re-add each fixed step to sustain). Inert on non-DYNAMIC. Applied via the inverse moment of inertia.
- abstractmethod create_fixed_joint(a: simvx.core.physics.world2d.BodyHandle, b: simvx.core.physics.world2d.BodyHandle) simvx.core.physics.world2d.JointHandle[source]¶
Weld two bodies: lock their full relative transform.
The current offset and relative angle are captured in
a’s frame at create and held there, so the two move as one rigid assembly and the whole assembly swings round whenaturns.
- abstractmethod create_pin_joint(a: simvx.core.physics.world2d.BodyHandle, b: simvx.core.physics.world2d.BodyHandle, anchor: simvx.core.math.Vec2) simvx.core.physics.world2d.JointHandle[source]¶
Pin two bodies at a single world-space point, rotation free.
The two bodies cannot separate at
anchorbut turn freely about it, so chaining pins builds a rope and pinning to a STATIC body builds a pendulum. The anchor is captured at create as a point in EACH body’s own frame and turned back into world axes by that body’s current angle every solver pass, so a pin on a spinning body orbits with it.Built-in backend caveat: convergence is a few sequential-impulse iterations, so a loaded chain sags and a hard yank stretches a link before it is pulled straight; the pymunk backend hangs the same chain on its rest length. See
docs/core/physics_backends.mdfor the measured sag.
- abstractmethod create_hinge_joint(a: simvx.core.physics.world2d.BodyHandle, b: simvx.core.physics.world2d.BodyHandle, anchor: simvx.core.math.Vec2) simvx.core.physics.world2d.JointHandle[source]¶
Hinge two bodies at
anchor.2D rotation is 1-DOF, so a 2D hinge has no
axisargument: it is a pin atanchor(this tier). Motors and angular limits are a follow-on.
- abstractmethod create_spring_joint(a: simvx.core.physics.world2d.BodyHandle, b: simvx.core.physics.world2d.BodyHandle, rest_length: float, stiffness: float, damping: float) simvx.core.physics.world2d.JointHandle[source]¶
Soft distance-spring between the two body centres (compliant).
rest_length < 0auto-captures the current centre distance as the rest length (the common “spring at its natural length on creation” case).
- abstractmethod create_groove_joint(a: simvx.core.physics.world2d.BodyHandle, b: simvx.core.physics.world2d.BodyHandle, groove_a: simvx.core.math.Vec2, groove_b: simvx.core.math.Vec2, anchor_b: simvx.core.math.Vec2) simvx.core.physics.world2d.JointHandle[source]¶
Constrain
b’s anchor to slide along a groove ona(2D-only).The pymunk-native slider-on-a-line constraint with no 3D equivalent. The groove is the segment
[groove_a, groove_b]ina’s frame;b’sanchor_b(inb’s frame) is constrained to lie on that line. Both stay in the frames they were given in, so the rail turns witha.What every backend promises is the anchor on the rail:
anchor_b, placed byb’s own current rotation, stays on the segmenta’s current rotation puts there, however either body is turning. That is the whole of the cross-backend contract, and the 2D contract suite measures it on both implementations.What is NOT promised is the slider’s own rotation. A groove leaves it free, so what the slider ends up pointing at is whatever each solver’s residual torque about the anchor made of it, and the two backends part company: on the suite’s own scenario – a carrier turning at 1 rad/s under a slider spun at 3 rad/s about an anchor a quarter of a unit off its centre – they are 0.33 rad apart after two seconds and 3.14 rad apart after ten, with both anchors still on the rail. A scene that needs the slider held at an angle welds or pins it rather than reading a heading off the groove.
Motion along the groove is free and no backend drives it: a slider is pushed by forces on
b. The two backends stop it at the endpoints with different firmness, measured indocs/core/physics_backends.md.
- abstractmethod remove_joint(handle: simvx.core.physics.world2d.JointHandle) None[source]¶
Remove a constraint; the handle is invalid afterwards.
A no-op if
handleis unknown (already removed, or silently dropped because one of its bodies was destroyed).
- abstract property body_count: int[source]¶
Number of bodies currently in the world (skip-empty-world fast path).
- abstractmethod clear() None[source]¶
Remove every body and joint, emptying the world.
2D sibling of :meth:
~simvx.core.physics.world.PhysicsWorld.clear. Returns the world to an empty state (body_count == 0) WITHOUT discarding the world, its- Attr:
gravity, or its backend; per-step edge-diff buffers and warm-start cache are reset; cached shape handles stay valid; handle counters keep advancing. After :meth:clear, re-:meth:register_bodiesbefore the bulk readers.
- abstractmethod step(dt: float) None[source]¶
Advance the whole world once by a fixed timestep
dt(seconds).
- abstractmethod drain_contact_events() list[simvx.core.physics.world2d.ContactEvent2D][source]¶
Return and CLEAR this step’s buffered enter/exit contact events.
- abstractmethod drain_overlap_events() list[simvx.core.physics.world2d.OverlapEvent2D][source]¶
Return and CLEAR this step’s buffered sensor-overlap events.
- abstractmethod set_one_way(handle: simvx.core.physics.world2d.BodyHandle, enabled: bool, normal: simvx.core.math.Vec2 = _DEFAULT_UP_2D) None[source]¶
Mark a body as a one-way platform (2D-only).
When enabled, the body only collides with bodies approaching from the
+normalside (landing on it); bodies passing up through it (moving along+normal) are not blocked.Turning one-way ON takes support away from whatever was resting on the side the platform stops resolving, so it wakes the body’s neighbours as well as the body, exactly as :meth:
set_body_filterdoes (see- Meth:
sleeping). A sleeping stack on a platform that becomes pass-through therefore falls, instead of hanging above geometry that no longer holds it. Any REAL change wakes the same set – turning one-way off or editing the normal adds support rather than takes it, but the bodies affected still need a step awake to settle against the new rule. A write that changes neither the flag nor the normal wakes nothing, so a game may drive this every frame.
Args: handle: Body handle. enabled: Whether one-way filtering is active. normal: World-space “solid side” normal (
Vec2, unit); the side a lander must approach from. Defaults to+Y(a floor).
- abstractmethod register_bodies(handles: list[simvx.core.physics.world2d.BodyHandle]) None[source]¶
Fix the body->row order used by the bulk readers.
After this call, :meth:
read_transforms/ :meth:read_velocitiesfill rowiwith the state ofhandles[i].len(handles)isN.
- abstractmethod read_transforms(out: numpy.ndarray) None[source]¶
Fill
outwith current body transforms, in place.Args: out: Pre-allocated array of shape
(N, 4), dtypefloat32, C-contiguous, whereNmatches the most recent :meth:register_bodies. Each row is[px, py, cos(theta), sin(theta)]: position xy followed by the rotation as a unit vector (cos/sinof the scalar angle, NOT the bare angle: interpolation lerps the unit vector with no +-pi wraparound). Rowicorresponds tohandles[i].
- abstractmethod read_velocities(out: numpy.ndarray) None[source]¶
Fill
outwith current body velocities, in place.Args: out: Pre-allocated array of shape
(N, 3), dtypefloat32, C-contiguous, whereNmatches the most recent :meth:register_bodies. Each row is[lx, ly, omega]: linear velocity xy followed by scalar angular velocity (radians/s, CCW positive). Rowicorresponds tohandles[i].
- abstractmethod raycast(origin: simvx.core.math.Vec2, direction: simvx.core.math.Vec2, max_dist: float, *, mask: int = 4294967295) simvx.core.physics.world2d.RaycastHit2D | None[source]¶
Cast a ray and return the nearest hit, or
None.
- abstractmethod raycast_all(origin: simvx.core.math.Vec2, direction: simvx.core.math.Vec2, max_dist: float, *, mask: int = 4294967295) list[simvx.core.physics.world2d.RaycastHit2D][source]¶
Cast a ray and return EVERY hit within
max_dist, sorted.
- abstractmethod shapecast(shape: simvx.core.physics.world2d.ShapeHandle, origin: simvx.core.math.Vec2, direction: simvx.core.math.Vec2, max_dist: float, *, mask: int = 4294967295) simvx.core.physics.world2d.SweepHit2D | None[source]¶
Sweep a shape along a ray, return the earliest-TOI contact.
- abstractmethod overlap(shape: simvx.core.physics.world2d.ShapeHandle, transform: Any, *, mask: int = 4294967295) list[simvx.core.physics.world2d.BodyHandle][source]¶
Return all bodies a static shape overlaps at
transform.Every body in the table is visible here, including a KINEMATIC character body.
- abstractmethod sweep_body(handle: simvx.core.physics.world2d.BodyHandle, motion: simvx.core.math.Vec2, *, from_transform: tuple[simvx.core.math.Vec2, float] | None = None, skin: float = 0.0) simvx.core.physics.world2d.SweepHit2D | None[source]¶
Cast a body’s shape along
motionand report the first blocker.2D sibling of :meth:
~simvx.core.physics.world.PhysicsWorld.sweep_body, with the same non-mutating contract and the same blocking predicate: not the mover, not a sensor, the canonical AND layer/mask rule, the 2D one-way filter, and an opposing contact normal (dot(motion_dir, separating_normal) < -1e-4). A contact reported at distance zero must still carry a true surface normal, never the cast axis.Args: handle: Body handle of the mover. motion: World-space displacement to sweep along (
Vec2). from_transform: Optional(position, rotation)pose to cast from instead of the body’s own stored pose; rotation is a scalar in radians. skin: Contact clearance to leave at the blocker, in world units; ignored by a substepped backend (see :attr:SweepHit2D.distance).Returns: The nearest blocking :class:
SweepHit2D, orNonewhen clear.
- move_and_collide(handle: simvx.core.physics.world2d.BodyHandle, motion: simvx.core.math.Vec2) simvx.core.physics.world2d.SweepHit2D | None[source]¶
Move a kinematic body by
motion, stop at the first contact.Concrete composition of :meth:
sweep_bodyand :meth:set_body_transform, identical on every backend: sweep, then advance to exactlySweepHit2D.distancealongmotion(the fullmotionwhen clear). The collide-and-slide policy for a character body lives insimvx.core.physics.slide, not here.
- __slots__¶
()
- simvx.core.physics.world2d.__all__¶
[‘BodyMode’, ‘DEFAULT_ANGULAR_DAMPING’, ‘DEFAULT_GRAVITY_SCALE’, ‘DEFAULT_LINEAR_DAMPING’, ‘Capabili…