Simple Physics
Simulations from Scratch
No physics engine, no external library — just the same handful of equations every 2D game uses. This module builds gravity, momentum, bouncing and projectile motion using plain Python math.
Numeric Integration: The Core Physics Loop
Every physics simulation boils down to the same two lines, run every frame: update velocity from acceleration, then update position from velocity. This is called Euler integration — simple, fast, and accurate enough for almost all 2D games.
class PhysicsBody:
def __init__(self, x, y):
self.pos = pygame.Vector2(x, y)
self.vel = pygame.Vector2(0, 0)
self.acc = pygame.Vector2(0, 0)
def update(self, dt):
self.vel += self.acc * dt # acceleration changes velocity
self.pos += self.vel * dt # velocity changes position
self.acc *= 0 # reset forces each frame
Gravity Simulation
Gravity is a constant downward acceleration applied to every falling object, every frame — nothing more exotic than adding a fixed value to acc.y before integrating.
GRAVITY = pygame.Vector2(0, 980) # pixels/sec^2, downward
def apply_gravity(body, dt):
body.acc += GRAVITY
body.vel += body.acc * dt
body.pos += body.vel * dt
body.acc = pygame.Vector2(0, 0)
Momentum & Elastic Collisions
Momentum is mass × velocity. When two objects collide elastically (like billiard balls), you can swap their velocities scaled by their relative masses — for equal-mass objects, the simplest and most common case in games, they simply swap velocities entirely.
def elastic_collision_equal_mass(ball_a, ball_b):
# Simplified 1D elastic collision along the line connecting the centers
direction = (ball_b.pos - ball_a.pos).normalize()
v_a_along = ball_a.vel.dot(direction)
v_b_along = ball_b.vel.dot(direction)
# equal masses: swap the velocity components along the collision axis
ball_a.vel += (v_b_along - v_a_along) * direction
ball_b.vel += (v_a_along - v_b_along) * direction
Bouncing & Restitution
Bouncing off a surface means reversing the velocity component perpendicular to that surface, scaled by a restitution coefficient (0 = no bounce, sticks; 1 = perfectly elastic, bounces forever at the same height).
RESTITUTION = 0.7 # energy retained per bounce (0-1)
def bounce_off_floor(body, floor_y, radius):
if body.pos.y + radius >= floor_y:
body.pos.y = floor_y - radius
body.vel.y *= -RESTITUTION # reverse and dampen vertical velocity
Projectile Motion
A projectile (thrown rock, fired arrow, cannonball) is just an object given an initial velocity at a launch angle, then left to gravity. Decompose the launch speed into x/y components with trigonometry:
import math
def launch_projectile(x, y, speed, angle_degrees):
angle_rad = math.radians(angle_degrees)
vx = speed * math.cos(angle_rad)
vy = -speed * math.sin(angle_rad) # negative: up is -y in Pygame
return PhysicsBody(x, y), pygame.Vector2(vx, vy)
body, initial_vel = launch_projectile(100, 500, speed=400, angle_degrees=45)
body.vel = initial_vel
Bouncing Balls Simulation
Combine everything above: spawn 10-20 PhysicsBody circles with random starting velocities, apply gravity every frame, bounce them off all four screen edges using the restitution formula, and optionally apply the elastic collision function whenever two balls overlap. This single demo exercises gravity, bouncing, momentum and integration together.
Watching Restitution in Action
Plotting a bouncing ball's height against time makes the effect of RESTITUTION < 1 impossible to miss: every impact returns a little less height than the last, until the ball settles on the floor.