Bounce and Friction
Making Objects Interact Realistically
Bounce and friction bring surfaces to life! They determine how objects respond to collisions - whether they bounce like rubber balls, slide like ice, or stick like velcro. Let's master these essential physics properties! โฝ๐ฑ
Understanding Bounce and Friction
๐พ The Sports Equipment Analogy
Different materials behave differently:
- Basketball: High bounce (elastic), moderate friction
- Bowling Ball: No bounce (inelastic), low friction
- Tennis Ball: Medium bounce, high friction (fuzzy surface)
- Ice Puck: Low bounce, very low friction
- Putty: No bounce, high friction (sticks)
Interactive Bounce and Friction Lab
Click to drop balls with different materials!
Material: Rubber Ball | Surface: Normal Floor | Balls: 0
Coefficient of Restitution (Bounce)
import pygame
import math
from typing import Optional
class BouncingObject:
"""Object with bounce physics"""
def __init__(self, x: float, y: float) -> None:
self.position: pygame.math.Vector2 = pygame.math.Vector2(x, y)
self.velocity: pygame.math.Vector2 = pygame.math.Vector2(0, 0)
self.radius: int = 20
# Coefficient of restitution (0 = no bounce, 1 = perfect bounce)
self.restitution: float = 0.8
# Material properties
self.material: str = "rubber"
def handle_bounce(self, surface_normal: pygame.math.Vector2, surface_restitution: float = 1.0) -> Optional[float]:
"""Handle bounce off a surface"""
# Calculate relative velocity along normal
velocity_along_normal = self.velocity.dot(surface_normal)
# Don't bounce if moving away from surface
if velocity_along_normal > 0:
return
# Calculate effective restitution (minimum of both materials)
effective_restitution = min(self.restitution, surface_restitution)
# Calculate impulse
impulse = -(1 + effective_restitution) * velocity_along_normal
# Apply impulse to velocity
self.velocity += impulse * surface_normal
# Energy loss visualization
energy_before = self.velocity.length_squared()
energy_after = self.velocity.length_squared()
energy_lost = energy_before - energy_after
return energy_lost
# Different material bounces
class MaterialBounce:
# Common material restitution values
MATERIALS: dict[str, float] = {
"super_ball": 0.95, # Almost perfect bounce
"rubber": 0.8, # Good bounce
"tennis_ball": 0.7, # Moderate bounce
"leather": 0.5, # Some bounce
"wood": 0.4, # Little bounce
"clay": 0.1, # Almost no bounce
"perfectly_elastic": 1.0, # No energy loss
"perfectly_inelastic": 0.0 # Complete energy loss
}
@staticmethod
def calculate_bounce_height(drop_height: float, restitution: float) -> float:
"""Calculate how high object bounces"""
# h_bounce = h_drop * eยฒ
return drop_height * (restitution ** 2)
@staticmethod
def calculate_velocity_after_bounce(velocity_before: float, restitution: float) -> float:
"""Calculate velocity after bounce"""
return -velocity_before * restitution
@staticmethod
def successive_bounces(initial_height: float, restitution: float, num_bounces: int) -> list[float]:
"""Calculate heights of successive bounces"""
heights = [initial_height]
current_height = initial_height
for _ in range(num_bounces):
current_height *= restitution ** 2
heights.append(current_height)
return heights
Friction Forces
# Friction implementation
class FrictionObject:
def __init__(self, x: float, y: float) -> None:
self.position: pygame.math.Vector2 = pygame.math.Vector2(x, y)
self.velocity: pygame.math.Vector2 = pygame.math.Vector2(0, 0)
self.mass: float = 1.0
# Friction coefficients
self.static_friction: float = 0.6 # Friction when stationary
self.kinetic_friction: float = 0.4 # Friction when moving
self.rolling_friction: float = 0.01 # For rolling objects
# State
self.is_rolling: bool = False
self.on_surface: bool = False
def apply_friction(self, normal_force: float, dt: float) -> None:
"""Apply friction force"""
if not self.on_surface or self.velocity.length() == 0:
return
# Determine friction type
if self.is_rolling:
friction_coefficient = self.rolling_friction
elif self.velocity.length() < 0.1: # Nearly stopped
friction_coefficient = self.static_friction
else:
friction_coefficient = self.kinetic_friction
# Calculate friction force (F = ฮผ * N)
friction_magnitude = friction_coefficient * normal_force
# Friction opposes motion
if self.velocity.length() > 0:
friction_direction = -self.velocity.normalize()
friction_force = friction_direction * friction_magnitude
# Apply friction
friction_acceleration = friction_force / self.mass
# Don't reverse direction (stop at zero)
velocity_change = friction_acceleration * dt
if velocity_change.length() > self.velocity.length():
self.velocity = pygame.math.Vector2(0, 0)
else:
self.velocity += velocity_change
# Different surface frictions
class SurfaceFriction:
SURFACES: dict[str, float] = {
"ice": 0.02, # Very slippery
"wet_ice": 0.05, # Slippery
"metal": 0.15, # Smooth
"wood": 0.3, # Moderate
"concrete": 0.5, # Rough
"rubber": 0.7, # High friction
"sand": 0.9, # Very high friction
"velcro": 1.5 # Extreme friction
}
@staticmethod
def calculate_stopping_distance(initial_velocity: float, friction_coefficient: float, gravity: float = 9.8) -> float:
"""Calculate distance to stop under friction"""
if friction_coefficient <= 0:
return float('inf')
deceleration = friction_coefficient * gravity
stopping_distance = (initial_velocity ** 2) / (2 * deceleration)
return stopping_distance
@staticmethod
def calculate_stopping_time(initial_velocity: float, friction_coefficient: float, gravity: float = 9.8) -> float:
"""Calculate time to stop under friction"""
if friction_coefficient <= 0:
return float('inf')
deceleration = friction_coefficient * gravity
stopping_time = initial_velocity / deceleration
return stopping_time
# Air resistance (drag)
class AirResistance:
def __init__(self, drag_coefficient: float = 0.47) -> None: # Sphere drag coefficient
self.drag_coefficient: float = drag_coefficient
self.air_density: float = 1.2 # kg/mยณ at sea level
def calculate_drag_force(self, velocity: pygame.math.Vector2, cross_section_area: float) -> pygame.math.Vector2:
"""Calculate air resistance force"""
# F_drag = 0.5 * ฯ * vยฒ * C_d * A
speed = velocity.length()
if speed == 0:
return pygame.math.Vector2(0, 0)
drag_magnitude = 0.5 * self.air_density * speed * speed * \
self.drag_coefficient * cross_section_area
# Drag opposes velocity
drag_direction = -velocity.normalize()
return drag_direction * drag_magnitude
Combined Bounce and Friction
# Complete physics object with bounce and friction
class CompletePhysicsObject:
def __init__(self, x: float, y: float, material: str = "rubber") -> None:
self.position: pygame.math.Vector2 = pygame.math.Vector2(x, y)
self.velocity: pygame.math.Vector2 = pygame.math.Vector2(0, 0)
self.acceleration: pygame.math.Vector2 = pygame.math.Vector2(0, 0)
# Physical properties
self.mass: float = 1.0
self.radius: int = 15
# Material properties
self.set_material(material)
# State
self.on_ground: bool = False
self.rotation: float = 0
self.angular_velocity: float = 0
def set_material(self, material: str) -> None:
"""Set material properties"""
materials = {
"rubber": {
"restitution": 0.8,
"static_friction": 0.9,
"kinetic_friction": 0.7,
"rolling_friction": 0.02
},
"steel": {
"restitution": 0.6,
"static_friction": 0.4,
"kinetic_friction": 0.3,
"rolling_friction": 0.001
},
"wood": {
"restitution": 0.4,
"static_friction": 0.5,
"kinetic_friction": 0.4,
"rolling_friction": 0.05
},
"ice": {
"restitution": 0.9,
"static_friction": 0.05,
"kinetic_friction": 0.02,
"rolling_friction": 0.001
}
}
if material in materials:
props = materials[material]
self.restitution = props["restitution"]
self.static_friction = props["static_friction"]
self.kinetic_friction = props["kinetic_friction"]
self.rolling_friction = props["rolling_friction"]
def handle_collision_with_surface(self, surface_point: pygame.math.Vector2, surface_normal: pygame.math.Vector2, surface_properties: dict[str, float]) -> None:
"""Handle collision with a surface"""
# Calculate relative velocity
relative_velocity = self.velocity
# Decompose velocity into normal and tangential components
velocity_normal = relative_velocity.dot(surface_normal) * surface_normal
velocity_tangent = relative_velocity - velocity_normal
# Apply restitution to normal component
effective_restitution = self.restitution * surface_properties.get("restitution", 1.0)
velocity_normal *= -effective_restitution
# Apply friction to tangential component
if velocity_tangent.length() > 0:
# Calculate normal force
normal_force = abs(velocity_normal.length()) * self.mass
# Determine friction coefficient
if velocity_tangent.length() < 0.1:
friction = self.static_friction
else:
friction = self.kinetic_friction
# Apply surface friction modifier
friction *= surface_properties.get("friction", 1.0)
# Calculate friction impulse
friction_impulse = min(friction * normal_force, velocity_tangent.length())
# Apply friction
if velocity_tangent.length() > 0:
velocity_tangent -= velocity_tangent.normalize() * friction_impulse
# Combine components
self.velocity = velocity_normal + velocity_tangent
# Update rotation for rolling
if self.on_ground:
self.angular_velocity = -velocity_tangent.x / self.radius
def update(self, dt: float) -> None:
"""Update physics"""
# Apply forces
self.velocity += self.acceleration * dt
# Apply air resistance
drag = self.velocity * self.velocity.length() * 0.001
self.velocity -= drag * dt
# Update position
self.position += self.velocity * dt
# Update rotation
self.rotation += self.angular_velocity * dt
# Reset acceleration
self.acceleration = pygame.math.Vector2(0, 0)
Complete Bounce and Friction Demo
import pygame
import math
import random
class BounceAndFrictionDemo:
def __init__(self):
pygame.init()
self.screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Bounce and Friction Demo")
self.clock = pygame.time.Clock()
self.font = pygame.font.Font(None, 24)
# Physics
self.gravity = pygame.math.Vector2(0, 500)
# Objects
self.balls = []
self.surfaces = []
# Setup demo
self.setup_surfaces()
self.current_material = "rubber"
def setup_surfaces(self):
"""Create different surface types"""
# Floor sections with different properties
self.surfaces = [
{
"rect": pygame.Rect(0, 500, 200, 100),
"type": "ice",
"restitution": 0.9,
"friction": 0.05,
"color": (200, 230, 255)
},
{
"rect": pygame.Rect(200, 500, 200, 100),
"type": "wood",
"restitution": 0.5,
"friction": 0.4,
"color": (139, 90, 43)
},
{
"rect": pygame.Rect(400, 500, 200, 100),
"type": "rubber",
"restitution": 0.8,
"friction": 0.9,
"color": (50, 50, 50)
},
{
"rect": pygame.Rect(600, 500, 200, 100),
"type": "trampoline",
"restitution": 1.2, # Adds energy!
"friction": 0.3,
"color": (100, 150, 255)
}
]
# Add angled surfaces
self.angled_surfaces = [
{
"start": (100, 300),
"end": (300, 400),
"restitution": 0.7,
"friction": 0.3
},
{
"start": (500, 400),
"end": (700, 300),
"restitution": 0.7,
"friction": 0.3
}
]
def create_ball(self, x, y, material):
"""Create a new ball"""
ball = PhysicsBall(x, y, material)
self.balls.append(ball)
return ball
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
elif event.type == pygame.MOUSEBUTTONDOWN:
# Create ball at mouse position
x, y = pygame.mouse.get_pos()
self.create_ball(x, y, self.current_material)
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_1:
self.current_material = "rubber"
elif event.key == pygame.K_2:
self.current_material = "steel"
elif event.key == pygame.K_3:
self.current_material = "tennis"
elif event.key == pygame.K_4:
self.current_material = "glass"
elif event.key == pygame.K_5:
self.current_material = "putty"
elif event.key == pygame.K_c:
self.balls.clear()
elif event.key == pygame.K_SPACE:
# Create comparison test
self.create_comparison_test()
return True
def create_comparison_test(self):
"""Create balls of different materials for comparison"""
self.balls.clear()
materials = ["rubber", "steel", "tennis", "glass", "putty"]
for i, material in enumerate(materials):
x = 100 + i * 140
y = 100
ball = self.create_ball(x, y, material)
ball.velocity.y = 0 # Drop from rest
def update(self, dt):
"""Update physics"""
for ball in self.balls[:]:
# Apply gravity
ball.apply_force(self.gravity * ball.mass)
# Update ball
ball.update(dt)
# Check surface collisions
self.check_surface_collisions(ball)
# Check angled surface collisions
self.check_angled_collisions(ball)
# Remove balls that fall off screen
if ball.position.y > 700:
self.balls.remove(ball)
def check_surface_collisions(self, ball):
"""Check collisions with floor surfaces"""
for surface in self.surfaces:
if (surface["rect"].left < ball.position.x < surface["rect"].right and
ball.position.y + ball.radius > surface["rect"].top):
# Collision detected
ball.position.y = surface["rect"].top - ball.radius
# Apply bounce
ball.velocity.y *= -surface["restitution"] * ball.restitution
# Apply friction
friction_force = surface["friction"] * ball.friction * abs(ball.velocity.y)
if abs(ball.velocity.x) > friction_force:
ball.velocity.x -= math.copysign(friction_force, ball.velocity.x)
else:
ball.velocity.x = 0
ball.on_ground = True
# Special effects for trampoline
if surface["type"] == "trampoline" and ball.velocity.y < -100:
# Add particle effect
self.create_bounce_effect(ball.position.x, surface["rect"].top)
def check_angled_collisions(self, ball):
"""Check collisions with angled surfaces"""
for surface in self.angled_surfaces:
# Line segment collision detection
closest_point = self.get_closest_point_on_line(
ball.position,
surface["start"],
surface["end"]
)
distance = (ball.position - pygame.math.Vector2(closest_point)).length()
if distance < ball.radius:
# Calculate surface normal
dx = surface["end"][0] - surface["start"][0]
dy = surface["end"][1] - surface["start"][1]
length = math.sqrt(dx*dx + dy*dy)
normal = pygame.math.Vector2(-dy/length, dx/length)
# Move ball out of surface
ball.position = pygame.math.Vector2(closest_point) + normal * ball.radius
# Reflect velocity
dot = ball.velocity.dot(normal)
ball.velocity -= 2 * dot * normal
ball.velocity *= surface["restitution"] * ball.restitution
def get_closest_point_on_line(self, point, line_start, line_end):
"""Get closest point on line segment to a point"""
line_vec = pygame.math.Vector2(line_end) - pygame.math.Vector2(line_start)
point_vec = point - pygame.math.Vector2(line_start)
line_length = line_vec.length()
if line_length == 0:
return line_start
line_unitvec = line_vec / line_length
proj_length = min(max(point_vec.dot(line_unitvec), 0), line_length)
return pygame.math.Vector2(line_start) + line_unitvec * proj_length
def create_bounce_effect(self, x, y):
"""Create visual effect for bounce"""
# Would create particles here
pass
def draw(self):
"""Draw everything"""
self.screen.fill((40, 40, 50))
# Draw surfaces
for surface in self.surfaces:
pygame.draw.rect(self.screen, surface["color"], surface["rect"])
# Draw surface label
label = self.font.render(surface["type"], True, (255, 255, 255))
label_rect = label.get_rect(center=(
surface["rect"].centerx,
surface["rect"].centery
))
self.screen.blit(label, label_rect)
# Draw angled surfaces
for surface in self.angled_surfaces:
pygame.draw.line(self.screen, (150, 150, 150),
surface["start"], surface["end"], 5)
# Draw balls
for ball in self.balls:
ball.draw(self.screen)
# Draw UI
self.draw_ui()
def draw_ui(self):
"""Draw user interface"""
texts = [
f"Current Material: {self.current_material}",
"1-5: Select Material | Click: Drop Ball",
"Space: Comparison Test | C: Clear",
f"Balls: {len(self.balls)}"
]
y_offset = 10
for text in texts:
rendered = self.font.render(text, True, (255, 255, 255))
self.screen.blit(rendered, (10, y_offset))
y_offset += 30
def run(self):
running = True
dt = 0
while running:
running = self.handle_events()
self.update(dt)
self.draw()
pygame.display.flip()
dt = self.clock.tick(60) / 1000.0
pygame.quit()
# Physics ball class
class PhysicsBall:
def __init__(self, x, y, material):
self.position = pygame.math.Vector2(x, y)
self.velocity = pygame.math.Vector2(random.uniform(-100, 100), 0)
self.acceleration = pygame.math.Vector2(0, 0)
self.mass = 1.0
self.radius = 15
self.rotation = 0
self.angular_velocity = 0
# Set material properties
self.set_material(material)
self.on_ground = False
self.trail = []
self.max_trail = 20
def set_material(self, material):
materials = {
"rubber": {
"restitution": 0.8,
"friction": 0.7,
"color": (255, 100, 100)
},
"steel": {
"restitution": 0.6,
"friction": 0.4,
"color": (192, 192, 192)
},
"tennis": {
"restitution": 0.7,
"friction": 0.9,
"color": (154, 205, 50)
},
"glass": {
"restitution": 0.9,
"friction": 0.2,
"color": (135, 206, 235)
},
"putty": {
"restitution": 0.1,
"friction": 0.95,
"color": (139, 115, 85)
}
}
if material in materials:
props = materials[material]
self.restitution = props["restitution"]
self.friction = props["friction"]
self.color = props["color"]
self.material = material
def apply_force(self, force):
self.acceleration += force / self.mass
def update(self, dt):
# Update velocity and position
self.velocity += self.acceleration * dt
self.position += self.velocity * dt
# Update rotation based on horizontal velocity
if self.on_ground:
self.angular_velocity = -self.velocity.x / self.radius
self.rotation += self.angular_velocity * dt
# Add to trail
self.trail.append(self.position.copy())
if len(self.trail) > self.max_trail:
self.trail.pop(0)
# Reset
self.acceleration = pygame.math.Vector2(0, 0)
self.on_ground = False
# Wall bounces
if self.position.x - self.radius < 0:
self.position.x = self.radius
self.velocity.x *= -self.restitution
elif self.position.x + self.radius > 800:
self.position.x = 800 - self.radius
self.velocity.x *= -self.restitution
def draw(self, screen):
# Draw trail
if len(self.trail) > 1:
for i in range(1, len(self.trail)):
alpha = i / len(self.trail)
pygame.draw.line(screen,
tuple(int(c * alpha) for c in self.color),
self.trail[i-1], self.trail[i], 2)
# Draw ball
pygame.draw.circle(screen, self.color,
(int(self.position.x), int(self.position.y)),
self.radius)
# Draw rotation indicator
end_x = self.position.x + math.cos(self.rotation) * self.radius * 0.8
end_y = self.position.y + math.sin(self.rotation) * self.radius * 0.8
pygame.draw.line(screen, (0, 0, 0),
(self.position.x, self.position.y),
(end_x, end_y), 2)
if __name__ == "__main__":
demo = BounceAndFrictionDemo()
demo.run()
Best Practices
โก Bounce and Friction Tips
- Energy Conservation: Perfect bounce (e=1) is rare in reality
- Material Pairs: Use minimum restitution of colliding objects
- Static vs Kinetic: Static friction > kinetic for realism
- Rolling Friction: Much less than sliding friction
- Threshold Values: Stop objects when velocity is very small
- Surface Properties: Combine object and surface materials
- Visual Feedback: Show energy loss through trails/effects
Practice Exercises
๐ฏ Physics Challenges!
- Pinball Machine: Different bumpers with varying restitution
- Ice Hockey: Low friction puck physics
- Basketball Game: Realistic ball bouncing and rim physics
- Material Tester: Compare different materials side by side
- Rube Goldberg: Chain reactions with varied materials
- Friction Racing: Cars on different surface types
Key Takeaways
- โก Restitution controls bounce height (0=dead, 1=perfect)
- ๐พ Different materials have characteristic bounce values
- ๐ง Friction opposes motion and depends on surface contact
- ๐ฑ Combine object and surface properties for realism
- ๐ Energy is lost in real collisions
- ๐ Rolling friction < sliding friction < static friction
- ๐ฎ Tune values for game feel over strict realism
๐๏ธโโ๏ธ Practice Exercise: Three Balls, One Floor
๐๏ธโโ๏ธ Exercise 1: Restitution Fights Friction in 50 Lines
Objective: Build a Pygame demo that drops three balls simultaneously from the same height onto a single floor and lets you watch the lesson's two surface-physics rules play against each other in real time. Each ball has its own restitution (the bounce-response coefficient e in the lesson's velocity.y *= -restitution floor-collision formula) โ rubber 0.8 (lots of bounces), steel 0.6 (a few), putty 0.1 (essentially dead on first contact, matching the lesson's material table). Each frame a ball is on the ground, the floor's kinetic friction decays its horizontal velocity via velocity.x *= (1 - friction * dt) โ the lesson's surface-friction pattern. A resting threshold (if abs(velocity) < 5: velocity = 0) is non-negotiable here: friction decay is asymptotic and would jitter forever without it (Best Practice 'Threshold Values: Stop objects when velocity is very small'). Press R to drop a fresh trio with a small horizontal nudge so the friction decay is visible too. Couples directly to chat-44 M1's PhysicsObject.update() shape and chat-44 M2's bounce reflection, with the new ingredient being per-material restitution and per-surface friction working together.
Instructions:
- Reuse the
PhysicsObjectshape from the velocity_acceleration lesson:position,velocity,acceleration(allVector2),mass; addrestitution(float) and acolorfor visual identity. - Create three balls in a list: rubber (
e=0.8, red), steel (e=0.6, gray), putty (e=0.1, brown) โ values from the lesson's material table. - Define one floor
FRICTION = 1.5(per-second friction coefficient โ values 0.5โ3 feel right for kinetic friction in pixels-per-second-squared scale). - Each frame: apply gravity via
apply_force(Vector2(0, 980 * mass))(chat-44 M2 pattern), integrate Euler, reset acceleration. - On floor hit (
position.y >= floor_y): clampposition.y = floor_y, reflect Y velocity with energy loss asvelocity.y *= -restitutionโ the lesson's core bounce formula. - If the ball is touching the floor (
position.y >= floor_y - 1), apply kinetic friction to horizontal velocity each frame:velocity.x *= (1 - FRICTION * dt). - Resting threshold:
if abs(velocity.y) < 5: velocity.y = 0ANDif abs(velocity.x) < 1: velocity.x = 0โ without these, the asymptotic friction decay never zeroes out and the balls jitter forever (Best Practice 'Threshold Values' verbatim). - Press R to reset all three balls to the start height with a small
velocity.x = 150kick so horizontal-friction decay is visible alongside vertical restitution-bounce decay.
๐ก Hint
Three sign / formula gotchas show up: (a) the bounce formula is velocity.y *= -restitution (multiplied, not subtracted) โ multiplying by a negative reflects direction AND scales magnitude in one operation, which is why e=0 kills the bounce (multiplies to zero) and e=1 preserves it (a perfectly elastic bounce); (b) friction decay is exponential, not linear: v *= (1 - f*dt) halves velocity every ln(2)/f seconds, so it gets tiny but never reaches zero โ that's exactly why the Best Practice 'Threshold Values' rule exists; (c) putty looks broken at first because e=0.1 with v_y=โ400 px/s on impact gives v_y=+40 after the bounce, dropping back to 0 within one frame at 60 FPS โ that is the correct putty behavior (the lesson's material table calls it 'No bounce, sticks').
โ Example Solution
import pygame
from pygame.math import Vector2
pygame.init()
W, H = 600, 400
FLOOR_Y = H - 30
GRAVITY = 980 # chat-44 M2 pattern: positive y because Pygame Y grows down
FRICTION = 1.5 # per-second kinetic friction coefficient
screen = pygame.display.set_mode((W, H))
pygame.display.set_caption("Three Balls, One Floor")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 20)
class Ball:
def __init__(self, x: float, y: float, restitution: float, color: tuple[int, int, int], label: str) -> None:
self.position: Vector2 = Vector2(x, y)
self.velocity: Vector2 = Vector2(150, 0) # small horizontal kick
self.acceleration: Vector2 = Vector2(0, 0)
self.mass: float = 1.0
self.restitution: float = restitution
self.color: tuple[int, int, int] = color
self.label: str = label
self.bounces: int = 0
def apply_force(self, force: Vector2) -> None:
self.acceleration += force / self.mass
def update(self, dt: float) -> None:
# Gravity each frame (chat-44 M2 pattern)
self.apply_force(Vector2(0, GRAVITY * self.mass))
# Euler integration
self.velocity += self.acceleration * dt
self.position += self.velocity * dt
# Floor collision: reflect Y velocity with energy loss
if self.position.y >= FLOOR_Y:
self.position.y = FLOOR_Y
if abs(self.velocity.y) > 5:
self.velocity.y *= -self.restitution
self.bounces += 1
else:
self.velocity.y = 0 # threshold: stop micro-bounces
# Kinetic friction decays horizontal velocity
self.velocity.x *= (1 - FRICTION * dt)
# Threshold for horizontal too
if abs(self.velocity.x) < 1:
self.velocity.x = 0
# Reset acceleration each frame
self.acceleration = Vector2(0, 0)
def make_balls() -> list[Ball]:
return [
Ball(80, 60, 0.8, (255, 100, 100), "rubber e=0.8"),
Ball(80, 100, 0.6, (192, 192, 192), "steel e=0.6"),
Ball(80, 140, 0.1, (139, 115, 85), "putty e=0.1"),
]
balls = make_balls()
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.KEYDOWN and event.key == pygame.K_r:
balls = make_balls()
for b in balls:
b.update(dt)
screen.fill((20, 20, 30))
pygame.draw.line(screen, (120, 120, 150), (0, FLOOR_Y), (W, FLOOR_Y), 2)
for i, b in enumerate(balls):
pygame.draw.circle(screen, b.color,
(int(b.position.x), int(b.position.y)), 12)
hud = font.render(
f"{b.label} bounces={b.bounces} vx={b.velocity.x:+.0f}",
True, b.color)
screen.blit(hud, (10, 10 + i * 22))
tip = font.render("R = reset ยท watch rubber bounce, steel settle, putty die",
True, (200, 200, 200))
screen.blit(tip, (10, H - 24))
pygame.display.flip()
pygame.quit()
๐ฏ Quick Quiz
Question 1: The lesson's bounce formula is velocity.y *= -restitution on floor contact. What does a restitution value of e = 1.0 mean physically?
Question 2: A rubber ball (e = 0.8) bounces off a floor coated with putty (e = 0.1). According to the lesson's Best Practice 'Material Pairs', what restitution value governs the actual bounce?
Question 3: The lesson's friction decay pattern is velocity.x *= (1 - friction * dt) each frame on the ground, and Best Practice 'Threshold Values: Stop objects when velocity is very small' says to also do if abs(velocity.x) < THRESHOLD: velocity.x = 0. Why is the threshold rule essential rather than optional?
What's Next?
Now that you understand bounce and friction, next we'll explore collision response - how objects react when they hit each other!