references/sprites-and-collision.md
# pygame sprites, collision & media (pygame-ce 2.5+)
Depth behind the pygame-core skill: group variants, pixel-perfect collision,
spritesheets, animation, and audio/text.
## Group variants
`pygame.sprite` ships several containers; pick by need:
| Class | Use for |
|-------|---------|
| `Group` | the general case; `update(*args)` + `draw(surface)` |
| `GroupSingle` | a slot holding at most one sprite (e.g. the player) |
| `LayeredUpdates` | groups that draw by a per-sprite `_layer` (z-order) |
| `RenderUpdates` | tracks dirty rects for partial-screen redraws |
```python
from pygame.sprite import LayeredUpdates
world = LayeredUpdates()
world.add(background, layer=0)
world.add(player, layer=10) # higher layer draws on top
world.add(ui, layer=20)
world.update(dt)
world.draw(screen)
```
A sprite removes itself from **all** groups with `self.kill()`. Membership is
two-way: `group.has(sprite)`, `sprite.groups()`.
## Collision functions
`spritecollide` and `groupcollide` use rect overlap by default. The optional
`collided` callback swaps in a more precise test:
```python
import pygame
from pygame.sprite import collide_rect, collide_circle, collide_mask
# Circle collision (set sprite.radius, or it's derived from the rect):
pygame.sprite.spritecollide(player, rocks, False, collide_circle)
# Pixel-perfect via masks (see below):
pygame.sprite.spritecollide(player, spikes, False, collide_mask)
```
Other helpers: `pygame.sprite.collide_rect_ratio(0.75)` (shrunken rect),
`spritecollideany` (fast boolean-ish "any hit"), and `Rect` methods
`colliderect`, `collidepoint`, `collidelist`, `collidelistall`.
## Pixel-perfect collision with masks
Rect collision is generous; for tight hitboxes build a `Mask` from each sprite's
alpha and test overlap:
```python
class Bullet(pygame.sprite.Sprite):
def __init__(self, image, pos):
super().__init__()
self.image = image.convert_alpha()
self.rect = self.image.get_rect(center=pos)
self.mask = pygame.mask.from_surface(self.image) # build once
# collide_mask uses each sprite's .mask:
if pygame.sprite.collide_mask(bullet, enemy):
enemy.kill()
```
Rebuild the mask if the sprite's image changes (e.g. each animation frame), or
keep a per-frame list of masks.
## Slicing a spritesheet
```python
def load_frames(path, frame_w, frame_h):
sheet = pygame.image.load(path).convert_alpha()
cols = sheet.get_width() // frame_w
rows = sheet.get_height() // frame_h
frames = []
for r in range(rows):
for c in range(cols):
rect = pygame.Rect(c * frame_w, r * frame_h, frame_w, frame_h)
frames.append(sheet.subsurface(rect)) # view into the sheet, no copy
return frames
```
## Simple frame animation
```python
class AnimatedSprite(pygame.sprite.Sprite):
def __init__(self, frames, fps, pos):
super().__init__()
self.frames = frames
self.frame_time = 1 / fps
self.timer = 0.0
self.index = 0
self.image = frames[0]
self.rect = self.image.get_rect(center=pos)
def update(self, dt):
self.timer += dt
while self.timer >= self.frame_time:
self.timer -= self.frame_time
self.index = (self.index + 1) % len(self.frames)
self.image = self.frames[self.index]
```
## Audio
```python
pygame.mixer.init() # or rely on pygame.init()
jump_sfx = pygame.mixer.Sound("jump.wav") # short SFX, fully loaded
jump_sfx.set_volume(0.5)
jump_sfx.play()
pygame.mixer.music.load("theme.ogg") # streamed music (one track at a time)
pygame.mixer.music.set_volume(0.3)
pygame.mixer.music.play(loops=-1) # -1 = loop forever
```
Prefer `.ogg`/`.wav`; keep many short effects as `Sound`, and long tracks on the
single `music` stream.
## Text
```python
font = pygame.font.Font(None, 36) # None = default font; or a .ttf path
# Antialiased text is slow to render — cache the Surface, re-render only on change.
label = font.render(f"Score: {score}", True, (255, 255, 255))
screen.blit(label, (10, 10))
```
Re-rendering text every frame is a common performance trap; render once and re-blit,
re-rendering only when the string changes.
SKILL.md
---
name: pygame-core
description: >
Structure a pygame (pygame-ce) game in Python: the init/event/update/draw loop,
delta-time movement, Surface/Rect blitting, keyboard/mouse input, and
Sprite/Group management with collision. Use when building or debugging a pygame
game — when the user mentions pygame, pygame-ce, the game loop, blit, Surface,
Rect, sprite groups, or clock.tick. Targets pygame-ce.
---
# pygame Core
Build the foundation of a pygame game in Python: the main loop, delta-time
movement, drawing with `Surface`/`Rect`, input, and `Sprite`/`Group` management.
Targets **pygame-ce 2.5.7** (the actively maintained community fork; same
`import pygame`).
## When to use
- Use when starting a pygame game, fixing the loop, frame-rate-dependent speed,
input handling, blitting, or sprite/group collision.
- Use when code does `import pygame` and the project depends on `pygame-ce`
(or `pygame`).
**When *not* to use:** Python language questions unrelated to pygame. 3D rendering
(pygame is 2D). For cross-engine save/load use `save-systems`; for rebindable input
architecture see `input-systems`.
## Core workflow
1. **Install pygame-ce, not legacy pygame.** `pip install pygame-ce` — it's the
maintained fork and imports as `pygame`. Don't install both in one environment.
2. **Init and open a window.** `pygame.init()`, `screen =
pygame.display.set_mode((w, h))`, `clock = pygame.time.Clock()`.
3. **Run one loop: events → update → draw → flip.** Pump the event queue every
frame (`for event in pygame.event.get()`), update state, redraw, then
`pygame.display.flip()`.
4. **Make it frame-rate independent.** Get `dt = clock.tick(60) / 1000` (seconds)
and scale all motion by `dt`. Keep positions as floats; blit at integer rects.
5. **Handle input two ways:** event-based (`KEYDOWN`/`MOUSEBUTTONDOWN`, for discrete
actions) and polled (`pygame.key.get_pressed()`, for held movement).
6. **Organise objects with `Sprite` + `Group`.** Subclass `pygame.sprite.Sprite`
with `image`/`rect`; `group.update(dt)` and `group.draw(screen)` handle the
batch. Run it and watch the window before assuming it works.
## Patterns
### 1. Minimal game loop (the skeleton)
```python
import pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("My Game")
clock = pygame.time.Clock()
running = True
while running:
dt = clock.tick(60) / 1000 # cap at 60 FPS; dt = seconds since last frame
for event in pygame.event.get(): # MUST drain the queue or the OS thinks it hung
if event.type == pygame.QUIT:
running = False
# update game state here, scaled by dt ...
screen.fill((18, 18, 28)) # clear each frame
# draw everything here ...
pygame.display.flip() # present the frame
pygame.quit()
```
### 2. Delta-time movement (frame-rate independent)
```python
from pygame.math import Vector2
pos = Vector2(100, 100) # keep position as floats
speed = 220 # PIXELS PER SECOND, not per frame
# inside the loop, after computing dt:
keys = pygame.key.get_pressed()
direction = Vector2(
keys[pygame.K_RIGHT] - keys[pygame.K_LEFT],
keys[pygame.K_DOWN] - keys[pygame.K_UP],
)
if direction.length_squared() > 0:
direction = direction.normalize() # equal speed on diagonals
pos += direction * speed * dt # RIGHT: dt-scaled
screen.blit(player_img, (round(pos.x), round(pos.y))) # blit at integer pixels
```
### 3. Input: events vs polling
```python
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN: # discrete press: jump, menu, pause
if event.key == pygame.K_SPACE:
jump()
elif event.key == pygame.K_ESCAPE:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
shoot_at(event.pos) # event.pos = (x, y)
# Polled state (read once per frame) for continuous/held input:
keys = pygame.key.get_pressed()
if keys[pygame.K_a]:
move_left(dt)
```
### 4. A Sprite subclass + a Group
```python
class Player(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
# convert() once at load makes blits much faster; _alpha keeps transparency.
self.image = pygame.image.load("player.png").convert_alpha()
self.rect = self.image.get_rect(center=(x, y))
self.pos = pygame.math.Vector2(self.rect.center)
self.speed = 240
def update(self, dt): # Group.update(dt) calls this per sprite
keys = pygame.key.get_pressed()
self.pos.x += (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]) * self.speed * dt
self.rect.center = (round(self.pos.x), round(self.pos.y))
all_sprites = pygame.sprite.Group()
all_sprites.add(Player(400, 300))
# in the loop:
all_sprites.update(dt) # calls each sprite's update(dt)
all_sprites.draw(screen) # blits each sprite at its rect
```
### 5. Collision detection
```python
# Sprite vs group: e.g. player picking up coins (True = remove collided coins).
collected = pygame.sprite.spritecollide(player, coins, dokill=True)
score += len(collected)
# Group vs group: bullets vs enemies (kill both on hit).
hits = pygame.sprite.groupcollide(bullets, enemies, True, True)
# Plain rect overlap (no sprites needed):
if player.rect.colliderect(door_rect):
open_door()
```
## Pitfalls
- **Window freezes / "not responding"** → you didn't pump the event queue. Call
`pygame.event.get()` (or `pygame.event.pump()`) every frame.
- **Speed differs on faster machines** → you moved by a fixed amount per frame.
Scale by `dt = clock.tick(fps) / 1000` and use pixels-per-second values.
- **Sub-pixel movement snaps/jitters** → `rect` coordinates are integers; store the
true position as a `Vector2` of floats and assign `rect.center = round(...)` each
frame.
- **Blits are slow / framerate drops** → call `.convert()` (opaque) or
`.convert_alpha()` (transparent) on loaded images once; un-converted surfaces blit
far slower.
- **Nothing appears** → you forgot `pygame.display.flip()` (or `update()`), or you
drew before `screen.fill(...)` so it was cleared away.
- **Wrong draw order** → pygame uses painter's order; later blits cover earlier ones.
Draw background first, sprites last.
- **`pip install pygame` got the old one** → for the maintained fork use
`pip install pygame-ce`; having both installed causes import conflicts.
- **Diagonal movement is faster** → normalise the direction vector before scaling by
speed.
## References
- For `Group` variants (`GroupSingle`, `LayeredUpdates` for z-order), pixel-perfect
collision with `mask`, slicing a spritesheet, simple animation, sound/music, and
text rendering, read `references/sprites-and-collision.md`.
## Related skills
- `love2d-core` — the same loop concepts in LÖVE/Lua.
- `bevy-ecs` — a heavier ECS engine when a project outgrows pygame.
- `input-systems` / `save-systems` — engine-agnostic input and persistence.
- `platformer` / `roguelike` — genre templates that pair with pygame.