В этой главе в игру «Инопланетное вторжение» будут добавлены пришельцы. Сначала мы добавим одного из них у верхнего края экрана, а потом сгенерируем целый флот. Пришельцы будут перемещаться в сторону и вниз; при этом те, в кого попадают снаряды, будут исчезать с экрана. Наконец, мы ограничим количество кораблей игрока, так что при гибели последнего корабля игра завершится.
В этой главе вы узнаете больше о Pygame и ведении крупного проекта. Вы также научитесь обнаруживать коллизии (столкновения) игровых объектов — например, снарядов и пришельцев. Выявление коллизий помогает определять взаимодействие элементов игры: например, ограничить перемещение персонажа областью между стенами лабиринта или организовать передачу мяча между двумя персонажами. Работа будет продолжаться на основе плана, к которому мы будем возвращаться время от времени, чтобы не отклоняться от цели во время написания кода.
Прежде чем браться за новый код для добавления флота пришельцев на экран, рассмотрим проект и обновим план.
Приступая к новой фазе разработки крупного проекта, всегда полезно вернуться к исходному плану и уточнить, чего же вы хотите добиться в том коде, который собираетесь написать. В этой главе мы выполним следующие действия.
• Проанализируем код и определим, нужно ли провести рефакторинг перед реализацией новых возможностей.
• Заполним верхнюю часть экрана таким количеством пришельцев, сколько поместится по горизонтали. Затем будем создавать дополнительные ряды пришельцев, пока не соберем полный флот.
• По величине интервалов вокруг первого пришельца и общим размерам экрана вычислим, сколько пришельцев поместится на экране. Для создания пришельцев, заполняющих верхнюю часть экрана, будет написан цикл.
• Организуем перемещение флота пришельцев в сторону и вниз, пока весь он не будет уничтожен, пришелец не столкнется с кораблем игрока или не достигнет земли. Если весь флот уничтожен, то программа создает новый флот. Если пришелец сталкивается с кораблем или землей, то программа уничтожает корабль и создает новый флот.
• Ограничим количество кораблей, которые могут использоваться игроком, и завершим игру в конце последней попытки.
Этот план будет уточняться по мере реализации новых возможностей, но для начала и этого достаточно.
Кроме того, проводите ревью кода, когда начинаете работу над новой серией возможностей проекта. С каждой новой фазой проект обычно становится более сложным, поэтому лучше всего заняться расчисткой излишне громоздкого или неэффективного кода. Ранее мы уже проводили рефакторинг, так что сейчас особой расчистки не потребуется.
Размещение одного пришельца на экране мало чем отличается от размещения корабля. Поведением каждого пришельца будет управлять класс Alien, который по своей структуре очень похож на класс Ship. Для простоты мы снова воспользуемся готовыми графическими изображениями. Вы можете найти собственное изображение пришельца или использовать изображение на рис. 13.1, доступное в дополнительных материалах к книге, размещенных по адресу https://ehmatthes.github.io/pcc_3e. Это изображение имеет серый фон, совпадающий с цветом фона экрана. Не забудьте сохранить выбранный файл в папке images.
Рис. 13.1. Пришелец, который будет использоваться для создания флота
Теперь можно написать класс Alien и сохранить его в файле alien.py:
alien.py
import pygame
from pygame.sprite import Sprite
class Alien(Sprite):
"""Класс, представляющий одного пришельца."""
def __init__(self, ai_game):
"""Инициализирует пришельца и задает его начальную позицию."""
super().__init__()
self.screen = ai_game.screen
# Загрузка изображения пришельца и назначение атрибута rect.
self.image = pygame.image.load('images/alien.bmp')
self.rect = self.image.get_rect()
# Каждый новый пришелец появляется в левом верхнем углу экрана.
❶ self.rect.x = self.rect.width
self.rect.y = self.rect.height
# Сохранение точной горизонтальной позиции пришельца.
❷ self.x = float(self.rect.x)
В основном этот класс похож на класс Ship (если не считать размещение пришельца). Изначально каждый пришелец размещается в левом верхнем углу экрана, при этом слева от него добавляется интервал, равный ширине пришельца, а над ним — интервал, равный высоте ❶. Нас в первую очередь интересует горизонтальная скорость пришельца, поэтому будем отслеживать точную горизонтальную позицию каждого из них ❷.
Классу Alien не нужен метод для вывода на экран; вместо этого мы воспользуемся методом групп Pygame, который автоматически рисует все элементы группы на экране.
Начнем с создания экземпляра Alien, чтобы первый пришелец появился на экране. Так как эта операция входит в подготовительную часть, код для текущего экземпляра будет добавлен в конец метода __init__() в классе AlienInvasion. Позднее будет создан целый флот вторжения, что потребует определенной работы, поэтому мы определим новый вспомогательный метод _create_fleet().
Порядок следования методов в классе может быть любым — важно лишь, чтобы в этом порядке существовала некая закономерность. Я размещу _create_fleet() непосредственно перед методом _update_screen(), но с таким же успехом его можно разместить в любой точке AlienInvasion. Начнем с импортирования класса Alien.
Обновленные операторы импортирования в файле alien_invasion.py выглядят так:
alien_invasion.py
--пропуск--
from bullet import Bullet
from alien import Alien
А это обновленный метод __init__():
alien_invasion.py
def __init__(self):
--пропуск--
self.ship = Ship(self)
self.bullets = pygame.sprite.Group()
self.aliens = pygame.sprite.Group()
self._create_fleet()
Создадим группу для хранения флота вторжения и вызовем метод _create_fleet(), который мы напишем чуть позже.
Новый метод _create_fleet() выглядит так:
alien_invasion.py
def _create_fleet(self):
"""Создает флот пришельцев."""
# Создание пришельца.
alien = Alien(self)
self.aliens.add(alien)
В этом методе создается один экземпляр Alien, который затем добавляется в группу для хранения флота. По умолчанию объект размещается в левом верхнем углу экрана — эта позиция прекрасно подходит для первого пришельца.
Чтобы пришелец появился на экране, программа вызывает метод draw() группы в _update_screen():
alien_invasion.py
def _update_screen(self):
--пропуск--
self.ship.blitme()
self.aliens.draw(self.screen)
pygame.display.flip()
При вызове метода draw() для группы Pygame выводит каждый элемент группы в позиции, определяемой его атрибутом rect. Метод получает один аргумент: поверхность для вывода элементов группы. На рис. 13.2 изображен первый пришелец.
После того как первый пришелец появится на экране, мы напишем код для вывода всего флота.
Рис. 13.2. Появился первый пришелец
Чтобы нарисовать флот пришельцев, нам нужно решить, как заполнить верхнюю часть экрана пришельцами, не перегружая окно игры. Существует несколько способов решения этой задачи. Мы будем добавлять пришельцев в верхней части экрана, заполняя ее, пока места для нового пришельца не останется. Затем мы повторим этот процесс, заполняя экран по вертикали и добавляя ряды пришельцев.
Теперь мы готовы сгенерировать полный ряд пришельцев. Чтобы создать полный ряд, мы сначала создадим одного пришельца, чтобы определить его ширину. Мы поместим пришельца в левую часть экрана, а затем будем добавлять его собратьев, пока не закончится пространство:
alien_invasion.py
def _create_fleet(self):
"""Создает флот пришельцев."""
# Создание пришельца и вычисление количества пришельцев в ряду.
# Интервал между соседними пришельцами равен ширине пришельца.
alien = Alien(self)
alien_width = alien.rect.width
❶ current_x = alien_width
❷ while current_x < (self.settings.screen_width - 2 * alien_width):
❸ new_alien = Alien(self)
❹ new_alien.x = current_x
new_alien.rect.x = current_x
self.aliens.add(new_alien)
❺ current_x += 2 * alien_width
Мы вычисляем ширину пришельца благодаря первому созданному пришельцу и определяем переменную current_x ❶. Она хранит позицию по горизонтали следующего пришельца, размещаемого на экране. Изначально мы присваиваем ей значение, равное ширине одного пришельца, чтобы отодвинуть пришельца во флоте от левого края экрана.
Далее запускается цикл while ❷, добавляющий пришельцев, пока пространства по горизонтали достаточно. Чтобы определить, хватит ли места для размещения еще одного пришельца, мы сравниваем значение переменной current_x с максимальным значением. Попробуем определить этот цикл следующим образом:
while current_x < self.settings.screen_width:
Рабочий на первый взгляд способ приводит к тому, что последний пришелец в ряду оказывается у крайнего правого края экрана. Поэтому мы добавляем небольшое пространство с правой стороны. Пока у правого края есть свободное пространство, равное ширине как минимум двух пришельцев, цикл повторяется и добавляет еще одного пришельца в ряд.
При каждой итерации, когда горизонтального пространства на экране достаточно для продолжения цикла, мы хотим выполнить две задачи: создать пришельца в корректной позиции и определить горизонтальную позицию следующего пришельца в ряду. Мы создаем пришельца и присваиваем значение переменной new_alien ❸. Затем используем точную горизонтальную позицию согласно текущему значению переменной current_x ❹. Мы позиционируем прямоугольник пришельца согласно тому же значению по оси X и добавляем нового пришельца в группу self.aliens.
Наконец, мы инкрементируем значение переменной current_x ❺. Мы добавляем значение, равное двукратной ширине пришельца, к горизонтальной позиции, чтобы переместиться за пределы только что добавленного пришельца и допустить интервал между пришельцами. Интерпретатор вновь оценивает условие в начале цикла while и определяет, хватит ли места для еще одного пришельца. Когда места не останется, цикл завершится и мы получим полный ряд пришельцев.
Запустив программу «Инопланетное вторжение», вы увидите, что на экране появился первый ряд пришельцев (рис. 13.3).
ПРИМЕЧАНИЕ
Не всегда очевидно, как именно выстроить цикл, подобный приведенному в этом подразделе. Одна из фишек программирования заключается в том, что первоначальные идеи решения подобной задачи необязательно должны быть правильными. Вполне допустимо написать цикл, размещающий пришельцев слишком далеко друг от друга, а затем корректировать код до тех пор, пока персонажи не разместятся должным образом.
Рис. 13.3. Первый ряд пришельцев
Если бы создание флота на этом было завершено, то функцию _create_fleet(), пожалуй, можно было бы оставить в таком виде, но нам предстоит еще много работы, поэтому мы немного подчистим код функции. Мы добавим новый вспомогательный метод _create_alien() и вызовем его из _create_fleet():
alien_invasion.py
def _create_fleet(self):
--пропуск--
while current_x < (self.settings.screen_width - 2 * alien_width):
self._create_alien(current_x)
current_x += 2 * alien_width
❶ def _create_alien(self, x_position):
"""Создает пришельца и размещает его в ряду."""
new_alien = Alien(self)
new_alien.x = x_position
new_alien.rect.x = x_position
self.aliens.add(new_alien)
Метод _create_alien() должен получать еще один параметр, кроме self: значение по оси X, указывающее, куда должен быть помещен пришелец ❶. В теле _create_alien() мы используем тот же код, созданный для _create_fleet(), не считая того, что вместо current_x применяем параметр x_position. Рефакторинг упрощает добавление новых рядов и создание всего флота.
Чтобы завершить создание флота, мы будем добавлять новые ряды, пока не закончится пространство. Мы используем вложенный цикл — поместим текущий цикл в еще один цикл while. Внутренний цикл будет размещать пришельцев в ряд по горизонтали согласно их координатам по оси X. Внешний — размещать по вертикали согласно их координатам по оси Y. Мы перестанем добавлять ряды, добравшись до нижней части экрана и оставив достаточно места для корабля и отстреливания пришельцев.
Ниже показано, как вложить описанные два цикла while в _create_fleet():
alien_invasion.py
def _create_fleet(self):
"""Создает флот пришельцев."""
# Создание пришельца и добавление других, пока остается место.
# Расстояние между пришельцами составляет одну ширину
# и одну высоту пришельца.
alien = Alien(self)
❶ alien_width, alien_height = alien.rect.size
❷ current_x, current_y = alien_width, alien_height
❸ while current_y < (self.settings.screen_height - 3 * alien_height):
while current_x < (self.settings.screen_width - 2 * alien_width):
❹ self._create_alien(current_x, current_y)
current_x += 2 * alien_width
❺ # Конец ряда: сбрасываем значение x и инкрементируем значение y.
current_x = alien_width
current_y += 2 * alien_height
Нам понадобится значение высоты пришельца, чтобы разместить ряды, поэтому мы извлекаем ширину и высоту пришельца с помощью атрибута size объекта rect пришельца ❶. Атрибут size объекта rect — это кортеж, содержащий его ширину и высоту.
Далее мы задаем начальные координаты по осям X и Y для размещения первого пришельца ❷. Мы размещаем его на одну ширину пришельца правее и на одну высоту пришельца ниже. Затем определяем цикл while, управляющий тем, сколько рядов будет размещено на экране ❸. Пока значение по оси Y для следующего ряда меньше высоты экрана минус три высоты пришельца, мы будем добавлять ряды. (Если нужное пространство не остается, исправим код позже.)
Мы вызываем функцию _create_alien() и передаем ей значения по осям Y и X ❹. Чуть позже мы изменим ее.
Обратите внимание на отступ в последних двух строках кода ❺. Они расположены внутри внешнего цикла while и вне внутреннего цикла while. Этот код запускается после завершения внутреннего цикла, однократно после создания каждого ряда. После добавления каждого ряда мы сбрасываем значение переменной current_x, чтобы первый пришелец в следующем ряду был помещен в ту же позицию, что и первый пришелец в предыдущих рядах. Затем добавляем двукратное значение высоты пришельца к текущему значению переменной current_y, чтобы следующий ряд располагался ниже на экране. Эти отступы в коде крайне важны; если у вас не получается правильно поместить флот пришельцев при запуске программы alien_invasion.py, то проверьте отступы всех строк в этих вложенных циклах.
Нам нужно изменить код функции _create_alien(), чтобы настроить положение пришельцев по вертикали:
def _create_alien(self, x_position, y_position):
"""Создает пришельца и помещает его во флот."""
new_alien = Alien(self)
new_alien.x = x_position
new_alien.rect.x = x_position
new_alien.rect.y = y_position
self.aliens.add(new_alien)
Мы изменили определение метода, чтобы использовать значение по оси Y нового пришельца, и настраиваем положение прямоугольника по вертикали в теле метода.
Если вы запустите игру сейчас, то увидите целый флот пришельцев (рис. 13.4).
В следующем разделе мы приведем флот в движение.
Рис. 13.4. На экране появился весь флот пришельцев
Упражнения
13.1. Звезды. Найдите изображение звезды. Создайте на экране сетку из звезд.
13.2. Улучшенные звезды. Чтобы звезды выглядели более реалистично, следует внести случайное отклонение при их размещении. Вспомните, что случайные числа генерируются следующим образом:
from random import randint
random_number = randint(-10,10)
Этот код возвращает случайное целое число в диапазоне от −10 до 10. Используя свой код из упражнения 13.1, измените позицию каждой звезды на случайную величину.
Флот пришельцев должен двигаться вправо по экрану, пока не дойдет до края; тогда флот опускается на заданную величину и начинает двигаться в обратном направлении. Это продолжается до тех пор, пока все пришельцы не будут сбиты, один из них не столкнется с кораблем или не достигнет низа экрана. Начнем с перемещения флота вправо.
Чтобы корабли пришельцев перемещались по экрану, мы воспользуемся методом update() из программы alien.py, который будет вызываться для каждого пришельца в группе. Сначала добавим настройку для управления скоростью каждого пришельца:
settings.py
def __init__(self):
--пропуск--
# Настройки пришельцев
self.alien_speed = 1.0
Настройка используется в реализации метода update() в файле alien.py:
alien.py
def __init__(self, ai_game):
"""Инициализирует пришельца и задает его начальную позицию."""
super().__init__()
self.screen = ai_game.screen
self.settings = ai_game.settings
--пропуск--
def update(self):
"""Перемещает пришельца вправо."""
❶ self.x += self.settings.alien_speed
❷ self.rect.x = self.x
Параметр settings создается в методе __init__(), чтобы к скорости пришельца можно было обратиться в методе update(). При каждом обновлении позиции пришельца мы смещаем его вправо на величину, хранящуюся в alien_speed. Точная позиция пришельца хранится в атрибуте self.x, который может принимать вещественные значения ❶. Затем значение self.x используется для обновления позиции прямоугольника пришельца ❷.
В основном цикле while уже содержатся вызовы обновления корабля и снарядов. Теперь необходимо обновить позицию каждого пришельца:
alien_invasion.py
while True:
self._check_events()
self.ship.update()
self._update_bullets()
self._update_aliens()
self._update_screen()
self.clock.tick(60)
Сейчас мы напишем код управления флотом, для которого будет создан новый метод _update_aliens(). Позиции пришельцев обновляются после обновления снарядов, так как скоро мы будем проверять, попали ли какие-либо снаряды в пришельцев.
Местоположение этого метода в модуле некритично. Но для улучшения структуры кода мы поместим его сразу же после _update_bullets() в соответствии с порядком вызова методов в цикле while. Первая версия _update_aliens() выглядит так:
alien_invasion.py
def _update_aliens(self):
"""Обновляет позиции всех пришельцев во флоте."""
self.aliens.update()
Мы используем метод update() для группы aliens, что приводит к автоматическому вызову метода update() каждого пришельца. Если вы запустите «Инопланетное вторжение» сейчас, то увидите, как флот двигается вправо и исчезает за краем экрана.
Теперь мы создадим настройки, благодаря которым флот перемещается вниз по экрану, а потом влево при достижении правого края экрана. Вот как реализуется это поведение:
settings.py
# Настройки пришельцев
self.alien_speed = 1.0
self.fleet_drop_speed = 10
# fleet_direction = 1 обозначает движение вправо; а -1 — влево.
self.fleet_direction = 1
Настройка fleet_drop_speed управляет величиной снижения флота при достижении им края. Эту скорость полезно отделить от горизонтальной скорости пришельцев, чтобы эти две скорости можно было изменять независимо.
Для настройки fleet_direction можно использовать строковое значение (например, 'left' или 'right'), но, скорее всего, в итоге придется воспользоваться набором операторов if-elif для проверки направления. Сейчас направлений всего два, поэтому мы используем значения 1 и –1 и будем переключаться между ними при каждом изменении направления флота. (Числа в данном случае особенно удобны, поскольку при движении вправо координата x каждого пришельца должна увеличиваться, а при перемещении влево — уменьшаться.)
Помимо вышеперечисленных, нам понадобится метод, позволяющий проверить, достиг ли пришелец одного из двух краев. Для этого необходимо внести в метод update() изменение, позволяющее каждому пришельцу двигаться в соответствующем направлении. Этот код является частью класса Alien:
alien.py
def check_edges(self):
"""Возвращает True, если пришелец находится у края экрана."""
screen_rect = self.screen.get_rect()
❶ return (self.rect.right >= screen_rect.right) or (self.rect.left <= 0)
def update(self):
"""Перемещает пришельца влево или вправо."""
❷ self.x += (self.settings.alien_speed * self.settings.fleet_direction)
self.rect.x = self.x
Вызов нового метода check_edges() для любого пришельца позволяет проверить, достиг ли он левого или правого края. У пришельца, находящегося у правого края, атрибут right его объекта rect больше или равен атрибуту right объекта rect экрана. У пришельца, находящегося у левого края, значение left меньше либо равно 0 ❶. Вместо того чтобы размещать это условие в блоке if, мы добавили его непосредственно в оператор return. В этом случае метод вернет True, если пришелец находится у правого или левого края, и False, если он не находится ни у одного из краев.
В метод update() будут внесены изменения, которые позволяют осуществлять перемещение влево и вправо. Для этого скорость пришельца умножается на значение fleet_direction ❷. Если значение fleet_direction равно 1, то значение alien_speed прибавляется к текущей позиции пришельца и он перемещается вправо; если же значение fleet_direction равно –1, то значение вычитается из позиции пришельца (который перемещается влево).
Когда пришелец доходит до края, весь флот должен опуститься вниз и изменить направление движения. Это означает, что в класс AlienInvasion придется внести изменения, поскольку именно здесь программа проверяет, достиг ли какой-либо пришелец левого или правого края. Для этого мы напишем функции _check_fleet_edges() и _change_fleet_direction(), а затем изменим _update_aliens(). Новые методы будут располагаться после _create_alien(), но я еще раз подчеркну, что конкретное размещение этих методов в классе несущественно.
alien_invasion.py
def _check_fleet_edges(self):
"""Реагирует на достижение пришельцем края экрана."""
❶ for alien in self.aliens.sprites():
if alien.check_edges():
❷ self.change_fleet_direction()
break
def _change_fleet_direction(self):
"""Опускает весь флот и меняет его направление."""
for alien in self.aliens.sprites():
❸ alien.rect.y += self.settings.fleet_drop_speed
self.settings.fleet_direction *= -1
Код _check_fleet_edges() перебирает флот и вызывает check_edges() для каждого пришельца ❶. Если check_edges() возвращает True, значит, пришелец находится у края и весь флот должен сменить направление, поэтому вызывается функция _change_fleet_direction() и происходит выход из цикла ❷. Данная функция перебирает пришельцев и уменьшает высоту каждого из них с помощью настройки fleet_drop_speed ❸; затем направление fleet_direction меняется на противоположное, для чего текущее значение умножается на –1. Строка, изменяющая направление, не является частью цикла for. Вертикальная позиция должна меняться для каждого пришельца, но направление всего флота должно измениться однократно.
Изменения в функции _update_aliens() выглядят так:
alien_invasion.py
def _update_aliens(self):
"""
Проверяет, достиг ли флот края экрана, с последующим обновлением
позиций всех пришельцев во флоте.
"""
self._check_fleet_edges()
self.aliens.update()
Перед обновлением позиции каждого пришельца будет вызываться метод _check_fleet_edges().
Если запустить игру сейчас, то флот будет двигаться влево-вправо между краями экрана и опускаться каждый раз, когда доберется до края. Теперь можно переходить к реализации уничтожения пришельцев и отслеживания тех, кто сталкивается с кораблем или достигает нижнего края экрана.
Упражнения
13.3. Капли. Найдите изображение дождевой капли и создайте сетку из капель. Капли должны постепенно опускаться вниз и исчезать у нижнего края экрана.
13.4. Дождь. Измените свой код в упражнении 13.3 так, чтобы при исчезновении ряда капель у нижнего края экрана новый ряд появлялся у верхнего края и начинал падение.
Итак, мы создали корабль и флот пришельцев. Но снаряды, достигая пришельцев, просто проходят насквозь, поскольку программа не проверяет коллизии. В игровом программировании коллизией (collision) называется перекрытие игровых элементов. Чтобы снаряды сбивали пришельцев, мы используем функцию sprite.groupcollide() для выявления коллизий между элементами двух групп.
Когда снаряд попадает в пришельца, программа должна немедленно узнать об этом, чтобы сбитый пришелец исчез с экрана. Для этого мы будем проверять коллизии сразу же после обновления позиции снаряда.
Метод sprite.groupcollide() сравнивает прямоугольник rect каждого элемента с прямоугольником rect каждого элемента другой группы. В данном случае он сравнивает прямоугольник каждого снаряда с прямоугольником каждого пришельца и возвращает словарь со снарядами и пришельцами, между которыми обнаружены коллизии. Каждый ключ в словаре представляет снаряд, а связанное с ним значение — пришельца, в которого попал снаряд. (Этот словарь будет использоваться в реализации системы подсчета очков в главе 14.)
Для проверки коллизий добавьте в конец функции update_bullets() следующий код:
alien_invasion.py
def _update_bullets(self):
"""Обновляет позиции снарядов и удаляет старые снаряды."""
--пропуск--
# Проверка попаданий в пришельцев.
# При обнаружении попадания удалить снаряд и пришельца.
collisions = pygame.sprite.groupcollide(
self.bullets, self.aliens, True, True)
Новый код перебирает сначала все снаряды в self.bullets, а затем всех пришельцев в self.aliens. Каждый раз, когда обнаруживается, что прямоугольник снаряда и пришелец пересекаются, groupcollide() добавляет пару «ключ — значение» в возвращаемый словарь. Два аргумента True сообщают Pygame, нужно ли удалять столкнувшиеся объекты: снаряд и пришельца. (Чтобы создать сверхмощный снаряд, который будет уничтожать всех пришельцев на своем пути, можно передать в первом аргументе False, а во втором True. Пришельцы, в которых попадает снаряд, будут исчезать, но все снаряды будут оставаться активными до верхнего края экрана.)
Если вы запустите «Инопланетное вторжение» сейчас, то пришельцы, в которых попадает снаряд, будут исчезать с экрана. На рис. 13.5 изображен частично уничтоженный флот.
Рис. 13.5. Снаряды уничтожают пришельцев!
Многие игровые возможности можно протестировать, просто запустив игру, но некоторые аспекты слишком утомительно проверять в обычной версии игры. Например, чтобы проверить, правильно ли обрабатывается уничтожение последнего пришельца, нам пришлось бы несколько раз сбивать всех пришельцев на экране.
Для тестирования конкретных аспектов игры можно изменить настройки так, чтобы упростить конкретную область. Например, можно уменьшить экран, чтобы на нем было меньше пришельцев, или увеличить скорость снаряда и количество снарядов, одновременно находящихся на экране.
Мое любимое изменение при тестировании игры «Инопланетное вторжение» — использование сверхшироких снарядов, которые остаются активными даже после попадания в пришельца (рис. 13.6). Попробуйте задать настройке bullet_width значение 300 (или даже 3000!) и посмотрите, сколько времени вам понадобится для уничтожения флота пришельцев!
Рис. 13.6. Сверхмощные снаряды упрощают тестирование некоторых аспектов игры
Такие изменения повышают эффективность тестирования, а заодно могут подсказать идеи для всевозможных игровых бонусов. (Только не забудьте восстановить нормальное состояние настроек после завершения тестирования.)
Одна из ключевых особенностей игры «Инопланетное вторжение» — бесконечные орды пришельцев: каждый раз, когда вы уничтожаете один флот, на его месте появляется другой.
Чтобы после уничтожения одного флота появлялся другой, сначала нужно убедиться в том, что группа aliens пуста. Если да, то вызывается метод _create_fleet(). Проверка будет выполняться в конце метода _update_bullets(), поскольку именно здесь уничтожаются отдельные пришельцы:
alien_invasion.py
def _update_bullets(self):
--пропуск--
❶ if not self.aliens:
# Уничтожение существующих снарядов и создание нового флота.
❷ self.bullets.empty()
self._create_fleet()
Программа проверяет, пуста ли группа aliens ❶. Пустая группа интерпретируется как False; это самый простой способ проверить группу на наличие элементов. Если группа пуста, то все существующие снаряды удаляются методом empty(), который убирает все существующие спрайты из группы ❷. Вызов метода _create_fleet() снова заполняет экран пришельцами.
Теперь сразу же после уничтожения текущего флота на экране появляется новый.
Попытавшись стрелять по пришельцам в текущем состоянии игры, можно заметить, что скорость движения снарядов неоптимальна — в вашей системе она может быть слишком высокой или низкой. На этой стадии можно изменить настройки, чтобы игра была более интересной и приятной. Имейте в виду, что она будет постепенно ускоряться, поэтому не делайте ее слишком быстрой в начале.
Скорость снарядов можно увеличить с помощью настройки bullet_speed в файле settings.py. Например, если задать в моей системе значение 2.5, то снаряды будут двигаться по экрану немного быстрее:
settings.py
# Настройки снарядов
self.bullet_speed = 2.5
self.bullet_width = 3
--пропуск--
Оптимальное значение этой настройки зависит от производительности вашей системы. Найдите значение, которое лучше подходит для вашей конкретной конфигурации.
Переработаем метод _update_bullets(), чтобы он не решал такое количество разных задач. Код обработки коллизий будет выделен в отдельный метод:
alien_invasion.py
def _update_bullets(self):
--пропуск--
# Уничтожение исчезнувших снарядов.
for bullet in self.bullets.copy():
if bullet.rect.bottom <= 0:
self.bullets.remove(bullet)
self._check_bullet_alien_collisions()
def _check_bullet_alien_collisions(self):
"""Обрабатывает коллизии снарядов с пришельцами."""
# Удаление снарядов и пришельцев, участвующих в коллизиях.
collisions = pygame.sprite.groupcollide(
self.bullets, self.aliens, True, True)
if not self.aliens:
# Уничтожение существующих снарядов и создание нового флота.
self.bullets.empty()
self._create_fleet()
Мы создали новый метод _check_bullet_alien_collisions() для выявления коллизий между снарядами и пришельцами и реагирования на уничтожение всего флота. Это сделано для того, чтобы сократить длину метода _update_bullets() и упростить дальнейшую разработку.
Упражнения
13.5. Боковая стрельба-2. После упражнения 12.6 «Боковая стрельба» программа была серьезно доработана. В этом упражнении вам предлагается усовершенствовать «Боковую стрельбу» до того же состояния, к которому была приведена игра «Инопланетное вторжение». Добавьте флот пришельцев и заставьте их перемещаться по горизонтали по направлению к кораблю. Другой вариант: напишите код, который размещает пришельцев в случайных позициях у правого края экрана, а затем заставляет их двигаться к кораблю. Кроме того, напишите код, который заставляет пришельцев исчезать при попаданиях.
Какое удовольствие от игры, в которой невозможно проиграть? Если игрок не успеет сбить флот достаточно быстро, то пришельцы уничтожат корабль при столкновении. При этом количество кораблей, используемых игроком, ограниченно, и корабль уничтожается, когда пришелец достигает нижнего края экрана. Игра завершается в тот момент, когда у игрока кончатся все корабли.
Начнем с проверки коллизий между пришельцами и кораблем, чтобы мы могли правильно обработать столкновения. Такие коллизии проверяются немедленно после обновления позиции каждого пришельца в классе AlienInvasion:
alien_invasion.py
def _update_aliens(self):
--пропуск--
self.aliens.update()
# Проверка коллизий "пришелец — корабль".
❶ if pygame.sprite.spritecollideany(self.ship, self.aliens):
❷ print("Ship hit!!!")
Функция spritecollideany() получает два аргумента: спрайт и группу. Функция пытается найти любой элемент группы, вступивший в коллизию со спрайтом, и останавливает цикл по группе сразу же после обнаружения столкнувшихся элементов. В данном случае он перебирает группу aliens и возвращает первого пришельца, столкнувшегося с кораблем ship.
Если ни одна коллизия не обнаружена, то spritecollideany() возвращает None и блок if не выполняется ❶. Если же будет обнаружен пришелец, столкнувшийся с кораблем, то метод возвращает этого пришельца и выполняется блок if: выводится сообщение Ship hit!!! ❷. При столкновении пришельца с кораблем необходимо выполнить ряд операций: удалить всех оставшихся пришельцев и снаряды, вернуть корабль в центр и создать новый флот. Прежде чем писать код всех этих операций, необходимо убедиться в том, что решение, позволяющее обнаружить коллизии между пришельцами и кораблем, работает правильно. Вызов функции print() всего лишь позволяет легко проверить правильность обнаружения коллизий.
Если вы запустите «Инопланетное вторжение», то при столкновении пришельца с кораблем в терминальном окне появляется сообщение Ship hit!!!. В ходе тестирования этого аспекта присвойте fleet_drop_speed более высокое значение (например, 50 или 100), чтобы пришельцы быстрее добирались до вашего корабля.
Теперь нужно разобраться, что же происходит при столкновении пришельца с кораблем. Вместо того чтобы уничтожать экземпляр ship и создавать новый, мы будем подсчитывать количество уничтоженных кораблей; для этого следует организовать сбор статистики по игре. (Статистика пригодится и для подсчета очков.)
Напишем новый класс GameStats для ведения статистики и сохраним его в файле game_stats.py:
game_stats.py
class GameStats:
"""Отслеживает статистику для игры "Инопланетное вторжение"."""
def __init__(self, ai_game):
"""Инициализирует статистику."""
self.settings = ai_game.settings
❶ self.reset_stats()
def reset_stats(self):
"""Инициализирует статистику, изменяющуюся в ходе игры."""
self.ships_left = self.settings.ship_limit
На все время работы игры «Инопланетное вторжение» будет создаваться один экземпляр GameStats, но часть статистики должна сбрасываться в начале каждой новой игры. Для этого бо́льшая часть статистики будет инициализироваться в методе reset_stats() вместо __init__(). Этот метод будет вызываться из __init__(), чтобы статистика правильно инициализировалась при первом создании экземпляра GameStats ❶, а метод reset_stats() будет вызываться в начале каждой новой игры.
Пока в игре используется всего один вид статистики — значение ships_left, изменяющееся в ходе игры. Количество кораблей в начале игры хранится в файле settings.py под именем ship_limit:
settings.py
# Настройки корабля
self.ship_speed = 1.5
self.ship_limit = 3
Кроме того, необходимо внести ряд изменений в файл alien_invasion.py, чтобы можно было создать экземпляр класса GameStats. Начнем с изменения операторов import в начале файла:
alien_invasion.py
import sys
from time import sleep
import pygame
from settings import Settings
from game_stats import GameStats
from ship import Ship
--пропуск--
Мы импортируем функцию sleep() из модуля time стандартной библиотеки Python, чтобы игру можно было на короткий момент приостановить в момент столкновения с кораблем. Кроме того, импортируем класс GameStats.
Экземпляр GameStats создается в методе __init__():
alien_invasion.py
def __init__(self):
--пропуск--
self.screen = pygame.display.set_mode(
(self.settings.screen_width, self.settings.screen_height))
pygame.display.set_caption("Alien Invasion")
# Создание экземпляра для хранения игровой статистики.
self.stats = GameStats(self)
self.ship = Ship(self)
--пропуск--
Экземпляр создается после создания игрового окна, но перед определением других игровых элементов (например, корабля).
Когда пришелец сталкивается с кораблем, программа уменьшает количество оставшихся кораблей на 1, уничтожает всех существующих пришельцев и снаряды, создает новый флот и возвращает корабль в середину экрана. Кроме того, игра ненадолго приостанавливается, чтобы игрок заметил столкновение и перестроился перед появлением нового флота.
Бо́льшая часть этого кода будет вынесена в новый метод _ship_hit(). Он вызывается из метода _update_aliens() при столкновении пришельца с кораблем:
alien_invasion.py
def _ship_hit(self):
"""Обрабатывает столкновение корабля с пришельцем."""
# Уменьшение ships_left.
❶ self.stats.ships_left -= 1
# Очистка групп aliens и bullets.
❷ self.aliens.empty()
self.bullets.empty()
# Создание нового флота и размещение корабля в центре.
❸ self._create_fleet()
self.ship.center_ship()
# Пауза.
❹ sleep(0.5)
Новый метод _ship_hit() управляет реакцией игры на столкновение корабля с пришельцем. Внутри _ship_hit() количество оставшихся кораблей уменьшается на 1 ❶, после чего происходит очистка групп aliens и bullets ❷.
Затем программа создает новый флот и выравнивает корабль по центру нижнего края ❸. (Вскоре мы добавим метод center_ship() в класс Ship.) Наконец, после внесения изменений во все игровые элементы, но до перерисовки изменений на экране делается короткая пауза, чтобы игрок увидел, что его корабль столкнулся с пришельцем ❹. Вызов функции sleep() приостанавливает программу на 0,5 секунды. После завершения паузы управление передается методу _update_screen(), который перерисовывает новый флот на экране.
Внутри метода _update_aliens() вызов функции print() заменяется вызовом _ship_hit() при столкновении пришельца с кораблем:
alien_invasion.py
def _update_aliens(self):
--пропуск--
if pygame.sprite.spritecollideany(self.ship, self.aliens):
self._ship_hit()
Новый метод center_ship() добавляется в конец файла ship.py:
ship.py
def center_ship(self):
"""Размещает корабль в центре нижней части экрана."""
self.rect.midbottom = self.screen_rect.midbottom
self.x = float(self.rect.x)
Выравнивание корабля по центру выполняется так же, как и в методе __init__(). После выравнивания сбрасывается атрибут self.x, чтобы в программе отслеживалась точная позиция корабля.
ПРИМЕЧАНИЕ
Обратите внимание: программа никогда не создает несколько кораблей. Один экземпляр ship используется на протяжении всей игры, а при столкновении с пришельцем он просто возвращается к центру экрана. О том, что у игрока не осталось ни одного корабля, программа узнает из атрибута ships_left.
Запустите игру, подстрелите нескольких пришельцев, а затем позвольте одному из них столкнуться с кораблем. Происходит небольшая пауза, на экране появляется новый флот вторжения, а корабль возвращается в центр нижней части экрана.
Если пришелец добирается до нижнего края экрана, то программа будет реагировать так же, как при столкновении с кораблем. Добавьте для проверки этого условия новый метод в файл alien_invasion.py:
alien_invasion.py
def _check_aliens_bottom(self):
"""Проверяет, добрались ли пришельцы до нижнего края экрана."""
for alien in self.aliens.sprites():
❶ if alien.rect.bottom >= self.settings.screen_height:
# Происходит то же, что при столкновении с кораблем.
self._ship_hit()
break
Метод check_aliens_bottom() проверяет, есть ли хотя бы один пришелец, добравшийся до нижнего края экрана. Условие выполняется, когда атрибут rect.bottom пришельца больше атрибута rect.bottom экрана или равен ему ❶. Если пришелец добрался до низа, то вызывается функция _ship_hit(). Если хотя бы один пришелец столкнулся с нижним краем, то проверять остальных уже не нужно, поэтому после вызова _ship_hit() цикл прерывается.
Этот метод вызывается из метода _update_aliens():
alien_invasion.py
def _update_aliens(self):
--пропуск--
# Проверка коллизий "пришелец — корабль".
if pygame.sprite.spritecollideany(self.ship, self.aliens):
self._ship_hit()
# Проверить, сталкиваются ли пришельцы с нижним краем экрана.
self._check_aliens_bottom()
Метод _check_aliens_bottom() вызывается после обновления позиций всех пришельцев и после проверки коллизий «пришелец — корабль». Теперь новый флот будет появляться как при столкновении корабля с пришельцем, так и в том случае, если кто-то из пришельцев смог добраться до нижнего края экрана.
Игра «Инопланетное вторжение» кажется завершенной, но длится бесконечно. Значение ships_left просто продолжает уходить в отрицательную бесконечность. Добавим новый атрибут — флаг game_active, который завершает игру после потери последнего корабля. Этот флаг устанавливается в конце метода __init__() в классе AlienInvasion:
alien_invasion.py
def __init__(self, ai_game):
--пропуск--
# Игра "Инопланетное вторжение" запускается в активном состоянии.
self.game_active = True
Добавим в ship_hit() код, который сбрасывает флаг game_active в состояние False при потере игроком последнего корабля:
alien_invasion.py
def _ship_hit(self):
"""Обрабатывает столкновение корабля с пришельцем."""
if stats.ships_left > 0:
# Уменьшение ships_left.
self.stats.ships_left -= 1
--пропуск--
# Пауза.
sleep(0.5)
else:
self.game_active = False
Бо́льшая часть кода _ship_hit() осталась неизменной. Весь существующий код был перемещен в блок if, который проверяет, остался ли у игрока хотя бы один корабль. Если корабли не кончились, то программа создает новый флот, делает паузу и продолжает игру. Если же игрок потерял последний корабль, то флаг game_active переводится в состояние False.
В файле alien_invasion.py необходимо определить части игры, которые должны выполняться всегда, и те части, которые будут выполняться только при активной игре:
alien_invasion.py
def run_game(self):
"""Запускает основной цикл игры."""
while True:
self._check_events()
if self.game_active:
self.ship.update()
self._update_bullets()
self._update_aliens()
self._update_screen()
self.clock.tick(60)
В основном цикле всегда должна вызываться функция _check_events(), даже если игра находится в неактивном состоянии. Например, программа все равно должна узнать о том, что пользователь нажал клавишу Q для завершения игры или щелкнул на кнопке закрытия окна. Кроме того, экран должен обновляться в то время, пока игрок решает, хочет ли он начать новую игру. Остальные вызовы функций должны происходить только при активной игре, поскольку в то время, когда игра неактивна, обновлять позиции игровых элементов не нужно.
В обновленной версии игра должна останавливаться после потери игроком последнего корабля.
Упражнения
13.6. Конец игры. В упражнении «Боковая стрельба» отслеживайте, сколько пришельцев сбил игрок и сколько раз пришельцы столкнулись с кораблем. Определите разумное условие завершения игры и останавливайте ее при возникновении этой ситуации.
В этой главе вы научились добавлять в игру большое количество одинаковых элементов на примере флота пришельцев. Вы узнали, как использовать вложенные циклы для создания сетки с элементами, а также привели игровые элементы в движение, вызывая метод update() каждого элемента. Вы научились управлять перемещением объектов на экране и обрабатывать различные события (например, достижение края экрана). Кроме того, вы узнали, как обнаруживать коллизии и реагировать на них (на примере попаданий снарядов в пришельцев и столкновений пришельцев с кораблем). В завершение главы вы изучили, как вести игровую статистику и использовать флаг game_active для проверки окончания игры.
В последней главе этого проекта будет добавлена кнопка Play, чтобы игрок мог самостоятельно запустить свою первую игру, а также повторить игру после ее завершения. После каждого уничтожения вражеского флота скорость игры будет возрастать, а мы реализуем систему подсчета очков. В результате вы получите полностью рабочую игру!