В этой главе создание игры «Инопланетное вторжение» будет завершено. Мы добавим кнопку Play для запуска игры по желанию игрока или перезапуска игры после ее завершения. Мы также изменим игру, чтобы она ускорялась при переходе игрока на следующий уровень, и реализуем систему подсчета очков. К концу главы вы будете знать достаточно, чтобы заняться разработкой игр, сложность которых нарастает по ходу игры и в которых реализована система подсчета очков.
В этом разделе мы добавим кнопку Play, которая отображается перед началом игры и появляется после ее завершения, чтобы игрок мог сыграть снова.
В текущей версии игра начинается сразу же после запуска программы alien_invasion.py. После очередных изменений игра будет запускаться в неактивном состоянии и предлагать игроку нажать кнопку Play для запуска. Для этого добавьте в метод __init__() класса AlienInvasion следующий код:
alien_invasion.py
def __init__(self):
"""Инициализирует игру и создает игровые ресурсы."""
pygame.init()
--пропуск--
# Игра запускается в неактивном состоянии.
self.game_active = False
Итак, программа запускается в неактивном состоянии, а игру можно начать только нажатием кнопки Play.
Поскольку в Pygame не существует встроенного метода создания кнопок, мы напишем класс Button для рисования заполненного прямоугольника с текстовой надписью. Следующий код может использоваться для создания кнопок в любой игре. Ниже приведена первая часть класса Button; сохраните ее в файле button.py:
button.py
import pygame.font
class Button:
"""Класс для создания кнопок для игры."""
❶ def __init__(self, ai_game, msg):
"""Инициализирует атрибуты кнопки."""
self.screen = ai_game.screen
self.screen_rect = self.screen.get_rect()
# Назначение размеров и свойств кнопок.
❷ self.width, self.height = 200, 50
self.button_color = (0, 135, 0)
self.text_color = (255, 255, 255)
❸ self.font = pygame.font.SysFont(None, 48)
# Создание объекта rect кнопки и выравнивание по центру экрана.
❹ self.rect = pygame.Rect(0, 0, self.width, self.height)
self.rect.center = self.screen_rect.center
# Сообщение кнопки создается только один раз.
❺ self.prep_msg(msg)
Сначала программа импортирует модуль pygame.font, который позволяет Pygame выводить текст на экран. Метод __init__() получает параметры self, объект ai_game и строку msg с текстом кнопки ❶. Затем устанавливаются размеры кнопки ❷, после чего атрибуты button_color и text_color задаются так, чтобы прямоугольник кнопки был окрашен в ярко-зеленый цвет, а текст выводился белым цветом.
Далее происходит подготовка атрибута font для вывода текста ❸. Аргумент None сообщает Pygame, что для текста должен использоваться шрифт по умолчанию, а значение 48 определяет кегль. Чтобы выровнять кнопку по центру экрана, мы создаем объект rect для кнопки ❹ и задаем его атрибут center в соответствии с одноименным атрибутом экрана.
Pygame выводит строку текста в виде графического изображения. Эта задача решается путем вызова метода _prep_msg() ❺.
Код _prep_msg() выглядит так:
button.py
def _prep_msg(self, msg):
"""Преобразует msg в прямоугольник и выравнивает текст по центру."""
❶ self.msg_image = self.font.render(msg, True, self.text_color,
self.button_color)
❷ self.msg_image_rect = self.msg_image.get_rect()
self.msg_image_rect.center = self.rect.center
Метод _prep_msg() должен получать параметр self и текст, который нужно вывести в графическом виде (msg). Вызов метода font.render() преобразует текст, хранящийся в msg, в изображение, которое затем сохраняется в self.msg_image ❶. Методу font.render() также передается логический признак режима сглаживания (antialiasing) текста. В остальных аргументах заданы цвет шрифта и цвет фона. В нашем примере режим сглаживания включен (True), а цвет фона совпадает с цветом фона кнопки. (Если цвет фона не указан, то Pygame пытается вывести шрифт с прозрачным фоном.)
Изображение текста выравнивается по центру кнопки, для чего создается объект rect изображения, а его атрибут center приводится в соответствие с одноименным атрибутом кнопки ❷.
Остается создать метод draw_button(), который может вызываться для отображения кнопки на экране:
button.py
def draw_button(self):
"""Отображает пустую кнопку и выводит сообщение."""
self.screen.fill(self.button_color, self.rect)
self.screen.blit(self.msg_image, self.msg_image_rect)
Вызов метода screen.fill() рисует прямоугольную часть кнопки. Затем вызов screen.blit() выводит изображение текста на экран, передавая изображение и связанный с ним объект rect. Класс Button готов.
Мы будем использовать класс Button для создания кнопки Play в классе AlienInvasion. Сначала обновим операторы import:
alien_invasion.py
--пропуск--
from game_stats import GameStats
from button import Button
Нам нужна только одна кнопка Play, поэтому создадим ее в методе __init__() класса AlienInvasion. Этот код можно разместить в самом конце метода:
alien_invasion.py
def __init__(self):
--пропуск--
self.game_active = False
# Создание кнопки Play.
self.play_button = Button(self, "Play")
Программа создает экземпляр Button с текстом Play, но не выводит кнопку на экран. Чтобы она там появилась, мы вызовем метод draw_button() кнопки в _update_screen():
alien_invasion.py
def _update_screen(self):
--пропуск--
self.aliens.draw(self.screen)
# Кнопка Play отображается в том случае, если игра неактивна.
if not self.game_active:
self.play_button.draw_button()
pygame.display.flip()
Чтобы кнопка Play не закрывалась другими элементами экрана, мы отображаем ее после всех остальных игровых элементов, но перед переключением на новый экран. Код заключается в блок if, чтобы кнопка отображалась только в неактивном состоянии игры.
Теперь при запуске «Инопланетное вторжение» в центре экрана отображается кнопка Play (рис. 14.1).
Рис. 14.1. Кнопка Play выводится, когда игра неактивна
Чтобы при нажатии кнопки Play запускалась новая игра, добавьте в конец _check_events() следующий блок elif для отслеживания событий мыши над кнопкой:
alien_invasion.py
def _check_events(self):
"""Обрабатывает нажатия клавиш и события мыши."""
for event in pygame.event.get():
if event.type == pygame.QUIT:
--пропуск--
❶ elif event.type == pygame.MOUSEBUTTONDOWN:
❷ mouse_pos = pygame.mouse.get_pos()
❸ self._check_play_button(mouse_pos)
Pygame обнаруживает событие MOUSEBUTTONDOWN, когда игрок щелкает в любой точке экрана ❶, но мы хотим ограничить игру, чтобы она реагировала только на щелчки на кнопке Play. Для этого будет использоваться метод pygame.mouse.get_pos(), возвращающий кортеж с координатами x и y точки щелчка ❷. Эти значения передаются новому методу _check_play_button()❸.
Ниже приведен код _check_play_button(), который я решил поместить после _check_events():
alien_invasion.py
def _check_play_button(self, mouse_pos):
"""Запускает новую игру при нажатии кнопки Play."""
❶ if self.play_button.rect.collidepoint(mouse_pos):
self.game_active = True
Метод collidepoint() используется для проверки того, находится ли точка щелчка в пределах области, определяемой прямоугольником кнопки Play ❶. Если да, то флаг game_active переводится в состояние True, и игра начинается!
К этому моменту вы сможете запустить игру и сыграть полноценную партию. После завершения игры значение game_active становится равным False, а кнопка Play снова появится на экране.
Только что написанный нами код работает при первом нажатии кнопки Play, но не работает после завершения первой игры, поскольку условия, приводящие к окончанию игры, еще не были сброшены.
Чтобы игра сбрасывалась при каждом нажатии кнопки Play, необходимо сбросить игровую статистику, стереть данные о старых пришельцах и снарядах, создать новый флот и вернуть корабль в центр нижней стороны экрана:
alien_invasion.py
def _check_play_button(self,mouse_pos):
"""Запускает новую игру при нажатии кнопки Play."""
if self.play_button.rect.collidepoint(mouse_pos):
# Сброс игровой статистики.
❶ self.stats.reset_stats()
self.game_active = True
# Очистка групп aliens и bullets.
❷ self.bullets.empty()
self.aliens.empty()
# Создание нового флота и размещение корабля в центре.
❸ self._create_fleet()
self.ship.center_ship()
Игровая статистика сбрасывается ❶, вследствие чего игрок получает три новых корабля. После этого флаг game_active переводится в состояние True (чтобы игра началась сразу же после выполнения кода функции), группы aliens и bullets очищаются ❷, создается новый флот, а корабль выравнивается по центру ❸.
После этих изменений игра будет правильно переходить в исходное состояние при каждом нажатии Play, и вы сможете сыграть столько раз, сколько вам захочется!
У кнопки Play в нашем приложении есть одна проблема: область кнопки на экране продолжает реагировать на щелчки, даже если сама кнопка не отображается. Если случайно щелкнуть на месте кнопки после начала игры, то она перезапустится!
Чтобы исправить этот недостаток, следует запускать игру только в том случае, если флаг game_active находится в состоянии False:
alien_invasion.py
def _check_play_button(self, mouse_pos):
"""Запускает новую игру при нажатии кнопки Play."""
❶ button_clicked = self.play_button.rect.collidepoint(mouse_pos)
❷ if button_clicked and not self.game_active:
# Сброс игровой статистики.
self.stats.reset_stats()
--пропуск--
Флаг button_clicked содержит значение True или False ❶, а игра перезапускается только в том случае, если пользователь нажал кнопку Play и при этом игра неактивна в настоящий момент ❷. Чтобы протестировать это поведение, запустите новую игру и многократно щелкайте в том месте, где должна находиться кнопка Play. Если все работает как положено, то нажатия кнопки не должны влиять на ход игры.
Указатель мыши должен быть видимым, чтобы пользователь мог начать игру, но после начала игры он только мешает. Чтобы исправить этот недостаток, мы скроем указатель мыши после того, как игра станет активной. Это можно сделать в блоке if в конце _check_play_button():
alien_invasion.py
def _check_play_button(self, mouse_pos):
"""Запускает новую игру при нажатии кнопки Play."""
button_clicked = self.play_button.rect.collidepoint(mouse_pos)
if button_clicked and not self.game_active:
--пропуск--
# Указатель мыши скрывается.
pygame.mouse.set_visible(False)
Благодаря вызову set_visible() со значением False Pygame получает указание скрыть указатель, когда тот находится над окном игры.
После завершения игры указатель должен появляться снова, чтобы игрок мог нажать кнопку Play для запуска новой игры. Эту задачу решает следующий код:
alien_invasion.py
def _ship_hit(self):
"""Обрабатывает столкновение корабля с пришельцем."""
if self.stats.ships_left > 0:
--пропуск--
else:
self.game_active = False
pygame.mouse.set_visible(True)
Указатель снова становится видимым сразу же после того, как игра становится неактивной, что происходит в _ship_hit(). Внимание к подобным деталям сделает вашу игру более профессиональной, а игрок сможет сосредоточиться на игре вместо того, чтобы разбираться в сложностях пользовательского интерфейса.
Упражнения
14.1. Запуск игры клавишей P. В «Инопланетном вторжении» игрок управляет кораблем с клавиатуры, поэтому для запуска игры тоже лучше использовать клавиатуру. Добавьте код, который позволит игроку запустить игру путем нажатия клавиши P. Возможно, часть кода из _check_play_button() стоит переместить в функцию start_game(), которая будет вызываться из _check_play_button() и _check_keydown_events().
14.2. Стрельба по мишени. Создайте у правого края экрана прямоугольник, который двигается вверх и вниз с постоянной скоростью. У левого края располагается корабль, который перемещается вверх и вниз игроком и стреляет по движущейся прямоугольной мишени. Добавьте кнопку Play для запуска игры. После трех промахов игра заканчивается, а на экране снова появляется кнопка Play. Нажатие этой кнопки перезапускает игру.
В текущей версии игры, после того как весь флот пришельцев будет уничтожен, игрок переходит на новый уровень, но сложность игры остается неизменной. Немного оживим игру и повысим ее сложность; для этого скорость игры будет повышаться каждый раз, когда игрок уничтожает весь флот.
Начнем с реорганизации класса Settings и разделения настроек игры на две категории: постоянные и изменяющиеся. Необходимо также проследить за тем, чтобы настройки, изменяющиеся в ходе игры, сбрасывались в исходное состояние в начале новой игры. Метод __init__() из файла settings.py выглядит так:
settings.py
def __init__(self):
"""Инициализирует статические настройки игры."""
# Настройки экрана
self.screen_width = 1200
self.screen_height = 800
self.bg_color = (230, 230, 230)
# Настройки корабля
self.ship_limit = 3
# Настройки снарядов
self.bullet_width = 3
self.bullet_height = 15
self.bullet_color = 60, 60, 60
self.bullets_allowed = 3
# Настройки пришельцев
self.fleet_drop_speed = 10
# Темп ускорения игры
❶ self.speedup_scale = 1.1
❷ self.initialize_dynamic_settings()
Значения, которые остаются неизменными, по-прежнему инициализируются в методе __init__(). Добавляется настройка speedup_scale ❶, управляющая быстротой нарастания скорости; значение 2 будет удваивать скорость каждый раз, когда игрок переходит на следующий уровень, а значение 1 сохранит скорость постоянной. С таким значением, как 1.1, скорость будет увеличиваться в достаточной степени, чтобы игра усложнилась, но не стала невозможной. Наконец, вызов initialize_dynamic_settings() инициализирует значения атрибутов, которые должны изменяться в ходе игры ❷.
Код initialize_dynamic_settings() выглядит так:
settings.py
def initialize_dynamic_settings(self):
"""Инициализирует настройки, изменяющиеся в ходе игры."""
self.ship_speed = 1.5
self.bullet_speed = 2.5
self.alien_speed = 1.0
# fleet_direction = 1 обозначает движение вправо; а -1 — влево.
self.fleet_direction = 1
Метод задает исходные значения скоростей корабля, снарядов и пришельцев. Эти скорости будут увеличиваться по ходу игры и сбрасываться каждый раз, когда игрок запускает новую игру. Мы добавляем в этот метод fleet_direction, чтобы пришельцы в начале новой игры всегда двигались вправо. Увеличивать значение fleet_drop_speed не нужно: когда пришельцы быстрее двигаются по горизонтали, они будут быстрее перемещаться и по вертикали.
Чтобы скорость корабля, снарядов и пришельцев увеличивалась каждый раз, когда игрок достигает нового уровня, мы напишем новый метод increase_speed():
settings.py
def increase_speed(self):
"""Увеличивает настройки скорости."""
self.ship_speed *= self.speedup_scale
self.bullet_speed *= self.speedup_scale
self.alien_speed *= self.speedup_scale
Чтобы увеличить скорость этих игровых элементов, мы умножаем каждую настройку скорости на значение speedup_scale.
Темп игры повышается путем вызова increase_speed() в check_bullet_alien_collisions() при уничтожении последнего пришельца во флоте, но перед созданием нового флота:
alien_invasion.py
def _check_bullet_alien_collisions(self):
--пропуск--
if not self.aliens:
# Уничтожение снарядов, повышение скорости и создание нового флота.
self.bullets.empty()
self._create_fleet()
self.settings.increase_speed()
Изменения значений настроек скорости ship_speed, alien_speed и bullet_speed достаточно для того, чтобы ускорить всю игру!
Каждый раз, когда игрок начинает новую игру, все измененные настройки должны вернуться к исходным значениям, иначе каждая новая игра будет начинаться с повышенными настройками скорости предыдущей игры:
alien_invasion.py
def _check_play_button(self, mouse_pos):
"""Запускает новую игру при нажатии кнопки Play."""
button_clicked = self.play_button.rect.collidepoint(mouse_pos)
if button_clicked and not self.game_active:
# Сброс игровых настроек.
self.settings.initialize_dynamic_settings()
--пропуск--
Игра «Инопланетное вторжение» стала достаточно сложной и интересной. Каждый раз, когда игрок очищает экран, игра должна слегка ускориться, а ее сложность — слегка возрасти. Если сложность возрастает слишком быстро, то уменьшите значение settings.speedup_scale, а если, наоборот, недостаточная — слегка увеличьте это значение. Найдите оптимальное значение, оценивая сложность игры за разумный промежуток времени. Первые несколько появлений флотов должны быть простыми, несколько следующих — сложными, но уничтожимыми, а при последующих попытках сложность должна возрастать до пределов, когда уничтожить флот практически невозможно.
Упражнения
14.3. Учебная стрельба с нарастающей сложностью. Начните с кода упражнения 14.2. Скорость мишени должна увеличиваться по ходу игры, а при нажатии игроком кнопки Play мишень должна возвращаться к исходной скорости.
14.4. Уровни сложности. Создайте в «Инопланетном вторжении» набор кнопок для выбора начальной сложности игры. Каждая кнопка должна присваивать атрибутам Settings значения, необходимые для создания различных уровней сложности.
Система подсчета очков позволит отслеживать счет игры в реальном времени; кроме того, на экране будут выводиться текущий рекорд, уровень и количество оставшихся кораблей.
Счет игры относится к игровой статистике, поэтому мы добавим атрибут score в класс GameStats:
game_stats.py
class GameStats:
--пропуск--
def reset_stats(self):
"""Инициализирует статистику, изменяющуюся в ходе игры."""
self.ships_left = self.settings.ship_limit
self.score = 0
Чтобы счет сбрасывался при запуске новой игры, мы инициализируем score в reset_stats(), а не в методе __init__().
Чтобы вывести счет на экран, мы сначала создаем новый класс Scoreboard. Пока он ограничивается выводом текущего счета, но мы используем его для вывода рекордного счета, уровня и количества оставшихся кораблей. Ниже приведена первая часть класса; сохраните ее под именем scoreboard.py:
scoreboard.py
import pygame.font
class Scoreboard:
"""Класс для вывода игровой информации."""
❶ def __init__(self, ai_game):
"""Инициализирует атрибуты подсчета очков."""
self.screen = ai_game.screen
self.screen_rect = self.screen.get_rect()
self.settings = ai_game.settings
self.stats = ai_game.stats
# Настройки шрифта для вывода счета.
❷ self.text_color = (30, 30, 30)
❸ self.font = pygame.font.SysFont(None, 48)
# Подготовка исходного изображения счета.
❹ self.prep_score()
Класс Scoreboard выводит текст на экран, поэтому код начинается с импортирования модуля pygame.font. Затем методу __init__() передается параметр ai_game для обращения к объектам settings, screen и stats, чтобы класс мог выводить информацию об отслеживаемых показателях ❶. Далее назначается цвет текста ❷ и создается экземпляр объекта шрифта ❸.
Чтобы преобразовать выводимый текст в изображение, мы вызываем метод prep_score()❹, который определяется следующим образом:
scoreboard.py
def prep_score(self):
"""Преобразует текущий счет в графическое изображение."""
❶ score_str = str(self.stats.score)
❷ self.score_image = self.font.render(score_str, True,
self.text_color, self.settings.bg_color)
# Вывод счета в правой верхней части экрана.
❸ self.score_rect = self.score_image.get_rect()
❹ self.score_rect.right = self.screen_rect.right - 20
❺ self.score_rect.top = 20
В методе prep_score() числовое значение stats.score преобразуется в строку ❶; эта строка передается методу render(), создающему изображение ❷. Чтобы счет был хорошо виден на экране, render() передаются цвет фона и цвет текста.
Счет размещается в правой верхней части экрана и расширяется влево по мере увеличения значения и ширины числа. Чтобы счет всегда оставался выровненным по правой стороне, мы создаем прямоугольник rect с именем score_rect ❸ и смещаем его правую сторону на 20 пикселов от правого края экрана ❹. Затем верхняя сторона прямоугольника смещается на 20 пикселов вниз от верхнего края экрана ❺.
Остается создать метод show_score() для вывода созданного графического изображения:
scoreboard.py
def show_score(self):
"""Выводит счет на экран."""
self.screen.blit(self.score_image, self.score_rect)
Метод выводит счет на экран в позиции, определяемой score_rect.
Чтобы вывести счет, мы создадим в классе AlienInvasion экземпляр Scoreboard. Начнем с изменения операторов import:
alien_invasion.py
--пропуск--
from game_stats import GameStats
from scoreboard import Scoreboard
--пропуск--
Затем создадим экземпляр Scoreboard в методе __init__():
alien_invasion.py
def __init__(self):
--пропуск--
pygame.display.set_caption("ALien Invasion")
# Создание экземпляров для хранения статистики и панели результатов.
self.stats = GameStats(self)
self.sb = Scoreboard(self)
--пропуск--
Затем мы выводим панель результатов на экран с помощью функции _update_screen():
alien_invasion.py
def _update_screen(self):
--пропуск--
self.aliens.draw(self.screen)
# Вывод информации о счете.
self.sb.show_score()
# Кнопка Play отображается в том случае, если игра неактивна.
--пропуск--
Метод show_score() вызывается непосредственно перед отображением кнопки Play.
Если запустить игру сейчас, то в правом верхнем углу экрана отображается счет 0. (Пока мы просто хотим убедиться в том, что счет отображается в нужном месте, прежде чем заниматься дальнейшей доработкой системы подсчета очков.) На рис. 14.2 изображено окно игры перед ее началом.
Рис. 14.2. Счет отображается в правом верхнем углу экрана
А теперь нужно организовать начисление очков за каждого пришельца!
Чтобы на экране выводился актуальный счет, мы будем обновлять значение stats.score при каждом попадании в пришельца, а затем вызывать prep_score() для обновления изображения счета. Но сначала нужно определить, сколько очков игрок будет получать за каждого пришельца:
settings.py
def initialize_dynamic_settings(self):
--пропуск--
# Подсчет очков
self.alien_points = 50
Стоимость каждого пришельца в очках будет увеличиваться по ходу игры. Чтобы значение сбрасывалось в начале каждой новой игры, мы задаем значение в initialize_dynamic_settings().
Счет за каждого сбитого пришельца будет обновляться с помощью функции _check_bullet_alien_collisions():
alien_invasion.py
def _check_bullet_alien_collisions(self):
"""Обрабатывает столкновение снарядов с пришельцами."""
# Удаление снарядов и пришельцев, участвующих в коллизиях.
collisions = pygame.sprite.groupcollide(self.bullets, self.aliens, True, True)
if collisions:
self.stats.score += self.settings.alien_points
self.sb.prep_score()
--пропуск--
При попадании снаряда в пришельца Pygame возвращает словарь collisions. Программа проверяет, существует ли словарь, и если да — стоимость пришельца добавляется к счету. Затем вызов prep_score() создает новое изображение для обновленного счета.
Теперь во время игры вы сможете набирать очки!
В текущей версии игры счет обновляется только после попадания в пришельца; как правило, такой подход работает нормально. Но старый счет выводится и после попадания в первого пришельца в новой игре.
Проблема решается путем инициализации счета при создании новой игры:
alien_invasion.py
def _check_play_button(self, mouse_pos):
--пропуск--
if button_clicked and not self.game_active:
--пропуск--
# Сброс игровой статистики.
self.stats.reset_stats()
self.sb.prep_score()
--пропуск--
Метод prep_score() вызывается при сбросе игровой статистики в начале новой игры. Счет, выводимый на экран, обнуляется.
В нынешней версии код при подсчете очков будет пропускать некоторых пришельцев. Например, если два снаряда попадают в пришельцев во время одного прохода цикла или если будет создан широкий снаряд для поражения нескольких пришельцев одновременно, то игрок получит очки только за одного подстреленного пришельца. Чтобы устранить этот недостаток, нужно доработать механизм обнаружения коллизий между снарядами и пришельцами.
В коде _check_bullet_alien_collisions() любой снаряд, столкнувшийся с пришельцем, становится ключом словаря collisions. С каждым снарядом связывается значение — список пришельцев, участвующих в коллизии. Переберем словарь collisions и убедимся в том, что очки начисляются за каждого подбитого пришельца:
alien_invasion.py
def _check_bullet_alien_collisions(self):
--пропуск--
if collisions:
for aliens in collisions.values():
self.stats.score += self.settings.alien_points * len(aliens)
self.sb.prep_score()
--пропуск--
Если словарь collisions был определен, то программа перебирает все значения в нем. Вспомните, что каждое значение представляет собой список пришельцев, в которых попал один снаряд. Стоимость каждого пришельца умножается на количество пришельцев в списке, а результат прибавляется к текущему счету. Чтобы протестировать эту систему, увеличьте ширину снаряда до 300 пикселов и убедитесь в том, что игра начисляет очки за каждого пришельца, в которого попал этот большой снаряд; затем верните ширину снаряда к нормальному значению.
Так как по мере достижения каждого нового уровня игра усложняется, за пришельцев на этих уровнях следует давать больше очков. Чтобы реализовать эту функциональность, мы добавим код, увеличивающий стоимость пришельцев при возрастании скорости игры:
settings.py
class Settings:
"""Класс для хранения всех настроек игры "Инопланетное вторжение"."""
def __init__(self):
--пропуск--
# Темп ускорения игры.
self.speedup_scale = 1.1
# Темп роста стоимости пришельцев.
❶ self.score_scale = 1.5
self.initialize_dynamic_settings()
def initialize_dynamic_settings(self):
--пропуск--
def increase_speed(self):
"""Увеличивает настройки скорости и стоимость пришельцев."""
self.ship_speed *= self.speedup_scale
self.bullet_speed *= self.speedup_scale
self.alien_speed *= self.speedup_scale
❷ self.alien_points = int(self.alien_points * self.score_scale)
В программе определяется коэффициент прироста начисляемых очков; он называется score_scale ❶. При небольшом увеличении скорости (1.1) игра быстро усложняется, но чтобы увидеть заметную разницу в очках, необходимо изменять стоимость пришельцев на бо́льшую величину (1.5). После увеличения скорости игры стоимость каждого попадания также увеличивается ❷. Чтобы счет возрастал на целое количество очков, в программе используется функция int().
Чтобы увидеть стоимость каждого пришельца, добавьте в метод increase_speed() в классе Settings функцию print():
settings.py
def increase_speed(self):
--пропуск--
self.alien_points = int(self.alien_points * self.score_scale)
print(self.alien_points)
Новое значение должно выводиться в терминальном окне каждый раз, когда игрок переходит на новый уровень.
ПРИМЕЧАНИЕ
Убедившись, что стоимость пришельцев действительно возрастает, не забудьте удалить вызов функции print(); в противном случае лишний вывод повлияет на быстродействие игры и будет отвлекать игрока.
В большинстве аркадных шутеров счет ведется значениями, кратными 10, и мы воспользуемся этой схемой в своей игре. Отформатируем счет так, чтобы в больших числах группы разрядов разделялись запятыми. Изменения вносятся в классе Scoreboard:
scoreboard.py
def prep_score(self):
"""Преобразует текущий счет в графическое изображение."""
rounded_score = round(self.stats.score, -1)
score_str = f"{rounded_score:,}"
self.score_image = self.font.render(score_str, True,
self.text_color, self.settings.bg_color)
--пропуск--
Функция round() обычно округляет дробное число до заданного количества знаков, переданного во втором аргументе. Но если во втором аргументе передается отрицательное число, то round() округляет значение до ближайших десятков, сотен, тысяч и т.д. Код дает Python указание округлить значение stats.score до десятков и сохранить его в rounded_score.
Затем мы используем для счета спецификатор формата в f-строке. Спецификатор формата (format specifier) — это специальным образом составленная последовательность символов, определяющая способ представления значения переменной. В данном случае благодаря последовательности :, Python получает указание вставить запятые в соответствующие места в предоставленном числовом значении. Теперь, запуская игру, вы увидите отформатированный, округленный счет, даже если набрали много очков (рис. 14.3).
Рис. 14.3. Округленный счет с разделителями групп
Каждый игрок желает превзойти предыдущий рекорд, поэтому мы будем отслеживать и выводить рекорды, чтобы игроку было к чему стремиться. Рекорды будут храниться в классе GameStats:
game_stats.py
def __init__(self, ai_game):
--пропуск--
# Рекорд не должен сбрасываться.
self.high_score = 0
Так как рекорд не должен сбрасываться при повторном запуске, значение high_score инициализируется в __init__(), а не в reset_stats().
Теперь изменим класс Scoreboard для отображения рекорда. Начнем с метода __init__():
scoreboard.py
def __init__(self, ai_game):
--пропуск--
# Подготовка изображений счетов.
self.prep_score()
❶ self.prep_high_score()
Рекорд должен отображаться отдельно от текущего счета, поэтому для подготовки его изображения понадобится новый метод prep_high_score() ❶:
scoreboard.py
def prep_high_score(self):
"""Преобразует рекордный счет в графическое изображение."""
❶ high_score = round(self.stats.high_score, -1)
high_score_str = f"{high_score:,}"
❷ self.high_score_image = self.font.render(high_score_str, True,
self.text_color, self.settings.bg_color)
# Рекорд выравнивается по центру верхней стороны экрана.
self.high_score_rect = self.high_score_image.get_rect()
❸ self.high_score_rect.centerx = self.screen_rect.centerx
❹ self.high_score_rect.top = self.score_rect.top
Рекорд округляется до десятков и форматируется с помощью запятых ❶. Затем для рекорда создается графическое изображение ❷, выполняется горизонтальное выравнивание прямоугольника по центру экрана ❸, а атрибут top прямоугольника приводится в соответствие с верхней стороной изображения счета ❹.
Теперь метод show_score() выводит текущий счет в правом верхнем углу, а рекорд — в центре верхней стороны экрана:
scoreboard.py
def show_score(self):
"""Выводит счет на экран."""
self.screen.blit(self.score_image, self.score_rect)
self.screen.blit(self.high_score_image, self.high_score_rect)
Для обновления рекорда в класс Scoreboard добавляется новая функция check_high_score():
scoreboard.py
def check_high_score(self):
"""Проверяет, появился ли новый рекорд."""
if self.stats.score > self.stats.high_score:
self.stats.high_score = self.stats.score
self.prep_high_score()
Метод check_high_score() сравнивает текущий счет с рекордом. Если текущий счет выше, то мы обновляем значение high_score и вызываем prep_high_score(), чтобы обновить изображение рекорда.
Метод check_high_score() должен вызываться при каждом попадании в пришельца после обновления счета в _check_bullet_alien_collisions():
alien_invasion.py
def check_bullet_alien_collisions(self):
--пропуск--
if collisions:
for aliens in collisions.values():
self.stats.score += self.settings.alien_points * len(aliens)
self.sb.prep_score()
self.check_high_score()
--пропуск--
Метод check_high_score() должен вызываться только в том случае, если словарь collisions присутствует, причем вызов выполняется после обновления счета для всех подбитых пришельцев.
Когда вы играете в «Инопланетное вторжение» впервые, текущий счет одновременно является рекордом, поэтому будет отображаться и как текущий счет, и как рекорд. Но в начале второй игры ваш предыдущий рекорд должен отображаться в середине, а текущий счет — справа (рис. 14.4).
Рис. 14.4. Рекордный счет выводится вверху экрана по центру
Чтобы в игре выводился текущий уровень, сначала в класс GameStats следует добавить атрибут для его представления. Чтобы уровень сбрасывался в начале каждой игры, инициализируйте его в reset_stats():
game_stats.py
def reset_stats(self):
"""Инициализирует статистику, изменяющуюся в ходе игры."""
self.ships_left = self.settings.ship_limit
self.score = 0
self.level = 1
Чтобы класс Scoreboard выводил текущий уровень, мы вызываем новый метод prep_level() из метода __init__():
scoreboard.py
def __init__(self, ai_game):
--пропуск--
self.prep_high_score()
self.prep_level()
Метод prep_level() выглядит так:
scoreboard.py
def prep_level(self):
"""Преобразует уровень в графическое изображение."""
level_str = str(self.stats.level)
❶ self.level_image = self.font.render(level_str, True,
self.text_color, self.settings.bg_color)
# Уровень выводится под текущим счетом.
self.level_rect = self.level_image.get_rect()
❷ self.level_rect.right = self.score_rect.right
❸ self.level_rect.top = self.score_rect.bottom + 10
Метод prep_level() создает изображение на базе значения, хранящегося в stats.level ❶, и приводит атрибут right изображения в соответствие с атрибутом right счета ❷. Затем атрибут top сдвигается на 10 пикселов ниже нижнего края изображения текущего счета, чтобы между счетом и уровнем оставался пустой интервал ❸.
В метод show_score() также необходимо внести изменения:
scoreboard.py
def show_score(self):
"""Выводит текущий счет, рекорд и количество оставшихся кораблей."""
self.screen.blit(self.score_image, self.score_rect)
self.screen.blit(self.high_score_image, self.high_score_rect)
self.screen.blit(self.level_image, self.level_rect)
Добавленная строка выводит на экран изображение, представляющее уровень.
Увеличение stats.level и обновление изображения уровня выполняются в _check_bullet_alien_collisions():
alien_invasion.py
def _check_bullet_alien_collisions(self):
--пропуск--
if not self.aliens:
# Уничтожить существующие снаряды и создать новый флот.
self.bullets.empty()
self.create_fleet()
self.settings.increase_speed()
# Увеличение уровня.
self.stats.level += 1
self.sb.prep_level()
Если все пришельцы уничтожены, то программа увеличивает значение stats.level и вызывает prep_level() для обновления уровня.
Чтобы убедиться в том, что изображения текущего счета и уровня правильно обновляются в начале новой игры, мы вызываем prep_level() при нажатии кнопки Play:
alien_invasion.py
def _check_play_button(self, mouse_pos):
--пропуск--
if button_clicked and not self.game_active:
--пропуск--
self.sb.prep_score()
self.sb.prep_level()
--пропуск--
Метод prep_level() вызывается сразу же после вызова prep_score().
Теперь количество пройденных уровней отображается на экране (рис. 14.5).
Рис. 14.5. Текущий уровень выводится под текущим счетом
ПРИМЕЧАНИЕ
В некоторых классических играх выводимая информация снабжается текстовыми метками: «Уровень», «Рекорд» и т.д. Мы их опустили, поскольку смысл этих чисел понятен каждому, кто сыграл в «Инопланетное вторжение». Если вы хотите видеть эти метки, то добавьте их в строки непосредственно перед вызовами метода font.render() в классе Scoreboard.
Остается вывести количество кораблей, оставшихся у игрока, однако на этот раз информация будет выводиться в графическом виде. Как во многих классических аркадных играх, в левом верхнем углу экрана программа рисует несколько изображений корабля. Каждый корабль обозначает одну оставшуюся попытку.
Для начала нужно сделать так, чтобы класс Ship наследовал от Sprite, — это необходимо для создания группы кораблей:
ship.py
import pygame
from pygame.sprite import Sprite
❶ class Ship(Sprite):
# Класс для управления кораблем.
def __init__(self, ai_game):
"""Инициализирует корабль и задает его начальную позицию."""
❷ super().__init__()
--пропуск--
Здесь мы импортируем Sprite, объявляем о наследовании Ship от Sprite ❶ и вызываем super() в начале метода __init__() ❷.
Далее необходимо изменить класс Scoreboard и создать группу кораблей для вывода на экран. Операторы import выглядят так:
scoreboard.py
import pygame.font
from pygame.sprite import Group
from ship import Ship
Мы собираемся создать группу кораблей, поэтому программа импортирует классы Group и Ship.
Метод __init__() выглядит так:
scoreboard.py
def __init__(self, ai_game):
"""Инициализирует атрибуты подсчета очков."""
self.ai_game = ai_game
self.screen = ai_game.screen
--пропуск--
self.prep_level()
self.prep_ships()
Экземпляр игры присваивается атрибуту, так как он понадобится нам для создания кораблей. Метод prep_ships() будет вызываться после prep_level() и выглядит так:
scoreboard.py
def prep_ships(self):
"""Сообщает количество оставшихся кораблей."""
❶ self.ships = Group()
❷ for ship_number in range(self.stats.ships_left):
ship = Ship(self.ai_game)
❸ ship.rect.x = 10 + ship_number * ship.rect.width
❹ ship.rect.y = 10
❺ self.ships.add(ship)
Метод prep_ships() создает пустую группу self.ships для хранения экземпляров кораблей ❶. В ходе ее заполнения цикл выполняется по одному разу для каждого корабля, оставшегося у игрока ❷. В цикле создается новый корабль, а его координата x задается так, чтобы корабли размещались рядом друг с другом, разделенные интервалами по 10 пикселов ❸. Координата y задается так, чтобы корабли были смещены на 10 пикселов от верхнего края экрана и выровнены по изображению текущего счета ❹. Наконец, каждый корабль добавляется в группу ships ❺.
Осталось вывести корабли на экран:
scoreboard.py
def show_score(self):
"""Выводит счета, уровень и количество кораблей на экран."""
self.screen.blit(self.score_image, self.score_rect)
self.screen.blit(self.high_score_image, self.high_score_rect)
self.screen.blit(self.level_image, self.level_rect)
self.ships.draw(self.screen)
При выводе кораблей на экран мы вызываем метод draw() для группы, а Pygame рисует каждый отдельный корабль.
Чтобы игрок видел, сколько попыток у него в начале игры, мы вызываем prep_ships() при запуске новой игры. Это происходит в функции _check_play_button() в классе AlienInvasion:
alien_invasion.py
def _check_play_button(self, mouse_pos):
--пропуск--
if button_clicked and not self.game_active:
--пропуск--
self.sb.prep_level()
self.sb.prep_ships()
--пропуск--
Метод prep_ships() также вызывается при столкновении пришельца с кораблем, чтобы изображение обновлялось при потере корабля:
alien_invasion.py
def _ship_hit(self):
"""Обрабатывает столкновение корабля с пришельцем."""
if self.stats.ships_left > 0:
# Уменьшение ships_left и обновление панели счета.
self.stats.ships_left -= 1
self.sb.prep_ships()
--пропуск--
Метод prep_ships() вызывается после уменьшения значения ships_left, так что при каждой потере корабля выводится правильное количество изображений.
На рис. 14.6 показана готовая система подсчета очков, при этом количество оставшихся кораблей отображается в левой верхней части экрана.
Рис. 14.6. Готовая система подсчета очков в игре «Инопланетное вторжение»
Упражнения
14.5. Исторический рекорд. В текущей версии рекорд сбрасывается каждый раз, когда игрок закрывает игру и перезапускает ее. Чтобы этого не происходило, запишите рекорд в файл перед вызовом sys.exit() и загрузите его при инициализации значения в классе GameStats.
14.6. Рефакторинг. Найдите функции и методы, которые решают несколько задач, и проведите рефакторинг, улучшающий структуру и эффективность кода. Например, переместите часть кода функции _check_bullet_alien_collisions(), которая запускает новый уровень при уничтожении флота, в функцию start_new_level(). Переместите также четыре метода, вызываемых в методе __init__() класса Scoreboard, в метод prep_images() для сокращения длины метода __init__(). Метод prep_images() также может быть полезным для _check_play_button() или start_game(), если вы уже провели рефакторинг _check_play_button().
ПРИМЕЧАНИЕ
Прежде чем браться за рефакторинг проекта, обратитесь к приложению Г. В нем рассказано, как восстановить рабочее состояние проекта, если в ходе рефакторинга были допущены ошибки.
14.7. Расширение игры. Подумайте над возможными расширениями. Например, пришельцы тоже могут стрелять по кораблю, или же вы можете добавить укрытия, за которыми может скрываться корабль (укрытия могут разрушаться снарядами, выпускаемыми с обеих сторон). Или добавьте звуковые эффекты (например, взрывы или звуки выстрелов), используя средства модуля pygame.mixer.
14.8. Боковая стрельба, финальная версия. Продолжайте разрабатывать приложение с боковой стрельбой, используя все, чему научились в этом проекте. Добавьте кнопку Play, обеспечьте ускорение игры в нужных местах и разработайте систему начисления очков. Не забывайте проводить рефакторинг в процессе работы и ищите возможности добавить такие настройки игры, которые не были показаны в этой главе.
В этой главе вы узнали, как создать кнопку Play для запуска новой игры, обнаруживать события мыши и скрывать указатель мыши в активных играх. Полученные знания помогут вам создать другие кнопки в играх — например, кнопку Help для вывода инструкций. Кроме того, вы научились изменять скорость по ходу игры, создавать прогрессивную систему подсчета очков и выводить информацию в текстовом и графическом виде.