Skip to main content

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:

Free-body diagram of a block on horizontal ground moving to the right. Four equal-length force arrows emanate from the block's centre: gravity points down, normal force points up, applied force points right, and friction points left, opposing motion.
Free-body diagram of a sliding block. Gravity (Fg) pulls down, the ground pushes back with the normal force (Fn), an applied force (Fa) drives motion, and friction (Ff) opposes it.
graph TD A["Surface Physics"] --> B["Bounce/Restitution"] A --> C["Friction"] A --> D["Materials"] B --> E["Elastic"] B --> F["Inelastic"] B --> G["Energy Loss"] C --> H["Static Friction"] C --> I["Kinetic Friction"] C --> J["Rolling Friction"] D --> K["Rubber"] D --> L["Metal"] D --> M["Ice"]

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

Practice Exercises

๐ŸŽฏ Physics Challenges!

  1. Pinball Machine: Different bumpers with varying restitution
  2. Ice Hockey: Low friction puck physics
  3. Basketball Game: Realistic ball bouncing and rim physics
  4. Material Tester: Compare different materials side by side
  5. Rube Goldberg: Chain reactions with varied materials
  6. Friction Racing: Cars on different surface types

Key Takeaways

๐Ÿ‹๏ธโ€โ™‚๏ธ 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:

  1. Reuse the PhysicsObject shape from the velocity_acceleration lesson: position, velocity, acceleration (all Vector2), mass; add restitution (float) and a color for visual identity.
  2. 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.
  3. 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).
  4. Each frame: apply gravity via apply_force(Vector2(0, 980 * mass)) (chat-44 M2 pattern), integrate Euler, reset acceleration.
  5. On floor hit (position.y >= floor_y): clamp position.y = floor_y, reflect Y velocity with energy loss as velocity.y *= -restitution โ€” the lesson's core bounce formula.
  6. 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).
  7. Resting threshold: if abs(velocity.y) < 5: velocity.y = 0 AND if 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).
  8. Press R to reset all three balls to the start height with a small velocity.x = 150 kick 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!