Module 12 of 20 Phase: Physics & Worlds

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.

⚛️
12 Lessons
This module
~3 hrs
Estimated time
📊
Intermediate-Advanced
Difficulty
🐍
Plain math
No external library
Section 1

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.

integration.py
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
Section 2

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_sim.py
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)
Section 3

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.

elastic_collision.py
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
Section 4

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

bounce.py
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
💡
Key insight: Each bounce should get slightly shorter because RESTITUTION < 1 removes a bit of energy every time — this is what makes a bouncing ball look realistic instead of bouncing at the same height forever.
Section 5

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:

projectile.py
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
Mini Project

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.

Section 6 · Data Chart

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.

0 150 300 Height (px) Time (each hump = one bounce) → 300px (drop) 170px 95px 55px 30px
Bounce height decays each impact when restitution < 1 — here each bounce keeps roughly 55-58% of the previous peak height.

🧠 Quick Knowledge Check

1. What is the correct order of operations in Euler integration each frame?
2. What does a restitution value of 0.7 mean when a ball bounces?
3. Why is vy negative in the projectile launch code for an upward angle?
4. What is momentum, in simple terms?
5. In the elastic collision example for equal masses, what effectively happens?
Finished Module 12?

Mark it complete to track your progress through Complete Pygame Mastery 2026.

🗒 Cheat Sheet 📝 Worksheet