Code of the Day
IntermediateSprites and Levels

Sprite groups in practice

Subclass pygame.sprite.Sprite for Player and Enemy, use Groups for update and draw, and detect collisions with groupcollide().

Game DevIntermediate10 min read
Recommended first
By the end of this lesson you will be able to:
  • 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.

Finished reading? Mark it complete to track your progress.

On this page