Спрайты и группы

Когда объектов в игре десятки, держать их по одному — каша. Спрайт-классы и группы наводят порядок: каждый объект знает, как себя обновлять и рисовать.
Суть: pygame.sprite.Sprite — это объект с image и rect. Группа Sprite Group хранит много спрайтов и одной командой обновляет и рисует их всех.

Пока у тебя один герой — можно хранить его в паре переменных. Но как только появляются десять врагов, двадцать пуль и горсть монеток, код превращается в свалку из списков и циклов. Pygame предлагает аккуратное решение, пришедшее из объектно-ориентированного программирования: спрайты и группы.

Спрайт (Sprite) — это класс-наследник pygame.sprite.Sprite, у которого есть два обязательных поля: image (как он выглядит) и rect (где он находится). У спрайта есть метод update, в котором он сам решает, как себя двигать. Это инкапсуляция: каждый объект отвечает за себя.

Группа (Group) — это умный контейнер для спрайтов. Вместо ручного цикла по списку ты пишешь enemies.update(dt) — и группа вызовет update у каждого спрайта. А enemies.draw(screen) нарисует их всех. Добавить врага — enemies.add(enemy), удалить (например, убитого) — enemy.kill(), и он сам исчезнет из всех групп.

Как работает под капотом

Группа хранит ссылки на спрайты. Когда ты зовёшь group.update(), она в цикле дёргает sprite.update() у каждого. Когда зовёшь group.draw(screen) — делает blit(sprite.image, sprite.rect) для всех. Вот картина:

            enemies = Group
              |
    +---------+---------+---------+
    v         v         v         v
  enemy1    enemy2    enemy3    enemy4
  image     image     image     image
  rect      rect      rect      rect

   enemies.update(dt) -> зовёт update у всех
   enemies.draw(screen) -> blit всех на экран
   enemy2.kill()  -> выпадает из группы сам

Класс спрайта на pygame (читаем):

class Enemy(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        self.image = pygame.image.load("enemy.png").convert_alpha()
        self.rect = self.image.get_rect(center=(x, y))
        self.speed = 120

    def update(self, dt):
        self.rect.x += self.speed * dt   # ползёт вправо
        if self.rect.left > 800:
            self.kill()                  # ушёл за экран — удаляем

enemies = pygame.sprite.Group()
enemies.add(Enemy(100, 200))

# в цикле:
enemies.update(dt)
enemies.draw(screen)

Идею «группа обновляет всех» можно показать на чистом Python. Сделаем мини-группу из объектов, у каждого свой update. Попробуй сам:

class Enemy:
    def __init__(self, name, x, speed):
        self.name, self.x, self.speed = name, x, speed
    def update(self, dt):
        self.x += self.speed * dt

group = [Enemy("A", 0, 100), Enemy("B", 50, 200), Enemy("C", 90, 60)]

dt = 0.5  # полсекунды
for e in group:          # это и делает group.update() внутри
    e.update(dt)

for e in group:
    print(f"{e.name}: x = {e.x:.0f}")

Несколько групп для одного спрайта

Один и тот же спрайт может состоять сразу в нескольких группах, и это очень удобно. Типичный приём: завести общую группу all_sprites для отрисовки и отдельные специализированные группы (enemies, coins, bullets) для коллизий. Каждого врага добавляют и в all_sprites, и в enemies. Тогда all_sprites.draw(screen) рисует вообще всё одной строкой, а spritecollide(player, enemies, ...) проверяет столкновения только с врагами. Когда враг гибнет, kill() вынимает его из обеих групп разом.

Есть и специальные виды групп. GroupSingle хранит ровно один спрайт — идеально для игрока. LayeredUpdates рисует спрайты по слоям, уважая «глубину», чтобы герой проходил перед одними объектами и за другими. Начинать стоит с обычной Group, но знать о существовании специализированных контейнеров полезно: когда игра вырастет, они избавят от ручной возни. Группы — это не просто списки, а продуманный инструмент управления толпой объектов, и чем раньше ты начнёшь им доверять, тем чище будет твой код.

Частые ошибки

  • Забыть super().__init__() в спрайте — группа не сможет с ним работать.
  • Не задать self.image или self.rectgroup.draw упадёт.
  • Удалять спрайт из списка во время итерации — лучше kill(), группа разрулит сама.

Best practices

  • Каждый тип объекта — отдельный класс-спрайт со своим update.
  • Держи спрайты в группах по смыслу: enemies, bullets, coins.
  • Заведи общую группу all_sprites для отрисовки, а специальные — для коллизий.

Итог: спрайт знает, как себя обновлять и рисовать, а группа управляет толпой одной командой. Это масштабируемый порядок вместо хаоса.

Проверьте себя
1. Какие два поля обязательны у спрайта pygame.sprite.Sprite?
Aname и score
Bimage и rect
Cx и y
Dspeed и color
2. Что делает метод sprite.kill()?
AЗакрывает игру
BУдаляет спрайт из всех групп, где он состоит
CОстанавливает музыку
DОбнуляет счёт