CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-pymunk

Pymunk is a easy-to-use pythonic 2D physics library built on Munk2D

Pending
Overview
Eval results
Files

physics-world.mddocs/

Physics World Management (Space)

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 Definition

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.
        """

Core Properties

Physics Configuration

# 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.
"""

Sleep Parameters

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.
"""

Collision Tuning

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.
"""

Threading

space.threads: int = 1
"""
Number of threads for simulation (max 2, only if threaded=True).
Default 1 maintains determinism. Not supported on Windows.
"""

Read-Only Properties

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"""

Object Management

Adding Objects

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)
    """

Removing Objects

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)
    """

Simulation Control

Time Stepping

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)
    """

Spatial Index Optimization

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"""

Spatial Queries

Point Queries

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.
    """

Line Segment Queries

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.
    """

Area and Shape Queries

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}")
    """

Collision Handling

Collision Callbacks

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
        )
    """

Post-Step Callbacks

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)
    """

Debug Visualization

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)
    """

Usage Examples

Basic Physics World

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}")

Collision Detection

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)

Advanced Spatial Queries

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)

Performance Optimization

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 resistance

The 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

docs

bodies-shapes.md

constraints.md

geometry.md

index.md

physics-world.md

utilities.md

visualization.md

tile.json