В этой главе в игру Alien Invasion будут добавлены пришельцы. Сначала мы добавим одного пришельца у верхнего края экрана, а потом сгенерируем целый флот. Пришельцы будут перемещаться в сторону и вниз; при этом пришельцы, в которых попадают пули, исчезают с экрана. Наконец, мы ограничим количество кораблей у игрока, так что при гибели последнего корабля игра завершается.
В этой главе вы узнаете больше о Pygame и о ведении крупного проекта. Вы также научитесь обнаруживать коллизии (столкновения) игровых объектов, например пуль и пришельцев. Обнаружение коллизий помогает определять взаимодействия между элементами игры: например, ограничить перемещение персонажа областью между стенами лабиринта или организовать передачу мяча между двумя персонажами. Работа будет продолжаться на основе плана, к которому мы будем время от времени возвращаться, чтобы не отклоняться от цели во время написания кода.
Итак, прежде чем браться за новый код для добавления флота пришельцев на экран, рассмотрим проект и обновим план.
Приступая к новой фазе разработки крупного проекта, всегда полезно вернуться к исходному плану и уточнить, чего же вы хотите добиться в том коде, который собираетесь написать. В этой главе мы:
• Проанализируем код и определим, нужно ли провести рефакторинг перед реализацией новых возможностей.
• Добавим в левом верхнем углу экрана одного пришельца, отделив его от краев экрана интервалами.
• По величине интервалов вокруг первого пришельца и общим размерам экрана вычислим, сколько пришельцев поместится на экране. Для создания пришельцев, заполняющих верхнюю часть экрана, будет написан цикл.
• Организуем перемещение флота пришельцев в сторону и вниз, пока весь флот не будет уничтожен, пока пришелец не столкнется с кораблем игрока или пока пришелец не достигнет земли. Если весь флот будет уничтожен, программа создает новый флот. Если пришелец сталкивается с кораблем или с землей, программа уничтожает корабль и создает новый флот.
• Ограничим количество кораблей, которые могут использоваться игроком, и завершим игру в конце последней попытки.
Этот план будет уточняться по мере реализации новых возможностей, но для начала и этого достаточно.
Также проводите анализ кода, когда вы начинаете работу над новой серией возможностей проекта. Так как с каждой новой фазой проект обычно становится более сложным, лучше всего заняться расчисткой излишне громоздкого или неэффективного кода. И хотя сейчас особой расчистки не потребуется, потому что мы уже проводили промежуточный рефакторинг, необходимость использовать мышь для закрытия игры каждый раз, когда потребуется протестировать новую функцию, раздражает. Добавим возможность быстрого завершения игры при нажатии клавиши Q:
game_functions.py
def check_keydown_events(event, ai_settings, screen, ship, bullets):
...
. .elif event.key == pygame.K_q:
. . . .sys.exit()
В check_keydown_events() добавляется новый блок, который завершает игру при нажатии клавиши Q. Это довольно безопасное изменение, потому что клавиша Q находится достаточно далеко от клавиш со стрелками и пробела, так что вероятность случайного нажатия Q и завершения игры невелика. Теперь при тестировании игру можно закрыть клавишей Q, не прибегая к использованию мыши.
Размещение одного пришельца на экране мало чем отличается от размещения корабля. Поведением каждого пришельца будет управлять класс с именем Alien, который по своей структуре очень похож на класс Ship. Для простоты мы снова воспользуемся готовыми графическими изображениями. Вы можете найти
Рис. 13.1. Пришелец, который будет использоваться для создания флота
собственное изображение пришельца или использовать изображение на рис. 13.1, доступное в ресурсах книги по адресу https://www.nostarch.com/pythoncrashcourse/. Это изображение имеет серый фон, совпадающий с цветом фона экрана. Не забудьте сохранить выбранный файл в каталоге images.
Теперь можно написать класс Alien:
alien.py
import pygame
from pygame.sprite import Sprite
class Alien(Sprite):
. ."""Класс, представляющий одного пришельца."""
. .def __init__(self, ai_settings, screen):
. . . ."""Инициализирует пришельца и задает его начальную позицию."""
. . . .super(Alien, self).__init__()
. . . .self.screen = screen
. . . .self.ai_settings = ai_settings
. . . .# Загрузка изображения пришельца и назначение атрибута rect.
. . . .self.image = pygame.image.load('images/alien.bmp')
. . . .self.rect = self.image.get_rect()
. . . .# Каждый новый пришелец появляется в левом верхнем углу экрана.
(1) . . . .self.rect.x = self.rect.width
. . . .self.rect.y = self.rect.height
. . . .# Сохранение точной позиции пришельца.
. . . .self.x = float(self.rect.x)
. .def blitme(self):
. . . ."""Выводит пришельца в текущем положении."""
. . . .self.screen.blit(self.image, self.rect)
В основном этот класс похож на класс Ship (если не считать размещения пришельца). Изначально каждый пришелец размещается в левом верхнем углу экрана, при этом слева от него добавляется интервал, равный ширине пришельца, а над ним — интервал, равный высоте (1) .
Создадим экземпляр Alien в alien_invasion.py:
alien_invasion.py
...
from ship import Ship
from alien import Alien
import game_functions as gf
def run_game():
...
. .# Создание пришельца.
. .alien = Alien(ai_settings, screen)
# Запуск основного цикла игры.
while True:
gf.check_events(ai_settings, screen, ship, bullets)
ship.update()
gf.update_bullets(bullets)
. . . .gf.update_screen(ai_settings, screen, ship, alien, bullets)
run_game()
Программа импортирует новый класс Alien и создает экземпляр Alien непосредственно перед входом в основной цикл while. Так как позиция пришельца еще не успела измениться, ничего нового в цикле не добавляется; изменения вносятся только в вызов update_screen(), которому передается экземпляр alien.
Рис. 13.2. Появился первый пришелец
Чтобы пришелец появился на экране, программа вызывает его метод blitme() в update_screen():
game_functions.py
def update_screen(ai_settings, screen, ship, alien, bullets):
...
# Все пули выводятся позади изображений корабля и пришельцев.
for bullet in bullets:
bullet.draw_bullet()
ship.blitme()
. .alien.blitme()
. . . . . .
# Отображение последнего прорисованного экрана.
pygame.display.flip()
Пришелец выводится после прорисовки корабля и пуль, так что пришельцы будут находиться на верхнем «слое» экрана. На рис. 13.2 изображен первый пришелец.
После того как первый пришелец появится на экране, мы напишем код для вывода всего флота.
Чтобы нарисовать флот пришельцев, необходимо вычислить, сколько пришельцев поместится в одном ряду и сколько рядов поместится по высоте. Сначала мы вычислим горизонтальные интервалы между пришельцами и создадим ряд; затем будет вычислен вертикальный интервал и создан весь флот.
Чтобы определить, сколько пришельцев помещается в одном ряду, сначала вычислим доступное горизонтальное пространство. Ширина экрана хранится в ai_settings.screen_width, но с обеих сторон экрана необходимо зарезервировать пустые интервалы. Определим их равными ширине одного пришельца. Так как ширина уменьшается на величину двух интервалов, доступное пространство равно ширине экрана за вычетом удвоенной ширины пришельца:
available_space_x = ai_settings.screen_width — (2 * alien_width)
Также необходимо зарезервировать интервалы между пришельцами; они будут составлять одну ширину пришельца. Пространство, необходимое для вывода одного пришельца, равно его удвоенной ширине: одна ширина для самого пришельца и еще одна для пустого интервала справа. Чтобы определить количество пришельцев на экране, разделим доступное пространство на удвоенную ширину пришельца:
number_aliens_x = available_space_x / (2 * alien_width)
Эти вычисления будут включены в программу при создании флота.
примечание
У вычислений в программировании есть одна замечательная особенность: не обязательно быть полностью уверенным в правильности формулы, когда вы ее пишете. Вы можете опробовать формулу на практике и посмотреть, что из этого получится. В худшем случае получится экран, до отказа забитый пришельцами, — или наоборот, пустой. В этом случае вы пересмотрите формулу на основании полученных результатов.
Чтобы создать один ряд пришельцев, сначала создадим в alien_invasion.py пустую группу с именем aliens для хранения всех пришельцев, а затем вызовем функцию в game_functions.py для создания флота:
alien_invasion.py
import pygame
from pygame.sprite import Group
from settings import Settings
from ship import Ship
import game_functions as gf
def run_game():
...
. .# Создание корабля, группы пуль и группы пришельцев.
ship = Ship(ai_settings, screen)
bullets = Group()
(1) . .aliens = Group()
. .
. .# Создание флота пришельцев.
(2) . .gf.create_fleet(ai_settings, screen, aliens)
# Запуск основного цикла игры.
while True:
...
(3) . . . .gf.update_screen(ai_settings, screen, ship, aliens, bullets)
run_game()
Так как пришельцы уже не создаются напрямую в alien_invasion.py, импортировать класс Alien в этот файл не обязательно.
Создайте пустую группу для хранения всех пришельцев в игре (1) . Затем создайте новую функцию create_fleet() (2), которую мы вскоре вызовем, и передайте ей ai_settings, объект screen и пустую группу aliens. Затем измените вызов update_screen(), чтобы предоставить функции доступ к группе пришельцев (3).
Также необходимо внести изменения в update_screen():
game_functions.py
def update_screen(ai_settings, screen, ship, aliens, bullets):
...
ship.blitme()
. .aliens.draw(screen)
. . . . . .
# Отображение последнего прорисованного экрана.
pygame.display.flip()
Когда вы вызываете метод draw() для группы, Pygame автоматически выводит каждый элемент группы в позиции, определяемой его атрибутом rect. В данном случае вызов aliens.draw(screen) рисует каждого пришельца в группе на экране.
Теперь можно перейти к созданию флота. Ниже приведена новая функция create_fleet(), которую мы поместим в конец game_functions.py. Также необходимо импортировать класс Alien, не забудьте добавить команду import в начало файла:
game_functions.py
...
from bullet import Bullet
from alien import Alien
...
def create_fleet(ai_settings, screen, aliens):
. ."""Создает флот пришельцев."""
. .# Создание пришельца и вычисление количества пришельцев в ряду.
. .# Интервал между соседними пришельцами равен одной ширине пришельца.
(1) . .alien = Alien(ai_settings, screen)
(2) . .alien_width = alien.rect.width
(3) . .available_space_x = ai_settings.screen_width - 2 * alien_width
(4) . .number_aliens_x = int(available_space_x / (2 * alien_width))
. .
. .# Создание первого ряда пришельцев.
(5) . .for alien_number in range(number_aliens_x):
. . . .# Создание пришельца и размещение его в ряду.
? . . . .alien = Alien(ai_settings, screen)
. . . .alien.x = alien_width + 2 * alien_width * alien_number
. . . .alien.rect.x = alien.x
. . . .aliens.add(alien)
Бульшая часть этого кода уже была описана ранее. Для размещения пришельцев необходимо знать ширину и высоту одного пришельца, и мы создаем его в точке (1) перед выполнением вычислений. Этот пришелец не войдет во флот, поэтому он не включается в группу aliens. В точке (2) ширина пришельца определяется по его атрибуту rect, а полученное значение сохраняется в alien_width, чтобы избежать лишних обращений к атрибуту rect. В точке (3) вычисляется горизонтальное пространство и количество пришельцев, которые в нем поместятся.
По сравнению с исходными формулами всего одно изменение: мы используем int(), чтобы вычисленное количество пришельцев (4) было целым, — во-первых, неясно, что делать с неполным пришельцем, а во-вторых, функция range() должна получать целое число. Функция int() отсекает дробную часть числа, фактически выполняя округление в меньшую сторону. (И это правильно: лучше оставить лишнее свободное место в каждом ряду, чем забивать ряды до отказа.)
Затем создается цикл от 0 до количества создаваемых пришельцев (5). В теле цикла создается новый пришелец, после чего задается его координата x для размещения его в ряду ?. Каждый пришелец сдвигается вправо на одну ширину от левого поля. Затем ширина пришельца умножается на 2, чтобы учесть полное пространство, выделенное для одного пришельца, включая пустой интервал справа, а полученная величина умножается на позицию пришельца в ряду. Затем новый пришелец добавляется в группу aliens.
Рис. 13.3. Первый ряд пришельцев
Запустив программу Alien Invasion, вы увидите, что на экране появился первый ряд пришельцев (рис. 13.3).
Первый ряд сдвинут влево, и это хорошо, потому что флот пришельцев должен двигаться вправо, пока не дойдет до края экрана, затем немного опуститься вниз, затем двигаться влево и т.д. Как и в классической игре Space Invaders, такое перемещение интереснее, чем постепенное снижение по прямой. Движение будет продолжаться до тех пор, пока все пришельцы не будут сбиты или пока пришелец не столкнется с кораблем или нижним краем экрана.
примечание
В зависимости от выбранной ширины экрана расположение первого ряда пришельцев в вашей системе может выглядеть немного иначе.
Если бы создание флота на этом было завершено, то функцию create_fleet(), пожалуй, можно было бы оставить в таком виде, но работа еще не закончена, поэтому мы немного подчистим код функции. Ниже приведена версия create_fleet() с двумя новыми функциями: get_number_aliens_x() и create_alien():
game_functions.py
(1) def get_number_aliens_x(ai_settings, alien_width):
. ."""Вычисляет количество пришельцев в ряду."""
available_space_x = ai_settings.screen_width - 2 * alien_width
number_aliens_x = int(available_space_x / (2 * alien_width))
. .return number_aliens_x
def create_alien(ai_settings, screen, aliens, alien_number):
. ."""Создает пришельца и размещает его в ряду."""
alien = Alien(ai_settings, screen)
(2) . .alien_width = alien.rect.width
alien.x = alien_width + 2 * alien_width * alien_number
alien.rect.x = alien.x
aliens.add(alien)
. .
def create_fleet(ai_settings, screen, aliens):
"""Создает флот пришельцев."""
# Создание пришельца и вычисление количества пришельцев в ряду.
alien = Alien(ai_settings, screen)
(3) . .number_aliens_x = get_number_aliens_x(ai_settings, alien.rect.width)
. .
# Создание первого ряда пришельцев.
for alien_number in range(number_aliens_x):
(4) . . . .create_alien(ai_settings, screen, aliens, alien_number)
Код get_number_aliens_x() нисколько не изменился по сравнению с create_fleet() (1) . Код create_alien() почти не изменился, разве что для определения ширины используется только что созданный пришелец (2). В точке (3) код вычисления горизонтальных интервалов заменяется вызовом get_number_aliens_x(), а строка с alien_width удалена, потому что теперь задача решается в create_alien(). В точке (4) вызывается функция create_alien(). Рефакторинг упрощает добавление новых строк и создание всего флота.
Чтобы завершить построение флота, определите количество рядов на экране и повторите цикл (создания пришельцев одного ряда) полученное количество раз. Чтобы определить количество рядов, мы вычисляем доступное вертикальное пространство, вычитая высоту пришельца (сверху), высоту корабля (снизу) и удвоенную высоту пришельца (снизу):
available_space_y = ai_settings.screen_height — 3 * alien_height — ship_height
В результате вокруг корабля образуется пустое пространство, чтобы у игрока было время начать стрельбу по пришельцам в начале каждого уровня. Под каждым рядом должно быть пустое место, равное высоте пришельца. Чтобы вычислить количество строк, мы делим свободное пространство на удвоенную высоту пришельца (как и прежде, если формула содержит ошибку, мы это немедленно увидим и внесем изменения, пока не получим нужные интервалы):
number_rows = available_height_y / (2 * alien_height)
Зная количество рядов во флоте, мы можем повторить код создания ряда:
game_functions.py
(1) def get_number_rows(ai_settings, ship_height, alien_height):
. ."""Определяет количество рядов, помещающихся на экране."""
(2) . .available_space_y = (ai_settings.screen_height -
. . . . . . . . . . . . . .(3 * alien_height) - ship_height)
. .number_rows = int(available_space_y / (2 * alien_height))
. .return number_rows
. .
def create_alien(ai_settings, screen, aliens, alien_number, row_number):
...
alien.x = alien_width + 2 * alien_width * alien_number
alien.rect.x = alien.x
(3) . .alien.rect.y = alien.rect.height + 2 * alien.rect.height * row_number
. .aliens.add(alien)
def create_fleet(ai_settings, screen, ship, aliens):
...
number_aliens_x = get_number_aliens_x(ai_settings, alien.rect.width)
. .number_rows = get_number_rows(ai_settings, ship.rect.height,
. . . .alien.rect.height)
. .
. .# Создание флота пришельцев.
(4) . .for row_number in range(number_rows):
for alien_number in range(number_aliens_x):
. . . . . .create_alien(ai_settings, screen, aliens, alien_number,
. . . . . . . .row_number)
Чтобы вычислить количество рядов, помещающихся на экране, мы включаем вычисления available_space_y и number_rows в функцию get_number_rows() (1) , похожую на get_number_aliens_x(). Вычисления заключаются в круглые скобки, чтобы их можно было разбить на две строки длиной 79 символов и менее, как указано в рекомендациях (2). Функция int() используется для того, чтобы предотвратить создание неполного ряда пришельцев.
Чтобы создать несколько строк, мы используем два вложенных цикла: внешний и внутренний (3). Внутренний цикл создает один ряд пришельцев. Внешний цикл считает от 0 до количества рядов; Python использует код создания одного ряда и повторяет его number_rows раз.
Чтобы создать вложенный цикл, напишите новый цикл for и снабдите повторяемый код отступом. (В большинстве текстовых редакторов операции создания и удаления блоков кода выполняются просто, но, если вам понадобится помощь, обращайтесь к приложению Б.) Затем при вызове create_alien() передается аргумент с номером ряда, чтобы каждый ряд находился на экране ниже предыдущих.
Определению create_alien() необходим параметр с номером ряда. В create_alien() мы изменяем координату y пришельца(3). Сначала прибавляется одна высота пришельца, чтобы создать пустое место у верхнего края экрана. Каждый новый ряд начинается на две высоты пришельца ниже последнего ряда, поэтому мы умножаем высоту пришельца на 2, а затем на номер ряда. Номер первого ряда равен 0, так что вертикальное расположение первого ряда остается неизменным. Все последующие ряды размещаются ниже на экране.
Определение create_fleet() также содержит новый параметр для объекта ship; следовательно, в вызов create_fleet() в alien_invasion.py необходимо добавить аргумент ship:
Рис. 13.4. На экране появился весь флот пришельцев
alien_invasion.py
# Создание флота пришельцев.
gf.create_fleet(ai_settings, screen, ship, aliens)
Если теперь запустить игру, вы увидите целый флот пришельцев (рис. 13.4).
В следующем разделе мы приведем флот в движение.
Упражнения
13-1. Звезды: найдите изображение звезды. Создайте на экране сетку из звезд.
13-2. Звезды-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_factor = 1
Настройка используется в реализации update():
alien.py
def update(self):
. ."""Перемещает пришельца вправо."""
(1) . . . .self.x += self.ai_settings.alien_speed_factor
(2) . . . .self.rect.x = self.x
При каждом обновлении позиции пришельца мы смещаем его вправо на величину, хранящуюся в alien_speed_factor. Точная позиция пришельца хранится в атрибуте self.x, который может принимать вещественные значения (1) . Затем значение self.x используется для обновления позиции прямоугольника пришельца (2).
В основном цикле while уже содержатся вызовы обновления корабля и пуль. Теперь необходимо также обновить позицию каждого пришельца:
alien_invasion.py
# Запуск основного цикла игры.
while True:
gf.check_events(ai_settings, screen, ship, bullets)
ship.update()
gf.update_bullets(bullets)
. .gf.update_aliens(aliens)
gf.update_screen(ai_settings, screen, ship, aliens, bullets)
Позиции пришельцев обновляются после обновления пуль, потому что скоро мы будем проверять, попали ли какие-либо пули в пришельцев.
Наконец, добавьте новую функцию update_aliens() в конец файла game_functions.py:
game_functions.py
def update_aliens(aliens):
. ."""Обновляет позиции всех пришельцев во флоте."""
. .aliens.update()
Мы используем метод update() для группы aliens, что приводит к автоматическому вызову метода update() каждого пришельца. Если запустить Alien Invasion сейчас, вы увидите, как флот двигается вправо и исчезает за краем экрана.
Теперь мы создадим настройки, которые перемещают флот вниз по экрану, а потом влево при достижении правого края экрана. Вот как реализуется это поведение:
settings.py
# Настройки пришельцев
self.alien_speed_factor = 1
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.py
. .def check_edges(self):
. . . ."""Возвращает True, если пришелец находится у края экрана."""
. . . .screen_rect = self.screen.get_rect()
(1) . . . .if self.rect.right >= screen_rect.right:
. . . . . .return True
(2) . . . .elif self.rect.left <= 0:
. . . . . .return True
. . . . . .
def update(self):
. . . ."""Перемещает пришельца влево или вправо."""
(3) . . . .self.x += (self.ai_settings.alien_speed_factor *
. . . . . . . . . . . .self.ai_settings.fleet_direction)
self.rect.x = self.x
Вызов нового метода check_edges() для любого пришельца позволяет проверить, достиг ли он левого или правого края. У пришельца, находящегося у правого края, атрибут right его атрибута rect больше или равен атрибуту right атрибута rect экрана (1) . У пришельца, находящегося у левого края, значение left меньше либо равно 0 (2).
В метод update() будут внесены изменения, обеспечивающие перемещение влево и вправо (3); для этого скорость пришельца умножается на значение fleet_direction. Если значение fleet_direction равно 1, то значение alien_speed_factor прибавляется к текущей позиции пришельца; если же значение fleet_direction равно –1, то значение вычитается из позиции пришельца (который перемещается влево).
Когда пришелец доходит до края, весь флот должен опуститься вниз и изменить направление движения. Это означает, что в game_functions.py необходимо внести серьезные изменения, потому что именно здесь программа проверяет, достиг ли какой-либо пришелец левого или правого края. Для этого мы напишем функции check_fleet_edges() и change_fleet_direction(), а затем изменим update_aliens():
game_functions.py
def check_fleet_edges(ai_settings, aliens):
. ."""Реагирует на достижение пришельцем края экрана."""
(1) . .for alien in aliens.sprites():
. . . .if alien.check_edges():
. . . . . .change_fleet_direction(ai_settings, aliens)
. . . . . .break
def change_fleet_direction(ai_settings, aliens):
. ."""Опускает весь флот и меняет направление флота."""
. .for alien in aliens.sprites():
(2) . . . .alien.rect.y += ai_settings.fleet_drop_speed
. .ai_settings.fleet_direction *= -1
. . . . . . . .
def update_aliens(ai_settings, aliens):
. ."""
. .Проверяет, достиг ли флот края экрана,
. . после чего обновляет позиции всех пришельцев во флоте.
. ."""
(3) . .check_fleet_edges(ai_settings, aliens)
aliens.update()
Функция check_fleet_edges() перебирает флот и вызывает check_edges() для каждого пришельца (1) . Если check_edges() возвращает True, значит, пришелец находится у края и весь флот должен сменить направление, поэтому вызывается функция change_fleet_direction() и происходит выход из цикла. Функция change_fleet_direction() перебирает пришельцев и уменьшает высоту каждого из них с использованием настройки fleet_drop_speed (2); затем направление fleet_direction меняется на противоположное, для чего текущее значение умножается на –1.
Мы изменили функцию update_aliens() и включили в нее проверку нахождения пришельцев у края вызовом check_fleet_edges() (3). Функция должна получать параметр ai_settings, поэтому аргумент ai_settings включается в вызов update_aliens():
alien_invasion.py
# Запуск основного цикла игры.
while True:
gf.check_events(ai_settings, screen, ship, bullets)
ship.update()
gf.update_bullets(bullets)
. . gf.update_aliens(ai_settings, aliens)
gf.update_screen(ai_settings, screen, ship, aliens, bullets)
Если запустить игру сейчас, флот будет двигаться влево-вправо между краями экрана и опускаться каждый раз, когда он доберется до края. Теперь можно переходить к реализации уничтожения и отслеживания пришельцев, сталкивающихся с кораблем или достигающих нижнего края экрана.
Упражнения
13-3. Капли: найдите изображение дождевой капли и создайте сетку из капель. Капли должны постепенно опускаться вниз и исчезать у нижнего края экрана.
13-4. Дождь: измените свой код в упражнении 13-3, чтобы при исчезновении ряда капель у нижнего края экрана новый ряд появлялся у верхнего края и начинал падение.
Итак, мы создали корабль и флот пришельцев — но, когда пули достигают пришельцев, они просто проходят насквозь, потому что программа не проверяет коллизии. В игровом программировании коллизией называется перекрытие игровых элементов. Чтобы пули сбивали пришельцев, метод sprite.groupcollide() используется для выявления коллизий между элементами двух групп.
Когда пуля попадает в пришельца, программа должна немедленно узнать об этом, чтобы сбитый пришелец исчез с экрана. Для этого мы будем проверять коллизии сразу же после обновления позиции пули.
Метод sprite.groupcollide() сравнивает прямоугольник rect каждой пули с прямоугольником rect каждого пришельца и возвращает словарь с пулями и пришельцами, между которыми обнаружены коллизии. Каждый ключ в словаре представляет пулю, а ассоциированное с ним значение — пришельца, в которого попала пуля. (Этот словарь будет использоваться в реализации системы подсчета очков счета в главе 14.)
Для проверки коллизий в функции update_bullets() используется следующий код:
game_functions.py
def update_bullets(aliens, bullets):
"""Обновляет позиции пуль и удаляет старые пули."""
...
. .# Проверка попаданий в пришельцев.
. .# При обнаружении попадания удалить пулю и пришельца.
. .collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
Новая строка сначала перебирает все пули в группе bullets, а затем перебирает всех пришельцев в группе aliens. Каждый раз, когда между прямоугольником пули и пришельца обнаруживается перекрытие, groupcollide() добавляет пару «ключ—значение» в возвращаемый словарь. Два аргумента True сообщают Pygame, нужно ли удалять столкнувшиеся объекты: пулю и пришельца. (Чтобы создать сверхмощную пулю, которая будет уничтожать всех пришельцев на своем пути, можно передать в первом аргументе False, а во втором True. Пришельцы, в которых попадает пуля, будут исчезать, но все пули будут оставаться активными до верхнего края экрана.)
При вызове update_bullets() передается аргумент aliens:
alien_invasion.py
# Запуск основного цикла игры.
while True:
gf.check_events(ai_settings, screen, ship, bullets)
ship.update()
. . gf.update_bullets(aliens, bullets)
gf.update_aliens(ai_settings, aliens)
gf.update_screen(ai_settings, screen, ship, aliens, bullets)
Рис. 13.5. Пули уничтожают пришельцев!
Если запустить Alien Invasion сейчас, пришельцы, в которых попадает пуля, будут исчезать с экрана. На рис. 13.5 изображен частично уничтоженный флот.
Многие игровые возможности тестируются простым запуском игры, но некоторые аспекты слишком утомительно тестировать в обычной версии игры. Например, чтобы проверить, правильно ли обрабатывается уничтожение последнего пришельца, нам пришлось бы несколько раз сбивать всех пришельцев на экране.
Для тестирования конкретных аспектов игры можно изменить игровые настройки так, чтобы упростить конкретную область. Например, можно уменьшить экран, чтобы на нем было меньше пришельцев, или увеличить скорость пули и количество пуль, одновременно находящихся на экране.
Мое любимое изменение при тестировании Alien Invasion — использование сверхшироких пуль, которые остаются активными даже после попадания в пришельца (рис. 13.6). Попробуйте задать настройке bullet_width значение 300 и посмотрите, сколько времени вам понадобится для уничтожения флота пришельцев!
Такие изменения повышают эффективность тестирования, а заодно могут подсказать идеи для всевозможных игровых бонусов. (Только не забудьте восстановить нормальное состояние настроек после завершения тестирования.)
Рис. 13.6. Сверхмощные пули упрощают тестирование некоторых аспектов игры
Одна из ключевых особенностей Alien Invasion — бесконечные орды пришельцев: каждый раз, когда вы уничтожаете один флот, на его месте появляется другой.
Чтобы после уничтожения одного флота появлялся другой, сначала нужно убедиться в том, что группа aliens пуста. Если она пуста, вызывается функция create_fleet(). Проверка будет выполняться в функции update_bullets(), потому что именно здесь уничтожаются отдельные пришельцы:
game_functions.py
def update_bullets(ai_settings, screen, ship, aliens, bullets):
...
# Проверка попаданий в пришельцев.
# При обнаружении попадания удалить пулю и пришельца.
collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
. .
(1) . .if len(aliens) == 0:
. . . .# Уничтожение существующих пуль и создание нового флота.
(2) . . . .bullets.empty()
. . . .create_fleet(ai_settings, screen, ship, aliens)
В точке (1) программа проверяет, пуста ли группа aliens. Если она пуста, то все существующие пули удаляются методом empty(), который удаляет все существующие спрайты из группы (2). Вызов метода create_fleet() снова заполняет экран пришельцами.
В определении update_bullets() теперь появились дополнительные параметры ai_settings, screen и ship, поэтому вызов update_bullets() в alien_invasion.py необходимо обновить:
alien_invasion.py
# Запуск основного цикла игры.
while True:
gf.check_events(ai_settings, screen, ship, bullets)
ship.update()
. . gf.update_bullets(ai_settings, screen, ship, aliens, bullets)
gf.update_aliens(ai_settings, aliens)
gf.update_screen(ai_settings, screen, ship, aliens, bullets)
Новый флот появляется сразу же после уничтожения текущего флота.
Попытавшись стрелять по пришельцам в текущем состоянии игры, вы заметите, что движение пуль немного замедлилось. Дело в том, что Pygame теперь выполняет больший объем работы при каждом проходе цикла. Скорость пуль можно увеличить настройкой bullet_speed_factor в settings.py. Если увеличить это значение (например, до 3), пули снова будут двигаться по экрану с разумной скоростью:
settings.py
# Настройки пуль
. .self.bullet_speed_factor = 3
self.bul
...
Оптимальное значение этой настройки зависит от производительности вашей системы. Найдите значение, которое лучше подходит для вашей конкретной конфигурации.
Переработаем функцию update_bullets(), чтобы она не решала такое количество разных задач. Код обработки коллизий будет выделен в отдельную функцию:
game_functions.py
def update_bullets(ai_settings, screen, ship, aliens, bullets):
...
# Уничтожение исчезнувших пуль.
for bullet in bullets.copy():
if bullet.rect.bottom <= 0:
bullets.remove(bullet)
check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets)
. . . .
def check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets):
. ."""Обработка коллизий пуль с пришельцами."""
. .# Удаление пуль и пришельцев, участвующих в коллизиях.
collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
if len(aliens) == 0:
# Уничтожение существующих пуль и создание нового флота.
bullets.empty()
create_fleet(ai_settings, screen, ship, aliens)
Мы создали новую функцию check_bullet_alien_collisions() для выявления коллизий между пулями и пришельцами и для реакции на уничтожение всего флота. Это сделано для того, чтобы сократить длину функции update_bullets() и упростить дальнейшую разработку.
Упражнения
13-5. Ловец: создайте игру с персонажем, который может двигаться влево и вправо у нижнего края экрана. Мяч появляется в случайной позиции у верхнего края и падает вниз с постоянной скоростью. Если персонаж «ловит» мяч, сталкиваясь с ним, мяч исчезает. Создавайте новый мяч каждый раз, когда персонаж ловит мяч или когда мяч исчезает у нижнего края экрана.
Какое удовольствие от игры, в которой невозможно проиграть? Если игрок не успеет сбить флот достаточно быстро, пришельцы уничтожат корабль при столкновении. При этом количество кораблей, используемых игроком, ограничено, и корабль уничтожается, когда пришелец достигает нижнего края экрана. Игра завершается в тот момент, когда у игрока кончатся все корабли.
Начнем с проверки коллизий между пришельцами и кораблем, чтобы мы могли правильно обработать столкновения с пришельцами. Коллизии «пришелец-корабль» проверяются немедленно после обновления позиции каждого пришельца:
game_functions.py
def update_aliens(ai_settings, ship, aliens):
"""
Проверяет, достиг ли флот края экрана,
после чего обновляет позиции всех пришельцев во флоте.
"""
check_fleet_edges(ai_settings, aliens)
aliens.update()
. .
. .# Проверка коллизий "пришелец-корабль".
(1) . .if pygame.sprite.spritecollideany(ship, aliens):
(2) . . . .print("Ship hit!!!")
Метод spritecollideany() получает два аргумента: спрайт и группу. Метод пытается найти любой элемент группы, вступивший в коллизию со спрайтом, и останавливает цикл по группе сразу же после обнаружения столкнувшегося элемента. В данном случае он перебирает группу aliens и возвращает первого пришельца, столкнувшегося с кораблем.
Если ни одна коллизия не обнаружена, spritecollideany() возвращает None, и блок if в точке (1) не выполняется. Если же будет обнаружен пришелец, столкнувшийся с кораблем, метод возвращает этого пришельца, и выполняется блок if: выводится сообщение Ship hit!!! (2). (При столкновении пришельца с кораблем необходимо выполнить ряд операций: удалить всех оставшихся пришельцев и пули, вернуть корабль в центр и создать новый флот. Прежде чем писать код всех этих операций, необходимо убедиться в том, что решение с обнаружением коллизий с кораблем работает правильно. Команда print всего лишь позволяет легко проверить правильность обнаружения коллизий.)
Далее необходимо передать ship функции update_aliens():
alien_invasion.py
# Запуск основного цикла игры.
while True:
gf.check_events(ai_settings, screen, ship, bullets)
ship.update()
gf.update_bullets(ai_settings, screen, ship, aliens, bullets)
. . gf.update_aliens(ai_settings, ship, aliens)
gf.update_screen(ai_settings, screen, ship, aliens, bullets)
Если вы запустите Alien Invasion, при столкновении пришельца с кораблем в терминальном окне появляется сообщение Ship hit!!!. В ходе тестирования этого аспекта присвойте alien_drop_speed более высокое значение (например, 50 или 100), чтобы пришельцы быстрее добирались до вашего корабля.
Теперь нужно разобраться, что же происходит при столкновении пришельца с кораблем. Вместо того чтобы уничтожать экземпляр ship и создавать новый, мы будем подсчитывать количество уничтоженных кораблей; для этого следует организовать сбор статистики по игре. (Статистика также пригодится для подсчета очков.)
Напишем новый класс GameStats для ведения статистики и сохраним его в файле game_stats.py:
game_stats.py
class GameStats():
. ."""Отслеживание статистики для игры Alien Invasion."""
. .
. .def __init__(self, ai_settings):
. . . ."""Инициализирует статистику."""
. . . .self.ai_settings = ai_settings
(1) . . . .self.reset_stats()
. . . .
. .def reset_stats(self):
. . . ."""Инициализирует статистику, изменяющуюся в ходе игры."""
. . . .self.ships_left = self.ai_settings.ship_limit
На все время работы Alien Invasion будет создаваться один экземпляр GameStats, но часть статистики должна сбрасываться в начале каждой новой игры. Для этого бульшая часть статистики будет инициализироваться в методе reset_stats() вместо __init__(). Этот метод будет вызываться из __init__(), чтобы статистика правильно инициализировалась при первом создании экземпляра GameStats (1) , а метод reset_stats() будет вызываться в начале каждой новой игры.
Пока в игре используется всего один вид статистики — значение ships_left, изменяющееся в ходе игры. Количество кораблей в начале игры хранится в settings.py под именем ship_limit:
settings.py
# Настройки корабля
self.ship_speed_factor = 1.5
self.ship_limit = 3
Также необходимо внести ряд изменений в alien_invasion.py для создания экземпляра GameStats:
alien_invasion.py
...
from settings import Settings
(1) from game_stats import GameStats
...
def run_game():
...
pygame.display.set_caption("Alien Invasion")
. .# Создание экземпляра для хранения игровой статистики.
(2) . .stats = GameStats(ai_settings)
...
# Запуск основного цикла игры.
while True:
...
gf.update_bullets(ai_settings, screen, ship, aliens, bullets)
(3) . . . .gf.update_aliens(ai_settings, stats, screen, ship, aliens, bullets)
...
Мы импортируем новый класс GameStats (1) , создаем экземпляр stats (2), а затем добавляем аргументы stats, screen и ship в вызов update_aliens() (3). Эти аргументы будут использоваться для отслеживания количества кораблей, оставшихся у игрока, и построения нового флота при столкновении пришельца с кораблем.
Когда пришелец сталкивается с кораблем, программа уменьшает количество оставшихся кораблей на 1, уничтожает всех существующих пришельцев и пули, создает новый флот и возвращает корабль в середину экрана. (Также игра ненадолго приостанавливается, чтобы игрок заметил столкновение и перестроился перед появлением нового флота.)
Бульшая часть этого кода будет включена в функцию ship_hit():
game_functions.py
import sys
(1) from time import sleep
import pygame
...
def ship_hit(ai_settings, stats, screen, ship, aliens, bullets):
. ."""Обрабатывает столкновение корабля с пришельцем."""
. .# Уменьшение ships_left.
(2) . .stats.ships_left -= 1
. .
. .# Очистка списков пришельцев и пуль.
(3) . .aliens.empty()
. .bullets.empty()
. .
. .# Создание нового флота и размещение корабля в центре.
(4) . .create_fleet(ai_settings, screen, ship, aliens)
. .ship.center_ship()
. .
. .# Пауза.
(5) . .sleep(0.5)
? def update_aliens(ai_settings, stats, screen, ship, aliens, bullets):
...
# Проверка коллизий "пришелец-корабль".
if pygame.sprite.spritecollideany(ship, aliens):
. . . .ship_hit(ai_settings, stats, screen, ship, aliens, bullets)
Сначала мы импортируем функцию sleep() из модуля time, чтобы приостановить игру (1) . Новая функция ship_hit() управляет реакцией игры на столкновение корабля с пришельцем. Внутри ship_hit() число оставшихся кораблей уменьшается на 1 (2), после чего происходит очистка групп aliens и bullets (3).
Затем программа создает новый флот и выравнивает корабль по центру нижнего края (4). (Вскоре мы добавим метод center_ship() в класс Ship.) Наконец, после внесения изменений во все игровые элементы, но до перерисовки изменений на экране делается короткая пауза, чтобы игрок увидел, что его корабль столкнулся с пришельцем (5). После завершения паузы sleep() код переходит к функции update_screen(), которая перерисовывает новый флот на экране.
Также необходимо обновить определение update_aliens() и добавить параметры stats, screen и bullets ?, чтобы эти значения можно было передать при вызове ship_hit().
Ниже приведен новый метод center_ship(); добавьте его в конец ship.py:
ship.py
def center_ship(self):
. ."""Размещает корабль в центре нижней стороны."""
. .self.center = self.screen_rect.centerx
Чтобы выровнять корабль по центру, мы задаем атрибуту center корабля значение, соответствующее центру экрана (полученное при помощи атрибута screen_rect).
примечание
Обратите внимание: программа никогда не создает более одного корабля. Один экземпляр ship используется на протяжении всей игры, а при столкновении с пришельцем он просто возвращается к центру экрана. О том, что у игрока не осталось ни одного корабля, программа узнаёт из атрибута ships_left.
Запустите игру, подстрелите нескольких пришельцев, а затем позвольте пришельцу столкнуться с кораблем. Происходит небольшая пауза, на экране появляется новый флот вторжения, а корабль возвращается в центр нижней части экрана.
Если пришелец добирается до нижнего края экрана, программа будет реагировать так же, как при столкновении с кораблем. Добавьте для проверки этого условия новую функцию, которая будет называться update_aliens():
game_functions.py
def check_aliens_bottom(ai_settings, stats, screen, ship, aliens, bullets):
. ."""Проверяет, добрались ли пришельцы до нижнего края экрана."""
. .screen_rect = screen.get_rect()
. .for alien in aliens.sprites():
(1) . . . .if alien.rect.bottom >= screen_rect.bottom:
. . . . . .# Происходит то же, что при столкновении с кораблем.
. . . . . .ship_hit(ai_settings, stats, screen, ship, aliens, bullets)
. . . . . .break
. . . . . .
def update_aliens(ai_settings, stats, screen, ship, aliens, bullets):
...
. .# Проверка пришельцев, добравшихся до нижнего края экрана.
(2) . .check_aliens_bottom(ai_settings, stats, screen, ship, aliens, bullets)
Функция check_aliens_bottom() проверяет, есть ли хотя бы один пришелец, добравшийся до нижнего края экрана. Условие выполняется, когда атрибут rect.bottom пришельца больше или равен атрибуту rect.bottom экрана (1) . Если пришелец добрался до низа, вызывается функция ship_hit(). Если хотя бы один пришелец добрался до нижнего края, проверять остальных уже не нужно, поэтому после вызова ship_hit() цикл прерывается.
Функция check_aliens_bottom() вызывается после обновления позиций всех пришельцев и после проверки столкновений «пришелец-корабль» (2). Теперь новый флот будет появляться как при столкновении корабля с пришельцем, так и в том случае, если кто-то из пришельцев смог добраться до нижнего края экрана.
Программа Alien Invasion уже на что-то похожа, но игра длится бесконечно. Значение ships_left просто продолжает уходить в отрицательную бесконечность. Добавим в GameStats новый атрибут — флаг game_active, который завершает игру после потери последнего корабля:
game_stats.py
def __init__(self, settings):
....
. .# Игра Alien Invasion запускается в активном состоянии.
. .self.game_active = True
Добавим в ship_hit() код, который сбрасывает флаг game_active в состояние False при потере игроком последнего корабля:
game_functions.py
def ship_hit(ai_settings, stats, screen, ship, aliens, bullets):
"""Обрабатывает столкновение корабля с пришельцем."""
. .if stats.ships_left > 0:
# Уменьшение ships_left.
stats.ships_left -= 1
...
# Пауза.
sleep(0.5)
. .else:
. . . .stats.game_active = False
Бульшая часть кода ship_hit() осталась неизменной. Весь существующий код был перемещен в блок if, который проверяет, что у игрока остался хотя бы один корабль. Если корабли не кончились, программа создает новый флот, делает паузу и продолжает игру. Если же игрок потерял последний корабль, флаг game_active переводится в состояние False.
В файле alien_invasion.py необходимо определить части игры, которые должны выполняться всегда, и те части, которые должны выполняться только при активной игре:
alien_invasion.py
# Запуск основного цикла игры.
while True:
gf.check_events(ai_settings, screen, ship, bullets)
. .if stats.game_active:
ship.update()
gf.update_bullets(ai_settings, screen, ship, aliens, bullets)
. . . .gf.update_aliens(ai_settings, ship, aliens)
gf.update_screen(ai_settings, screen, ship, aliens, bullets)
В основном цикле всегда должна вызываться функция check_events(), даже если игра находится в неактивном состоянии. Например, программа все равно должна узнать о том, что пользователь нажал клавишу Q для завершения игры или щелкнул на кнопке закрытия окна. Также экран должен обновляться в то время, пока игрок решает, хочет ли он начать новую игру. Остальные вызовы функций должны происходить только при активной игре, потому что в то время, когда игра не активна, обновлять позиции игровых элементов не нужно.
В обновленной версии игра должна останавливаться после потери игроком последнего корабля.
Упражнения
13-6. Конец игры: в коде из упражнения 13-5 (с. 274) подсчитывайте, сколько раз игрок не поймал мяч. После трех промахов игра должна заканчиваться.
В этой главе вы научились добавлять в игру большое количество одинаковых элементов на примере флота пришельцев. Вы узнали, как использовать вложенные циклы для создания сетки с элементами, а также привели игровые элементы в движение, вызывая метод update() каждого элемента. Вы научились управлять перемещением объектов на экране и обрабатывать различные события (например, достижение края экрана). Вы также узнали, как обнаруживать коллизии и реагировать на них (на примере попаданий пуль в пришельцев и столкновений пришельцев с кораблем). В завершение главы рассматривалась тема ведения игровой статистики и использования флага для проверки окончания игры.
В последней главе этого проекта будет добавлена кнопка Play, чтобы игрок мог самостоятельно запустить свою первую игру, а также повторить игру после ее завершения. После каждого уничтожения вражеского флота скорость игры будет возрастать, а мы реализуем систему подсчета очков. В результате вы получите полностью работоспособную игру!