Learning Goals
3 minBy the end of this lesson you can:
- Make a ball bounce off the floor by reversing and scaling
vy(vy = -vy * BOUNCINESS) on floor impact. - Apply sliding friction by multiplying
vxby a value just below 1 (e.g.0.9) each frame so horizontal motion slows gradually. - Stop tiny residual bounces by clamping near-zero velocities to 0 so the ball settles cleanly.
Warm-Up · Gravity from PZ-35
5 minLast lesson you used gravity. What does this trace produce after three frames?
vy = 0.0 GRAVITY = 1.0 FLOOR_Y = 200 y = 100 for frame in range(3): vy += GRAVITY y += vy if y >= FLOOR_Y: y = FLOOR_Y vy = 0.0 print(f"frame {frame}: y={y:.1f} vy={vy:.1f}")
Show the answer
Output
frame 0: y=101.0 vy=1.0 frame 1: y=103.0 vy=2.0 frame 2: y=106.0 vy=3.0
The ball accelerates downward every frame. It never reaches FLOOR_Y = 200 in just three frames — that would need more iterations. Today you change the floor landing line to bounce instead of stop.
New Concept · Bouncing and Friction
12 minThink of a rubber ball hitting a wooden floor. It doesn't bounce back to the same height — it loses some energy each bounce and eventually rests. That energy loss is captured by a single number called the coefficient of restitution (or just "bounciness"). A value of 1.0 is a perfect bounce; 0.0 is a dead thud.
Bouncing off the floor
Instead of setting vy = 0 on impact, reverse the sign and multiply by the bounciness factor:
BOUNCINESS = 0.7 # 70 % of energy survives each bounce # inside update(), replace the floor landing logic: if ball_y + RADIUS >= FLOOR_Y: ball_y = FLOOR_Y - RADIUS vy = -vy * BOUNCINESS
-vy flips the direction (upward); * 0.7 shrinks the speed to 70 %. The ball rises less each time until it settles.
Settling — stopping tiny bounces
After many small bounces vy becomes a tiny number like -0.003. The ball jitters forever. Stop it once the bounce is too small to see:
if ball_y + RADIUS >= FLOOR_Y: ball_y = FLOOR_Y - RADIUS vy = -vy * BOUNCINESS if abs(vy) < 0.5: # too small to see — just stop vy = 0.0
Sliding friction on vx
Friction reduces horizontal speed a little every frame. Multiply vx by a value just below 1:
FRICTION = 0.9 # 10 % of vx is lost each frame def update(): global vx if ball_y + RADIUS >= FLOOR_Y: # only apply friction when on the floor vx *= FRICTION
Worked Example · A Bouncing Ball
12 minDaniel Tan wants a ball that is kicked with a random velocity, bounces off the floor and left/right walls, and gradually rolls to a stop. Save as bounce_ball.py:
# bounce_ball.py — bouncing ball with energy loss and friction import pgzrun import random WIDTH = 600 HEIGHT = 400 TITLE = "Bouncing Ball" GRAVITY = 0.4 BOUNCINESS = 0.72 FRICTION = 0.88 FLOOR_Y = 370 RADIUS = 18
ball_x = 100.0 ball_y = 100.0 vx = random.uniform(3, 6) vy = 0.0 def reset(): global ball_x, ball_y, vx, vy ball_x = 100.0 ball_y = 100.0 vx = random.uniform(3, 6) vy = 0.0 def update(): global ball_x, ball_y, vx, vy vy += GRAVITY ball_x += vx ball_y += vy # floor bounce if ball_y + RADIUS >= FLOOR_Y: ball_y = FLOOR_Y - RADIUS vy = -vy * BOUNCINESS if abs(vy) < 0.5: vy = 0.0 vx *= FRICTION # left wall if ball_x - RADIUS <= 0: ball_x = RADIUS vx = -vx * BOUNCINESS # right wall if ball_x + RADIUS >= WIDTH: ball_x = WIDTH - RADIUS vx = -vx * BOUNCINESS
def on_key_down(key): if key == keys.SPACE: reset() def draw(): screen.fill("lightyellow") screen.draw.filled_rect(Rect((0, FLOOR_Y), (WIDTH, HEIGHT - FLOOR_Y)), "sienna") screen.draw.filled_circle((int(ball_x), int(ball_y)), RADIUS, "crimson") screen.draw.text("SPACE = new kick", topleft=(10, 10), fontsize=20, color="grey") pgzrun.go()
Press SPACE several times to see different kick trajectories. Notice the ball's bounces get shorter each time until it rolls to a stop.
Try It Yourself
13 minAdd a ceiling at y = 30 (leave room for the hint text). When the ball hits the ceiling, apply the same bounce formula as the floor. Use the same BOUNCINESS value.
CEILING_Y = 30 # inside update(): if ball_y - RADIUS <= CEILING_Y: ball_y = CEILING_Y + RADIUS vy = -vy * BOUNCINESS
Let the player change BOUNCINESS with the UP and DOWN keys (range 0.1 to 1.0, step 0.05). Display the current value on screen. Notice how 1.0 gives a perfect bounce and 0.1 gives an almost dead thud.
bounciness = 0.72 def on_key_down(key): global bounciness if key == keys.UP: bounciness = min(1.0, bounciness + 0.05) if key == keys.DOWN: bounciness = max(0.1, bounciness - 0.05)
Mini-Challenge · Multiple Balls
8 minCombine today's physics with lists from Level 1. Spawn 5 balls, each starting at a random x position with a random vx. Store them as a list of dicts and loop through the list in update() and draw().
It works if…
5 balls bounce independently, each with different trajectories, all settling on the floor at different times
Show one possible solution
import random NUM_BALLS = 5 COLOURS = ["crimson", "dodgerblue", "gold", "limegreen", "orange"] balls = [ { "x": float(random.randint(50, 550)), "y": float(random.randint(50, 150)), "vx": random.uniform(-4, 4), "vy": 0.0, "colour": COLOURS[i], } for i in range(NUM_BALLS) ] def update(): for b in balls: b["vy"] += GRAVITY b["x"] += b["vx"] b["y"] += b["vy"] if b["y"] + RADIUS >= FLOOR_Y: b["y"] = FLOOR_Y - RADIUS b["vy"] = -b["vy"] * BOUNCINESS if abs(b["vy"]) < 0.5: b["vy"] = 0.0 b["vx"] *= FRICTION if b["x"] - RADIUS <= 0: b["x"] = RADIUS b["vx"] = abs(b["vx"]) * BOUNCINESS if b["x"] + RADIUS >= WIDTH: b["x"] = WIDTH - RADIUS b["vx"] = -abs(b["vx"]) * BOUNCINESS
Store each ball as a dict and loop through the list — same physics code applies to every ball.
Recap
3 minBouncing is one line: vy = -vy * BOUNCINESS. Friction is one line: vx *= FRICTION. Together they simulate energy loss so objects settle naturally rather than bouncing forever. Clamping tiny velocities to zero prevents invisible micro-jitter. These same patterns apply to any physics object in any game.
Vocabulary Card
- BOUNCINESS (coefficient of restitution)
- A value between 0 and 1 that scales the velocity after a bounce. 1.0 = perfect elastic bounce; 0.0 = dead stop.
- FRICTION
- A value just below 1 that multiplies
vxeach frame, slowing horizontal motion gradually. - velocity clamping
- Setting
vy = 0whenabs(vy) < thresholdto stop invisible micro-bounces.
Homework
4 minExtend the bouncing ball demo so the ball changes colour every time it hits the floor. Keep a list of colours (e.g. ["crimson", "gold", "dodgerblue", "limegreen"]) and cycle through them using the modulo operator (%) and a counter. Save as colour_bounce.py.
Sample · colour_bounce.py
# colour_bounce.py — ball changes colour each floor bounce import pgzrun WIDTH = 600 HEIGHT = 400 GRAVITY = 0.4 BOUNCINESS = 0.72 FRICTION = 0.88 FLOOR_Y = 370 RADIUS = 18 COLOURS = ["crimson", "gold", "dodgerblue", "limegreen", "orange", "violet"] colour_index = 0 ball_x = 100.0 ball_y = 100.0 vx = 4.5 vy = 0.0 def update(): global ball_x, ball_y, vx, vy, colour_index vy += GRAVITY ball_x += vx ball_y += vy if ball_y + RADIUS >= FLOOR_Y: ball_y = FLOOR_Y - RADIUS vy = -vy * BOUNCINESS if abs(vy) < 0.5: vy = 0.0 vx *= FRICTION colour_index = (colour_index + 1) % len(COLOURS) if ball_x - RADIUS <= 0: ball_x = RADIUS vx = -vx * BOUNCINESS if ball_x + RADIUS >= WIDTH: ball_x = WIDTH - RADIUS vx = -vx * BOUNCINESS def draw(): screen.fill("lightyellow") screen.draw.filled_rect(Rect((0, FLOOR_Y), (WIDTH, HEIGHT - FLOOR_Y)), "sienna") screen.draw.filled_circle((int(ball_x), int(ball_y)), RADIUS, COLOURS[colour_index]) pgzrun.go()
(colour_index + 1) % len(COLOURS) wraps the index back to 0 after the last colour — the modulo operator is perfect for cycling through a list.