python4 min read

PyGame Tutorial: Learn Game Development from Scratch (2026)

PyGame Tutorial: Learn Game Development from Scratch (2026)

Published:  |  Category: Python  |  Reading time: ~15 min
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.