Классы Sprite и Group

В программировании игр спрайтом называют объект, который предстает перед пользователем в виде анимированного изображения и в большинстве случаев предполагает взаимодействие с ним. Другими словами, все что в игре не является фоном, а интерактивным объектом-картинкой – это спрайт.

Хотя каждый спрайт может быть уникальным, у всех есть нечто общее, что в pygame вынесено в отдельный класс Sprite, находящийся в модуле pygame.sprite.

На базе этого класса следует создавать собственные классы спрайтов и уже от них объекты. Таким образом, класс pygame.sprite.Sprite играет роль своего рода абстрактного класса. Хотя таковым не является, можно создавать объекты непосредственно от Sprite.

В модуле pygame.sprite кроме класса Sprite есть класс Group и родственные ему, которые предназначены для объединения спрайтов в группы. Это позволяет вызывать один метод группы, который, например, обновит состояние всех спрайтов, входящих в эту группу.

Почти все предопределенные методы класса pygame.sprite.Sprite касаются добавления экземпляра в группу, удаления из нее, проверки вхождения. Только метод update() затрагивает поведение самого спрайта, этот метод следует переопределить в производном от Sprite классе.

Рассмотрим, как это работает. В примерах кода ниже сначала одна, а потом и множество машинок перемещаются сверху вниз. Каждая такая машинка – объект-спрайт, созданный от класса Car, который является дочерним от Sprite.

В конструкторе производного от Sprite класса необходимо вызвать конструктор родительского класса, а также обзавестись экземплярами Surface и Rect, имена которых должны быть соответственно self.image и self.rect. Так надо, чтобы с экземплярами класса могли работать методы группы. В остальном вы можете добавлять любые атрибуты.

Как создается поверхность (а также прямоугольная область), неважно. В примере ниже это делается с помощью функции load(). Однако в конструктор может передаваться уже подготовленный экземпляр Surface.

from random import randint
import pygame
pygame.init()
W = 400
H = 400
WHITE = (255, 255, 255)
 
 
class Car(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))
 
 
sc = pygame.display.set_mode((W, H))
 
# координата x будет случайна
car1 = Car(randint(1, W), 'car1.png')
 
while 1:
    for i in pygame.event.get():
        if i.type == pygame.QUIT:
            exit()
 
    sc.fill(WHITE)
    sc.blit(car1.image, car1.rect)
    pygame.display.update()
    pygame.time.delay(20)
    # машинка ездит сверху вниз
    if car1.rect.y < H:
        car1.rect.y += 2
    else:
        car1.rect.y = 0

В данном случае мы изменяем свойства экземпляра за пределами класса. Правильней будет делать это в методе update():

...
class Car(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))
 
    def update(self):
        if self.rect.y < H:
            self.rect.y += 2
        else:
            self.rect.y = 0
 
 
sc = pygame.display.set_mode((W, H))
 
car1 = Car(randint(1, W), 'car1.png')
 
while 1:
    for i in pygame.event.get():
        if i.type == pygame.QUIT:
            exit()
 
    sc.fill(WHITE)
    sc.blit(car1.image, car1.rect)
    pygame.display.update()
    pygame.time.delay(20)
 
    car1.update()

Теперь представим, что у нас не одна машинка, а три:

car1.png  car2.png  car3.png

...
car1 = Car(randint(1, W), 'car1.png')
car2 = Car(randint(1, W), 'car2.png')
car3 = Car(randint(1, W), 'car3.png')
 
while 1:
    for i in pygame.event.get():
        if i.type == pygame.QUIT:
            exit()
 
    sc.fill(WHITE)
 
    sc.blit(car1.image, car1.rect)
    sc.blit(car2.image, car2.rect)
    sc.blit(car3.image, car3.rect)
 
    pygame.display.update()
    pygame.time.delay(20)
 
    car1.update()
    car2.update()
    car3.update()

Если будет 100 машинок, придется 100 раз вызвать blit() и update(). Класс Group решает эту проблему. Добавлять спрайты в группу можно методом add() группы (по одной или все вместе).

У групп есть методы update() и draw(). Метод update() группы вызывает методы update() всех входящих в нее объектов. А метод draw() выполняет метод blit(). При этом в draw() надо передать поверхность, на которой будет происходить отрисовка:

...
 
sc = pygame.display.set_mode((W, H))
 
cars = pygame.sprite.Group()
cars.add(Car(randint(1, W), 'car1.png'),
         Car(randint(1, W), 'car2.png'))
cars.add(Car(randint(1, W), 'car3.png'))
 
while 1:
    for i in pygame.event.get():
        if i.type == pygame.QUIT:
            exit()
 
    sc.fill(WHITE)
 
    cars.draw(sc)
 
    pygame.display.update()
    pygame.time.delay(20)
 
    cars.update()

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

Потребуется таймер, который устанавливается вызовом функции pygame.time.set_timer(). В примере ниже через каждые 3 секунды будет генерироваться событие, значение поля type которого совпадает с константой pygame.USEREVENT. И как только это событие будет происходить, будет создаваться новый объект.

from random import randint
import pygame
pygame.init()
pygame.time.set_timer(pygame.USEREVENT, 3000)
 
W = 400
H = 400
WHITE = (255, 255, 255)
CARS = ('car1.png', 'car2.png', 'car3.png')
CARS_SURF = []  # для хранения готовых машин-поверхностей
 
# надо установить видео режим до вызова image.load()
sc = pygame.display.set_mode((W, H))
 
for i in range(len(CARS)):
    CARS_SURF.append(pygame.image.load(CARS[i]).convert_alpha())
 
 
class Car(pygame.sprite.Sprite):
    def __init__(self, x, surf, group):
        pygame.sprite.Sprite.__init__(self)
        self.image = surf
        self.rect = self.image.get_rect(center=(x, 0))
        self.add(group)  # добавляем в группу
        self.speed = randint(1, 3)  # у машин будет разная скорость
 
    def update(self):
        if self.rect.y < H:
            self.rect.y += self.speed
        else:
            # теперь не перебрасываем вверх,
            # а удаляем из всех групп
            self.kill()
 
cars = pygame.sprite.Group()
 
# добавляем первую машину, которая появляется сразу
Car(randint(1, W), CARS_SURF[randint(0, 2)], cars)
 
while 1:
    for i in pygame.event.get():
        if i.type == pygame.QUIT:
            exit()
        elif i.type == pygame.USEREVENT:
            Car(randint(1, W), CARS_SURF[randint(0, 2)], cars)
 
    sc.fill(WHITE)
    cars.draw(sc)
    pygame.display.update()
    pygame.time.delay(20)
    cars.update()

Метод kill() спрайта удаляет его из всех групп, в которых он содержится. Есть метод remove(), который удаляет только из указанных в качестве аргумента групп. У спрайтов также как у групп есть метод add(). Только в данном случае ему передается не объект, а группа.

Практическая работа

В модуле pygame.sprite есть ряд функций для проверки коллизий спрайтов. Одна из них spritecollideany() проверяет, столкнулся ли конкретный спрайт с любым из спрайтов из группы. Функция принимает первым аргументом спрайт, чья коллизия проверяется, вторым – группу.

Измените программу выше так, чтобы машинки появлялись чаще. Добавьте спрайт, который "едет" навстречу всем другим и управляется стрелками влево и вправо на клавиатуре. Цель игры – не допустить столкновения. Если оно происходит, то программа завершается.

Курс с примерами решений практических работ: android-приложение, pdf-версия.

Комментарии

Создаются два шара, с одинаковой массой (m), один шар имеет скорость (в данном случае только по координате Х) второй шар стоит на месте. В чем проблема данного кода? иногда отталкиваются корректно, но в основном останавливаются на месте или бывало проходили сквозь. С физикой плохо дружу, формулы брал в интернете.
for i in range(0, len(list_ball)):  # Берем каждый шар из все существующих
    first = list_ball[i]  # для удобства использования
    list_ball.remove(first)  # удаляем его со списка что бы при сравнивании с остальными, он не сравнивался сам с собой
    for o in range(0, len(list_ball)):  # Берем по очереди другие шары
        second = list_ball[o]  # для удобства использования
        if first.rect.colliderect(second.rect):  # проверяем на столкновение
	    f_speedx0 = first.speedx  # Для использования скорости до удара
	    first.speedx = ((first.m - second.m) * f_speedx0) / (first.m + second.m)  # m - масса, выводим из закона сохранения импульса
	    second.speedx = (2 * first.m * f_speedx0) / (first.m + second.m)
	first.update(image)  # метод класса для расчета новых координат
	second.update(image)
    list_ball.insert(i, first)  # возвращаем в список
    sc.blit(first.image, first.rect)  # отрисовываем
    sc.blit(second.image, second.rect)

from random import randint
import pygame
pygame.init()
pygame.time.set_timer(pygame.USEREVENT, 250)
 
FPS = 24
W = 600
H = 800
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
CARS = ('/home/user/Изображения/car1.png', '/home/user/Изображения/car2.png', \
        '/home/user/Изображения/car3.png')
CARS_SURF = []  # для хранения готовых машин-поверхностей
motion = 'STOP'
clock = pygame.time.Clock()
font = pygame.font.Font(pygame.font.match_font('dejavusans'), 36)
text1 = font.render('Game over', 1, (180,0,0))
 
# надо установить видео режим до вызова image.load()
sc = pygame.display.set_mode((W, H))
 
for i in range(len(CARS)):
    CARS_SURF.append(pygame.image.load(CARS[i]).convert_alpha())
 
 
class Car(pygame.sprite.Sprite):
    def __init__(self, x, surf, group):
        pygame.sprite.Sprite.__init__(self)
        self.image = surf
        self.rect = self.image.get_rect(center=(x, 0))
        self.add(group)  # добавляем в группу
        self.speed = randint(1, 3)  # у машин будет разная скорость
 
    def update(self):
        if self.rect.y < H:
            self.rect.y += self.speed
        else:
            self.kill()
 
class User_car(pygame.sprite.Sprite):
    def __init__(self, x, surf):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.transform.rotate(surf, 180)
        self.rect = self.image.get_rect(center=(x, H))
 
cars = pygame.sprite.Group()
 
# добавляем первую машину, которая появляется сразу
Car(randint(1, W), CARS_SURF[randint(0, 2)], cars)
user_car1 = User_car(randint(1, W), CARS_SURF[randint(0, 2)])
 
while 1:
    for i in pygame.event.get():
        if i.type == pygame.QUIT:
            pygame.quit()
            break
        elif i.type == pygame.USEREVENT:
            Car(randint(1, W), CARS_SURF[randint(0, 2)], cars)
        elif i.type == pygame.KEYDOWN:
            if i.key == pygame.K_LEFT:
                motion = 'LEFT'
            elif i.key == pygame.K_RIGHT:
                motion = 'RIGHT'
            elif i.key == pygame.K_UP:
                motion = 'UP'
            elif i.key == pygame.K_DOWN:
                motion = 'DOWN'            
            else:
                motion = 'STOP'
 
    if motion == 'LEFT':
        user_car1.rect[0] -= 3
    elif motion == 'RIGHT':
        user_car1.rect[0] += 3
    elif motion == 'UP':
        user_car1.rect[1] -= 3
    elif motion == 'DOWN':
        user_car1.rect[1] += 3
 
    if pygame.sprite.spritecollideany(user_car1, cars) != None:
        sc.fill(BLACK)
        sc.blit(text1, (W//2,H//2))
        pygame.display.update()
        break
    else:
        sc.fill(WHITE)
        sc.blit(user_car1.image, user_car1.rect)
        cars.draw(sc)
        pygame.display.update()
        cars.update()
 
    pygame.time.delay(FPS)

Ответ на от Winter23Rus

import pygame
import random
 
pygame.init()
 
CARS = ['car1.png', 'car2.png', 'car3.png']
 
 
pygame.time.set_timer(pygame.USEREVENT, 200)
FPS = 60
SCREEN = (600, 400)
BLACK = (0, 0, 0)
GAME_OVER = False
sc = pygame.display.set_mode(SCREEN)
 
game_over_text = pygame.font.SysFont('arial', 36).render("GAME OVER", 1, (255, 0, 0))
game_over_rect = game_over_text.get_rect(center=(SCREEN[0]//2, SCREEN[1]//2))
 
 
class Car(pygame.sprite.Sprite):
    def __init__(self, x, surf, group):
        super().__init__()
        self.image = surf
        self.rect = self.image.get_rect(center=(x, 0))
        self.speed = random.randint(2, 7)
        self.add(group)
 
    def update(self, *args):
        if self.rect.y < SCREEN[1]:
            self.rect.y += self.speed
        else:
            self.kill()
 
 
class HeroCar(pygame.sprite.Sprite):
    def __init__(self, X, path, group):
        super().__init__()
        self.image = pygame.image.load(path).convert_alpha()
        self.rect = self.image.get_rect(center=(X, SCREEN[1]))
        self.add(group)
 
    def update(self, move):
        if move == "LEFT":
            self.rect.x -= 3
        elif move == "RIGHT":
            self.rect.x += 3
 
 
hero_group = pygame.sprite.Group()
hero = HeroCar(SCREEN[0]//2, 'car.png', hero_group)
 
 
cars = pygame.sprite.Group()
play = True
 
while play:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            play = False
 
        if event.type == pygame.USEREVENT and not GAME_OVER:
            surf = pygame.image.load(CARS[random.randint(0, 2)]).convert_alpha()
            x = random.randint(1, SCREEN[0])
            Car(x, surf, cars)
 
    keys = pygame.key.get_pressed()
    if keys[pygame.K_RIGHT] and hero.rect.x + hero.rect.width < SCREEN[0] and not GAME_OVER:
        hero_group.update('RIGHT')
 
    if keys[pygame.K_LEFT] and hero.rect.x > 0 and not GAME_OVER:
        hero_group.update('LEFT')
 
    sc.fill(BLACK)
 
    hero_group.draw(sc)
 
    cars.draw(sc)
    cars.update()
 
    if pygame.sprite.spritecollideany(hero, cars):
        GAME_OVER = True
        cars.empty()
        hero.rect.x = SCREEN[0]//2
 
    if GAME_OVER:
        sc.blit(game_over_text, game_over_rect)
 
    pygame.display.update()
 
    pygame.time.Clock().tick(FPS)

""" Sprite и соприкосновение """
from random import randint
import pygame
pygame.init()
pygame.time.set_timer(pygame.USEREVENT, 2000)# раз в 2 сек
W = 400
H = 400
WHITE = (255, 255, 255)
 
sc = pygame.display.set_mode((W, H))
CARS=('car.bmp', 'car1.bmp')#список изображений
CARS_SURF=[]
 
for i in range(len(CARS)):
    CARS_SURF.append(pygame.image.load(CARS[i]).convert())#добавление изображения
 
class Car(pygame.sprite.Sprite):#класс машин 
    def __init__(self, x, surf,group):
        pygame.sprite.Sprite.__init__(self)
        self.image = surf
        self.rect = self.image.get_rect(center=(x, W))
        self.add(group)
        self.speed=(randint(1,10))
        self.image.set_colorkey((255,255,255))#добавил 
    def update(self):#обновление /изменяем св-ва класса за его пределами 
        if self.rect.y > -100:
            self.rect.y -= self.speed
        else:
            self.kill
 
cars = pygame.sprite.Group()#
Car(randint(25,W-25), CARS_SURF[0],cars)#создание первой машины - спрайта
 
class Car_up(pygame.sprite.Sprite):#класс машин едущих навстречу
    def __init__(self,x,surf):
        pygame.sprite.Sprite.__init__(self)
        self.image = surf
        self.rect = self.image.get_rect(center=(x, 50))
        self.image.set_colorkey((255,255,255))
 
car_up=Car_up(200, CARS_SURF[1])#создание спрайта-машины едущей навстречу
sc.blit(car_up.image, car_up.rect)#
 
while 1:
    a=pygame.event.get()
    for i in a:#выход
        if i.type == pygame.QUIT:#выход
            pygame.quit()
 
        elif i.type== pygame.USEREVENT:#        
            Car(randint(25,W-25), CARS_SURF[0],cars)#создание новой машины
 
    keys = pygame.key.get_pressed()#отслеживает была ли зажата и какая кнопка
    if keys[pygame.K_RIGHT]:#вправо движение
        car_up.rect.x+=2
    elif keys[pygame.K_LEFT]:#влево движение
        car_up.rect.x-=2
 
    if pygame.sprite.spritecollide(car_up,cars,False):#если было соприкосновение одного с кем-то из группы то
        pygame.quit()#      то выход
 
    sc.fill(WHITE)#
    cars.draw(sc)#отрисовать поверх-ть
    sc.blit(car_up.image, car_up.rect)#отрисовать поверх-ть
    pygame.display.update()#обновить
    pygame.time.delay(20)
    cars.update()#обновить класс машин