Sprite groups in practice
Subclass pygame.sprite.Sprite for Player and Enemy, use Groups for update and draw, and detect collisions with groupcollide().
- Subclass Sprite for Player and Enemy with image and rect attributes
- Add sprites to Groups and call update/draw on the group
- Use groupcollide() to detect player-enemy hits and kill() enemies on contact
This lesson puts the sprite contract into working code. You will build a Player
and an Enemy, place them in groups, and replace the manual collision loops
from the beginner labs with a single groupcollide() call.
Pyodide (the in-browser Python runner) does not support pygame's display
system. Read through this code carefully in the browser, then run it locally:
pip install pygame followed by python sprites.py.
The code
import pygame
import sys
import random
pygame.init()
SCREEN_W, SCREEN_H = 640, 480
screen = pygame.display.set_mode((SCREEN_W, SCREEN_H))
pygame.display.set_caption("Sprite groups demo")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 30)
# ── Sprite subclasses ────────────────────────────────────────────────────────
class Player(pygame.sprite.Sprite):
SPEED = 5
def __init__(self, *groups):
super().__init__(*groups)
self.image = pygame.Surface((40, 40))
self.image.fill((100, 210, 120)) # green square as placeholder
self.rect = self.image.get_rect(center=(SCREEN_W // 2, SCREEN_H // 2))
def update(self, keys):
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
# Clamp to window
self.rect.clamp_ip(pygame.Rect(0, 0, SCREEN_W, SCREEN_H))
class Enemy(pygame.sprite.Sprite):
def __init__(self, *groups):
super().__init__(*groups)
self.image = pygame.Surface((32, 32))
self.image.fill((210, 80, 80)) # red square
x = random.choice([random.randint(0, SCREEN_W // 2 - 60),
random.randint(SCREEN_W // 2 + 60, SCREEN_W - 32)])
y = random.randint(0, SCREEN_H - 32)
self.rect = self.image.get_rect(topleft=(x, y))
# Each enemy drifts in a random direction
self.vx = random.choice([-2, -1, 1, 2])
self.vy = random.choice([-2, -1, 1, 2])
def update(self, keys): # keys unused but keeps the signature uniform
self.rect.x += self.vx
self.rect.y += self.vy
# Bounce off window edges
if self.rect.left < 0 or self.rect.right > SCREEN_W: self.vx *= -1
if self.rect.top < 0 or self.rect.bottom > SCREEN_H: self.vy *= -1
# ── Groups ───────────────────────────────────────────────────────────────────
all_sprites = pygame.sprite.Group()
enemies = pygame.sprite.Group()
player = Player(all_sprites)
for _ in range(6):
Enemy(all_sprites, enemies)
# ── Main loop ────────────────────────────────────────────────────────────────
kills = 0
running = True
while running:
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
# Spawn a new enemy on SPACE
Enemy(all_sprites, enemies)
keys = pygame.key.get_pressed()
# One call updates every sprite
all_sprites.update(keys)
# groupcollide returns {player_sprite: [list of hit enemies]}
# kill_a=False keeps the player; kill_b=True removes hit enemies
hits = pygame.sprite.groupcollide(
pygame.sprite.GroupSingle(player), enemies, False, True)
if hits:
kills += len(hits[player])
screen.fill((20, 20, 35))
all_sprites.draw(screen) # one call draws every sprite at its rect
info = font.render(
f"Enemies killed: {kills} SPACE = spawn enemy", True, (255, 255, 255))
screen.blit(info, (10, 10))
pygame.display.flip()
clock.tick(60)
pygame.quit()
sys.exit()Key points
Sprites register themselves into groups by passing the group(s) to
super().__init__(*groups). This is the idiomatic pattern — you do not call
group.add(sprite) explicitly at the call site.
all_sprites.update(keys) forwards keys to every sprite's update()
method. Both Player.update and Enemy.update accept the same signature even
though Enemy ignores keys. Keeping the signature uniform avoids a TypeError.
groupcollide(group_a, group_b, kill_a, kill_b) is a multiset check using
each sprite's rect. The kill_b=True flag calls enemy.kill() automatically
on any enemy that overlaps the player. If you want the player to also die, pass
kill_a=True as well.
pygame.sprite.spritecollideany(sprite, group) is a faster alternative when
you only need to know whether any collision occurred, not which sprites
collided. Use it for simple hit tests; use groupcollide when you need to
know the full set of collisions.
Where to go next
Next: sprite animation — cycling through a sequence of surfaces to make sprites appear to move.
The Sprite class
pygame.sprite.Sprite enforces a contract — image plus rect — that lets Groups manage update, draw, and collision for every member at once.
Sprite animation
Bring sprites to life by cycling through a sequence of surfaces, controlling playback speed with a frame counter, and loading frames from a sprite sheet.