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:
| Function | Use 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.inf | Object 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:
| Phase | When | Typical Use |
|---|---|---|
begin | First frame of contact | Accept/reject collision, trigger effects |
pre_solve | Every frame during contact, before resolution | Modify surface properties dynamically |
post_solve | Every frame during contact, after resolution | Read impulse forces for damage calculation |
separate | Contact ends | Stop 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:
| Scenario | Approximate 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
-
Sleep idle bodies —
space.sleep_time_threshold = 0.5puts resting bodies to sleep, skipping them in the solver until disturbed. -
Simplify shapes — a complex polygon with 20 vertices costs more than two boxes. Use the fewest vertices that approximate the visual shape.
-
Reduce iterations —
space.iterations = 10(default) controls solver accuracy. Dropping to 5 is faster but less stable for stacks. -
Batch removals — removing shapes inside a callback crashes the simulation. Collect removals in a list and process them after
space.step(). -
Use segments for terrain — a static terrain made of
Segmentshapes is cheaper than equivalent polygon shapes. -
Spatial hash tuning —
space.use_spatial_hash(dim=50, count=2000)wheredimis the cell size. Setdimclose 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.
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.