Что такое спрайты и как с ними работать

Ссылка на проект занятия (lesson 9.flowballs.zip): https://github.com/selfedu-rus/pygame

Представьте, что мы делаем игру, в которой множество подвижных объектов, с которым взаимодействует пользователь. Как в этом случае правильно спроектировать программу на Pygame, чтобы эффективно обрабатывать движения и взаимодействия? Для этого была специально создана ветка:

pygame.sprite

для работы со спрайтами. Вообще в игровом процессе спрайт – это любой подвижный объект. И когда таких объектов много, то класс:

pygame.sprite.Sprite

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

Для начала создадим базовый каркас приложения. Он вам уже знаком и выглядит следующим образом:

import pygame
 
pygame.init()
 
BLACK = (0, 0, 0)
W, H = 1000, 570
 
sc = pygame.display.set_mode((W, H))
 
clock = pygame.time.Clock()
FPS = 60
 
while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            exit()
 
    clock.tick(FPS)

Давайте теперь добавим сюда подвижный объект в виде падающего сверху шарика. Для этого мы воспользуемся классом Sprite и на его основе создадим новый класс Ball для обработки падающих шариков. Этот класс мы объявим в отдельном файле ball.py, чтобы сохранить модульность нашей программы:

import pygame
 
class Ball(pygame.sprite.Sprite):
    def __init__(self, x, filename):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(filename).convert_alpha()
        self.rect = self.image.get_rect(center=(x, 0))

Значит, здесь мы определяем наш собственный класс Ball, наследуя его от базового класса Sprite. В результате, мы расширяем функциональность этого базового класса, определяя в конструкторе два обязательных свойства:

  • image – графическое представление спрайта (ссылка на Surface);
  • rect – положение и размер спрайта.

Затем, эти свойства будут автоматически использоваться для обработки групп спрайтов. Поэтому они необходимы и должны называться именно так.

Отлично, это мы сделали. Теперь в основном модуле подключим этот файл и создадим шар через класс Ball:

from ball import Ball
...
speed = 1
b1 = Ball(W//2, 'images/ball_bear.png')

После этого в главном цикле реализуем движение шара b1:

    sc.fill(BLACK)
    sc.blit(b1.image, b1.rect)
    pygame.display.update()
 
    clock.tick(FPS)
 
    if b1.rect.y < H-20:
        b1.rect.y += speed
    else:
        b1.rect.y = 0

И при запуске программы увидим как шар в виде медведя падает вниз. Давайте для красоты добавим еще фон. Сначала загрузим его:

bg = pygame.image.load('images/back1.jpg').convert()

а, затем, в главном цикле будем перерисовывать вместо вызова fill(BLACK):

sc.blit(bg, (0, 0))

Получим такой вид игрового процесса:

Далее, смотрите, вот это изменение координат спрайта непосредственно в главном цикле – не лучшая практика. Лучше определить метод update() непосредственно в классе Ball:

    def update(self, *args):
        if self.rect.y < args[0] - 20:
            self.rect.y += self.speed
        else:
            self.rect.y = 0

А в конструктор добавим параметр speed:

    def __init__(self, x, speed, filename):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(filename).convert_alpha()
        self.rect = self.image.get_rect(center=(x, 0))
        self.speed = speed

После этого, создаем шарик, указывая три параметра:

b1 = Ball(W//2, speed, 'images/ball_bear.png')

и вызываем метод update в главном цикле:

b1.update(H)

Мы здесь дополнительно передаем высоту окна, чтобы метод update «знал» когда останавливать падение шарика.

Отлично, это мы сделали. Но представим теперь, что у нас одновременно падают несколько шариков. Тогда их нужно сначала создать, например, вот так:

b1 = Ball(W//2, speed, 'images/ball_bear.png')
b2 = Ball(W//2-250, 2, 'images/ball_fox.png')
b3 = Ball(W//2+100, 3, 'images/ball_panda.png')

А, затем, в главном цикле перерисовывать:

    sc.blit(bg, (0, 0))
    sc.blit(b1.image, b1.rect)
    sc.blit(b2.image, b2.rect)
    sc.blit(b3.image, b3.rect)
    pygame.display.update()
 
    clock.tick(FPS)
 
    b1.update(H)
    b2.update(H)
    b3.update(H)

Группы спрайтов

Текст программы выглядит не очень. Здесь, по сути, происходит дублирование кода, что не есть хорошо. И так как это типовая задача при создании игр, то в Pygame для ее решения было предложено спрайты объединять в группы и обрабатывать их единым образом с позиции этой группы.

Для создания группы используется класс:

pygame.sprite.Group()

например, так:

balls = pygame.sprite.Group()

и, далее, с помощью метода add мы добавляем в группу наши спрайты:

balls.add(Ball(W//2, 1, 'images/ball_bear.png'))
balls.add(Ball(W//2-250, 2, 'images/ball_fox.png'),
          Ball(W//2+100, 3, 'images/ball_panda.png'))

Обратите внимание, метод add может добавлять как отдельный спрайт, так и несколько спрайтов. Затем, в главном цикле для прорисовки всей группы достаточно вызвать метод:

balls.draw(sc)

Здесь sc – это поверхность, на которой рисуется группа спрайтов. И, далее, с помощью метода update() группы выполняется вызов такого же метода у каждого спрайта:

balls.update(H)

Причем, параметр H будет передан в вызов метода update каждого спрайта. Как видите, благодаря использованию групп, программа стала гораздо понятнее и проще.

Если графически представить работу группы, то это можно изобразить в виде следующей коллекции спрайтов:

И при вызове метода balls.update(H) группы происходит последовательный вызов этого же метода у спрайтов, принадлежащих этой группе.

Давайте еще немного разовьем тему с шариками и сделаем так, чтобы новый появлялся каждые две секунды и падал только до земли, после чего просто исчезал бы. Для этого мы вначале подготовим наборы поверхностей с разными изображениями шаров:

balls_images = ['ball_bear.png', 'ball_fox.png', 'ball_panda.png']
balls_surf = [pygame.image.load('images/'+path).convert_alpha() for path in balls_images]

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

def createBall(group):
    indx = randint(0, len(balls_surf)-1)
    x = randint(20, W-20)
    speed = randint(1, 4)
 
    return Ball(x, speed, balls_surf[indx], group)

Смотрите, мы здесь в конструктор передаем немного другие параметры: вместо пути к изображению – ссылку на слой с нарисованным шаром, а последний параметр – ссылка на группу, к которой добавляется шар. Причем, добавление реализуем непосредственно в классе Ball. Так как он унаследован от базового класса Sprite, то у Ball есть метод add(), который позволяет добавлять спрайт в указанную группу. И есть методы kill() и remove(), которые удаляют спрайт из группы:

Мы воспользуемся этим функционалом и перепишем класс Ball в следующем виде:

import pygame
 
class Ball(pygame.sprite.Sprite):
    def __init__(self, x, speed, surf, group):
        pygame.sprite.Sprite.__init__(self)
        self.image = surf
        self.rect = self.image.get_rect(center=(x, 0))
        self.speed = speed
        self.add(group)
 
    def update(self, *args):
        if self.rect.y < args[0] - 20:
            self.rect.y += self.speed
        else:
            self.kill()

Смотрите, при создании объекта спрайт сначала добавляется к группе, а когда шар долетает до земли, то удаляется из группы. И так как на объект нет других ссылок, то он автоматически уничтожается сборщиком мусора языка Python.

Ну и, наконец, нам нужно каждые две секунды создавать новые шары. Для этого воспользуемся модулем

pygame.time

и определим таймер, который каждые 2000 мс (то есть, 2 сек) будет генерировать событие USEREVENT:

pygame.time.set_timer(pygame.USEREVENT, 2000)

Затем, в главном цикле мы будем отслеживать это пользовательское событие и по нему создавать новый шар:

createBall(balls)
 
while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            exit()
        elif event.type == pygame.USEREVENT:
            createBall(balls)
 
    sc.blit(bg, (0, 0))
    balls.draw(sc)
    pygame.display.update()
 
    clock.tick(FPS)
 
    balls.update(H)

То есть, вначале перед циклом создаем первый шар, а все последующие создаются каждые две секунды. Причем, они будут автоматически добавляться в группу balls, а при достижении земли – удаляться из нее, что очень удобно.

Вот так осуществляется групповая работа со спрайтами в Pygame.