Pymunk is a easy-to-use pythonic 2D physics library built on Munk2D
—
Pymunk provides comprehensive debug visualization capabilities for popular graphics libraries, enabling easy debugging and prototyping of physics simulations with visual feedback.
The foundation for all debug drawing implementations, providing customizable rendering options and colors.
class SpaceDebugDrawOptions:
"""
Base class for configuring debug drawing of physics spaces.
Provides customizable colors, drawing flags, and transformation options.
Subclassed by library-specific implementations (pygame, pyglet, matplotlib).
"""
# Drawing flags (combine with bitwise OR)
DRAW_SHAPES: int # Draw collision shapes
DRAW_CONSTRAINTS: int # Draw joints/constraints
DRAW_COLLISION_POINTS: int # Draw collision contact points
def __init__(self) -> None:
"""
Create debug draw options with default settings.
Default flags enable drawing of shapes, constraints, and collision points.
"""# Control what elements are drawn
options.flags: int = (
SpaceDebugDrawOptions.DRAW_SHAPES |
SpaceDebugDrawOptions.DRAW_CONSTRAINTS |
SpaceDebugDrawOptions.DRAW_COLLISION_POINTS
)
"""
Bitmask controlling what elements to draw.
Combine flags with bitwise OR to enable multiple elements.
Examples:
options.flags = SpaceDebugDrawOptions.DRAW_SHAPES # Only shapes
options.flags = SpaceDebugDrawOptions.DRAW_SHAPES | SpaceDebugDrawOptions.DRAW_CONSTRAINTS # Shapes + constraints
options.flags = 0 # Draw nothing
"""# Shape colors based on body type
options.shape_dynamic_color: SpaceDebugColor = SpaceDebugColor(52, 152, 219, 255)
"""Color for dynamic body shapes (blue)"""
options.shape_static_color: SpaceDebugColor = SpaceDebugColor(149, 165, 166, 255)
"""Color for static body shapes (gray)"""
options.shape_kinematic_color: SpaceDebugColor = SpaceDebugColor(39, 174, 96, 255)
"""Color for kinematic body shapes (green)"""
options.shape_sleeping_color: SpaceDebugColor = SpaceDebugColor(114, 148, 168, 255)
"""Color for sleeping body shapes (darker blue)"""
# Element-specific colors
options.shape_outline_color: SpaceDebugColor = SpaceDebugColor(44, 62, 80, 255)
"""Color for shape outlines (dark blue-gray)"""
options.constraint_color: SpaceDebugColor = SpaceDebugColor(142, 68, 173, 255)
"""Color for constraints/joints (purple)"""
options.collision_point_color: SpaceDebugColor = SpaceDebugColor(231, 76, 60, 255)
"""Color for collision points (red)"""options.transform: Transform = Transform.identity()
"""
Transformation applied to all drawing coordinates.
Use to implement camera/viewport transformations:
- Translation: Move view to follow objects
- Scaling: Zoom in/out
- Rotation: Rotate view
Example:
# Camera following player
camera_pos = player.position
options.transform = Transform.translation(-camera_pos.x, -camera_pos.y)
# Zoom and center
zoom = 2.0
center = (400, 300) # Screen center
options.transform = (
Transform.translation(*center) @
Transform.scaling(zoom) @
Transform.translation(-camera_pos.x, -camera_pos.y)
)
"""class SpaceDebugColor(NamedTuple):
"""
RGBA color tuple for debug drawing.
Values are typically 0-255 for integer colors or 0.0-1.0 for float colors.
"""
r: float # Red component
g: float # Green component
b: float # Blue component
a: float # Alpha component (opacity)
def as_int(self) -> tuple[int, int, int, int]:
"""
Return color as integer tuple (0-255 range).
Example:
color = SpaceDebugColor(1.0, 0.5, 0.0, 1.0)
rgba_int = color.as_int() # (255, 128, 0, 255)
"""
def as_float(self) -> tuple[float, float, float, float]:
"""
Return color as float tuple (0.0-1.0 range).
Example:
color = SpaceDebugColor(255, 128, 0, 255)
rgba_float = color.as_float() # (1.0, 0.5, 0.0, 1.0)
"""
# Common colors
RED = SpaceDebugColor(255, 0, 0, 255)
GREEN = SpaceDebugColor(0, 255, 0, 255)
BLUE = SpaceDebugColor(0, 0, 255, 255)
WHITE = SpaceDebugColor(255, 255, 255, 255)
BLACK = SpaceDebugColor(0, 0, 0, 255)
TRANSPARENT = SpaceDebugColor(0, 0, 0, 0)High-performance debug drawing for Pygame applications with coordinate system handling.
import pygame
import pymunk
import pymunk.pygame_util
# Initialize Pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()
# Create physics space
space = pymunk.Space()
space.gravity = (0, -981)
# Create debug drawing options
draw_options = pymunk.pygame_util.DrawOptions(screen)
# Main loop
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Clear screen
screen.fill((255, 255, 255)) # White background
# Step physics
dt = clock.tick(60) / 1000.0
space.step(dt)
# Draw physics debug visualization
space.debug_draw(draw_options)
pygame.display.flip()
pygame.quit()import pymunk.pygame_util
# Pygame uses Y-down coordinates, physics often uses Y-up
# Configure coordinate system behavior:
pymunk.pygame_util.positive_y_is_up = False # Default - Y increases downward
"""
Control Y-axis direction:
- False (default): Y increases downward (standard Pygame)
- True: Y increases upward (mathematical convention)
When True: When False:
y ^ +------ > x
| |
+---> x v y
"""
# If using Y-up physics, set gravity appropriately:
if pymunk.pygame_util.positive_y_is_up:
space.gravity = (0, -981) # Downward gravity
else:
space.gravity = (0, 981) # "Upward" gravity (actually downward in screen coords)def get_mouse_pos(surface: pygame.Surface) -> Vec2d:
"""
Get mouse position converted to physics coordinates.
Handles coordinate system conversion based on positive_y_is_up setting.
Example:
mouse_pos = pymunk.pygame_util.get_mouse_pos(screen)
hit = space.point_query_nearest(mouse_pos, 0, pymunk.ShapeFilter())
"""
def to_pygame(point: Vec2d, surface: pygame.Surface) -> tuple[int, int]:
"""
Convert physics coordinates to Pygame screen coordinates.
Args:
point: Physics world coordinate
surface: Pygame surface for height reference
Returns:
(x, y) tuple in Pygame coordinates
"""
def from_pygame(point: tuple[int, int], surface: pygame.Surface) -> Vec2d:
"""
Convert Pygame screen coordinates to physics coordinates.
Args:
point: Pygame coordinate (x, y)
surface: Pygame surface for height reference
Returns:
Vec2d in physics world coordinates
"""import pygame
import pymunk.pygame_util
# Create debug options
draw_options = pymunk.pygame_util.DrawOptions(screen)
# Customize colors
draw_options.shape_outline_color = pymunk.SpaceDebugColor(255, 0, 0, 255) # Red outlines
draw_options.constraint_color = pymunk.SpaceDebugColor(0, 255, 0, 255) # Green constraints
draw_options.collision_point_color = pymunk.SpaceDebugColor(255, 255, 0, 255) # Yellow collision points
# Set individual shape colors
circle_shape.color = pygame.Color("pink")
box_shape.color = (0, 255, 255, 255) # Cyan
# Control what gets drawn
draw_options.flags = pymunk.SpaceDebugDrawOptions.DRAW_SHAPES # Only shapes, no constraintsOpenGL-based debug drawing with batched rendering for performance.
import pyglet
import pymunk
import pymunk.pyglet_util
# Create window
window = pyglet.window.Window(800, 600, "Physics Debug")
# Create physics space
space = pymunk.Space()
space.gravity = (0, -981)
# Create debug drawing options
draw_options = pymunk.pyglet_util.DrawOptions()
@window.event
def on_draw():
window.clear()
# Draw physics debug visualization
space.debug_draw(draw_options)
def update(dt):
space.step(dt)
# Schedule physics updates
pyglet.clock.schedule_interval(update, 1/60.0)
# Run application
pyglet.app.run()import pyglet
import pymunk.pyglet_util
# Create custom batch for optimized rendering
batch = pyglet.graphics.Batch()
# Create debug options with custom batch
draw_options = pymunk.pyglet_util.DrawOptions(batch=batch)
@window.event
def on_draw():
window.clear()
# Draw physics (adds to batch, doesn't render immediately)
space.debug_draw(draw_options)
# Render entire batch efficiently
batch.draw()import pyglet
import pymunk.pyglet_util
draw_options = pymunk.pyglet_util.DrawOptions()
@window.event
def on_draw():
window.clear()
# Use context manager for automatic OpenGL state management
with draw_options:
space.debug_draw(draw_options)import pyglet
import pymunk.pyglet_util
# Set shape-specific colors
circle_shape.color = (255, 0, 0, 255) # Red circle
box_shape.color = (0, 255, 0, 255) # Green box
segment_shape.color = (0, 0, 255, 255) # Blue segment
# Use Pyglet color utilities
import pyglet.image
circle_shape.color = pyglet.image.get_color("pink")Publication-quality debug visualization for analysis and documentation.
import matplotlib.pyplot as plt
import pymunk
import pymunk.matplotlib_util
# Create figure and axes
fig, ax = plt.subplots(figsize=(10, 8))
# Create physics space
space = pymunk.Space()
space.gravity = (0, -981)
# Add some physics objects...
# ... physics simulation code ...
# Create debug drawing options
draw_options = pymunk.matplotlib_util.DrawOptions(ax)
# Draw physics state
space.debug_draw(draw_options)
# Configure plot
ax.set_xlim(0, 800)
ax.set_ylim(0, 600)
ax.set_aspect('equal')
ax.grid(True, alpha=0.3)
ax.set_title('Physics Simulation Debug View')
plt.show()import matplotlib.pyplot as plt
import matplotlib.animation as animation
import pymunk
import pymunk.matplotlib_util
fig, ax = plt.subplots()
space = pymunk.Space()
draw_options = pymunk.matplotlib_util.DrawOptions(ax)
def animate(frame):
ax.clear()
# Step physics
space.step(1/60.0)
# Draw current state
space.debug_draw(draw_options)
# Configure axes
ax.set_xlim(0, 800)
ax.set_ylim(0, 600)
ax.set_aspect('equal')
ax.set_title(f'Frame {frame}')
# Create animation
anim = animation.FuncAnimation(fig, animate, interval=16, blit=False)
plt.show()
# Save as video
# anim.save('physics_simulation.mp4', writer='ffmpeg', fps=60)import matplotlib.pyplot as plt
import pymunk.matplotlib_util
# Set shape colors using matplotlib color formats
circle_shape.color = (1.0, 0.0, 0.0, 1.0) # Red (RGBA floats)
box_shape.color = 'blue' # Named color
segment_shape.color = '#00FF00' # Hex color
# Create debug options with custom styling
draw_options = pymunk.matplotlib_util.DrawOptions(ax)
draw_options.shape_outline_color = pymunk.SpaceDebugColor(0, 0, 0, 255) # Black outlinesimport pymunk
class Camera:
def __init__(self, position=(0, 0), zoom=1.0):
self.position = pymunk.Vec2d(*position)
self.zoom = zoom
def follow_body(self, body, screen_size):
"""Make camera follow a body"""
self.position = body.position
def get_transform(self, screen_size):
"""Get transform matrix for debug drawing"""
screen_center = pymunk.Vec2d(*screen_size) * 0.5
return (
pymunk.Transform.translation(*screen_center) @
pymunk.Transform.scaling(self.zoom) @
pymunk.Transform.translation(-self.position.x, -self.position.y)
)
# Usage with any debug drawing system
camera = Camera()
camera.follow_body(player_body, (800, 600))
draw_options.transform = camera.get_transform((800, 600))
space.debug_draw(draw_options)import pymunk
class SelectiveDrawOptions(pymunk.pygame_util.DrawOptions):
"""Custom draw options that only draw specific shapes"""
def __init__(self, surface, shape_filter=None):
super().__init__(surface)
self.shape_filter = shape_filter or (lambda shape: True)
def draw_shape(self, shape):
"""Override to implement selective drawing"""
if self.shape_filter(shape):
super().draw_shape(shape)
# Filter functions
def draw_only_dynamic(shape):
return shape.body.body_type == pymunk.Body.DYNAMIC
def draw_only_circles(shape):
return isinstance(shape, pymunk.Circle)
# Usage
draw_options = SelectiveDrawOptions(screen, draw_only_dynamic)
space.debug_draw(draw_options)import time
import pymunk
class ProfiledDrawOptions(pymunk.pygame_util.DrawOptions):
"""Draw options with performance monitoring"""
def __init__(self, surface):
super().__init__(surface)
self.draw_time = 0
self.shape_count = 0
def draw_shape(self, shape):
start_time = time.perf_counter()
super().draw_shape(shape)
self.draw_time += time.perf_counter() - start_time
self.shape_count += 1
def reset_stats(self):
self.draw_time = 0
self.shape_count = 0
# Usage
draw_options = ProfiledDrawOptions(screen)
space.debug_draw(draw_options)
print(f"Drew {draw_options.shape_count} shapes in {draw_options.draw_time:.3f}s")
draw_options.reset_stats()import pygame
import pymunk
import pymunk.pygame_util
import math
# Initialize Pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Physics Debug Visualization")
clock = pygame.time.Clock()
# Create physics world
space = pymunk.Space()
space.gravity = (0, 981) # Downward gravity for Y-down coordinate system
# Create ground
ground_body = space.static_body
ground = pymunk.Segment(ground_body, (0, 550), (800, 550), 5)
ground.friction = 0.7
space.add(ground)
# Create falling objects
def create_ball(x, y):
mass = 10
radius = 20
moment = pymunk.moment_for_circle(mass, 0, radius)
body = pymunk.Body(mass, moment)
body.position = x, y
shape = pymunk.Circle(body, radius)
shape.friction = 0.7
shape.elasticity = 0.8
shape.color = pygame.Color("red") # Custom color
space.add(body, shape)
def create_box(x, y):
mass = 15
size = (40, 40)
moment = pymunk.moment_for_box(mass, size)
body = pymunk.Body(mass, moment)
body.position = x, y
shape = pymunk.Poly.create_box(body, size)
shape.friction = 0.7
shape.elasticity = 0.3
shape.color = pygame.Color("blue")
space.add(body, shape)
# Create debug drawing options
draw_options = pymunk.pygame_util.DrawOptions(screen)
draw_options.flags = (
pymunk.SpaceDebugDrawOptions.DRAW_SHAPES |
pymunk.SpaceDebugDrawOptions.DRAW_CONSTRAINTS
) # Don't draw collision points for cleaner view
# Main game loop
running = True
while running:
dt = clock.tick(60) / 1000.0
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
mouse_pos = pygame.mouse.get_pos()
if event.button == 1: # Left click - ball
create_ball(*mouse_pos)
elif event.button == 3: # Right click - box
create_box(*mouse_pos)
# Step physics
space.step(dt)
# Clear screen
screen.fill((255, 255, 255))
# Draw physics debug visualization
space.debug_draw(draw_options)
# Draw UI
font = pygame.font.Font(None, 36)
text = font.render("Left click: Ball, Right click: Box", True, (0, 0, 0))
screen.blit(text, (10, 10))
pygame.display.flip()
pygame.quit()import pygame
import pymunk
import pymunk.pygame_util
# Create multiple views for different debug information
screen = pygame.display.set_mode((1200, 600))
# Create subsurfaces for different views
main_view = screen.subsurface((0, 0, 800, 600))
detail_view = screen.subsurface((800, 0, 400, 300))
stats_view = screen.subsurface((800, 300, 400, 300))
# Create different draw options for each view
main_draw = pymunk.pygame_util.DrawOptions(main_view)
main_draw.flags = pymunk.SpaceDebugDrawOptions.DRAW_SHAPES
detail_draw = pymunk.pygame_util.DrawOptions(detail_view)
detail_draw.flags = (
pymunk.SpaceDebugDrawOptions.DRAW_SHAPES |
pymunk.SpaceDebugDrawOptions.DRAW_CONSTRAINTS |
pymunk.SpaceDebugDrawOptions.DRAW_COLLISION_POINTS
)
# Different zoom levels
main_draw.transform = pymunk.Transform.identity()
detail_draw.transform = pymunk.Transform.scaling(2.0) # 2x zoom
# In main loop:
main_view.fill((255, 255, 255))
detail_view.fill((240, 240, 240))
space.debug_draw(main_draw)
space.debug_draw(detail_draw)
# Draw stats on stats_view
# ... statistics rendering code ...Pymunk's visualization utilities provide powerful debugging capabilities across multiple graphics libraries, enabling rapid prototyping, debugging, and analysis of physics simulations with minimal setup and maximum flexibility.
Install with Tessl CLI
npx tessl i tessl/pypi-pymunk