| name | pygame |
| description | Python 2D game development - sprites, surfaces, events, sound, fonts, game loops, collision detection, and input handling |
| metadata | {"author":"mte90","version":"1.0.0","tags":["python","game-development","2d-games","pygame","graphics","game-engine"]} |
Pygame
Python game development library.
Overview
Pygame is a set of Python modules designed for writing video games. It provides functionality for creating graphics, handling input, playing sounds, and more.
Key Features:
- 2D graphics and sprites
- Event handling
- Sound and music playback
- Font rendering
- Collision detection
- Game loops
- Hardware acceleration
Installation
pip install pygame-ce
pip install pygame
pip install pygame-ce[fonts]
pygame-ce (Community Edition)
pygame-ce is the maintained fork of pygame. Key differences:
- 20-30% faster performance in many benchmarks
IS_CE flag to detect pygame-ce
- Better Python 3.10+ support
- Active development and bug fixes
import pygame
print(pygame.ver)
if hasattr(pygame, 'IS_CE'):
print("Using pygame-ce!")
Getting Started
Basic Window
import pygame
import sys
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("My Game")
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
screen.fill((0, 0, 0))
pygame.draw.rect(screen, (255, 0, 0), (100, 100, 50, 50))
pygame.display.flip()
pygame.quit()
sys.exit()
Drawing
Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
CYAN = (0, 255, 255)
MAGENTA = (255, 0, 255)
TRANSPARENT = (0, 0, 0, 0)
SEMI_RED = (255, 0, 0, 128)
Shapes
pygame.draw.rect(screen, RED, (x, y, width, height))
pygame.draw.rect(screen, RED, (x, y, width, height), 2)
pygame.draw.circle(screen, GREEN, (center_x, center_y), radius)
pygame.draw.circle(screen, GREEN, (cx, cy, radius), 2)
pygame.draw.line(screen, BLUE, (x1, y1), (x2, y2), width)
pygame.draw.aaline(screen, BLUE, (x1, y1), (x2, y2))
points = [(x1, y1), (x2, y2), (x3, y3)]
pygame.draw.polygon(screen, YELLOW, points)
pygame.draw.polygon(screen, YELLOW, points, 2)
pygame.draw.ellipse(screen, MAGENTA, (x, y, width, height))
pygame.draw.arc(screen, CYAN, (x, y, width, height), start_angle, end_angle)
points = [(0, 0), (100, 100), (200, 0)]
pygame.draw.lines(screen, WHITE, False, points, 2)
Surfaces
surface = pygame.Surface((width, height))
surface = pygame.Surface((width, height), pygame.SRCALPHA)
surface.fill(RED)
surface.fill((0, 0, 0, 128), pygame.Rect(0, 0, 100, 100))
screen.blit(surface, (x, y))
scaled = pygame.transform.scale(surface, (new_width, new_height))
rotated = pygame.transform.rotate(surface, angle)
flipped = pygame.transform.flip(surface, flip_x, flip_y)
Performance Optimization
Image Conversion
Always convert images after loading to match the display format:
screen.blit(pygame.image.load("sprite.png"), (0, 0))
sprite = pygame.image.load("sprite.png").convert()
sprite_alpha = pygame.image.load("player.png").convert_alpha()
RLEACCEL for Static Surfaces
For surfaces that rarely change, RLEACCEL speeds up repeated blitting:
static_bg = pygame.Surface((800, 600))
static_bg.fill((50, 50, 50))
static_bg = static_bg.convert()
static_bg.set_colorkey((0, 0, 0), pygame.RLEACCEL)
static_bg.set_alpha(128, pygame.RLEACCEL)
Batched Blits with blits()
Use blits() instead of multiple blit() calls for better performance:
screen.blit(sprite1, (x1, y1))
screen.blit(sprite2, (x2, y2))
screen.blit(sprite3, (x3, y3))
screen.blits([(sprite1, (x1, y1)), (sprite2, (x2, y2)), (sprite3, (x3, y3))])
screen.blits([(sprite, dest_rect1), (sprite, dest_rect2)], doreturn=0)
Dirty Rect Optimization
For games with many sprites where only some move, use dirty rect tracking:
class OptimizedSprite(pygame.sprite.DirtySprite):
def __init__(self, x, y):
super().__init__()
self.image = pygame.Surface((32, 32))
self.image.fill(GREEN)
self.rect = self.image.get_rect()
self.rect.topleft = (x, y)
self.dirty = 1
def update(self):
if moved:
self.dirty = 1
def render_dirty_rects(screen, sprite_group, background):
for sprite in sprite_group:
if sprite.dirty:
if sprite.visible:
screen.blit(background, sprite.rect, sprite.rect)
if sprite.visible:
screen.blit(sprite.image, sprite.rect)
sprite.dirty = 0
Pre-rendered Surfaces
Cache expensive operations:
while running:
rotated = pygame.transform.rotate(original_image, angle)
screen.blit(rotated, pos)
rotations = {angle: pygame.transform.rotate(original, angle)
for angle in range(0, 360, 5)}
while running:
screen.blit(rotations[int(angle) % 360], pos)
screen.blit(pygame.transform.scale(small_image, (100, 100)), pos)
sizes = {size: pygame.transform.scale(image, (size, size)) for size in [32, 64, 128]}
Sprites
Sprite Class
class Player(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
self.image = pygame.Surface((32, 32))
self.image.fill(GREEN)
self.rect = self.image.get_rect()
self.rect.topleft = (x, y)
self.speed = 5
def update(self):
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
self.rect.x -= self.speed
if keys[pygame.K_RIGHT]:
self.rect.x += self.speed
if keys[pygame.K_UP]:
self.rect.y -= self.speed
if keys[pygame.K_DOWN]:
self.rect.y += self.speed
def draw(self, screen):
screen.blit(self.image, self.rect)
player = Player(100, 100)
all_sprites.add(player)
all_sprites.draw(screen)
Sprite Groups
all_sprites = pygame.sprite.Group()
enemies = pygame.sprite.Group()
bullets = pygame.sprite.Group()
player = Player(100, 100)
all_sprites.add(player)
enemies.add(enemy1, enemy2)
all_sprites.update()
all_sprites.draw(screen)
player.kill()
if player in all_sprites:
print("Player is alive")
Loading Images
image = pygame.image.load("sprite.png")
image = pygame.image.load("sprite.png").convert_alpha()
image = image.convert()
image = image.convert_alpha()
import io
image = pygame.image.load(io.BytesIO(image_data))
Events
Event Types
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
print("Space pressed")
if event.key == pygame.K_ESCAPE:
running = False
if event.type == pygame.KEYUP:
if event.key == pygame.K_SPACE:
print("Space released")
if event.type == pygame.MOUSEBUTTONDOWN:
x, y = event.pos
button = event.button
if button == 1:
print(f"Left click at {x}, {y}")
if event.type == pygame.MOUSEMOTION:
x, y = event.pos
rel_x, rel_y = event.rel
if event.type == pygame.JOYBUTTONDOWN:
if event.button == 0:
print("Joystick A pressed")
if event.type == pygame.WINDOWFOCUSLOST:
print("Window lost focus")
if event.type == pygame.WINDOWRESIZED:
print(f"Window resized to {event.x}x{event.y}")
Input States
keys = pygame.key.get_pressed()
if keys[pygame.K_SPACE]:
print("Space is held down")
mouse_pos = pygame.mouse.get_pos()
mouse_buttons = pygame.mouse.get_pressed()
if mouse_buttons[0]:
print("Left mouse button held")
joystick = pygame.joystick.Joystick(0)
axes = joystick.get_numaxes()
for i in range(axes):
value = joystick.get_axis(i)
Collision Detection
if pygame.sprite.collide_rect(sprite1, sprite2):
print("Collision!")
hits = pygame.sprite.spritecollide(player, enemies, True)
for enemy in hits:
score += 10
pygame.sprite.groupcollide(bullets, enemies, True, True)
pygame.sprite.collide_circle(sprite1, sprite2)
pygame.sprite.spritecollide(player, enemies, True, pygame.sprite.collide_circle)
mask1 = pygame.mask.from_surface(sprite1.image)
mask2 = pygame.mask.from_surface(sprite2.image)
if sprite1.rect.colliderect(sprite2.rect):
offset = (sprite2.rect.x - sprite1.rect.x, sprite2.rect.y - sprite1.rect.y)
if mask1.overlap(mask2, offset):
print("Pixel-perfect collision!")
Sound
pygame.mixer.init()
shoot_sound = pygame.mixer.Sound("shoot.wav")
explosion_sound = pygame.mixer.Sound("explosion.wav")
shoot_sound.set_volume(0.5)
explosion_sound.set_volume(0.8)
shoot_sound.play()
shoot_sound.play(maxtime=500)
pygame.mixer.music.load("bgm.mp3")
pygame.mixer.music.play(-1)
pygame.mixer.music.pause()
pygame.mixer.music.unpause()
pygame.mixer.music.stop()
pygame.mixer.music.set_volume(0.5)
Fonts
pygame.font.init()
font = pygame.font.Font(None, 36)
font = pygame.font.Font("custom.ttf", 36)
text_surface = font.render("Hello, World!", True, WHITE)
text_surface = font.render("Hello", False, RED)
width, height = font.size("Hello")
screen.blit(text_surface, (x, y))
Time and Delta Time
import pygame
import time
clock = pygame.time.Clock()
clock.tick(60)
fps = clock.get_fps()
last_time = time.time()
while True:
dt = time.time() - last_time
last_time = time.time()
player.x += player.speed * dt
Fixed Timestep Game Loop
The basic clock.tick(fps) approach varies the timestep when framerate drops, causing inconsistent physics. Fixed timestep updates physics at regular intervals while allowing interpolated rendering:
class Game:
def __init__(self):
pygame.init()
self.screen = pygame.display.set_mode((800, 600))
self.clock = pygame.time.Clock()
self.running = True
self.fixed_dt = 1/60
self.accumulator = 0.0
def run(self):
while self.running:
dt = self.clock.tick(60) / 1000.0
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
self.accumulator += dt
while self.accumulator >= self.fixed_dt:
self.fixed_update(self.fixed_dt)
self.accumulator -= self.fixed_dt
interpolation = self.accumulator / self.fixed_dt
self.render(interpolation)
pygame.quit()
def fixed_update(self, dt):
"""Physics/update at fixed 60 Hz"""
player.update_physics(dt)
def render(self, interp):
"""Render with interpolation factor (0.0 to 1.0)"""
self.screen.fill((0, 0, 0))
for sprite in all_sprites:
render_x = sprite.x + (sprite.vx * interp)
render_y = sprite.y + (sprite.vy * interp)
self.screen.blit(sprite.image, (render_x, render_y))
pygame.display.flip()
Why fixed timestep matters:
- Consistent physics regardless of framerate
- Deterministic network games (same simulation everywhere)
- No "spiral of death" when frame time exceeds update time
- Interpolation makes rendering smooth even when physics runs at lower rate
Camera
class Camera:
def __init__(self, width, height):
self.camera = pygame.Rect(0, 0, width, height)
self.width = width
self.height = height
def apply(self, entity):
return entity.rect.move(self.camera.topleft)
def apply_rect(self, rect):
return rect.move(self.camera.topleft)
def update(self, target):
x = -target.rect.x + int(SCREEN_WIDTH / 2)
y = -target.rect.y + int(SCREEN_HEIGHT / 2)
x = min(0, x)
y = min(0, y)
x = max(-(self.width - SCREEN_WIDTH), x)
y = max(-(self.height - SCREEN_HEIGHT), y)
self.camera = pygame.Rect(x, y, self.width, self.height)
camera = Camera(map_width, map_height)
for entity in all_sprites:
screen.blit(entity.image, camera.apply(entity))
UI Elements
Button
class Button:
def __init__(self, x, y, width, height, text, callback):
self.rect = pygame.Rect(x, y, width, height)
self.text = text
self.callback = callback
self.color = (100, 100, 100)
self.hover_color = (150, 150, 150)
self.font = pygame.font.Font(None, 36)
def handle_event(self, event):
if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1 and self.rect.collidepoint(event.pos):
self.callback()
def draw(self, screen):
mouse_pos = pygame.mouse.get_pos()
color = self.hover_color if self.rect.collidepoint(mouse_pos) else self.color
pygame.draw.rect(screen, color, self.rect)
pygame.draw.rect(screen, WHITE, self.rect, 2)
text_surface = self.font.render(self.text, True, WHITE)
text_rect = text_surface.get_rect(center=self.rect.center)
screen.blit(text_surface, text_rect)
Best Practices
1. Use Sprite Groups
all_sprites = pygame.sprite.Group()
all_sprites.update()
all_sprites.draw(screen)
2. Delta Time
player.x += speed * dt
3. Pre-load Resources
def load_game():
global player_image, enemy_image, shoot_sound
player_image = pygame.image.load("player.png").convert_alpha()
enemy_image = pygame.image.load("enemy.png").convert_alpha()
shoot_sound = pygame.mixer.Sound("shoot.wav")
4. Clean Exit
try:
game_loop()
finally:
pygame.quit()
sys.exit()
Game Loop Template
import pygame
import sys
class Game:
def __init__(self):
pygame.init()
self.screen = pygame.display.set_mode((800, 600))
self.clock = pygame.time.Clock()
self.running = True
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
def update(self):
pass
def draw(self):
self.screen.fill((0, 0, 0))
pygame.display.flip()
def run(self):
while self.running:
dt = self.clock.tick(60) / 1000.0
self.handle_events()
self.update()
self.draw()
pygame.quit()
sys.exit()
if __name__ == "__main__":
game = Game()
game.run()
References