Флот пришельцев должен двигаться вправо по экрану, пока не дойдет до края; тогда флот опускается на заданную величину и начинает двигаться в обратном направлении. Это продолжается до тех пор, пока все пришельцы не будут сбиты, один из них не столкнется с кораблем или не достигнет низа экрана. Начнем с перемещения флота вправо.
Перемещение вправо
Чтобы корабли пришельцев перемещались по экрану, мы воспользуемся методом 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()
Переработаем функцию 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. Ловец: создайте игру с персонажем, который может двигаться влево и вправо у нижнего края экрана. Мяч появляется в случайной позиции у верхнего края и падает вниз с постоянной скоростью. Если персонаж «ловит» мяч, сталкиваясь с ним, мяч исчезает. Создавайте новый мяч каждый раз, когда персонаж ловит мяч или когда мяч исчезает у нижнего края экрана.