simvx.core.physics.pymunk_backend

Role

The FIRST real native :class:~simvx.core.physics.world2d.Physics2DWorld implementation, proving the P1 backend seam end-to-end (until now only stub backends exercised it). It is an OPTIONAL accelerator: installing pymunk (the pymunk extra) makes :class:~simvx.core.physics.root.PhysicsRoot2D auto-select it, exactly mirroring the engine’s miniaudio “installed -> used” model. When pymunk is 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, so the SAME

class:

~simvx.core.physics.world2d.CharacterMoveResult2D semantics hold even though Chipmunk2D’s solver differs from the pure-Python tier.

What maps cleanly to Chipmunk2D

  • bodies / motion types: BodyMode STATIC/KINEMATIC/DYNAMIC -> pymunk.Body STATIC/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 seam’s fixed dt.

  • 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 stiff DampedRotarySpring to lock the relative angle).

  • queries: raycast -> segment_query; shapecast/overlap -> shape_query.

  • events: a per-pair on_collision handler edge-diffs body / sensor pairs into the seam’s two event streams.

What needs care (documented honesty caveats)

  • CombineMode: Chipmunk multiplies friction (mu_a * mu_b) and takes the max of elasticity. There is no per-contact combine-mode switch, so the seam combines the coefficients itself (via :func:~simvx.core.physics.material._combine) at body-creation time using a single representative-material pair where possible; a per-PAIR combine mode that differs across every neighbour cannot be honoured natively and degrades to Chipmunk’s built-in rule (see _combine_into_shape).

  • character controller: pymunk has NO native character controller, so it is implemented exactly like :class:BuiltinPhysics2D – a kinematic collider swept with shape_query + deflect-and-slide + floor/slope classification – so the CharacterMoveResult2D contract is identical.

  • KINEMATIC bulk WRITE: pymunk.batch SET is experimental, so per-body writes are used on the (cold) set paths; the bulk READ uses pymunk.batch zero-copy when present.

PymunkPhysics2D: the optional native (Chipmunk2D / pymunk) 2D backend (Stage P4).

Module Contents

Classes

PymunkPhysics2D

Native Chipmunk2D (pymunk) 2D backend implementing the full seam.

Functions

register

Self-register the pymunk backend with the selection seam.

Data

API

class simvx.core.physics.pymunk_backend.PymunkPhysics2D(*, gravity: simvx.core.math.Vec2 | None = None)

Bases: simvx.core.physics.world2d.Physics2DWorld

Native Chipmunk2D (pymunk) 2D backend implementing the full seam.

See :class:~simvx.core.physics.world2d.Physics2DWorld for the contract. Every @abstractmethod is implemented over a single pymunk.Space; the character controller is a kinematic collider with deflect-and-slide (pymunk has no native one), so the CharacterMoveResult2D contract matches Builtin.

Initialization

Initialise the world.

Args: gravity: World gravity acceleration vector (Vec2), metres/s^2. Y-up: Vec2(0, -9.81) is “down”.

property body_count: int
clear() None

Remove every body, character, and joint, emptying the world.

See :meth:~simvx.core.physics.world2d.Physics2DWorld.clear. Routes through the existing remove_joint / destroy_character / destroy_body so each is correctly removed from the live pymunk.Space (joints first, then characters, 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
create_box(half_extents: simvx.core.math.Vec2) simvx.core.physics.world2d.ShapeHandle
create_capsule(radius: float, height: float) simvx.core.physics.world2d.ShapeHandle
create_segment(a: simvx.core.math.Vec2, b: simvx.core.math.Vec2, radius: float = 0.0) simvx.core.physics.world2d.ShapeHandle
create_convex_polygon(points: numpy.ndarray) simvx.core.physics.world2d.ShapeHandle
create_concave_polygon(segments: numpy.ndarray) simvx.core.physics.world2d.ShapeHandle
create_body(shape: simvx.core.physics.world2d.ShapeHandle, body_type: simvx.core.physics.world.BodyMode, transform: object, *, mass: float = 1.0, collision_layer: int = 1, collision_mask: int = 4294967295, is_sensor: bool = False, friction: float = 0.5, restitution: float = 0.0, friction_combine: simvx.core.physics.material.CombineMode = CombineMode.AVERAGE, restitution_combine: simvx.core.physics.material.CombineMode = CombineMode.AVERAGE, continuous: bool = False) simvx.core.physics.world2d.BodyHandle
destroy_body(handle: simvx.core.physics.world2d.BodyHandle) None
set_body_transform(handle: simvx.core.physics.world2d.BodyHandle, transform: object) None
set_body_velocity(handle: simvx.core.physics.world2d.BodyHandle, linear: simvx.core.math.Vec2, angular: float = 0.0) None
set_body_mode(handle: simvx.core.physics.world2d.BodyHandle, mode: simvx.core.physics.world.BodyMode) None
body_velocity(handle: simvx.core.physics.world2d.BodyHandle) tuple[simvx.core.math.Vec2, float]
body_transform(handle: simvx.core.physics.world2d.BodyHandle) tuple[simvx.core.math.Vec2, float]
sleeping(handle: simvx.core.physics.world2d.BodyHandle) bool
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
apply_force(handle: simvx.core.physics.world2d.BodyHandle, force: simvx.core.math.Vec2, *, at: simvx.core.math.Vec2 | None = None) None
apply_torque(handle: simvx.core.physics.world2d.BodyHandle, torque: float) None
create_fixed_joint(a: simvx.core.physics.world2d.BodyHandle, b: simvx.core.physics.world2d.BodyHandle) simvx.core.physics.world2d.JointHandle
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
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
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
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
remove_joint(handle: simvx.core.physics.world2d.JointHandle) None
set_one_way(handle: simvx.core.physics.world2d.BodyHandle, enabled: bool, normal: simvx.core.math.Vec2 = _DEFAULT_UP_2D) None
step(dt: float) None
drain_contact_events() list[simvx.core.physics.world2d.ContactEvent2D]
drain_overlap_events() list[simvx.core.physics.world2d.OverlapEvent2D]
register_bodies(handles: list[simvx.core.physics.world2d.BodyHandle]) None
read_transforms(out: numpy.ndarray) None
read_velocities(out: numpy.ndarray) None
raycast(origin: simvx.core.math.Vec2, direction: simvx.core.math.Vec2, max_dist: float, *, mask: int = 4294967295) simvx.core.physics.world2d.RaycastHit2D | None
raycast_all(origin: simvx.core.math.Vec2, direction: simvx.core.math.Vec2, max_dist: float, *, mask: int = 4294967295) list[simvx.core.physics.world2d.RaycastHit2D]
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.Contact2D | None
overlap(shape: simvx.core.physics.world2d.ShapeHandle, transform: object, *, mask: int = 4294967295) list[simvx.core.physics.world2d.BodyHandle]
move_and_collide(handle: simvx.core.physics.world2d.BodyHandle, motion: simvx.core.math.Vec2) simvx.core.physics.world2d.Contact2D | None
create_character(shape: simvx.core.physics.world2d.ShapeHandle, transform: object, *, up: simvx.core.math.Vec2 = _DEFAULT_UP_2D, slope_limit: float = math.radians(45.0), step_height: float = 0.0, skin_width: float = 0.001, collision_layer: int = 1, collision_mask: int = 4294967295) simvx.core.physics.world2d.CharacterHandle
destroy_character(handle: simvx.core.physics.world2d.CharacterHandle) None
set_character_transform(handle: simvx.core.physics.world2d.CharacterHandle, transform: object) None
character_transform(handle: simvx.core.physics.world2d.CharacterHandle) tuple[simvx.core.math.Vec2, float]
character_move_and_slide(handle: simvx.core.physics.world2d.CharacterHandle, velocity: simvx.core.math.Vec2, dt: float, *, up: simvx.core.math.Vec2, max_slides: int = 4) simvx.core.physics.world2d.CharacterMoveResult2D

Collide-and-slide identical in contract to :meth:BuiltinPhysics2D.character_move_and_slide.

Sweeps the kinematic character along velocity * dt; on each blocking contact it advances to the contact, classifies floor / wall / ceiling from the normal vs up (+ slope_limit), deflects remaining motion AND velocity out of the surface, and repeats up to max_slides. A dedicated downward ground probe establishes on_floor / floor_normal for a character resting on a surface (the horizontal sweep ignores the non-opposing floor). Step-up over step_height is a single up/forward/down probe (basic tier; multi-step stairs are a follow-on).

capabilities() frozenset[simvx.core.physics.capability.Capability]
__slots__

()

simvx.core.physics.pymunk_backend.register() None

Self-register the pymunk backend with the selection seam.

Called on import (below) and idempotent (register_backend replaces 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’]