simvx.core.physics.pymunk_backend¶
What maps cleanly to Chipmunk2D¶
bodies / motion types:
BodyModeSTATIC/KINEMATIC/DYNAMIC ->pymunk.BodySTATIC/KINEMATIC/DYNAMIC; mass + per-shape moment.shapes: Circle/Box(Poly)/Capsule(rounded poly)/Segment/ConvexPolygon(Poly)/ ConcavePolygon(static segment soup) ->
pymunk.Circle/pymunk.Poly/pymunk.Segment.step:
space.step(dt)at the caller’s fixed timestep.forces:
apply_force_at_world_point/apply_impulse_at_world_point/ per-step torque accumulation.joints: fixed/pin/hinge/spring/groove ->
PinJoint/PivotJoint/DampedSpring/GrooveJoint(a weld is a PivotJoint + a stiffDampedRotarySpringto lock the relative angle).queries: raycast ->
segment_query; shapecast/overlap ->shape_query.events: a per-pair
on_collisionhandler edge-diffs body / sensor pairs into the engine’s two event streams (contacts and sensor overlaps).
What needs care (documented honesty caveats)¶
CombineMode: Chipmunk MULTIPLIES both coefficients (
mu_a * mu_bfor friction,e_a * e_bfor elasticity) and has no per-contact combine-mode switch, so the engine’s per-coefficient :class:~simvx.core.physics.material.CombineModecannot be honoured natively for every neighbour pair. The raw coefficients are handed to Chipmunk and its fixed rule decides (see_combine_into_shape).swept motion: Chipmunk exposes no swept-shape time-of-impact for an un-added shape, so :meth:
PymunkPhysics2D.sweep_bodysubsteps a transient probe body withshape_query, exactly like :class:BuiltinPhysics2D.distanceis therefore the last substep proved non-penetrating, not an exact touch distance.KINEMATIC bulk WRITE:
pymunk.batchSET is experimental, so per-body writes are used on the (cold) set paths; the bulk READ usespymunk.batchzero-copy when present.collision layers: Chipmunk’s
cpBitmaskis 32 bits wide, so only the low 32 bits of a collision layer survive into the native broadphase. A body whose layer uses only bits at or above bit 32 is invisible to the broadphase and therefore to queries, while Builtin (full-width Python ints) still sees it.
PymunkPhysics2D: the optional native (Chipmunk2D / pymunk) 2D backend.
A native :class:~simvx.core.physics.world2d.Physics2DWorld implementation and
an OPTIONAL accelerator: installing pymunk (the pymunk extra) makes
- class:
~simvx.core.physics.root.PhysicsRoot2Dauto-select it, exactly mirroring the engine’s miniaudio “installed -> used” model. Whenpymunkis absent, auto-discovery silently falls back to- class:
~simvx.core.physics.builtin.world2d.BuiltinPhysics2D(no crash, no warning spam).
The contract is parity, not bit-identity: “switching backends never changes how a
game plays”. A game using CharacterBody2D must behave the same (within
tolerance) on pymunk as on Builtin: a character is an ordinary KINEMATIC body here
too, and the shared collide-and-slide policy in
- mod:
simvx.core.physics.slidedrives it through the same- meth:
~simvx.core.physics.world2d.Physics2DWorld.sweep_bodyprimitive, even though Chipmunk2D’s solver differs from the pure-Python tier.
Module Contents¶
Classes¶
Native Chipmunk2D (pymunk) 2D backend implementing the full 2D world interface. |
Functions¶
Self-register the pymunk backend with the backend registry. |
Data¶
API¶
- class simvx.core.physics.pymunk_backend.PymunkPhysics2D(*, gravity: simvx.core.math.Vec2 | None = None)[source]¶
Bases:
simvx.core.physics.world2d.Physics2DWorldNative Chipmunk2D (pymunk) 2D backend implementing the full 2D world interface.
See :class:
~simvx.core.physics.world2d.Physics2DWorldfor the contract. Every@abstractmethodis implemented over a singlepymunk.Space.Initialization
Initialise the world.
Args: gravity: World gravity acceleration vector (
Vec2), metres/s^2. Y-up:Vec2(0, -9.81)is “down”.- capabilities() frozenset[simvx.core.physics.capability.Capability][source]¶
Advertise the measured contact impulse and sensors-see-static.
Chipmunk exposes each arbiter’s
total_impulse, so the ENTER payload is a real measurement: the solver’s total impulse projected onto the contact normal and summed over the pair’s arbiters (see_collect_enter_impulses). No cross-platform determinism, vehicles or soft bodies.SENSOR_DETECTS_STATICis real, and it is this adapter that makes it so: Chipmunk keeps static shapes in an index of their own and queries it only against the moving one, so a static-vs-static pair is never generated, and a sensor is therefore held KINEMATIC (see- Func:
_motion_mode) to put it in the index that IS queried. Measured on a sensor overlapping one box: one ENTER whichever mode the box is in, and one for another STATIC sensor too, where before the STATIC pairs reported nothing.
SLEEPis absent, and that is a property of Chipmunk rather than of this adapter: acpSpacesleeps nothing until it is given a finite sleep-time threshold, and asking a body in such a space to sleep aborts the process at the C level instead of raising. Nothing here ever falls asleep, so :meth:sleepingis permanently False and the whole sleep surface is inert; see the capability’s own documentation for what a caller does about it.
- clear() None[source]¶
Remove every body and joint, emptying the world.
See :meth:
~simvx.core.physics.world2d.Physics2DWorld.clear. Routes through the existingremove_joint/destroy_bodyso each is correctly removed from the livepymunk.Space(joints first, then bodies). The per-step edge-diff buffers are reset too. Gravity, shapes, and handle counters are intentionally NOT reset.
- create_circle(radius: float) simvx.core.physics.world2d.ShapeHandle[source]¶
- create_box(half_extents: simvx.core.math.Vec2) simvx.core.physics.world2d.ShapeHandle[source]¶
- create_capsule(radius: float, height: float) simvx.core.physics.world2d.ShapeHandle[source]¶
- create_segment(a: simvx.core.math.Vec2, b: simvx.core.math.Vec2, radius: float = 0.0) simvx.core.physics.world2d.ShapeHandle[source]¶
- create_convex_polygon(points: numpy.ndarray) simvx.core.physics.world2d.ShapeHandle[source]¶
- create_concave_polygon(segments: numpy.ndarray) simvx.core.physics.world2d.ShapeHandle[source]¶
- destroy_shape(shape: simvx.core.physics.world2d.ShapeHandle) None[source]¶
Release this world’s record of a shape handle.
See :meth:
~simvx.core.physics.world2d.Physics2DWorld.destroy_shape. A handle here names a_ShapeDefspec, not a livepymunk.Shape: the Chipmunk shapes belong to the bodies built from it, which keep them and keep simulating. Dropping the spec is the whole of it. Unknown handles are a silent no-op.
- create_body(shape: simvx.core.physics.world2d.ShapeHandle, body_type: simvx.core.physics.world.BodyMode, transform: object, *, 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]¶
- destroy_body(handle: simvx.core.physics.world2d.BodyHandle) None[source]¶
- set_body_transform(handle: simvx.core.physics.world2d.BodyHandle, transform: object, *, scale: simvx.core.math.Vec2 | None = None, wake: bool = True) None[source]¶
- set_body_velocity(handle: simvx.core.physics.world2d.BodyHandle, linear: simvx.core.math.Vec2, angular: float = 0.0) None[source]¶
- set_body_mode(handle: simvx.core.physics.world2d.BodyHandle, mode: simvx.core.physics.world.BodyMode, *, wake: bool = True) None[source]¶
- set_body_mass(handle: simvx.core.physics.world2d.BodyHandle, mass: float, *, wake: bool = True) None[source]¶
- set_body_filter(handle: simvx.core.physics.world2d.BodyHandle, collision_layer: int, collision_mask: int, *, wake: bool = True) None[source]¶
- set_body_material(handle: simvx.core.physics.world2d.BodyHandle, material: simvx.core.physics.material.PhysicsMaterial | None) None[source]¶
- set_body_damping(handle: simvx.core.physics.world2d.BodyHandle, linear: float, angular: float) None[source]¶
- set_body_gravity_scale(handle: simvx.core.physics.world2d.BodyHandle, scale: float) None[source]¶
- set_body_continuous(handle: simvx.core.physics.world2d.BodyHandle, enabled: bool) None[source]¶
Accept the flag and integrate discretely: Chipmunk2D has no CCD.
The one live setter this backend cannot honour, for the same reason it already ignores
create_body’scontinuousargument: Chipmunk has no swept / speculative integration of any kind, so there is nothing to switch on. Advertised through the absence of- Attr:
~simvx.core.physics.capability.Capability.CONTINUOUSrather than raised, so a scene that merely prefers CCD still runs and a caller that requires it can check for it by name instead of by backend.
- set_body_shape(handle: simvx.core.physics.world2d.BodyHandle, shape: simvx.core.physics.world2d.ShapeHandle, *, wake: bool = True) None[source]¶
- body_velocity(handle: simvx.core.physics.world2d.BodyHandle) tuple[simvx.core.math.Vec2, float][source]¶
- body_transform(handle: simvx.core.physics.world2d.BodyHandle) tuple[simvx.core.math.Vec2, float][source]¶
- body_mass(handle: simvx.core.physics.world2d.BodyHandle) float[source]¶
- wake(handle: simvx.core.physics.world2d.BodyHandle) None[source]¶
- sleep(handle: simvx.core.physics.world2d.BodyHandle) None[source]¶
Accept the call and do nothing: this space sleeps nothing.
The one sleep-surface call that CANNOT be forwarded. Chipmunk asserts
sleep_time_threshold < INFINITYinsidecpBodySleepand aborts the whole process when it does not hold, and this space leaves the threshold at its default of infinity, so forwarding would turn a harmless call into a crash. Advertised through the absence of- Attr:
~simvx.core.physics.capability.Capability.SLEEPrather than raised, exactly as :meth:set_body_continuoushandles the missing CCD.
- set_body_can_sleep(handle: simvx.core.physics.world2d.BodyHandle, enabled: bool) None[source]¶
Record the flag; nothing sleeps here, so nothing changes.
Kept honest rather than dropped: a caller that reads it back gets what it set, and forbidding sleep is already true of every body in this space.
- sleeping(handle: simvx.core.physics.world2d.BodyHandle) bool[source]¶
- 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_force(handle: simvx.core.physics.world2d.BodyHandle, force: simvx.core.math.Vec2, *, at: simvx.core.math.Vec2 | None = None) None[source]¶
- apply_torque(handle: simvx.core.physics.world2d.BodyHandle, torque: float) None[source]¶
- create_fixed_joint(a: simvx.core.physics.world2d.BodyHandle, b: simvx.core.physics.world2d.BodyHandle) simvx.core.physics.world2d.JointHandle[source]¶
- 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]¶
- 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]¶
- 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]¶
- 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]¶
- remove_joint(handle: simvx.core.physics.world2d.JointHandle) None[source]¶
- set_one_way(handle: simvx.core.physics.world2d.BodyHandle, enabled: bool, normal: simvx.core.math.Vec2 = _DEFAULT_UP_2D) None[source]¶
- drain_contact_events() list[simvx.core.physics.world2d.ContactEvent2D][source]¶
- drain_overlap_events() list[simvx.core.physics.world2d.OverlapEvent2D][source]¶
- register_bodies(handles: list[simvx.core.physics.world2d.BodyHandle]) None[source]¶
- raycast(origin: simvx.core.math.Vec2, direction: simvx.core.math.Vec2, max_dist: float, *, mask: int = 4294967295) simvx.core.physics.world2d.RaycastHit2D | None[source]¶
- 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]¶
- 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]¶
- overlap(shape: simvx.core.physics.world2d.ShapeHandle, transform: object, *, mask: int = 4294967295) list[simvx.core.physics.world2d.BodyHandle][source]¶
- 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]¶
Substepped, non-mutating sweep of a body’s shapes against the space.
See :meth:
~simvx.core.physics.world2d.Physics2DWorld.sweep_body. Chipmunk exposes no swept-shape time-of-impact for an un-added shape, so this substeps alongmotionwith a step count sized from the mover’s smallest feature and capped at 64; the substep that penetrates a blocking body brackets the contact, and the bracket is BISECTED against that blocker alone, sodistanceis a bound good to|motion| / (substeps * 2**_SWEEP_REFINE_STEPS)rather than to one whole substep. It is still a lower bound and may be exactly0.0for a sweep that begins in contact.Structurally non-mutating: the sweep runs on a transient un-added probe body built by :meth:
_probe_body, so the space’s spatial index is never touched (the mover’s own shapes stay indexed at its real pose, andfrom_transformcosts nothing).skinis accepted and IGNORED: the substep quantum already exceeds any sane skin.A body blocks only when it is not the mover, is not a sensor, passes the canonical AND layer/mask rule, is not rejected by the one-way filter, and presents a contact that OPPOSES the sweep. Fraction
0is never sampled (the substep loop starts atst = 1), so this backend cannot report the cast axis as a normal.
- property solver_iterations: int¶
- property position_iterations: int¶
- property sleep_time_threshold: float¶
- property sleep_velocity_threshold: float¶
- property contact_slop: float¶
- move_and_collide(handle: simvx.core.physics.world2d.BodyHandle, motion: simvx.core.math.Vec2) simvx.core.physics.world2d.SweepHit2D | None¶
- __slots__¶
()
- simvx.core.physics.pymunk_backend.register() None[source]¶
Self-register the pymunk backend with the backend registry.
Called on import (below) and idempotent (
register_backendreplaces a same- named entry), so importing this module installs the"pymunk"backend as an auto-discoverable native (mirroring miniaudio’s “installed -> used” model).
- simvx.core.physics.pymunk_backend.__all__¶
[‘PymunkPhysics2D’, ‘register’]