Lesson 8 of 8

Advanced Physics Games

Master complex physics with particle systems and advanced mechanics!

Learning Objectives

  • Create particle systems with realistic physics
  • Build a complete physics-based puzzle game
  • Implement advanced collision detection
  • Design your own physics game from scratch

Particle Systems & Explosions

Create amazing visual effects with hundreds of particles that follow physics laws:

import pygame
import random
import math
import sys

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Particle Explosion!")
clock = pygame.time.Clock()

class Particle:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        angle = random.uniform(0, 2 * math.pi)
        speed = random.uniform(2, 8)
        self.velocity_x = math.cos(angle) * speed
        self.velocity_y = math.sin(angle) * speed
        self.gravity = 0.2
        self.life = 60
        self.color = [random.randint(200, 255), random.randint(100, 200), random.randint(50, 150)]
    
    def update(self):
        self.velocity_y += self.gravity
        self.x += self.velocity_x
        self.y += self.velocity_y
        self.life -= 1
        
        # Fade color as particle ages
        for i in range(3):
            self.color[i] = max(0, self.color[i] - 3)
    
    def draw(self, screen):
        if self.life > 0:
            pygame.draw.circle(screen, self.color, (int(self.x), int(self.y)), max(1, self.life // 10))

# Colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 100, 100)

particles = []

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.MOUSEBUTTONDOWN:
            # Create explosion at mouse position
            mouse_x, mouse_y = pygame.mouse.get_pos()
            for _ in range(50):  # Create 50 particles
                particles.append(Particle(mouse_x, mouse_y))
    
    # Update particles
    for particle in particles[:]:
        particle.update()
        if particle.life <= 0:
            particles.remove(particle)
    
    # Draw everything
    screen.fill(BLACK)
    
    for particle in particles:
        particle.draw(screen)
    
    # Instructions
    font = pygame.font.Font(None, 36)
    text = font.render("Click anywhere to create explosions!", True, WHITE)
    screen.blit(text, (10, 10))
    
    pygame.display.flip()
    clock.tick(60)

pygame.quit()
sys.exit()

Physics Concepts: This particle system demonstrates gravity, velocity, and life cycles. Each particle starts with random velocity and gradually falls due to gravity while fading away.

Physics Puzzle Game

Build a complete physics-based puzzle game where players aim and shoot a ball to hit targets:

import pygame
import math
import sys

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Physics Puzzle Game!")
clock = pygame.time.Clock()

# Colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
BLUE = (100, 150, 255)
RED = (255, 100, 100)
GREEN = (100, 255, 100)
BROWN = (139, 69, 19)

class Ball:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.radius = 15
        self.velocity_x = 0
        self.velocity_y = 0
        self.gravity = 0.5
        self.friction = 0.99
        self.bounce = 0.8
        self.dragging = False
    
    def update(self, obstacles):
        # Apply gravity
        self.velocity_y += self.gravity
        
        # Apply friction
        self.velocity_x *= self.friction
        self.velocity_y *= self.friction
        
        # Update position
        self.x += self.velocity_x
        self.y += self.velocity_y
        
        # Check collisions with obstacles
        for obstacle in obstacles:
            if self.check_collision(obstacle):
                self.resolve_collision(obstacle)
        
        # Bounce off walls
        if self.x - self.radius <= 0 or self.x + self.radius >= 800:
            self.velocity_x = -self.velocity_x * self.bounce
            self.x = max(self.radius, min(800 - self.radius, self.x))
        
        # Bounce off ground and ceiling
        if self.y - self.radius <= 0 or self.y + self.radius >= 600:
            self.velocity_y = -self.velocity_y * self.bounce
            self.y = max(self.radius, min(600 - self.radius, self.y))
    
    def check_collision(self, obstacle):
        closest_x = max(obstacle['x'], min(self.x, obstacle['x'] + obstacle['width']))
        closest_y = max(obstacle['y'], min(self.y, obstacle['y'] + obstacle['height']))
        distance = math.sqrt((self.x - closest_x)**2 + (self.y - closest_y)**2)
        return distance < self.radius
    
    def resolve_collision(self, obstacle):
        # Simple collision response
        center_x = obstacle['x'] + obstacle['width'] / 2
        center_y = obstacle['y'] + obstacle['height'] / 2
        
        # Push ball away from obstacle center
        dx = self.x - center_x
        dy = self.y - center_y
        distance = math.sqrt(dx*dx + dy*dy)
        
        if distance > 0:
            # Normalize and apply bounce
            dx /= distance
            dy /= distance
            self.velocity_x = dx * 5
            self.velocity_y = dy * 5
    
    def draw(self, screen):
        pygame.draw.circle(screen, BLUE, (int(self.x), int(self.y)), self.radius)
        pygame.draw.circle(screen, WHITE, (int(self.x - 5), int(self.y - 5)), 3)

class Target:
    def __init__(self, x, y, width, height):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.hit = False
    
    def check_hit(self, ball):
        if (ball.x - ball.radius < self.x + self.width and
            ball.x + ball.radius > self.x and
            ball.y - ball.radius < self.y + self.height and
            ball.y + ball.radius > self.y):
            self.hit = True
    
    def draw(self, screen):
        color = GREEN if self.hit else RED
        pygame.draw.rect(screen, color, (self.x, self.y, self.width, self.height))

# Create obstacles
obstacles = [
    {'x': 200, 'y': 300, 'width': 100, 'height': 20},
    {'x': 400, 'y': 200, 'width': 20, 'height': 100},
    {'x': 500, 'y': 400, 'width': 150, 'height': 20},
    {'x': 100, 'y': 500, 'width': 200, 'height': 20},
]

# Create target and ball
target = Target(650, 100, 50, 50)
ball = Ball(50, 50)
mouse_start = None
level_complete = False

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.MOUSEBUTTONDOWN:
            if not level_complete:
                mouse_start = pygame.mouse.get_pos()
                ball.dragging = True
        elif event.type == pygame.MOUSEBUTTONUP:
            if ball.dragging and mouse_start:
                mouse_x, mouse_y = pygame.mouse.get_pos()
                power = min(math.sqrt((mouse_start[0] - mouse_x)**2 + (mouse_start[1] - mouse_y)**2), 100)
                angle = math.atan2(mouse_y - mouse_start[1], mouse_x - mouse_start[0])
                
                ball.velocity_x = math.cos(angle) * power * 0.2
                ball.velocity_y = math.sin(angle) * power * 0.2
                ball.dragging = False
                mouse_start = None
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_r:
                # Reset level
                ball = Ball(50, 50)
                target.hit = False
                level_complete = False
    
    # Update game
    if not level_complete:
        ball.update(obstacles)
        target.check_hit(ball)
        if target.hit:
            level_complete = True
    
    # Draw everything
    screen.fill(BLACK)
    
    # Draw obstacles
    for obstacle in obstacles:
        pygame.draw.rect(screen, BROWN, (obstacle['x'], obstacle['y'], obstacle['width'], obstacle['height']))
    
    # Draw target
    target.draw(screen)
    
    # Draw ball
    ball.draw(screen)
    
    # Draw aim line when dragging
    if ball.dragging and mouse_start:
        mouse_x, mouse_y = pygame.mouse.get_pos()
        pygame.draw.line(screen, WHITE, mouse_start, (mouse_x, mouse_y), 2)
    
    # Instructions
    font = pygame.font.Font(None, 36)
    if level_complete:
        text = font.render("Level Complete! Press R to restart", True, GREEN)
    else:
        text = font.render("Click and drag to aim the ball!", True, WHITE)
    screen.blit(text, (10, 10))
    
    pygame.display.flip()
    clock.tick(60)

pygame.quit()
sys.exit()

Game Mechanics: This puzzle game combines aiming, projectile physics, collision detection, and win conditions. Players must use physics to navigate obstacles and hit the target.

Tkinter Ball & Paddle Game

Here's a classic ball and paddle game using tkinter. This version includes improved keyboard handling for Spyder:

import tkinter as tk
import random
import time

class Ball:
    def __init__(self, canvas, paddle, color):
        self.canvas = canvas
        self.paddle = paddle
        self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
        self.canvas.move(self.id, 245, 100)
        starts = [-3, -2, -1, 1, 2, 3]
        random.shuffle(starts)
        self.x = starts[0]
        self.y = -3
        self.canvas_height = self.canvas.winfo_height()
        self.canvas_width = self.canvas.winfo_width()
        self.hit_bottom = False

    def hit_paddle(self, pos):
        paddle_pos = self.canvas.coords(self.paddle.id)
        # pos = [x1, y1, x2, y2] for the ball
        # paddle_pos = [x1, y1, x2, y2] for the paddle
        if pos[2] >= paddle_pos[0] and pos[0] <= paddle_pos[2]:
            if pos[3] >= paddle_pos[1] and pos[3] <= paddle_pos[3]:
                return True
        return False

    def draw(self):
        self.canvas.move(self.id, self.x, self.y)
        pos = self.canvas.coords(self.id)

        if pos[1] <= 0:
            self.y = 3
        if pos[3] >= self.canvas_height:
            self.hit_bottom = True
        if self.hit_paddle(pos):
            self.y = -3
        if pos[0] <= 0:
            self.x = 3
        if pos[2] >= self.canvas_width:
            self.x = -3


class Paddle:
    def __init__(self, root, canvas, color):
        self.canvas = canvas
        self.id = canvas.create_rectangle(0, 0, 100, 10, fill=color)
        self.canvas.move(self.id, 200, 300)
        self.x = 0
        self.canvas_width = self.canvas.winfo_width()
        self.left_pressed = False
        self.right_pressed = False
        
        # Bind arrow keys to root and canvas for better Spyder compatibility
        root.bind('<Left>', self.turn_left)
        root.bind('<Right>', self.turn_right)
        root.bind('<KeyRelease-Left>', self.stop_move)
        root.bind('<KeyRelease-Right>', self.stop_move)
        
        # Also bind to canvas
        canvas.bind('<Left>', self.turn_left)
        canvas.bind('<Right>', self.turn_right)
        canvas.bind('<KeyRelease-Left>', self.stop_move)
        canvas.bind('<KeyRelease-Right>', self.stop_move)
        
        # Make canvas focusable
        canvas.focus_set()

    def draw(self):
        # Continuous movement based on key states
        if self.left_pressed:
            self.x = -3
        elif self.right_pressed:
            self.x = 3
        else:
            self.x = 0
            
        self.canvas.move(self.id, self.x, 0)
        pos = self.canvas.coords(self.id)
        if pos[0] <= 0:
            self.canvas.coords(self.id, 0, pos[1], 100, pos[3])
        elif pos[2] >= self.canvas_width:
            self.canvas.coords(self.id, self.canvas_width-100, pos[1], self.canvas_width, pos[3])

    def turn_left(self, event):
        self.left_pressed = True
        self.right_pressed = False

    def turn_right(self, event):
        self.right_pressed = True
        self.left_pressed = False

    def stop_move(self, event):
        if event.keysym == 'Left':
            self.left_pressed = False
        elif event.keysym == 'Right':
            self.right_pressed = False


def main():
    root = tk.Tk()
    root.title("Ball & Paddle Game (Spyder Compatible)")
    root.resizable(0, 0)
    root.wm_attributes("-topmost", 1)

    canvas = tk.Canvas(root, width=500, height=400, bd=0, highlightthickness=0, bg='black')
    canvas.pack()

    # Important: update before getting width/height and set focus
    root.update()
    canvas.focus_set()
    
    # Add instructions
    instructions = canvas.create_text(250, 50, text="Use Arrow Keys to Move Paddle", 
                                    fill='white', font=('Arial', 12))

    paddle = Paddle(root, canvas, 'blue')
    ball = Ball(canvas, paddle, 'red')

    # Main game loop
    while True:
        if not ball.hit_bottom:
            ball.draw()
            paddle.draw()
            root.update_idletasks()
            root.update()
            time.sleep(0.01)
        else:
            # Game over
            canvas.create_text(250, 200, text="Game Over!", 
                             font=('Arial', 24), fill='red')
            canvas.create_text(250, 230, text="Close window to exit", 
                             font=('Arial', 12), fill='white')
            break

    # Keep window open after game over
    root.mainloop()


if __name__ == "__main__":
    main()

Spyder Compatibility: This version includes improved keyboard handling for Spyder. The paddle uses continuous movement tracking and binds keys to both root and canvas for better responsiveness in IDE environments.

Tkinter vs Pygame: While tkinter comes built-in with Python, pygame offers better game development features. Tkinter is great for simple games and learning, but pygame provides smoother animation and better input handling for complex games.

Your Physics Game Challenge

Design Your Own Physics Game!

Now it's time to create your own physics-based game! Here are some ideas to get you started:

  • Gravity Platformer: Create a character that can jump between platforms with realistic gravity
  • Pinball Machine: Build a pinball game with bouncing balls and flippers
  • Angry Birds Style: Launch projectiles to knock down structures
  • Water Simulation: Create flowing water or liquid physics