Pymunk is a easy-to-use pythonic 2D physics library built on Munk2D
—
The Space class is the central physics world that contains and manages all bodies, shapes, and constraints. It handles collision detection, constraint solving, gravity, and time integration.
class Space:
"""
Spaces are the basic unit of simulation. You add rigid bodies, shapes
and joints to it and then step them all forward together through time.
A Space can be copied and pickled. Note that any post step callbacks are
not copied. Also note that some internal collision cache data is not copied,
which can make the simulation a bit unstable the first few steps of the
fresh copy.
"""
def __init__(self, threaded: bool = False) -> None:
"""
Create a new instance of the Space.
Args:
threaded: If True, enables multi-threaded simulation (not available on Windows).
Even with threaded=True, you must set Space.threads=2 to use more than one thread.
"""# Solver accuracy vs performance
space.iterations: int = 10
"""
Number of solver iterations per step. Higher values increase accuracy but use more CPU.
Default 10 is sufficient for most games.
"""
# Global forces
space.gravity: Vec2d = Vec2d(0, 0)
"""
Global gravity vector applied to all dynamic bodies.
Can be overridden per-body with custom velocity integration functions.
"""
space.damping: float = 1.0
"""
Global velocity damping factor. 0.9 means bodies lose 10% velocity per second.
1.0 = no damping. Can be overridden per-body.
"""space.idle_speed_threshold: float = 0
"""
Speed threshold for a body to be considered idle for sleeping.
Default 0 means space estimates based on gravity.
"""
space.sleep_time_threshold: float = float('inf')
"""
Time bodies must remain idle before falling asleep.
Default infinity disables sleeping. Set to enable sleeping optimization.
"""space.collision_slop: float = 0.1
"""
Amount of overlap between shapes that is allowed for stability.
Set as high as possible without visible overlapping.
"""
space.collision_bias: float # ~0.002 (calculated)
"""
How fast overlapping shapes are pushed apart (0-1).
Controls percentage of overlap remaining after 1 second.
Rarely needs adjustment.
"""
space.collision_persistence: float = 3.0
"""
Number of frames collision solutions are cached to prevent jittering.
Rarely needs adjustment.
"""space.threads: int = 1
"""
Number of threads for simulation (max 2, only if threaded=True).
Default 1 maintains determinism. Not supported on Windows.
"""space.current_time_step: float
"""Current or most recent timestep from Space.step()"""
space.static_body: Body
"""Built-in static body for attaching static shapes"""
space.shapes: KeysView[Shape]
"""View of all shapes in the space"""
space.bodies: KeysView[Body]
"""View of all bodies in the space"""
space.constraints: KeysView[Constraint]
"""View of all constraints in the space"""def add(self, *objs: Union[Body, Shape, Constraint]) -> None:
"""
Add one or many shapes, bodies or constraints to the space.
Objects can be added even from collision callbacks - they will be
added at the end of the current step.
Args:
*objs: Bodies, shapes, and/or constraints to add
Example:
space.add(body, shape, constraint)
space.add(body)
space.add(shape1, shape2, joint)
"""def remove(self, *objs: Union[Body, Shape, Constraint]) -> None:
"""
Remove one or many shapes, bodies or constraints from the space.
Objects can be removed from collision callbacks - removal happens
at the end of the current step or removal call.
Args:
*objs: Objects to remove
Note:
When removing bodies, also remove attached shapes and constraints
to avoid dangling references.
Example:
space.remove(body, shape)
space.remove(constraint)
"""def step(self, dt: float) -> None:
"""
Advance the simulation by the given time step.
Using fixed time steps is highly recommended for stability and
contact persistence efficiency.
Args:
dt: Time step in seconds
Example:
# 60 FPS with substeps for accuracy
steps = 5
for _ in range(steps):
space.step(1/60.0/steps)
"""def use_spatial_hash(self, dim: float, count: int) -> None:
"""
Switch from default bounding box tree to spatial hash indexing.
Spatial hash can be faster for large numbers (1000s) of same-sized objects
but requires tuning and is usually slower for varied object sizes.
Args:
dim: Hash cell size - should match average collision shape size
count: Minimum hash table size - try ~10x number of objects
Example:
# For 500 objects of ~50 pixel size
space.use_spatial_hash(50.0, 5000)
"""
def reindex_shape(self, shape: Shape) -> None:
"""Update spatial index for a single shape after manual position changes"""
def reindex_shapes_for_body(self, body: Body) -> None:
"""Update spatial index for all shapes on a body after position changes"""
def reindex_static(self) -> None:
"""Update spatial index for all static shapes after moving them"""def point_query(
self,
point: tuple[float, float],
max_distance: float,
shape_filter: ShapeFilter
) -> list[PointQueryInfo]:
"""
Query for shapes near a point within max_distance.
Args:
point: Query point (x, y)
max_distance: Maximum distance to search
- 0.0: point must be inside shapes
- negative: point must be certain depth inside shapes
shape_filter: Collision filter to apply
Returns:
List of PointQueryInfo with shape, point, distance, gradient
Example:
hits = space.point_query((100, 200), 50, pymunk.ShapeFilter())
for hit in hits:
print(f"Hit {hit.shape} at distance {hit.distance}")
"""
def point_query_nearest(
self,
point: tuple[float, float],
max_distance: float,
shape_filter: ShapeFilter
) -> Optional[PointQueryInfo]:
"""
Query for the single nearest shape to a point.
Returns None if no shapes found within max_distance.
Same parameters and behavior as point_query but returns only closest hit.
"""def segment_query(
self,
start: tuple[float, float],
end: tuple[float, float],
radius: float,
shape_filter: ShapeFilter
) -> list[SegmentQueryInfo]:
"""
Query for shapes intersecting a line segment with thickness.
Args:
start: Segment start point (x, y)
end: Segment end point (x, y)
radius: Segment thickness radius (0 for infinitely thin line)
shape_filter: Collision filter to apply
Returns:
List of SegmentQueryInfo with shape, point, normal, alpha
Example:
# Raycast from (0,0) to (100,100)
hits = space.segment_query((0, 0), (100, 100), 0, pymunk.ShapeFilter())
for hit in hits:
print(f"Hit {hit.shape} at {hit.point} with normal {hit.normal}")
"""
def segment_query_first(
self,
start: tuple[float, float],
end: tuple[float, float],
radius: float,
shape_filter: ShapeFilter
) -> Optional[SegmentQueryInfo]:
"""
Query for the first shape hit by a line segment.
Returns None if no shapes intersected.
Same parameters as segment_query but returns only the first/closest hit.
"""def bb_query(self, bb: BB, shape_filter: ShapeFilter) -> list[Shape]:
"""
Query for shapes overlapping a bounding box.
Args:
bb: Bounding box to query
shape_filter: Collision filter to apply
Returns:
List of shapes overlapping the bounding box
Example:
bb = pymunk.BB(left=0, bottom=0, right=100, top=100)
shapes = space.bb_query(bb, pymunk.ShapeFilter())
"""
def shape_query(self, shape: Shape) -> list[ShapeQueryInfo]:
"""
Query for shapes overlapping the given shape.
Args:
shape: Shape to test overlaps with
Returns:
List of ShapeQueryInfo with overlapping shapes and contact info
Example:
test_shape = pymunk.Circle(None, 25, (100, 100))
overlaps = space.shape_query(test_shape)
for overlap in overlaps:
print(f"Overlapping with {overlap.shape}")
"""def on_collision(
self,
collision_type_a: Optional[int] = None,
collision_type_b: Optional[int] = None,
begin: Optional[Callable] = None,
pre_solve: Optional[Callable] = None,
post_solve: Optional[Callable] = None,
separate: Optional[Callable] = None,
data: Any = None
) -> None:
"""
Set callbacks for collision handling between specific collision types.
Args:
collision_type_a: First collision type (None matches any)
collision_type_b: Second collision type (None matches any)
begin: Called when shapes first touch
pre_solve: Called before collision resolution
post_solve: Called after collision resolution
separate: Called when shapes stop touching
data: User data passed to callbacks
Callback Signature:
def callback(arbiter: Arbiter, space: Space, data: Any) -> bool:
# Return True to process collision, False to ignore
return True
Example:
def player_enemy_collision(arbiter, space, data):
player_shape, enemy_shape = arbiter.shapes
print("Player hit enemy!")
return True # Process collision normally
space.on_collision(
collision_type_a=PLAYER_TYPE,
collision_type_b=ENEMY_TYPE,
begin=player_enemy_collision
)
"""def add_post_step_callback(
self,
func: Callable,
key: Hashable,
*args,
**kwargs
) -> None:
"""
Add a callback to be called after the current simulation step.
Useful for making changes that can't be done during collision callbacks,
like adding/removing objects or changing properties.
Args:
func: Function to call after step completes
key: Unique key for this callback (prevents duplicates)
*args, **kwargs: Arguments passed to func
Example:
def remove_object(space, obj):
space.remove(obj)
# Schedule removal after current step
space.add_post_step_callback(remove_object, "remove_ball", ball)
"""def debug_draw(self, options: SpaceDebugDrawOptions) -> None:
"""
Draw debug visualization of the space using provided draw options.
Args:
options: Debug drawing implementation (pygame_util, pyglet_util, etc.)
Example:
import pygame
import pymunk.pygame_util
screen = pygame.display.set_mode((800, 600))
draw_options = pymunk.pygame_util.DrawOptions(screen)
# Customize colors
draw_options.shape_outline_color = (255, 0, 0, 255) # Red outlines
space.debug_draw(draw_options)
"""import pymunk
# Create physics world
space = pymunk.Space()
space.gravity = (0, -982) # Earth gravity
space.iterations = 15 # Higher accuracy
# Create ground
ground = space.static_body
ground_shape = pymunk.Segment(ground, (0, 0), (800, 0), 5)
ground_shape.friction = 0.7
space.add(ground_shape)
# Create falling box
mass = 10
size = (50, 50)
moment = pymunk.moment_for_box(mass, size)
body = pymunk.Body(mass, moment)
body.position = 400, 300
shape = pymunk.Poly.create_box(body, size)
shape.friction = 0.7
shape.elasticity = 0.8
space.add(body, shape)
# Run simulation
dt = 1/60.0
for i in range(300): # 5 seconds
space.step(dt)
print(f"Position: {body.position}, Velocity: {body.velocity}")import pymunk
BALL_TYPE = 1
WALL_TYPE = 2
def ball_wall_collision(arbiter, space, data):
"""Handle ball hitting wall"""
impulse = arbiter.total_impulse
if abs(impulse) > 100: # Hard hit
print(f"Ball bounced hard! Impulse: {impulse}")
return True
space = pymunk.Space()
space.on_collision(BALL_TYPE, WALL_TYPE, begin=ball_wall_collision)
# Create ball with collision type
ball_body = pymunk.Body(1, pymunk.moment_for_circle(1, 0, 20))
ball_shape = pymunk.Circle(ball_body, 20)
ball_shape.collision_type = BALL_TYPE
ball_shape.elasticity = 0.9
# Create wall with collision type
wall_body = space.static_body
wall_shape = pymunk.Segment(wall_body, (0, 0), (800, 0), 5)
wall_shape.collision_type = WALL_TYPE
space.add(ball_body, ball_shape, wall_shape)import pymunk
space = pymunk.Space()
# ... add objects to space ...
# Point query - find what's under mouse cursor
mouse_pos = (400, 300)
shapes_under_mouse = space.point_query(
mouse_pos, 0, pymunk.ShapeFilter()
)
if shapes_under_mouse:
print(f"Mouse over: {shapes_under_mouse[0].shape}")
# Raycast - line of sight check
start = (100, 100)
end = (700, 500)
line_of_sight = space.segment_query_first(
start, end, 0, pymunk.ShapeFilter()
)
if line_of_sight:
print(f"LOS blocked at {line_of_sight.point}")
else:
print("Clear line of sight")
# Area query - explosion radius
explosion_center = (400, 300)
explosion_radius = 100
explosion_bb = pymunk.BB.newForCircle(explosion_center, explosion_radius)
affected_shapes = space.bb_query(explosion_bb, pymunk.ShapeFilter())
for shape in affected_shapes:
# Apply explosion force
if shape.body.body_type == pymunk.Body.DYNAMIC:
direction = shape.body.position - explosion_center
force = direction.normalized() * 10000
shape.body.apply_impulse_at_world_point(force, explosion_center)import pymunk
space = pymunk.Space(threaded=True) # Enable threading
space.threads = 2 # Use 2 threads
# For large numbers of similar-sized objects
if num_objects > 1000:
avg_object_size = 25 # Average collision shape size
space.use_spatial_hash(avg_object_size, num_objects * 10)
# Enable sleeping for better performance
space.sleep_time_threshold = 0.5 # Sleep after 0.5 seconds idle
# Optimize solver iterations vs accuracy
space.iterations = 8 # Reduce for better performance
# Use appropriate gravity and damping
space.gravity = (0, -981) # Realistic gravity
space.damping = 0.999 # Small amount of air resistanceThe Space class provides complete control over physics simulation with efficient spatial queries, flexible collision handling, and performance optimization options suitable for games, simulations, and interactive applications.
Install with Tessl CLI
npx tessl i tessl/pypi-pymunk