Python Pymunk Physics — Deep Dive

Chipmunk2D Under the Hood

Pymunk wraps Chipmunk2D via CFFI (C Foreign Function Interface). The C library handles the computationally expensive work — broadphase collision detection, narrowphase contact generation, constraint solving, and position integration. Python code sets up the simulation and reads results, keeping the per-frame Python overhead minimal.

Chipmunk2D uses a Separating Axis Theorem (SAT) approach for narrowphase collision between convex polygons, and specialized routines for circle-circle and circle-polygon cases. Broadphase uses a spatial hash that partitions the world into grid cells, so only nearby shape pairs are tested.

Setting Up a Production Simulation

import pymunk

# Create the world
space = pymunk.Space()
space.gravity = (0, -981)  # pixels per second squared
space.damping = 0.9         # global velocity damping

# Static ground
ground_body = space.static_body
ground_shape = pymunk.Segment(ground_body, (0, 50), (800, 50), 5)
ground_shape.elasticity = 0.5
ground_shape.friction = 0.8
space.add(ground_shape)

# Dynamic ball
mass = 1.0
radius = 20
moment = pymunk.moment_for_circle(mass, 0, radius)
ball_body = pymunk.Body(mass, moment)
ball_body.position = (400, 500)
ball_shape = pymunk.Circle(ball_body, radius)
ball_shape.elasticity = 0.7
ball_shape.friction = 0.5
space.add(ball_body, ball_shape)

Moment of Inertia

The moment parameter controls how easily an object rotates. Pymunk provides helper functions:

FunctionUse Case
moment_for_circle(mass, inner_r, outer_r)Balls, wheels
moment_for_box(mass, (w, h))Rectangles
moment_for_poly(mass, vertices)Arbitrary convex polygons
pymunk.infObject that never rotates

Getting the moment wrong makes objects spin unrealistically fast or refuse to rotate at all.

Collision Filtering

Collision Types and Handlers

Each shape has a collision_type integer. You register handlers for specific pairs:

BALL = 1
WALL = 2
SENSOR = 3

ball_shape.collision_type = BALL
wall_shape.collision_type = WALL
sensor_shape.collision_type = SENSOR

def ball_hits_wall(arbiter, space, data):
    # Play bounce sound, spawn particles
    ball_shape, wall_shape = arbiter.shapes
    impact_speed = arbiter.total_ke  # kinetic energy of impact
    if impact_speed > 5000:
        spawn_particles(arbiter.contact_point_set.points[0].point_a)
    return True  # allow collision

handler = space.add_collision_handler(BALL, WALL)
handler.begin = ball_hits_wall

Callback Phases

Collision handlers have four phases:

PhaseWhenTypical Use
beginFirst frame of contactAccept/reject collision, trigger effects
pre_solveEvery frame during contact, before resolutionModify surface properties dynamically
post_solveEvery frame during contact, after resolutionRead impulse forces for damage calculation
separateContact endsStop looping sound effects

Categories and Masks

For bulk filtering without handlers, use bitmask categories:

from pymunk import ShapeFilter

CATEGORY_PLAYER = 0b0001
CATEGORY_ENEMY  = 0b0010
CATEGORY_BULLET = 0b0100
CATEGORY_WALL   = 0b1000

# Player collides with walls and enemies, not own bullets
player_shape.filter = ShapeFilter(
    categories=CATEGORY_PLAYER,
    mask=CATEGORY_WALL | CATEGORY_ENEMY
)

# Bullets collide with walls and enemies, not the player who fired
bullet_shape.filter = ShapeFilter(
    categories=CATEGORY_BULLET,
    mask=CATEGORY_WALL | CATEGORY_ENEMY
)

Advanced Constraints

Ragdoll Construction

def create_limb(space, pos, size, mass=1.0):
    moment = pymunk.moment_for_box(mass, size)
    body = pymunk.Body(mass, moment)
    body.position = pos
    shape = pymunk.Poly.create_box(body, size)
    shape.friction = 0.5
    space.add(body, shape)
    return body

torso = create_limb(space, (400, 300), (30, 60), mass=5.0)
head = create_limb(space, (400, 345), (20, 20), mass=1.0)
upper_arm_l = create_limb(space, (375, 320), (10, 30))
lower_arm_l = create_limb(space, (375, 290), (8, 25))

# Connect head to torso with a pivot (neck joint)
neck = pymunk.PivotJoint(torso, head, (400, 330))
space.add(neck)

# Limit neck rotation
neck_limit = pymunk.RotaryLimitJoint(torso, head, -0.3, 0.3)
space.add(neck_limit)

# Shoulder joint
shoulder_l = pymunk.PivotJoint(torso, upper_arm_l, (385, 325))
space.add(shoulder_l)

# Elbow joint
elbow_l = pymunk.PivotJoint(upper_arm_l, lower_arm_l, (375, 305))
elbow_limit = pymunk.RotaryLimitJoint(upper_arm_l, lower_arm_l, 0, 2.5)
space.add(elbow_l, elbow_limit)

Vehicle with Suspension

# Car body
car_body = pymunk.Body(10, pymunk.moment_for_box(10, (120, 30)))
car_body.position = (200, 200)
car_shape = pymunk.Poly.create_box(car_body, (120, 30))
space.add(car_body, car_shape)

# Wheels
for x_offset in [-45, 45]:
    wheel_body = pymunk.Body(2, pymunk.moment_for_circle(2, 0, 15))
    wheel_body.position = (200 + x_offset, 175)
    wheel_shape = pymunk.Circle(wheel_body, 15)
    wheel_shape.friction = 1.5
    space.add(wheel_body, wheel_shape)

    # Damped spring suspension
    spring = pymunk.DampedSpring(
        car_body, wheel_body,
        anchor_a=(x_offset, -15), anchor_b=(0, 0),
        rest_length=25, stiffness=300, damping=10
    )
    space.add(spring)

    # Groove to keep wheel under car
    groove = pymunk.GrooveJoint(
        car_body, wheel_body,
        groove_a=(x_offset, -10), groove_b=(x_offset, -40),
        anchor_b=(0, 0)
    )
    space.add(groove)

Spatial Queries

Pymunk supports querying the space without running a full simulation step:

# Point query — what's at this position?
hits = space.point_query((mouse_x, mouse_y), max_distance=0, shape_filter=pymunk.ShapeFilter())

# Segment query — raycast from A to B
results = space.segment_query((0, 300), (800, 300), radius=1, shape_filter=pymunk.ShapeFilter())
for info in results:
    print(f"Hit {info.shape} at {info.point}, distance {info.alpha * 800}")

# Bounding box query — what's in this rectangle?
bb = pymunk.BB(100, 100, 300, 300)
shapes_in_area = space.bb_query(bb, pymunk.ShapeFilter())

# Shape query — what overlaps this shape?
test_shape = pymunk.Circle(space.static_body, 50)
test_shape.unsafe_set_radius(50)  # for query purposes
overlapping = space.shape_query(test_shape)

These queries are essential for AI line-of-sight, area-of-effect attacks, and mouse picking.

Fixed Timestep Pattern

Physics simulations are most stable with a fixed timestep. When your rendering frame rate varies:

PHYSICS_DT = 1.0 / 120.0  # 120 Hz physics
accumulator = 0.0

def game_loop(render_dt):
    global accumulator
    accumulator += render_dt

    while accumulator >= PHYSICS_DT:
        space.step(PHYSICS_DT)
        accumulator -= PHYSICS_DT

    # Interpolation factor for smooth rendering
    alpha = accumulator / PHYSICS_DT
    # Render positions = lerp(previous_pos, current_pos, alpha)

A 120 Hz physics rate prevents tunneling (fast objects passing through thin walls) while the renderer runs at whatever rate the display supports.

Performance Tuning

Benchmarks

Chipmunk2D on a modern machine handles:

ScenarioApproximate Limit (60 FPS)
Circles falling into a pile~2,000–3,000 bodies
Ragdolls with constraints~100–200 ragdolls
Static level + dynamic projectiles~5,000+ projectiles

Optimization Techniques

  1. Sleep idle bodiesspace.sleep_time_threshold = 0.5 puts resting bodies to sleep, skipping them in the solver until disturbed.

  2. Simplify shapes — a complex polygon with 20 vertices costs more than two boxes. Use the fewest vertices that approximate the visual shape.

  3. Reduce iterationsspace.iterations = 10 (default) controls solver accuracy. Dropping to 5 is faster but less stable for stacks.

  4. Batch removals — removing shapes inside a callback crashes the simulation. Collect removals in a list and process them after space.step().

  5. Use segments for terrain — a static terrain made of Segment shapes is cheaper than equivalent polygon shapes.

  6. Spatial hash tuningspace.use_spatial_hash(dim=50, count=2000) where dim is the cell size. Set dim close to the average shape size for optimal performance.

# Enable sleeping for idle bodies
space.sleep_time_threshold = 0.5
space.idle_speed_threshold = 10

# Tune spatial hash for a game with ~20px average shapes
space.use_spatial_hash(20, 2000)

Integration with Arcade

The Arcade game library has built-in Pymunk support:

import arcade
from arcade.pymunk_physics_engine import PymunkPhysicsEngine

class GameWindow(arcade.Window):
    def setup(self):
        self.physics = PymunkPhysicsEngine(damping=0.9, gravity=(0, -1500))

        self.player = arcade.Sprite("player.png")
        self.physics.add_sprite(self.player, mass=2, friction=0.6,
                                collision_type="player",
                                body_type=PymunkPhysicsEngine.DYNAMIC)

        for wall in self.wall_list:
            self.physics.add_sprite(wall, body_type=PymunkPhysicsEngine.STATIC,
                                    collision_type="wall")

    def on_update(self, delta_time):
        self.physics.step()

This eliminates the manual work of syncing sprite positions with physics body positions.

Determinism and Replays

Chipmunk2D is deterministic for a given build — identical inputs produce identical outputs. This enables:

  • Replays — record player inputs, replay them to reproduce the exact simulation.
  • Lockstep multiplayer — send only inputs between clients; each runs the same simulation.
  • Automated testing — assert exact positions after N steps.

Caveat: determinism can break across different CPU architectures or compiler settings due to floating-point ordering. Pin your Pymunk version and test on target platforms.

The one thing to remember: Pymunk puts Chipmunk2D’s high-performance C physics engine behind a clean Python API — combine bodies, shapes, joints, and spatial queries to build anything from billiards to ragdoll platformers.

pythonpymunkphysics-simulation

See Also

  • Python Arcade Library Think of a magical art table that draws your game characters, listens when you press buttons, and cleans up the mess — that's Python Arcade.
  • Python Audio Fingerprinting Ever wonder how Shazam identifies a song from just a few seconds of noisy audio? Audio fingerprinting is the magic behind it, and Python can do it too.
  • Python Barcode Generation Picture the stripy labels on grocery items to understand how Python can create those machine-readable barcodes from numbers.
  • Python Cellular Automata Imagine a checkerboard where each square follows simple rules to turn on or off — and suddenly complex patterns emerge like magic.
  • Python Godot Gdscript Bridge Imagine speaking English to a friend who speaks French, with a translator in the middle — that's how Python talks to the Godot game engine.