Create your first visual programs and simple games!
Pygame is a special Python library that makes it easy to create games and graphics. Let's install it first!
pip install pygameIf you don't have internet access, here are several alternatives:
https://pypi.org/project/pygame/#files on a computer with internetcd C:\path\to\wheel\filepip install pygame-2.5.2-cp311-cp311-win_amd64.whlUse WinPython or Anaconda distributions that come with pygame pre-installed. These can be downloaded once and used offline.
Python's built-in turtle module works offline and can create graphics and simple games. We'll show examples of both!
Try this in Spyder console to verify pygame works:
import pygame
print("Pygame version:", pygame.version.ver) If you can't install pygame, Python's built-in turtle module works great for graphics and simple games!
import turtle
# Create a screen
screen = turtle.Screen()
screen.bgcolor("lightblue")
screen.title("My First Graphics Window")
screen.setup(width=800, height=600)
# Create a turtle
my_turtle = turtle.Turtle()
my_turtle.shape("turtle")
my_turtle.color("green")
# Draw a colorful square
colors = ["red", "blue", "yellow", "purple"]
for i in range(4):
my_turtle.color(colors[i])
my_turtle.forward(100)
my_turtle.right(90)
# Keep window open
screen.exitonclick() # Click to close 💡 Tip: Turtle graphics is perfect for learning! It's built into Python, works offline, and teaches the same programming concepts as pygame.
Let's create a colorful window that stays open until you close it:
import pygame
import sys
# Initialize Pygame
pygame.init()
# Set up the display
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("My First Graphics Window!")
# Colors (Red, Green, Blue values)
BLUE = (0, 100, 255)
WHITE = (255, 255, 255)
# Main game loop
running = True
while running:
# Handle events
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Fill the screen with blue
screen.fill(BLUE)
# Update the display
pygame.display.flip()
# Quit
pygame.quit()
sys.exit() Run this code! You should see a blue window appear. Click the X to close it.
Now let's add some colorful shapes to our window:
# Rectangle
pygame.draw.rect(screen, RED, (50, 50, 100, 75))
# Circle
pygame.draw.circle(screen, GREEN, (400, 300), 50)
# Line
pygame.draw.line(screen, YELLOW, (0, 0), (800, 600), 5) RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
PURPLE = (255, 0, 255)
ORANGE = (255, 165, 0) import pygame
import sys
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Colorful Shapes!")
# Colors
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
PURPLE = (255, 0, 255)
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Black background
screen.fill(BLACK)
# Draw shapes
pygame.draw.rect(screen, RED, (100, 100, 150, 100)) # Red rectangle
pygame.draw.circle(screen, GREEN, (400, 200), 75) # Green circle
pygame.draw.rect(screen, BLUE, (500, 400, 100, 100)) # Blue square
pygame.draw.circle(screen, YELLOW, (200, 450), 50) # Yellow circle
pygame.draw.line(screen, PURPLE, (0, 300), (800, 300), 10) # Purple line
pygame.display.flip()
pygame.quit()
sys.exit() The real fun begins when we make objects move around the screen:
import pygame
import sys
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Bouncing Ball!")
clock = pygame.time.Clock()
# Colors
BLACK = (0, 0, 0)
RED = (255, 0, 0)
# Ball properties
ball_x = 400
ball_y = 300
ball_speed_x = 5
ball_speed_y = 3
ball_radius = 25
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Move the ball
ball_x += ball_speed_x
ball_y += ball_speed_y
# Bounce off walls
if ball_x <= ball_radius or ball_x >= 800 - ball_radius:
ball_speed_x = -ball_speed_x
if ball_y <= ball_radius or ball_y >= 600 - ball_radius:
ball_speed_y = -ball_speed_y
# Draw everything
screen.fill(BLACK)
pygame.draw.circle(screen, RED, (int(ball_x), int(ball_y)), ball_radius)
pygame.display.flip()
clock.tick(60) # 60 FPS
pygame.quit()
sys.exit() Ready for something amazing? Let's create a ball that bounces around inside a 3D cube! This uses pure Python with tkinter (no extra installations needed).
Ball has (x, y, z) coordinates - width, height, and depth!
Ball moves with (vx, vy, vz) speeds in each direction
3D coordinates get "flattened" to 2D screen using math!
Objects farther away appear smaller and darker
import tkinter as tk
import math
import time
class Ball3D:
def __init__(self):
# 3D position and velocity
self.x, self.y, self.z = 0.0, 0.0, 0.0
self.vx, self.vy, self.vz = 3.2, 2.8, 2.1
# Cube boundaries (-L to +L in each direction)
self.L = 4.0
self.radius = 0.3
# Display settings
self.focal_length = 8.0 # Controls perspective
self.camera_distance = 12.0
# Splash effects for wall hits
self.splash_effects = [] # List of active splash effects
self.last_hit_wall = None # Track which wall was hit
# Trail system for ball path
self.trail_points = [] # List of (x, y, z, timestamp) points
self.max_trail_length = 50 # Maximum number of trail points
# 3D rotation for mouse control
self.rotation_x = 0.0 # Rotation around X axis (up/down)
self.rotation_y = 0.0 # Rotation around Y axis (left/right)
def update(self, dt):
"""Update ball position and handle wall bounces"""
# Move the ball
self.x += self.vx * dt
self.y += self.vy * dt
self.z += self.vz * dt
# Bounce off walls (with slight energy loss for realism)
bounce_damping = 0.98
# Check X walls (left/right - red/orange)
if self.x <= -self.L + self.radius or self.x >= self.L - self.radius:
self.vx = -self.vx * bounce_damping
self.x = max(-self.L + self.radius, min(self.L - self.radius, self.x))
# Add splash effect
wall_type = "left" if self.x <= 0 else "right"
self.add_splash_effect(self.x, self.y, self.z, wall_type)
# Check Y walls (bottom/top - blue/green)
if self.y <= -self.L + self.radius or self.y >= self.L - self.radius:
self.vy = -self.vy * bounce_damping
self.y = max(-self.L + self.radius, min(self.L - self.radius, self.y))
# Add splash effect
wall_type = "bottom" if self.y <= 0 else "top"
self.add_splash_effect(self.x, self.y, self.z, wall_type)
# Check Z walls (back/front - purple/yellow)
if self.z <= -self.L + self.radius or self.z >= self.L - self.radius:
self.vz = -self.vz * bounce_damping
self.z = max(-self.L + self.radius, min(self.L - self.radius, self.z))
# Add splash effect
wall_type = "back" if self.z <= 0 else "front"
self.add_splash_effect(self.x, self.y, self.z, wall_type)
# Update splash effects (fade them out over time)
self.splash_effects = [(x, y, z, wall, age + dt) for x, y, z, wall, age in self.splash_effects if age + dt < 0.5]
# Add current position to trail
import time
current_time = time.time()
self.trail_points.append((self.x, self.y, self.z, current_time))
# Limit trail length
if len(self.trail_points) > self.max_trail_length:
self.trail_points.pop(0)
def add_splash_effect(self, x, y, z, wall_type):
"""Add a splash effect at the collision point"""
self.splash_effects.append((x, y, z, wall_type, 0.0)) # (x, y, z, wall_type, age)
def project_to_2d(self, x3d, y3d, z3d, canvas_width, canvas_height):
"""Convert 3D coordinates to 2D screen coordinates with rotation"""
import math
# Apply 3D rotations
# Rotate around Y axis (left/right rotation)
cos_y = math.cos(self.rotation_y)
sin_y = math.sin(self.rotation_y)
x_rot = x3d * cos_y - z3d * sin_y
z_rot = x3d * sin_y + z3d * cos_y
y_rot = y3d
# Rotate around X axis (up/down rotation)
cos_x = math.cos(self.rotation_x)
sin_x = math.sin(self.rotation_x)
x_final = x_rot
y_final = y_rot * cos_x - z_rot * sin_x
z_final = y_rot * sin_x + z_rot * cos_x
# Perspective projection formula
screen_x = canvas_width/2 + self.focal_length * x_final / (z_final + self.camera_distance) * 50
screen_y = canvas_height/2 - self.focal_length * y_final / (z_final + self.camera_distance) * 50
# Calculate apparent size based on distance
apparent_radius = self.radius * self.focal_length / (z_final + self.camera_distance) * 50
return screen_x, screen_y, apparent_radius
class Cube3DVisualizer:
def __init__(self):
self.root = tk.Tk()
self.root.title("3D Bouncing Ball in a Cube!")
self.root.geometry("800x600")
# Create canvas
self.canvas = tk.Canvas(self.root, width=800, height=600, bg='black')
self.canvas.pack()
# Create ball
self.ball = Ball3D()
# Control buttons
button_frame = tk.Frame(self.root)
button_frame.pack(pady=10)
self.paused = False
self.pause_btn = tk.Button(button_frame, text="Pause", command=self.toggle_pause)
self.pause_btn.pack(side=tk.LEFT, padx=5)
reset_btn = tk.Button(button_frame, text="Reset", command=self.reset_ball)
reset_btn.pack(side=tk.LEFT, padx=5)
# Trail toggle
self.show_trail = tk.BooleanVar(value=True)
trail_btn = tk.Checkbutton(button_frame, text="Show Trail", variable=self.show_trail)
trail_btn.pack(side=tk.LEFT, padx=5)
# Speed control
tk.Label(button_frame, text="Speed:").pack(side=tk.LEFT, padx=5)
self.speed_scale = tk.Scale(button_frame, from_=0.1, to=3.0, resolution=0.1,
orient=tk.HORIZONTAL, length=150)
self.speed_scale.set(1.0)
self.speed_scale.pack(side=tk.LEFT, padx=5)
# Mouse control variables
self.mouse_down = False
self.last_mouse_x = 0
self.last_mouse_y = 0
# Bind mouse events for 3D rotation
self.canvas.bind("<Button-1>", self.mouse_down_event)
self.canvas.bind("<B1-Motion>", self.mouse_drag_event)
self.canvas.bind("<ButtonRelease-1>", self.mouse_up_event)
# Bind keyboard shortcuts
self.root.bind("<KeyPress-r>", self.keyboard_reset)
self.root.bind("<KeyPress-R>", self.keyboard_reset)
self.root.bind("<KeyPress-p>", self.keyboard_pause)
self.root.bind("<KeyPress-P>", self.keyboard_pause)
# Make sure window can receive keyboard focus
self.root.focus_set()
# Start animation
self.last_time = time.time()
self.animate()
def toggle_pause(self):
self.paused = not self.paused
self.pause_btn.config(text="Resume" if self.paused else "Pause")
def reset_ball(self):
self.ball.x, self.ball.y, self.ball.z = 0.0, 0.0, 0.0
self.ball.vx, self.ball.vy, self.ball.vz = 3.2, 2.8, 2.1
self.ball.splash_effects = [] # Clear splash effects
self.ball.trail_points = [] # Clear trail
def mouse_down_event(self, event):
"""Handle mouse button press for rotation"""
self.mouse_down = True
self.last_mouse_x = event.x
self.last_mouse_y = event.y
def mouse_drag_event(self, event):
"""Handle mouse drag for 3D rotation"""
if self.mouse_down:
# Calculate mouse movement
dx = event.x - self.last_mouse_x
dy = event.y - self.last_mouse_y
# Update rotation (sensitivity factor of 0.01)
self.ball.rotation_y += dx * 0.01 # Horizontal movement rotates around Y axis
self.ball.rotation_x += dy * 0.01 # Vertical movement rotates around X axis
# Update last mouse position
self.last_mouse_x = event.x
self.last_mouse_y = event.y
def mouse_up_event(self, event):
"""Handle mouse button release"""
self.mouse_down = False
def keyboard_reset(self, event):
"""Handle R key press for reset"""
self.reset_ball()
def keyboard_pause(self, event):
"""Handle P key press for pause/resume"""
self.toggle_pause()
def draw_cube_edges(self):
"""Draw the colorful wireframe cube with different colored walls"""
L = self.ball.L
# Define the 8 corners of the cube
corners = [
(-L, -L, -L), (L, -L, -L), (L, L, -L), (-L, L, -L), # Back face
(-L, -L, L), (L, -L, L), (L, L, L), (-L, L, L) # Front face
]
# Project corners to 2D
projected = []
for corner in corners:
x2d, y2d, _ = self.ball.project_to_2d(corner[0], corner[1], corner[2],
self.canvas.winfo_width(),
self.canvas.winfo_height())
projected.append((x2d, y2d))
# Draw cube edges with different colors for each wall
# Wall colors: left=red, right=orange, bottom=blue, top=green, back=purple, front=yellow
edge_colors = [
# Back face (purple)
((0,1), 'purple'), ((1,2), 'purple'), ((2,3), 'purple'), ((3,0), 'purple'),
# Front face (yellow)
((4,5), 'yellow'), ((5,6), 'yellow'), ((6,7), 'yellow'), ((7,4), 'yellow'),
# Connecting edges - colored by which walls they connect
((0,4), 'red'), # Left wall
((1,5), 'orange'), # Right wall
((2,6), 'green'), # Top wall
((3,7), 'blue') # Bottom wall
]
for (edge, color) in edge_colors:
x1, y1 = projected[edge[0]]
x2, y2 = projected[edge[1]]
self.canvas.create_line(x1, y1, x2, y2, fill=color, width=3)
def animate(self):
if not self.paused:
# Calculate time step
current_time = time.time()
dt = (current_time - self.last_time) * self.speed_scale.get()
self.last_time = current_time
# Update ball physics
self.ball.update(dt)
# Clear canvas
self.canvas.delete("all")
# Draw cube wireframe
self.draw_cube_edges()
# Project ball to 2D and draw it
ball_x2d, ball_y2d, ball_radius2d = self.ball.project_to_2d(
self.ball.x, self.ball.y, self.ball.z,
self.canvas.winfo_width(), self.canvas.winfo_height()
)
# Color based on depth (farther = darker)
depth_factor = (self.ball.z + self.ball.camera_distance) / (2 * self.ball.camera_distance)
depth_factor = max(0.3, min(1.0, depth_factor)) # Keep it visible
red_value = int(255 * depth_factor)
color = f"#{red_value:02x}4040"
# Draw the ball
self.canvas.create_oval(
ball_x2d - ball_radius2d, ball_y2d - ball_radius2d,
ball_x2d + ball_radius2d, ball_y2d + ball_radius2d,
fill=color, outline='white', width=2
)
# Draw splash effects at wall collision points
self.draw_splash_effects()
# Draw ball trail if enabled
if self.show_trail.get():
self.draw_trail()
# Add position info
info_text = f"Position: ({self.ball.x:.1f}, {self.ball.y:.1f}, {self.ball.z:.1f})"
self.canvas.create_text(10, 10, text=info_text, fill='white', anchor='nw', font=('Arial', 12))
# Continue animation
self.root.after(16, self.animate) # ~60 FPS
def draw_splash_effects(self):
"""Draw colorful splash effects where ball hits walls"""
# Wall colors match the cube edges
wall_colors = {
'left': '#FF4444', # Red
'right': '#FF8844', # Orange
'bottom': '#4444FF', # Blue
'top': '#44FF44', # Green
'back': '#8844FF', # Purple
'front': '#FFFF44' # Yellow
}
for splash_x, splash_y, splash_z, wall_type, age in self.ball.splash_effects:
# Project splash position to 2D
splash_x2d, splash_y2d, splash_size = self.ball.project_to_2d(
splash_x, splash_y, splash_z,
self.canvas.winfo_width(), self.canvas.winfo_height()
)
# Calculate fade effect (newer = brighter, older = more transparent)
fade_factor = 1.0 - (age / 0.5) # Fade over 0.5 seconds
fade_factor = max(0.0, min(1.0, fade_factor))
# Get wall color and make it fade
base_color = wall_colors.get(wall_type, '#FFFFFF')
# Draw multiple circles for splash effect (ripples)
for i in range(3):
ripple_size = splash_size * (1 + i * 0.5) * (1 + age * 2) # Expand over time
alpha_effect = fade_factor * (1 - i * 0.3) # Inner ripples brighter
if alpha_effect > 0.1: # Only draw if visible enough
# Create fading color effect
color_intensity = int(255 * alpha_effect)
if wall_type in ['left', 'right']: # Red/Orange walls
color = f"#{color_intensity:02x}4444"
elif wall_type in ['bottom', 'top']: # Blue/Green walls
color = f"#44{color_intensity:02x}44" if wall_type == 'top' else f"#4444{color_intensity:02x}"
else: # Purple/Yellow walls
if wall_type == 'back': # Purple
color = f"#{color_intensity//2:02x}44{color_intensity:02x}"
else: # Yellow
color = f"#{color_intensity:02x}{color_intensity:02x}44"
# Draw the ripple circle
self.canvas.create_oval(
splash_x2d - ripple_size, splash_y2d - ripple_size,
splash_x2d + ripple_size, splash_y2d + ripple_size,
outline=color, width=2, fill=''
)
def draw_trail(self):
"""Draw the ball's path trail"""
if len(self.ball.trail_points) < 2:
return
# Draw lines connecting trail points
for i in range(1, len(self.ball.trail_points)):
# Get current and previous trail points
prev_x, prev_y, prev_z, prev_time = self.ball.trail_points[i-1]
curr_x, curr_y, curr_z, curr_time = self.ball.trail_points[i]
# Project both points to 2D
prev_x2d, prev_y2d, _ = self.ball.project_to_2d(
prev_x, prev_y, prev_z,
self.canvas.winfo_width(), self.canvas.winfo_height()
)
curr_x2d, curr_y2d, _ = self.ball.project_to_2d(
curr_x, curr_y, curr_z,
self.canvas.winfo_width(), self.canvas.winfo_height()
)
# Calculate fade factor based on position in trail (newer = brighter)
fade_factor = i / len(self.ball.trail_points)
alpha = int(255 * fade_factor)
# Create fading white trail
trail_color = f"#{alpha:02x}{alpha:02x}{alpha:02x}"
# Draw trail segment
self.canvas.create_line(
prev_x2d, prev_y2d, curr_x2d, curr_y2d,
fill=trail_color, width=2
)
def run(self):
self.root.mainloop()
# Run the 3D bouncing ball simulation
if __name__ == "__main__":
print("🎮 Starting 3D Bouncing Ball Simulation!")
print("📐 Watch how 3D coordinates get projected to 2D!")
print("🎯 Notice how the ball gets smaller/larger as it moves away/closer!")
print("🖱️ Drag mouse to rotate view, check 'Show Trail' to see path")
print("⌨️ Press 'R' to reset, 'P' to pause/resume")
visualizer = Cube3DVisualizer()
visualizer.run() 3D Position: The ball has (x, y, z) coordinates in 3D space
Perspective Math: We use screen_x = focal_length * x / (z + distance) to flatten 3D to 2D
Depth Effects: Objects farther away appear smaller and darker
Vector Bouncing: Each wall bounce flips one velocity component (vx, vy, or vz)
Colorful Walls: Each wall has a unique color - Red/Orange (left/right), Blue/Green (bottom/top), Purple/Yellow (back/front)
Splash Effects: Rippling circles appear where the ball hits walls, matching the wall colors and fading over time
Ball Trail: A fading white line shows the ball's path through 3D space (can be toggled on/off)
Mouse Rotation: Click and drag to rotate the 3D view - horizontal movement spins left/right, vertical movement tilts up/down
Keyboard Shortcuts: Press 'R' to reset the simulation, 'P' to pause/resume - just like professional software!
Want to see this simulation in action right now? We've created a web version that runs directly in your browser with all the same features!
Perfect for Class: Use the web version to demonstrate 3D physics concepts on a projector or interactive whiteboard!
self.L = 4.0focal_length to change the perspective effectself.vy -= 9.8 * dt in the update functionself.max_trail_length = 50Choose your challenge level and create something amazing:
Tip: RGB colors go from 0 to 255. Try (255, 100, 50) for orange or (100, 255, 100) for light green!
Create multiple bouncing balls with different colors:
# Add a second ball
ball2_x, ball2_y = 200, 150
ball2_speed_x, ball2_speed_y = -3, 4
BLUE = (0, 100, 255)
# In main loop, draw both:
pygame.draw.circle(screen, RED, (int(ball_x), int(ball_y)), ball_radius)
pygame.draw.circle(screen, BLUE, (int(ball2_x), int(ball2_y)), ball_radius) Add multiple balls to the 3D cube:
# In Cube3DVisualizer.__init__():
self.balls = [Ball3D() for _ in range(3)]
self.balls[1].x, self.balls[1].vx = 2.0, -2.5
self.balls[2].z, self.balls[2].vz = -2.0, 3.0
# In animate(), loop through all balls:
for ball in self.balls:
ball.update(dt) Super Challenges: Add gravity, ball collisions, or trails that fade over time!
Build the classic Pong game with paddles and ball physics
Create a space shooter with enemies and power-ups