Pymunk is a easy-to-use pythonic 2D physics library built on Munk2D
—
Bodies represent rigid objects with mass, position, velocity, and rotation, while Shapes define the collision geometry and material properties attached to bodies. Together they form the fundamental components of physics simulation.
The Body class represents a rigid body with physical properties like mass, moment of inertia, position, velocity, and rotation.
class Body:
"""
A rigid body with mass, position, velocity and rotation.
Bodies can be copied and pickled. Sleeping bodies wake up in the copy.
When copied, spaces, shapes or constraints attached to the body are not copied.
"""
# Body type constants
DYNAMIC: int # Normal simulated bodies affected by forces
KINEMATIC: int # User-controlled bodies with infinite mass
STATIC: int # Non-moving bodies (usually terrain)
def __init__(
self,
mass: float = 0,
moment: float = 0,
body_type: int = DYNAMIC
) -> None:
"""
Create a new body.
Args:
mass: Body mass (must be > 0 for dynamic bodies in space)
moment: Moment of inertia (must be > 0 for dynamic bodies in space)
body_type: DYNAMIC, KINEMATIC, or STATIC
"""# Dynamic bodies - normal physics objects
body = pymunk.Body(mass=10, moment=pymunk.moment_for_circle(10, 0, 25))
body.body_type == pymunk.Body.DYNAMIC # True
# - Affected by gravity, forces, and collisions
# - Have finite mass and can be moved by physics
# - Most game objects (balls, boxes, characters)
# Kinematic bodies - user-controlled objects
kinematic_body = pymunk.Body(body_type=pymunk.Body.KINEMATIC)
# - Controlled by setting velocity, not affected by forces
# - Have infinite mass, don't respond to collisions
# - Good for moving platforms, elevators, doors
# - Objects touching kinematic bodies cannot fall asleep
# Static bodies - immovable terrain
static_body = pymunk.Body(body_type=pymunk.Body.STATIC)
# Or use space.static_body
# - Never move (except manual repositioning)
# - Infinite mass, provide collision boundaries
# - Optimized for performance (no collision checks with other static bodies)
# - Ground, walls, level geometry# Identity
body.id: int
"""Unique identifier for the body (changes on copy/pickle)"""
# Physical properties
body.mass: float
"""Mass of the body (must be > 0 for dynamic bodies in space)"""
body.moment: float
"""
Moment of inertia - rotational mass.
Can be set to float('inf') to prevent rotation.
"""
body.body_type: int
"""Body type: DYNAMIC, KINEMATIC, or STATIC"""
# Position and orientation
body.position: Vec2d
"""Position in world coordinates. Call space.reindex_shapes_for_body() after manual changes."""
body.center_of_gravity: Vec2d = Vec2d(0, 0)
"""Center of gravity offset in body local coordinates"""
body.angle: float
"""Rotation angle in radians. Body rotates around center of gravity."""
body.rotation_vector: Vec2d
"""Unit vector representing current rotation (readonly)"""
# Motion properties
body.velocity: Vec2d
"""Linear velocity of center of gravity"""
body.angular_velocity: float
"""Angular velocity in radians per second"""
# Force accumulation (reset each timestep)
body.force: Vec2d
"""Force applied to center of gravity (manual forces only)"""
body.torque: float
"""Torque applied to the body (manual torques only)"""body.kinetic_energy: float
"""Current kinetic energy of the body (readonly)"""
body.is_sleeping: bool
"""Whether the body is currently sleeping for optimization (readonly)"""
body.space: Optional[Space]
"""Space the body belongs to, or None (readonly)"""
body.shapes: KeysView[Shape]
"""View of all shapes attached to this body (readonly)"""
body.constraints: KeysView[Constraint]
"""View of all constraints attached to this body (readonly)"""def apply_force_at_world_point(
self,
force: tuple[float, float],
point: tuple[float, float]
) -> None:
"""
Apply force as if applied from world point.
Forces are accumulated and applied during physics step.
Use for continuous forces like thrusters or wind.
Args:
force: Force vector in Newtons
point: World position where force is applied
Example:
# Apply upward thrust at right side of body
body.apply_force_at_world_point((0, 1000), body.position + (50, 0))
"""
def apply_force_at_local_point(
self,
force: tuple[float, float],
point: tuple[float, float] = (0, 0)
) -> None:
"""
Apply force as if applied from body local point.
Args:
force: Force vector in body coordinates
point: Local position where force is applied (default center)
"""
def apply_impulse_at_world_point(
self,
impulse: tuple[float, float],
point: tuple[float, float]
) -> None:
"""
Apply impulse as if applied from world point.
Impulses cause immediate velocity changes.
Use for instantaneous events like explosions or collisions.
Args:
impulse: Impulse vector (force * time)
point: World position where impulse is applied
Example:
# Explosion impulse
direction = target_pos - explosion_center
impulse = direction.normalized() * 500
body.apply_impulse_at_world_point(impulse, explosion_center)
"""
def apply_impulse_at_local_point(
self,
impulse: tuple[float, float],
point: tuple[float, float] = (0, 0)
) -> None:
"""Apply impulse as if applied from body local point."""def local_to_world(self, v: tuple[float, float]) -> Vec2d:
"""
Convert body local coordinates to world space.
Local coordinates have (0,0) at center of gravity and rotate with body.
Args:
v: Vector in body local coordinates
Returns:
Vector in world coordinates
Example:
# Get world position of point on body
local_point = (25, 0) # 25 units right of center
world_point = body.local_to_world(local_point)
"""
def world_to_local(self, v: tuple[float, float]) -> Vec2d:
"""
Convert world space coordinates to body local coordinates.
Args:
v: Vector in world coordinates
Returns:
Vector in body local coordinates
"""
def velocity_at_world_point(self, point: tuple[float, float]) -> Vec2d:
"""
Get velocity at a world point on the body.
Accounts for both linear and angular velocity.
Useful for surface velocity calculations.
Args:
point: World position
Returns:
Velocity at that point
Example:
# Get velocity at edge of rotating wheel
edge_point = body.position + (wheel_radius, 0)
edge_velocity = body.velocity_at_world_point(edge_point)
"""
def velocity_at_local_point(self, point: tuple[float, float]) -> Vec2d:
"""Get velocity at a body local point."""def activate(self) -> None:
"""Wake up a sleeping body."""
def sleep(self) -> None:
"""Force body to sleep (optimization for inactive bodies)."""
def sleep_with_group(self, body: 'Body') -> None:
"""
Sleep this body together with another body.
Bodies in the same sleep group wake up together when one is disturbed.
Useful for connected objects like a stack of boxes.
"""body.velocity_func: Callable[[Body, Vec2d, float, float], None]
"""
Custom velocity integration function called each timestep.
Function signature: velocity_func(body, gravity, damping, dt)
Override to implement custom gravity, damping, or other effects per body.
Example:
def custom_gravity(body, gravity, damping, dt):
# Custom gravity based on body position
if body.position.y > 500:
gravity = Vec2d(0, -500) # Weaker gravity at height
pymunk.Body.update_velocity(body, gravity, damping, dt)
body.velocity_func = custom_gravity
"""
body.position_func: Callable[[Body, float], None]
"""
Custom position integration function called each timestep.
Function signature: position_func(body, dt)
Override to implement custom movement behavior.
"""Shapes define collision geometry and material properties. All shapes inherit from the base Shape class.
class Shape:
"""
Base class for all collision shapes.
Shapes can be copied and pickled. The attached body is also copied.
"""
# Physical properties
shape.mass: float = 0
"""
Mass of the shape for automatic body mass calculation.
Alternative to setting body mass directly.
"""
shape.density: float = 0
"""
Density (mass per unit area) for automatic mass calculation.
More intuitive than setting mass directly.
"""
shape.moment: float
"""Moment of inertia contribution from this shape (readonly)"""
shape.area: float
"""Area of the shape (readonly)"""
shape.center_of_gravity: Vec2d
"""Center of gravity of the shape (readonly)"""
# Material properties
shape.friction: float = 0.7
"""
Friction coefficient (0 = frictionless, higher = more friction).
Combined with other shape's friction during collision.
"""
shape.elasticity: float = 0.0
"""
Bounce/restitution coefficient (0 = no bounce, 1 = perfect bounce).
Combined with other shape's elasticity during collision.
"""
shape.surface_velocity: Vec2d = Vec2d(0, 0)
"""
Surface velocity for conveyor belt effects.
Only affects friction calculation, not collision resolution.
"""
# Collision properties
shape.collision_type: int = 0
"""Collision type identifier for callback filtering"""
shape.filter: ShapeFilter = ShapeFilter()
"""Collision filter controlling which shapes can collide"""
shape.sensor: bool = False
"""
If True, shape detects collisions but doesn't respond physically.
Useful for trigger areas, pickups, detection zones.
"""
# Relationships
shape.body: Optional[Body]
"""Body this shape is attached to (can be None)"""
shape.space: Optional[Space]
"""Space this shape belongs to (readonly)"""
shape.bb: BB
"""Cached bounding box - valid after cache_bb() or space.step()"""def point_query(self, p: tuple[float, float]) -> PointQueryInfo:
"""
Test if point lies within shape.
Args:
p: Point to test
Returns:
PointQueryInfo with distance (negative if inside), closest point, etc.
"""
def segment_query(
self,
start: tuple[float, float],
end: tuple[float, float],
radius: float = 0
) -> Optional[SegmentQueryInfo]:
"""
Test line segment intersection with shape.
Returns:
SegmentQueryInfo if intersected, None otherwise
"""
def shapes_collide(self, other: 'Shape') -> ContactPointSet:
"""
Get collision information between two shapes.
Returns:
ContactPointSet with contact points and normal
"""
def cache_bb(self) -> BB:
"""Update and return shape's bounding box"""
def update(self, transform: Transform) -> BB:
"""Update shape with explicit transform (for unattached shapes)"""class Circle(Shape):
"""
Circular collision shape - fastest and simplest collision shape.
Perfect for balls, wheels, coins, circular objects.
"""
def __init__(
self,
body: Optional[Body],
radius: float,
offset: tuple[float, float] = (0, 0)
) -> None:
"""
Create a circle shape.
Args:
body: Body to attach to (can be None)
radius: Circle radius
offset: Offset from body center of gravity
Example:
# Circle at body center
circle = pymunk.Circle(body, 25)
# Circle offset from body center
circle = pymunk.Circle(body, 15, offset=(10, 5))
"""
# Properties
radius: float
"""Radius of the circle"""
offset: Vec2d
"""Offset from body center of gravity"""
# Unsafe modification (use carefully during simulation)
def unsafe_set_radius(self, radius: float) -> None:
"""Change radius during simulation (may cause instability)"""
def unsafe_set_offset(self, offset: tuple[float, float]) -> None:
"""Change offset during simulation (may cause instability)"""class Segment(Shape):
"""
Line segment collision shape with thickness.
Mainly for static shapes like walls, floors, ramps.
Can be beveled for thickness.
"""
def __init__(
self,
body: Optional[Body],
a: tuple[float, float],
b: tuple[float, float],
radius: float
) -> None:
"""
Create a line segment shape.
Args:
body: Body to attach to (can be None)
a: First endpoint
b: Second endpoint
radius: Thickness radius (0 for infinitely thin)
Example:
# Ground segment
ground = pymunk.Segment(static_body, (0, 0), (800, 0), 5)
# Sloped platform
ramp = pymunk.Segment(body, (0, 0), (100, 50), 3)
"""
# Properties
a: Vec2d
"""First endpoint of the segment"""
b: Vec2d
"""Second endpoint of the segment"""
normal: Vec2d
"""Normal vector of the segment (readonly)"""
radius: float
"""Thickness radius of the segment"""
# Unsafe modification
def unsafe_set_endpoints(
self,
a: tuple[float, float],
b: tuple[float, float]
) -> None:
"""Change endpoints during simulation (may cause instability)"""
def unsafe_set_radius(self, radius: float) -> None:
"""Change radius during simulation (may cause instability)"""
def set_neighbors(
self,
prev: tuple[float, float],
next: tuple[float, float]
) -> None:
"""
Set neighboring segments for smooth collision.
Prevents objects from catching on segment joints.
Args:
prev: Previous segment endpoint
next: Next segment endpoint
"""class Poly(Shape):
"""
Convex polygon collision shape.
Most flexible but slowest collision shape.
Automatically converts concave polygons to convex hull.
"""
def __init__(
self,
body: Optional[Body],
vertices: Sequence[tuple[float, float]],
transform: Optional[Transform] = None,
radius: float = 0
) -> None:
"""
Create a polygon shape.
Args:
body: Body to attach to (can be None)
vertices: List of vertices in counter-clockwise order
transform: Optional transform applied to vertices
radius: Corner rounding radius (reduces catching on edges)
Note:
Vertices should be centered around (0,0) or use transform.
Concave polygons are automatically converted to convex hull.
Example:
# Box polygon
size = (50, 30)
vertices = [
(-size[0]/2, -size[1]/2),
(size[0]/2, -size[1]/2),
(size[0]/2, size[1]/2),
(-size[0]/2, size[1]/2)
]
poly = pymunk.Poly(body, vertices)
# Triangle
vertices = [(0, 20), (-20, -20), (20, -20)]
triangle = pymunk.Poly(body, vertices, radius=2)
"""
# Properties
radius: float
"""Corner rounding radius"""
# Methods
def get_vertices(self) -> list[Vec2d]:
"""Get list of vertices in world coordinates"""
def unsafe_set_vertices(
self,
vertices: Sequence[tuple[float, float]],
transform: Optional[Transform] = None
) -> None:
"""Change vertices during simulation (may cause instability)"""
def unsafe_set_radius(self, radius: float) -> None:
"""Change radius during simulation (may cause instability)"""
# Static factory methods
@staticmethod
def create_box(
body: Optional[Body],
size: tuple[float, float],
radius: float = 0
) -> 'Poly':
"""
Create a box polygon.
Args:
body: Body to attach to
size: (width, height) of box
radius: Corner rounding radius
Example:
box = pymunk.Poly.create_box(body, (60, 40), radius=2)
"""
@staticmethod
def create_box_bb(
body: Optional[Body],
bb: BB,
radius: float = 0
) -> 'Poly':
"""
Create box polygon from bounding box.
Args:
body: Body to attach to
bb: Bounding box defining box shape
radius: Corner rounding radius
"""import pymunk
import math
# Create physics space
space = pymunk.Space()
space.gravity = (0, -981)
# Static ground
ground_body = space.static_body
ground = pymunk.Segment(ground_body, (0, 0), (800, 0), 5)
ground.friction = 0.7
space.add(ground)
# Dynamic ball
ball_mass = 10
ball_radius = 25
ball_moment = pymunk.moment_for_circle(ball_mass, 0, ball_radius)
ball_body = pymunk.Body(ball_mass, ball_moment)
ball_body.position = 400, 300
ball_shape = pymunk.Circle(ball_body, ball_radius)
ball_shape.friction = 0.7
ball_shape.elasticity = 0.9 # Bouncy
space.add(ball_body, ball_shape)
# Dynamic box
box_mass = 15
box_size = (40, 60)
box_moment = pymunk.moment_for_box(box_mass, box_size)
box_body = pymunk.Body(box_mass, box_moment)
box_body.position = 200, 400
box_shape = pymunk.Poly.create_box(box_body, box_size)
box_shape.friction = 0.5
space.add(box_body, box_shape)import pymunk
# Different material types
def create_material_shapes(body):
# Ice - slippery, bouncy
ice_shape = pymunk.Circle(body, 20)
ice_shape.friction = 0.1 # Very slippery
ice_shape.elasticity = 0.8 # Quite bouncy
# Rubber - grippy, very bouncy
rubber_shape = pymunk.Circle(body, 20)
rubber_shape.friction = 1.2 # Very grippy
rubber_shape.elasticity = 0.95 # Almost perfect bounce
# Metal - medium friction, little bounce
metal_shape = pymunk.Circle(body, 20)
metal_shape.friction = 0.7 # Moderate friction
metal_shape.elasticity = 0.2 # Little bounce
# Wood - medium properties
wood_shape = pymunk.Circle(body, 20)
wood_shape.friction = 0.4 # Some friction
wood_shape.elasticity = 0.3 # Some bounce
# Conveyor belt effect
conveyor = pymunk.Segment(static_body, (100, 50), (300, 50), 5)
conveyor.surface_velocity = (100, 0) # Move objects rightward
conveyor.friction = 0.8 # Need friction for surface velocity to workimport pymunk
# Method 1: Set body mass directly
mass = 10
moment = pymunk.moment_for_circle(mass, 0, 25)
body = pymunk.Body(mass, moment)
circle = pymunk.Circle(body, 25)
# Method 2: Let shapes calculate mass automatically
body = pymunk.Body() # No mass/moment specified
# Set shape density
circle = pymunk.Circle(body, 25)
circle.density = 0.1 # Light material
poly = pymunk.Poly.create_box(body, (50, 50))
poly.density = 0.2 # Heavier material
# Body mass/moment calculated automatically from shapes
space.add(body, circle, poly)
# Method 3: Set shape mass directly
circle = pymunk.Circle(body, 25)
circle.mass = 5 # 5 units of mass
poly = pymunk.Poly.create_box(body, (50, 50))
poly.mass = 15 # 15 units of mass
# Total body mass will be 20import pymunk
import math
space = pymunk.Space()
body = pymunk.Body(10, pymunk.moment_for_circle(10, 0, 25))
body.position = 400, 300
# Continuous forces (applied each frame)
def apply_thrust():
# Rocket thrust at back of ship
thrust_force = (0, 1000) # Upward thrust
thrust_point = body.position + (0, -25) # Back of ship
body.apply_force_at_world_point(thrust_force, thrust_point)
# Impulse forces (one-time application)
def explosion(explosion_center, strength):
direction = body.position - explosion_center
distance = abs(direction)
if distance > 0:
# Impulse falls off with distance
impulse_magnitude = strength / (distance * distance)
impulse = direction.normalized() * impulse_magnitude
body.apply_impulse_at_world_point(impulse, explosion_center)
# Torque for rotation
body.torque = 500 # Spin clockwise
# Local force application
# Force applied at local point creates both linear and angular motion
local_force = (100, 0) # Rightward force
local_point = (0, 25) # Top of object
body.apply_force_at_local_point(local_force, local_point)import pymunk
import math
# Create kinematic platform
platform_body = pymunk.Body(body_type=pymunk.Body.KINEMATIC)
platform_shape = pymunk.Poly.create_box(platform_body, (100, 20))
platform_body.position = 200, 100
space.add(platform_body, platform_shape)
# Control kinematic body motion
def update_platform(dt):
# Sine wave motion
time = space.current_time_step
# Horizontal oscillation
platform_body.velocity = (50 * math.cos(time), 0)
# Or set position directly (less stable)
# new_x = 200 + 50 * math.sin(time)
# platform_body.position = new_x, 100
# space.reindex_shapes_for_body(platform_body)
# Moving elevator
elevator_body = pymunk.Body(body_type=pymunk.Body.KINEMATIC)
elevator_shape = pymunk.Poly.create_box(elevator_body, (80, 15))
def move_elevator_up():
elevator_body.velocity = (0, 100) # Move up
def move_elevator_down():
elevator_body.velocity = (0, -100) # Move down
def stop_elevator():
elevator_body.velocity = (0, 0) # Stopimport pymunk
# Create sensor shape for trigger areas
trigger_body = space.static_body
trigger_shape = pymunk.Circle(trigger_body, 50, (400, 300))
trigger_shape.sensor = True # No physical collision response
trigger_shape.collision_type = TRIGGER_TYPE
# Player shape
player_body = pymunk.Body(1, pymunk.moment_for_circle(1, 0, 15))
player_shape = pymunk.Circle(player_body, 15)
player_shape.collision_type = PLAYER_TYPE
def trigger_callback(arbiter, space, data):
"""Called when player enters/exits trigger area"""
trigger_shape, player_shape = arbiter.shapes
print("Player in trigger area!")
return False # Don't process as physical collision
space.on_collision(TRIGGER_TYPE, PLAYER_TYPE, begin=trigger_callback)
# Pickup items (remove on contact)
pickup_body = space.static_body
pickup_shape = pymunk.Circle(pickup_body, 10, (300, 200))
pickup_shape.sensor = True
pickup_shape.collision_type = PICKUP_TYPE
def pickup_callback(arbiter, space, data):
pickup_shape, player_shape = arbiter.shapes
# Schedule removal after step completes
space.add_post_step_callback(remove_pickup, pickup_shape, pickup_shape)
return False
def remove_pickup(space, shape):
space.remove(shape)
print("Collected pickup!")import pymunk
# Test if point is inside shape
test_point = (100, 200)
query_result = shape.point_query(test_point)
if query_result.distance < 0:
print("Point is inside shape!")
print(f"Distance to surface: {abs(query_result.distance)}")
# Line of sight / raycast using segment query
start = player_body.position
end = enemy_body.position
line_query = shape.segment_query(start, end, radius=0)
if line_query:
print(f"Line blocked at {line_query.point}")
print(f"Surface normal: {line_query.normal}")
else:
print("Clear line of sight")
# Shape vs shape collision test
collision_info = shape1.shapes_collide(shape2)
if collision_info.points:
print("Shapes are colliding!")
for point in collision_info.points:
print(f"Contact at {point.point_a}")Bodies and shapes provide the foundation for realistic physics simulation with intuitive APIs for mass properties, material behavior, force application, and collision detection suitable for games, simulations, and interactive applications.
Install with Tessl CLI
npx tessl i tessl/pypi-pymunk