Стрелялка с Pygame №3: столкновения и стрельба

Третья часть проекта «Стрелялка с Pygame». В этот раз в игре появятся столкновения между игроком и врагами, а также пули, которыми игрок будет стрелять.

В этой серии уроков будет создана полноценная игра на языке Python с помощью библиотеки Pygame. Она будет особенно интересна начинающим программистам, которые уже знакомы с основами языка и хотят углубить знания, а также узнать, что лежит в основе создания игр.

Столкновения

Столкновения — важная часть разработки игр. Обнаружение столкновений подразумевает необходимость распознать, когда один объект в игре касается другого. Ответ на столкновение — это то, что случится в момент столкновения: когда Марио подберет монетку, когда меч Линка нанесет урон врагу и так далее.

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

Ограничивающая рамка

У каждого спрайта в Pygame есть атрибут rect, определяющий его координаты и размер. Объект rect в Pygame представлен в формате [x, y, width, height], где x и y представляют собой верхний левый угол прямоугольника. Другое название для этого прямоугольника — ограничивающая рамка, потому что она является границей объекта.

Обнаружение столкновений называется AABB (axis-aligned bounding box или «параллельный осям ограничивающий параллелепипед»). Такое название объясняется тем, что прямоугольник выравнивается в соответствии с осями экрана, которые не наклоняются. Обнаружение столкновений AABB столь популярно, потому что работает быстро — компьютер молниеносно сравнивает координаты прямоугольников, что особенно удобно, когда на экране много объектов.

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

Визуализация столкновений

На изображении видно, что только прямоугольник №3 сталкивается с большим черным прямоугольником. №1 пересекается с осью x, а №2 — с осью y. Но для пересечения двух прямоугольников, должны пересекаться обе их оси. Вот как это преподнести в коде:

if mob.rect.right > player.rect.left and \
   mob.rect.left < player.rect.right and \
   mob.rect.bottom > player.rect.top and \
   mob.rect.top < player.rect.bottom:
       collide = True

К счастью, в Pygame есть встроенная функция spritecollide() для выполнения того же самого.

Столкновение мобов с игроком

В раздел «update» игрового цикла необходимо добавить следующую команду:

# Обновление
all_sprites.update()

# Проверка, не ударил ли моб игрока
hits = pygame.sprite.spritecollide(player, mobs, False)
if hits:
    running = False

spritecollide() принимает 3 аргумента: название спрайта, который нужно проверять, название группы для сравнения и значения True или False для параметра dokill. Последний параметр позволяет указать, должен ли объект удаляться при столкновении. Если нужно было, например, проверить, подобрал ли игрок монетку, необходимо указать значение True так, чтобы монетка пропала.

Результат команды spritecollide() — это список спрайтов, которые столкнулись с игроком (он может быть не один). Присвоим его переменной hits.

Если список hits будет непустым, значение инструкции if окажется True. В результате значение running изменится на False, и игра закончится.

Стрельба

Спрайт пули

Пришло время добавить еще один спрайт — пули. Это будет спрайт, который появляется в момент выстрела над спрайтом игрока и двигается вверх с большой скоростью. Определение спрайта вам знакомо, поэтому вот сразу готовый класс Bullet:

class Bullet(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface((10, 20))
        self.image.fill(YELLOW)
        self.rect = self.image.get_rect()
        self.rect.bottom = y
        self.rect.centerx = x
        self.speedy = -10

    def update(self):
        self.rect.y += self.speedy
        # убить, если он заходит за верхнюю часть экрана
        if self.rect.bottom < 0:
            self.kill()

В метод __init__() спрайта пули нужно передать значения x и y, чтобы указать спрайту, где появляться. Поскольку спрайт игрока может двигаться, то место появления будет соответствовать местоположению игрока. Значение speedy будет отрицательным, чтобы он спрайт двигался наверх.

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

Событие keypress

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

for event in pygame.event.get():
    # проверка для закрытия окна
    if event.type == pygame.QUIT:
        running = False
    elif event.type == pygame.KEYDOWN:
        if event.key == pygame.K_SPACE:
            player.shoot()

Новый код проверяет событие KEYDOWN, и если таковое наблюдается, проверяет нажата ли кнопка K_SPACE. Если да — запускается метод игрока shoot().

Появление пули

В первую очередь необходимо добавить группу для пуль:

bullets = pygame.sprite.Group()

Теперь можно создавать следующий метод в классе Player:

def shoot(self):
    bullet = Bullet(self.rect.centerx, self.rect.top)
    all_sprites.add(bullet)
    bullets.add(bullet)

Все что делает метод shoot() — создает пулю, используя в качестве места появления верхнюю центральную часть игрока. После этого нужно убедиться, что пуля добавлена в all_sptires (чтобы она отрисовалась и обновилась) и в bullets, которая будет использоваться для столкновений.

Столкновения пуль

Теперь нужно проверить, задела ли пуля моба. Отличие в том, что есть несколько пуль (в группе bullets) и несколько мобов (в группе mobs), поэтому нельзя использовать spritecollide() как в прошлый раз, потому что в этой функции сравнивается только один спрайт с группой. Вместо этого нужно использовать groupcollide():

# Обновление
all_sprites.update()

# Проверка, не ударил ли моб игрока
hits = pygame.sprite.groupcollide(mobs, bullets, True, True)
for hit in hits:
    m = Mob()
    all_sprites.add(m)
    mobs.add(m)

Функция groupcollide() похожа на spritecollide() за исключением того, что нужно указывать две группы для сравнения, а возвращать функция будет список задетых мобов. Также есть два значения dokill для каждой из групп.

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

Теперь это начинает напоминать реальную игру:

Стрельба по врагам

Код урока — shmup-3.py

В следующем уроке вы узнаете, как добавить графику в игре вместо того, чтобы использовать цветные прямоугольники.