PyGame Tutorial: Learn Game Development from Scratch (2026)
I built my first PyGame project in a weekend — a Space Invaders clone — and was amazed that a few hundred lines of Python could produce something playable. PyGame provides SDL2 bindings for input handling, graphics, sound, and collision detection, giving you a low-level canvas to build games without the overhead of a full engine. It's how I learned game loops, event-driven programming, and basic physics.
We'll build a 2D platformer with player movement, gravity, collisions, sprite animation, and scoring. Every concept — the game loop, sprites, surfaces, event handling — maps directly to larger game frameworks, so what you learn here transfers to Unity or Godot later.
The Game Loop and Initial Setup
The core of any game is the loop: process input, update state, render, repeat. PyGame requires pygame.init() to start modules, then you create a display surface with set_mode. Clock.tick(FPS) locks the frame rate. Surface is the fundamental drawing object — the display is a surface, images are surfaces, text is rendered to surfaces.
import pygame
import sys
pygame.init()
SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("My Platformer")
clock = pygame.time.Clock()
FPS = 60
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill((135, 206, 235))
pygame.display.flip()
clock.tick(FPS)
Sprites and the Sprite Group
Sprites represent game objects — players, enemies, coins. PyGame's Sprite base class provides rect (position and size) and image, plus group collision detection. A sprite group holds multiple sprites and can draw them all at once or check collisions. I create sprites by subclassing pygame.sprite.Sprite and overriding the __init__ and update methods.
class Player(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
self.image = pygame.Surface((50, 50))
self.image.fill((255, 0, 0))
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
self.vel_y = 0
self.on_ground = False
def update(self):
self.rect.x += 0
self.rect.y += self.vel_y
all_sprites = pygame.sprite.Group()
player = Player(100, 300)
all_sprites.add(player)
# In loop:
all_sprites.update()
all_sprites.draw(screen)
Player Movement and Event Handling
Keyboard input is handled via the event queue (pygame.KEYDOWN / KEYUP) and via pygame.key.get_pressed() for continuous movement. I map WASD keys to velocity changes. Jumping applies an upward velocity if the player is on the ground, and gravity is added each frame.
GRAVITY = 0.5
JUMP_STRENGTH = -12
MOVE_SPEED = 5
def update(self):
keys = pygame.key.get_pressed()
self.vel_x = 0
if keys[pygame.K_LEFT]:
self.vel_x = -MOVE_SPEED
if keys[pygame.K_RIGHT]:
self.vel_x = MOVE_SPEED
if keys[pygame.K_SPACE] and self.on_ground:
self.vel_y = JUMP_STRENGTH
self.on_ground = False
self.vel_y += GRAVITY
self.rect.x += self.vel_x
self.rect.y += self.vel_y
Collision Detection with Platforms
PyGame provides pygame.sprite.collide_rect for AABB collision detection. For platformers, I separate horizontal and vertical collision resolution to prevent sliding issues. After moving on each axis, check for overlap with platforms and snap the player out.
class Platform(pygame.sprite.Sprite):
def __init__(self, x, y, w, h):
super().__init__()
self.image = pygame.Surface((w, h))
self.image.fill((0, 128, 0))
self.rect = self.image.get_rect(topleft=(x, y))
# In Player.update:
hits = pygame.sprite.spritecollide(self, platforms, False)
for hit in hits:
if self.vel_x > 0:
self.rect.right = hit.rect.left
elif self.vel_x < 0:
self.rect.left = hit.rect.right
hits = pygame.sprite.spritecollide(self, platforms, False)
for hit in hits:
if self.vel_y > 0:
self.rect.bottom = hit.rect.top
self.vel_y = 0
self.on_ground = True
elif self.vel_y < 0:
self.rect.top = hit.rect.bottom
self.vel_y = 0
Animations and Image Loading
Static colored rectangles get boring fast. PyGame loads images with pygame.image.load() and converts them to the display format for faster blitting. For sprite animation, I store a list of frames and cycle through them based on a timer.
sheet = pygame.image.load('player_sprites.png').convert_alpha()
frame_width, frame_height = 64, 64
frames_right = []
for i in range(4):
frame = sheet.subsurface((i * frame_width, 0, frame_width, frame_height))
frames_right.append(frame)
self.frames_right = frames_right
self.frames_left = [pygame.transform.flip(f, True, False) for f in frames_right]
self.current_frame = 0
self.animation_timer = 0
# In update:
if abs(self.vel_x) > 0:
self.animation_timer += 1
if self.animation_timer > 10:
self.current_frame = (self.current_frame + 1) % len(self.frames_right)
self.animation_timer = 0
if self.vel_x > 0:
self.image = self.frames_right[self.current_frame]
else:
self.image = self.frames_left[self.current_frame]
Sound Effects and Font Rendering
pygame.mixer loads and plays sound effects and background music. Font rendering uses pygame.font.Font — I load a TTF file for custom fonts, or use the default font. Combine these for a complete game feel: jump sounds, background music, and score display.
pygame.mixer.init()
jump_sound = pygame.mixer.Sound('jump.wav')
pygame.mixer.music.load('background.ogg')
pygame.mixer.music.play(-1)
font = pygame.font.Font(None, 36)
score = 0
score_text = font.render(f"Score: {score}", True, (255, 255, 255))
screen.blit(score_text, (10, 10))
Frequently Asked Questions
Is PyGame good for 3D games?
No. PyGame is a 2D library built on SDL2. For 3D, use a proper 3D engine like Godot, Unity, or Panda3D. PyGame can do basic 3D through its OpenGL wrapper, but it's not practical for anything complex.
How do I package a PyGame game for distribution?
Use PyInstaller to bundle your script, assets, and Python interpreter into a standalone executable. pyinstaller --onefile --windowed game.py creates a single .exe on Windows.
Why is my game lagging?
Check the frame rate (clock.tick). Common causes: loading images inside the game loop, drawing too many large surfaces without optimization, or using Python lists for collision detection with hundreds of sprites.
How do I make pixel-perfect collision detection?
Use pygame.sprite.collide_mask() instead of collide_rect. This requires setting self.mask = pygame.mask.from_surface(self.image) on each sprite.
Originally published on Ayodhyyya. Last updated June 1, 2026.