The little white circle, known in the digital world as Pingy, was having an existential crisis. He was a ball, and balls were meant to bounce, to fly with crisp, perfect elasticity. Yet, here he was, plastered against the blue paddle named Paddles.
It wasn't a normal contact; it was a cosmic entanglement.
"Let go of me, you big rectangle!" Pingy screamed, vibrating furiously.
Paddles, a Kinematic Body whose every move was dictated by a distant, jittery mouse cursor, didn't mean to hold him. "I'm trying, I swear! I've executed my full impulse to separate us, but the time step is too big!"
Pingy's crisis was the result of a flaw in their digital reality: penetration caused by Discrete Collision Detection (DCD).
The game's clock, ticking at 60 frames per second, was supposed to check Pingy's position at t1 and then again at
t2. But Pingy was fast—too fast. In one time slice, he'd jumped from being outside Paddles to being halfway inside him.
The Pymunk Sequential Impulse Solver, their benevolent god, immediately registered the crime. "Overlap! Apply impulse to separate!"
But before the impulse could fully push Pingy back to the surface, the next time step arrived. Gravity was pulling Pingy down, and Paddles' surface was pushing him up, all while Pingy’s internal velocity was still trying to move him forward.
"I'm stuck in the Zeno's Paradox of physics engines!" Pingy wailed. "I'm forever approaching the exit, but never quite reaching it!"
The paddle surface felt like a sticky, repulsive field. Every millisecond, the solver applied a new impulse, resulting in a rapid, microscopic vibration that translated to an infuriating standstill on the screen.
Suddenly, a change came. A distant developer—their true god—finally adjusted the code, not by implementing the godly Offset Geometric Contact (OGC) model (which was reserved for fancy, billowing capes in ), but by simply making the right choice for the current world:
ball_shape.friction = 0.0
With the removal of that final, tiny opposing force, the Sequential Impulse Solver finally won its tug-of-war. The microscopic forces holding Pingy hostage vanished. The final, forceful impulse took hold.
BWOOONG...
Pingy shot away from Paddles' surface with the perfect, glorious elasticity he was born with. He was flying, bouncing crisply off the walls, a free ball once more.
"I'm un-stuck!" Pingy cheered, realizing the solution to his existential problem wasn't magic, but just a properly applied zero-friction constraint that allowed the Impulse Solver to do its job unimpeded. The world was stable again.
Now some raw technical discussion...
Pymunk, being a wrapper for the Chipmunk engine, relies on an Impulse-Based Solver combined with a simple form of geometric offset to ensure stability.
Pymunk's Technology to Prevent Sticking
The "stuck ball" is a result of Discrete Collision Detection (DCD) failing at high speeds. Pymunk avoids this using a more robust, impulse-based physics loop.
Physics Concept | Pymunk Implementation in Script | How It Prevents Sticking |
1. Sequential Impulse Solver | space.step(1.0 / FPS) | This is the core technology. Instead of just pushing objects apart when penetration is detected, the solver iteratively calculates the precise impulse (force X |
2. Non-Penetration Constraint | Built into the pymunk.Circle and pymunk.Segment shapes. | Pymunk treats the shapes as having a defined, non-negotiable surface thickness. When two shapes get too close, the solver activates a distance constraint. This is analogous to a simple, baked-in form of the Offset Geometric Contact (OGC) idea: it ensures the geometric boundaries are never violated. |
3. High Elasticity | ball_shape.elasticity = 1.0 | In |
4. Zero Friction | ball_shape.friction = 0.0 | Friction is a force that opposes motion between surfaces. Setting it to |
5. Kinematic Paddle Control | paddle_body = pymunk.Body(body_type=pymunk.Body.KINEMATIC) | By using a Kinematic body and controlling its position directly (paddle_body.position = (clamped_x, PADDLE_Y) ), you tell the solver the paddle's movement is guaranteed. This simplifies the physics calculation, allowing the solver to focus purely on resolving the ball's collision against a predictable target, which improves stability. |
Why OGC is Not Used (and Why Pymunk is Enough)
While the problem the ball-and-paddle encounters is conceptually the same as what Offset Geometric Contact (OGC) solves, Pymunk/Chipmunk doesn't need to implement the complex OGC model because of three key differences:
Low Dimensionality (2D): Pymunk is a 2D engine. OGC is primarily designed for complex 3D simulations of co-dimensional objects (thin cloth, shells, etc.) where penetration is far more difficult to detect and resolve.
Simple Geometry: Your simulation uses simple geometries (circles and line segments). Pymunk's solver is highly optimized for these shapes.
Efficiency over Guarantee: OGC provides a mathematical guarantee of zero penetration, which is extremely expensive. Pymunk prioritizes real-time efficiency and uses its impulse solver to achieve a highly reliable near-zero penetration state, which is perfectly sufficient for games.
In short, Pymunk uses a combination of an Impulse-Based Solver and Constraint Satisfaction to ensure the ball never violates the surface of the paddle, making the expensive, complex methods like OGC unnecessary for a simple, stable
game.
The Source Code...
import pymunk
import pymunk.pygame_util
import pygame
# 1. Import the DrawOptions class for modern Pymunk drawing
from pymunk.pygame_util import DrawOptions
pygame.init()
screen_width = 800
screen_height = 600
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("Pymunk Ping Pong (No Sticking)")
# --- Physics Space Setup ---
space = pymunk.Space()
# For classic ping pong, set gravity to (0, 0)
space.gravity = (0, 0)
# --- Ball: Dynamic Circle ---
mass = 1
radius = 10
moment = pymunk.moment_for_circle(mass, 0, radius)
ball_body = pymunk.Body(mass, moment)
ball_body.position = (screen_width // 2, screen_height // 2)
# Give the ball an initial velocity to start the game
ball_body.velocity = (300, 200)
ball_shape = pymunk.Circle(ball_body, radius)
ball_shape.elasticity = 1.0 # Perfect bounce for ping pong
ball_shape.friction = 0.0 # No friction to maintain speed
ball_shape.color = (255, 255, 255, 255) # White ball
space.add(ball_body, ball_shape)
# --- Paddle (Player): Kinematic Body ---
# Use a Kinematic body so we can move it manually without physics forces
PADDLE_WIDTH = 100
PADDLE_HEIGHT = 15
PADDLE_Y = screen_height - 50
paddle_body = pymunk.Body(body_type=pymunk.Body.KINEMATIC)
paddle_body.position = (screen_width // 2, PADDLE_Y)
# Segment shape for the paddle surface
paddle_shape = pymunk.Segment(
paddle_body,
(-PADDLE_WIDTH // 2, 0), # Start relative to body center
(PADDLE_WIDTH // 2, 0), # End relative to body center
PADDLE_HEIGHT // 2 # Thickness
)
paddle_shape.elasticity = 1.0
paddle_shape.friction = 0.0
paddle_shape.color = (0, 100, 255, 255) # Blue paddle
space.add(paddle_body, paddle_shape)
# --- Walls/Boundaries (Static Segments) ---
# Create four static segments to form the screen boundaries
boundary_lines = [
pymunk.Segment(space.static_body, (0, 0), (screen_width, 0), 1), # Top
pymunk.Segment(space.static_body, (0, screen_height), (screen_width, screen_height), 1), # Bottom
pymunk.Segment(space.static_body, (0, 0), (0, screen_height), 1), # Left
pymunk.Segment(space.static_body, (screen_width, 0), (screen_width, screen_height), 1) # Right
]
for line in boundary_lines:
line.elasticity = 1.0
line.friction = 0.0
line.color = (100, 100, 100, 255)
space.add(line)
# --- Game Loop ---
running = True
clock = pygame.time.Clock()
FPS = 60
# 2. Initialize DrawOptions, passing the screen surface
options = DrawOptions(screen)
while running:
# --- Input Handling ---
mouse_x, _ = pygame.mouse.get_pos()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Move paddle to follow the mouse's X position
target_x = mouse_x
# Clamp paddle position within screen boundaries
min_x = PADDLE_WIDTH // 2
max_x = screen_width - PADDLE_WIDTH // 2
clamped_x = max(min_x, min(target_x, max_x))
# Move the kinematic body
paddle_body.position = (clamped_x, PADDLE_Y)
# --- Physics Step (Solver) ---
# The time step duration is passed as the argument
space.step(1.0 / FPS)
# --- Drawing ---
screen.fill((10, 20, 30)) # Dark blue background
# 3. Call debug_draw() on the space, passing the options
space.debug_draw(options)
pygame.display.flip()
clock.tick(FPS)
pygame.quit()