Game Development with Pygame Course
Every game, no matter how simple or complex, runs on a cycle that repeats endlessly while the game is active. This cycle is called the Game Loop.
Every game, no matter how simple or complex, runs on a cycle that repeats endlessly while the game is active. This cycle is called the Game Loop.
┌────────────────────────────────────────────────────┐
│ GAME LOOP │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ INPUT │ → │ UPDATE │ → │ RENDER │ ──┐ │
│ └──────────┘ └──────────┘ └──────────┘ │ │
│ ↑ │ │
│ └──────────────────────────────────────┘ │
│ │
│ Repeats ~60 times per second (60 FPS) │
└────────────────────────────────────────────────────┘
Every game, no matter how simple or complex, runs on a cycle that repeats endlessly while the game is active. This cycle is called the Game Loop.
┌────────────────────────────────────────────────────┐
│ GAME LOOP │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ INPUT │ → │ UPDATE │ → │ RENDER │ ──┐ │
│ └──────────┘ └──────────┘ └──────────┘ │ │
│ ↑ │ │
│ └──────────────────────────────────────┘ │
│ │
│ Repeats ~60 times per second (60 FPS) │
└────────────────────────────────────────────────────┘
Each iteration of the loop is called a frame and produces one image on screen. At 60 FPS, it creates the illusion of smooth motion — just like a flipbook.
Detects what the player is doing:
QUIT)Warning
Critical rule: If you don’t process events every frame, the queue fills up and the window freezes.
All game logic happens here:
Draws the current state on screen:
fill()pygame.display.flip()Note
Why clear before drawing? Without clearing, objects leave “trails” on screen because previous frames remain visible.
FPS indicates how many times per second the Game Loop executes.
| FPS | Perception |
|---|---|
| < 15 | Noticeably choppy |
| 30 | Playable, standard on older consoles |
| 60 | Smooth, current standard |
| 120+ | Very smooth, for high-refresh-rate monitors |
Without FPS control, the game runs as fast as the CPU allows — the same game would run at completely different speeds on different hardware.
Pygame uses two image buffers:
pygame.display.flip() swaps both buffers. This prevents tearing (seeing a half-drawn frame).
| Method | When it fires | Typical use |
|---|---|---|
pygame.event.get() |
Once when pressed | Jump, shoot, open menu |
pygame.key.get_pressed() |
Every frame while held | Move character |
Common mistake: using KEYDOWN for movement. The character moves one step, stops, and only then starts moving smoothly (the delay comes from the OS). The solution is get_pressed() for continuous movement.
The origin (0, 0) is at the top-left corner, and the Y axis grows downward.
(0,0) ──────────────────→ X+
│
│ (100, 50) ●
│
│ (400, 300) ●
↓
Y+
The origin (0, 0) is at the top-left corner, and the Y axis grows downward.
(0,0) ──────────────────→ X+
│
│ (100, 50) ●
│
│ (400, 300) ●
↓
Y+
Important
To move an object upward on screen, you must subtract from Y. This differs from conventional math where Y grows upward.
Every Pygame game follows this structure:
import pygame
import sys
# ── BLOCK 1: Initialization ───────────────────────────────
pygame.init()
WIDTH, HEIGHT = 800, 600
FPS = 60
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
BLUE = (50, 120, 220)
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Module 1 — Game Loop")
clock = pygame.time.Clock()# ── BLOCK 2: Initial state ────────────────────────────────
x, y = WIDTH // 2, HEIGHT // 2
speed = 4
# ── BLOCK 3: Game Loop ────────────────────────────────────
while True:
# PHASE 1 — INPUT
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]: x -= speed
if keys[pygame.K_RIGHT]: x += speed
if keys[pygame.K_UP]: y -= speed
if keys[pygame.K_DOWN]: y += speed
# PHASE 2 — UPDATE
x = max(0, min(x, WIDTH - 40))
y = max(0, min(y, HEIGHT - 40))
# PHASE 3 — RENDER
screen.fill(BLACK)
pygame.draw.rect(screen, BLUE, (x, y, 40, 40))
pygame.display.flip()
clock.tick(FPS)| Event | Description |
|---|---|
pygame.QUIT |
Click on the window’s X button |
pygame.KEYDOWN |
Key just pressed |
pygame.KEYUP |
Key released |
pygame.MOUSEBUTTONDOWN |
Mouse button pressed |
pygame.MOUSEBUTTONUP |
Mouse button released |
pygame.MOUSEMOTION |
Mouse moved (includes position and delta) |
pygame.MOUSEWHEEL |
Mouse scroll |
| Category | Keys |
|---|---|
| Movement | K_LEFT, K_RIGHT, K_UP, K_DOWN |
| Actions | K_SPACE, K_RETURN, K_ESCAPE |
| Letters | K_a … K_z |
| Numbers | K_0 … K_9 |
| Modifiers | K_LSHIFT, K_RSHIFT, K_LCTRL, K_RCTRL |
All are constants from the pygame module.
KEYDOWN (event): fires only once when pressed
Frame 0: KEYDOWN detected → character moves 1 time
Frame 1: no event (key still held but nothing happens)
Frame 2: no event
...
get_pressed(): fires EVERY frame while the key is held
Frame 0: get_pressed → character moves (smooth movement)
Frame 1: get_pressed → character moves (smooth movement)
Frame 2: get_pressed → character moves (smooth movement)
...
Tip
Use KEYDOWN for single actions (jump, shoot) and get_pressed() for continuous movement.
Pygame’s Clock limits how fast the game loop runs.
import time
def simulate_clock_tick(target_fps, frames=5):
"""
Simulates how clock.tick(fps) controls the loop speed.
In Pygame, clock.tick(60) blocks until 1/60 seconds have passed.
"""
frame_duration = 1.0 / target_fps # seconds per frame
for i in range(frames):
start = time.time()
time.sleep(0.002) # Simulates 2ms of game work
work_time = time.time() - start
wait = frame_duration - work_time
if wait > 0:
time.sleep(wait)
total_time = time.time() - start
real_fps = 1.0 / total_time
print(f"Frame {i+1}: work={work_time*1000:.1f}ms, "
f"wait={wait*1000:.1f}ms, real FPS≈{real_fps:.0f}")
simulate_clock_tick(target_fps=30, frames=4)Calculate the coordinates to place objects at different positions on screen.
WIDTH = 800
HEIGHT = 600
SIZE = 40 # Object size
positions = {
'Top-left corner': (0, 0),
'Top-right corner': (WIDTH - SIZE, 0),
'Bottom-left corner': (0, HEIGHT - SIZE),
'Bottom-right corner': (WIDTH - SIZE, HEIGHT - SIZE),
'Exact center': (WIDTH // 2 - SIZE // 2, HEIGHT // 2 - SIZE // 2),
'Horizontal center, top': (WIDTH // 2 - SIZE // 2, 20),
}
for name, coords in positions.items():
print(f" {name:<35} → {coords}")Key concepts:
Input:
event.get() → single actionsget_pressed() → continuous movementCoordinates: