Изучаем Python. Программирование игр, визуализация данных, веб-приложения

Мэтиз Эрик

14. Ведение счета

 

В этой главе построение игры Alien Invasion будет завершено. Мы добавим кнопку Play для запуска игры по желанию игрока или перезапуска игры после ее завершения. Мы также изменим игру, чтобы она ускорялась при переходе игрока на следующий уровень, и реализуем систему подсчета очков. К концу главы вы будете знать достаточно, чтобы заняться разработкой игр, сложность которых нарастает по ходу игры и в которых реализована система подсчета очков.

 

Добавление кнопки Play

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

В текущей версии игра начинается сразу же после запуска alien_invasion.py. После очередных изменений игра будет запускаться в неактивном состоянии и предлагать игроку нажать кнопку Play для запуска. Для этого включите следующий код в game_stats.py:

game_stats.py

def __init__(self, ai_settings):

"""Инициализирует статистику."""

self.ai_settings = ai_settings

self.reset_stats()

. .# Игра запускается в неактивном состоянии.

. .self.game_active = False

def reset_stats(self):

...

Итак, программа запускается в неактивном состоянии, а игру можно запустить только нажатием кнопки Play.

 

Создание класса Button

Так как в Pygame не существует встроенного метода создания кнопок, мы напишем класс Button для создания заполненного прямоугольника с текстовой надписью. Следующий код может использоваться для создания кнопок в любой игре. Ниже приведена первая часть класса Button; сохраните ее в файле button.py:

button.py

import pygame.font

class Button():

(1) . .def __init__(self, ai_settings, screen, msg):

. . . ."""Инициализирует атрибуты кнопки."""

. . . .self.screen = screen

. . . .self.screen_rect = screen.get_rect()

. . . .

. . . .# Назначение размеров и свойств кнопок.

(2) . . . .self.width, self.height = 200, 50

. . . .self.button_color = (0, 255, 0)

. . . .self.text_color = (255, 255, 255)

(3) . . . .self.font = pygame.font.SysFont(None, 48)

. . . .

. . . .# Построение объекта rect кнопки и выравнивание по центру экрана.

(4) . . . .self.rect = pygame.Rect(0, 0, self.width, self.height)

. . . .self.rect.center = self.screen_rect.center

. . . .

. . . .# Сообщение кнопки создается только один раз.

(5) . . . .self.prep_msg(msg)

Сначала программа импортирует модуль pygame.font, который позволяет Pygame выводить текст на экран. Метод __init__() получает параметры self, объекты ai_settings и screen, а также строку msg с текстом кнопки (1) . Размеры кнопки задаются в точке (2), после чего атрибуты button_color и text_color задаются так, чтобы прямоугольник кнопки был окрашен в ярко-зеленый цвет, а текст выводился белым цветом.

В точке (3) происходит подготовка атрибута font для вывода текста. Аргумент None сообщает Pygame, что для вывода текста должен использоваться шрифт по умолчанию, а значение 48 определяет размер текста. Чтобы выровнять кнопку по центру экрана, мы создаем объект rect для кнопки (4) и задаем его атрибут center в соответствии с одноименным атрибутом экрана.

Pygame выводит строку текста в виде графического изображения. В точке (5) эта задача решается методом prep_msg(). Код prep_msg() выглядит так:

button.py

def prep_msg(self, msg):

. ."""Преобразует msg в прямоугольник и выравнивает текст по центру."""

(1) . .self.msg_image = self.font.render(msg, True, self.text_color,

. . . .self.button_color)

(2) . .self.msg_image_rect = self.msg_image.get_rect()

. .self.msg_image_rect.center = self.rect.center

Метод prep_msg() должен получать параметр self и текст, который нужно вывести в графическом виде (msg). Вызов font.render() преобразует текст, хранящийся в msg, в изображение, которое затем сохраняется в msg_image (1) . Методу font.render() также передается логический признак режима сглаживания текста. В остальных аргументах передаются цвет шрифта и цвет фона. В нашем примере режим сглаживания включен (True), а цвет фона совпадает с цветом фона кнопки. (Если цвет фона не указан, Pygame пытается вывести шрифт с прозрачным фоном.)

В точке (3) изображение текста выравнивается по центру кнопки, для чего создается объект 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. Так как нам нужна только одна кнопка Play, мы создадим кнопку прямо в файле alien_invasion.py:

alien_invasion.py

...

from game_stats import GameStats

from button import Button

...

def run_game():

...

pygame.display.set_caption("Alien Invasion")

. .

. .# Создание кнопки Play.

(1) . .play_button = Button(ai_settings, screen, "Play")

...

# Запуск основного цикла игры.

while True:

...

(2) . . . .gf.update_screen(ai_settings, screen, stats, ship, aliens, bullets,

. . . . . .play_button)

run_game()

Программа импортирует класс Button и создает экземпляр play_button (1) , после чего передает play_button функции update_screen(), чтобы кнопка появлялась при обновлении экрана (2).

Затем следует внести изменения в update_screen(), чтобы кнопка Play появлялась только в неактивном состоянии игры:

game_functions.py

def update_screen(ai_settings, screen, stats, ship, aliens, bullets,

. . . .play_button):

"""Обновляет изображения на экране и отображает новый экран."""

...

. .

. .# Кнопка Play отображается в том случае, если игра неактивна.

. .if not stats.game_active:

. . . .play_button.draw_button()

. . . . . .

# Отображение последнего прорисованного экрана.

pygame.display.flip()

Чтобы кнопка Play не закрывалась другими элементами экрана, мы отображаем ее после всех остальных игровых элементов, но перед переключением на новый экран. Теперь при запуске Alien Invasion в центре экрана отображается кнопка Play (рис. 14.1).

Рис. 14.1. Кнопка Play выводится тогда, когда игра неактивна

 

Запуск игры

Чтобы при нажатии кнопки Play запускалась новая игра, добавьте в файл game_functions.py следующий код для отслеживания событий мыши над кнопкой:

game_functions.py

def check_events(ai_settings, screen, stats, play_button, ship, bullets):

"""Обрабатывает нажатия клавиш и события мыши."""

for event in pygame.event.get():

if event.type == pygame.QUIT:

...

(1) . . . .elif event.type == pygame.MOUSEBUTTONDOWN:

(2) . . . . . .mouse_x, mouse_y = pygame.mouse.get_pos()

(3) . . . . . .check_play_button(stats, play_button, mouse_x, mouse_y)

def check_play_button(stats, play_button, mouse_x, mouse_y):

. ."""Запускает новую игру при нажатии кнопки Play."""

(4) . .if play_button.rect.collidepoint(mouse_x, mouse_y):

. . . .stats.game_active = True

Обновленное определение check_events() получает параметры stats и play_button. Параметр stats будет использоваться для обращения к флагу game_active, а play_button — для проверки того, была ли нажата кнопка Play.

Pygame обнаруживает событие MOUSEBUTTONDOWN, когда игрок щелкает в любой точке экрана (1) , но мы хотим ограничить игру, чтобы она реагировала только на щелчки на кнопке Play. Для этого будет использоваться метод pygame.mouse.get_pos(), возвращающий кортеж с координатами x и y точки щелчка (2). Эти значения передаются функции check_play_button() (3), которая использует метод collidepoint() для проверки того, находится ли точка щелчка в пределах области, определяемой прямоугольником кнопки Play (4). Если точка находится в пределах кнопки, флаг game_active переводится в состояние True, и игра начинается!

При вызове check_events() в alien_invasion.py должны передаваться два дополнительных аргумента, stats и play_button:

alien_invasion.py

# Запуск основного цикла игры.

while True:

gf.check_events(ai_settings, screen, stats, play_button, ship,

. . bullets)

...

К этому моменту вы сможете запустить и сыграть полноценную игру. После завершения игры значение game_active становится равным False, а кнопка Play снова появится на экране.

 

Сброс игры

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

Чтобы игра сбрасывалась при каждом нажатии кнопки Play, необходимо сбросить игровую статистику, стереть старых пришельцев и пули, построить новый флот и вернуть корабль в центр нижней стороны:

game_functions.py

def check_play_button(ai_settings, screen, stats, play_button, ship, aliens,

. . . .bullets, mouse_x, mouse_y):

"""Запускает новую игру при нажатии кнопки Play."""

if play_button.rect.collidepoint(mouse_x, mouse_y):

. . . .# Сброс игровой статистики.

(1) . . . .stats.reset_stats()

stats.game_active = True

. . . .

. . . .# Очистка списков пришельцев и пуль.

(2) . . . .aliens.empty()

. . . .bullets.empty()

. . . .

. . . .# Создание нового флота и размещение корабля в центре.

(3) . . . .create_fleet(ai_settings, screen, ship, aliens)

. . . .ship.center_ship()

Мы обновляем определение check_play_button(), чтобы в нем были доступны объекты ai_settings, stats, ship, aliens и bullets. Эти объекты необходимы для сброса настроек, изменившихся в ходе игры, и для обновления визуальных элементов игры.

В точке (1) обновляется игровая статистика, вследствие чего игрок получает три новых корабля. После этого флаг game_active переводится в состояние True (чтобы игра началась сразу же после выполнения кода функции), группы aliens и bullets очищаются (2), создается новый флот, а корабль выравнивается по ­центру (3).

Для этого необходимо изменить определение check_events(), как и вызов check_play_button():

game_functions.py

def check_events(ai_settings, screen, stats, play_button, ship, aliens,

. . . .bullets):

"""Обрабатывает нажатия клавиш и события мыши."""

for event in pygame.event.get():

if event.type == pygame.QUIT:

...

elif event.type == pygame.MOUSEBUTTONDOWN:

mouse_x, mouse_y = pygame.mouse.get_pos()

(1) . . . . . .check_play_button(ai_settings, screen, stats, play_ button, ship,

. . . . . . . .aliens, bullets, mouse_x, mouse_y)

Определению check_events() необходим параметр aliens, который будет передаваться check_play_button(). Также обновляется вызов check_play_button() с включением соответствующих аргументов (1) .

Теперь обновите вызов check_events() в alien_invasion.py, чтобы в нем передавался аргумент aliens:

alien_invasion.py

# Запуск основного цикла игры.

while True:

. .gf.check_events(ai_settings, screen, stats, play_button, ship,

. . . .aliens, bullets)

...

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

 

Блокировка кнопки Play

У кнопки Play в нашем приложении есть одна проблема: область кнопки на экране продолжает реагировать на щелчки, даже если кнопка Play не отображается. Если случайно щелкнуть на месте кнопки Play после начала игры, то игра перезапустится!

Чтобы исправить этот недостаток, следует запускать игру только в том случае, если флаг game_active находится в состоянии False:

game_functions.py

def check_play_button(ai_settings, screen, stats, play_button, ship, aliens,

bullets, mouse_x, mouse_y):

"""Запускает новую игру при нажатии кнопки Play."""

(1) . .button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)

(2) . .if button_clicked and not stats.game_active:

# Сброс игровой статистики.

...

Флаг button_clicked содержит значение True или False (1) ; а игра перезапускается только в том случае, если пользователь нажал кнопку Play, а игра не активна в данный момент (2). Чтобы протестировать это поведение, запустите новую игру и многократно щелкайте в том месте, где должна находиться кнопка Play. Если все работает так, как положено, нажатия кнопки Play не должны влиять на ход игры.

 

Сокрытие указателя мыши

Указатель мыши должен быть видимым, чтобы пользователь мог начать игру, но после начала игры он только мешает. Чтобы исправить этот недостаток, мы скроем указатель мыши после того, как игра станет активной:

game_functions.py

def check_play_button(ai_settings, screen, stats, play_button, ship, aliens,

bullets, mouse_x, mouse_y):

"""Запускает новую игру при нажатии кнопки Play."""

button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)

if button_clicked and not stats.game_active:

. . . .# Указатель мыши скрывается.

. . . .pygame.mouse.set_visible(False)

...

Вызов set_visible() со значением False приказывает Pygame скрыть указатель, когда он находится над окном игры.

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

game_functions.py

def ship_hit(ai_settings, screen, stats, ship, aliens, bullets):

"""Обрабатывает столкновение корабля с пришельцем."""

if stats.ships_left > 0:

...

else:

stats.game_active = False

. . . .pygame.mouse.set_visible(True)

Указатель снова становится видимым сразу же после того, как игра становится неактивной, что происходит в ship_hit(). Внимание к подобным деталям сделает вашу игру более профессиональной, а игрок сможет сосредоточиться на игре вместо того, чтобы разбираться в сложностях пользовательского интерфейса.

Упражнения

14-1. Запуск игры клавишей P: так как в Alien Invasion игрок управляет кораблем с клавиатуры, для запуска игры также лучше использовать клавиатуру. Добавьте код, с которым игрок сможет запустить игру нажатием клавиши 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

. . . .

. .# Темп ускорения игры

(1) . .self.speedup_scale = 1.1

(2) . .self.initialize_dynamic_settings()

Значения, которые остаются неизменными, по-прежнему инициализируются в методе __init__(). В точке (1) добавляется настройка speedup_scale, управляющая быстротой нарастания скорости; со значением 2 скорость удваивается каждый раз, когда игрок переходит на следующий уровень, а со значением 1 скорость остается постоянной. С таким значением, как 1,1, скорость будет увеличиваться в достаточной степени, чтобы игра усложнилась, но не стала невозможной. Наконец, вызов initialize_dynamic_settings() инициализирует значения атрибутов, которые должны изменяться в ходе игры (2).

Код initialize_dynamic_settings() выглядит так:

settings.py

def initialize_dynamic_settings(self):

. ."""Инициализирует настройки, изменяющиеся в ходе игры."""

. .self.ship_speed_factor = 1.5

. .self.bullet_speed_factor = 3

. .self.alien_speed_factor = 1

. .# fleet_direction = 1 обозначает движение вправо; а -1 - влево.

. .self.fleet_direction = 1

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

Для увеличения скорости корабля, пуль и пришельцев каждый раз, когда игрок достигает нового уровня, в программе используется функция increase_speed():

settings.py

def increase_speed(self):

. ."""Увеличивает настройки скорости."""

. .self.ship_speed_factor *= self.speedup_scale

. .self.bullet_speed_factor *= self.speedup_scale

. .self.alien_speed_factor *= self.speedup_scale

Чтобы увеличить скорость этих игровых элементов, мы умножаем каждую настройку скорости на значение speedup_scale.

Темп игры повышается вызовом increase_speed() в check_bullet_alien_collisions() при уничтожении последнего пришельца во флоте, но перед созданием нового флота:

game_functions.py

def check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets):

...

if len(aliens) == 0:

. . . .# Уничтожение пуль, повышение скорости и создание нового флота.

bullets.empty()

. . . .ai_settings.increase_speed()

create_fleet(ai_settings, screen, ship, aliens)

Изменения значений настроек скорости ship_speed_factor, alien_speed_factor и bullet_speed_factor достаточно для того, чтобы ускорить всю игру!

 

Сброс скорости

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

game_functions.py

def check_play_button(ai_settings, screen, stats, play_button, ship, aliens,

bullets, mouse_x, mouse_y):

"""Запускает новую игру при нажатии кнопки Play."""

button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)

if button_clicked and not stats.game_active:

. . . .# Сброс игровых настроек.

. . . .ai_settings.initialize_dynamic_settings()

# Указатель мыши скрывается.

pygame.mouse.set_visible(False)

...

Игра Alien Invasion стала достаточно сложной и интересной. Каждый раз, когда игрок очищает экран, игра должна слегка ускориться, а ее сложность должна слегка возрасти. Если сложность игры возрастает слишком быстро, уменьшите значение settings.speedup_scale, а если наоборот, сложность недостаточна, — слегка увеличьте это значение. Найдите оптимальное значение, оценивая сложность игры за разумный промежуток времени. Первая пара флотов должна быть простой, несколько следующих — сложными, но возможными, а при последующих попытках сложность должна становиться практически безнадежной.

Упражнения

14-3. Учебная стрельба с нарастающей сложностью: начните с кода упражнения 14-2 (с. 288). Скорость мишени должна увеличиваться по ходу игры, а при нажатии игроком кнопки Play мишень должна возвращаться к исходной скорости.

 

Подсчет очков

Система подсчета очков позволит отслеживать счет игры в реальном времени; также на экране будет выводиться текущий рекорд, уровень и количество оставшихся кораблей. Счет игры также относится к игровой статистике, поэтому мы добавим атрибут score в класс GameStats:

game_stats.py

class GameStats():

...

def reset_stats(self):

"""Инициализирует статистику, изменяющуюся в ходе игры."""

self.ships_left = self.ai_settings.ship_limit

. . . .self.score = 0

Чтобы счет сбрасывался при запуске новой игры, мы инициализируем score в reset_stats() вместо __init__().

 

Вывод счета

Чтобы вывести счет на экран, мы сначала создаем новый класс Scoreboard. Пока этот класс ограничивается выводом текущего счета, но мы используем его для вывода рекордного счета, уровня и количества оставшихся кораблей. Ниже приведена первая часть класса; сохраните ее под именем scoreboard.py:

scoreboard.py

import pygame.font

class Scoreboard():

. ."""Класс для вывода игровой информации."""

(1) . .def __init__(self, ai_settings, screen, stats):

. . . ."""Инициализирует атрибуты подсчета очков."""

. . . .self.screen = screen

. . . .self.screen_rect = screen.get_rect()

. . . .self.ai_settings = ai_settings

. . . .self.stats = stats

. . . .

. . . .# Настройки шрифта для вывода счета.

(2) . . . .self.text_color = (30, 30, 30)

(3) . . . .self.font = pygame.font.SysFont(None, 48)

. . . .# Подготовка исходного изображения.

(4) . . . .self.prep_score()

Так как Scoreboard выводит текст на экран, код начинается с импортирования модуля pygame.font. Затем __init__() передаются параметры ai_settings, screen и stats, чтобы класс мог выводить информацию об отслеживаемых показателях (1) . Далее назначается цвет текста (2) и создается экземпляр объекта шрифта (3).

Чтобы преобразовать выводимый текст в изображение, мы вызываем метод prep_score() (4), который определяется следующим образом:

scoreboard.py

. .def prep_score(self):

. . . ."""Преобразует текущий счет в графическое изображение."""

(1) . . . .score_str = str(self.stats.score)

(2) . . . .self.score_image = self.font.render(score_str, True, self. text_color,

. . . . . .self.ai_settings.bg_color)

. . . .

. . . .# Вывод счета в правой верхней части экрана.

(3) . . . .self.score_rect = self.score_image.get_rect()

(4) . . . .self.score_rect.right = self.screen_rect.right - 20

(5) . . . .self.score_rect.top = 20

В методе prep_score() преобразуем числовое значение stats.score в строку (1) ; эта строка передается методу render(), создающему изображение (2). Чтобы счет был хорошо виден на экране, мы передаем render() цвет фона и цвет текста.

Счет размещается в правой верхней части экрана и расширяется влево с ростом значения и ширины числа. Чтобы счет всегда оставался выровненным по правой стороне, мы создаем прямоугольник rect с именем score_rect (3) и смещаем его правую сторону на 20 пикселов от правого края экрана (4). Затем верхняя сторона прямоугольника смещается на 20 пикселов вниз от верхнего края экрана (5).

Остается создать метод show_score() для вывода построенного графического изображения:

scoreboard.py

def show_score(self):

. ."""Выводит счет на экран."""

. .self.screen.blit(self.score_image, self.score_rect)

Метод выводит счет на экран в позиции, определяемой score_rect.

 

Создание экземпляра Scoreboard

Чтобы вывести счет, мы создадим в alien_invasion.py экземпляр Scoreboard:

alien_invasion.py

...

from game_stats import GameStats

from scoreboard import Scoreboard

...

def run_game():

...

. .# Создание экземпляров GameStats и Scoreboard.

stats = GameStats(ai_settings)

(1) . .sb = Scoreboard(ai_settings, screen, stats)

...

# Запуск основного цикла игры.

while True:

...

(2) . . . .gf.update_screen(ai_settings, screen, stats, sb, ship, aliens,

. . . . . .bullets, play_button)

run_game()

Мы импортируем новый класс Scoreboard и создаем экземпляр sb после создания экземпляра stats (1) . Затем экземпляр sb передается методу update_screen(), чтобы счет можно было вывести на экран (2).

Для отображения счета метод update_screen() изменяется следующим образом:

game_functions.py

def update_screen(ai_settings, screen, stats, sb, ship, aliens, bullets,

. . . .play_button):

...

. .# Вывод счета.

. .sb.show_score()

. .

# Кнопка Play отображается в том случае, если игра неактивна.

if not stats.game_active:

play_button.draw_button()

# Отображение последнего прорисованного экрана.

pygame.display.flip()

Мы добавляем sb в список параметров, определяющих update_screen(), и вызываем show_score() непосредственно перед отображением кнопки Play.

Если запустить Alien Invasion сейчас, в правом верхнем углу экрана отображается счет 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():

game_functions.py

def check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship,

. . . .aliens, bullets):

"""Обработка коллизий пуль с пришельцами."""

# Удаление пуль и пришельцев, участвующих в коллизиях.

collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)

. .if collisions:

(1) . . . .stats.score += ai_settings.alien_points

. . . .sb.prep_score()

...

Мы обновляем определение check_bullet_alien_collisions() и включаем параметры stats и sb, чтобы функция могла обновлять счет и рекорд. При попадании пули в пришельца Pygame возвращает словарь collisions. Программа проверяет, существует ли словарь, и если существует — стоимость пришельца добавляется к счету (1) . Затем вызов prep_score() создает новое изображение для обновленного счета.

Также необходимо обновить определение update_bullets(), чтобы соответствующие аргументы передавались между функциями:

game_functions.py

def update_bullets(ai_settings, screen, stats, sb, ship, aliens, bullets):

"""Обновляет позиции пуль и удаляет старые пули."""

...

. .check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship,

. . . .aliens, bullets)

Определению update_bullets() необходимы дополнительные параметры stats и sb. Кроме того, вызов check_bullet_alien_collisions() должен включать аргументы stats и sb.

Также необходимо изменить вызов update_bullets() в основном цикле while:

alien_invasion.py

# Запуск основного цикла игры.

while True:

gf.check_events(ai_settings, screen, stats, play_button, ship,

aliens, bullets)

if stats.game_active:

ship.update()

. . . .gf.update_bullets(ai_settings, screen, stats, sb, ship, aliens,

. . . . . .bullets)

...

При вызове update_bullets() должны передаваться аргументы stats и sb.

Теперь во время игры вы сможете набирать очки!

 

Начисление очков за все попадания

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

В функции check_bullet_alien_collisions() любая пуля, столкнувшаяся с пришельцем, становится ключом словаря collisions. С каждой пулей связывается значение — список пришельцев, участвующих в коллизии. Переберем словарь collisions и убедимся в том, что очки начисляются за каждого подбитого пришельца:

game_functions.py

def check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship,

aliens, bullets):

...

if collisions:

(1) . . . .for aliens in collisions.values():

. . . . . .stats.score += ai_settings.alien_points * len(aliens)

sb.prep_score()

...

Если словарь collisions был определен, программа перебирает все значения в словаре collisions. Вспомните, что каждое значение представляет собой список пришельцев, в которых попала одна пуля. Стоимость каждого пришельца умножается на количество пришельцев в списке, а результат прибавляется к текущему счету.

Чтобы протестировать эту систему, увеличьте ширину пули до 300 пикселов и убедитесь в том, что игра начисляет очки за каждого пришельца, в которого попала эта большая пуля; затем верните ширину пули к нормальному состоянию.

 

Увеличение стоимости пришельцев

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

settings.py

class Settings():

"""Класс для хранения всех настроек игры Alien Invasion."""

def __init__(self):

...

# Темп ускорения игры

self.speedup_scale = 1.1

. . . .# Темп роста стоимости пришельцев

(1) . . . .self.score_scale = 1.5

. . . .self.initialize_dynamic_settings()

. . . .

def increase_speed(self):

. . . ."""Увеличивает настройки скорости и стоимость пришельцев."""

self.ship_speed_factor *= self.speedup_scale

self.bullet_speed_factor *= self.speedup_scale

self.alien_speed_factor *= self.speedup_scale

. . . .

(2) . . . .self.alien_points = int(self.alien_points * self.score_ scale)

В программе определяется коэффициент прироста начисляемых очков; он называется score_scale (1) . С небольшим увеличением скорости (1,1) игра быстро усложняется, но, чтобы увидеть заметную разницу в очках, необходимо изменять стоимость пришельцев в большем темпе (1,5). После увеличения скорости игры стоимость каждого попадания также увеличивается (2). Чтобы счет возрастал на целое количество очков, в программе используется функция 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):

"""Преобразует текущий счет в графическое изображение."""

(1) . . . .rounded_score = int(round(self.stats.score, -1))

(2) . . . .score_str = "{:,}".format(rounded_score)

self.score_image = self.font.render(score_str, True, self.text_color,

self.ai_settings.bg_color)

...

Функция round() обычно округляет дробное число до заданного количества знаков, переданного во втором аргументе. Но если во втором аргументе передается отрицательное число, round() округляет значение до ближайших десятков, сотен, тысяч и т.д. Код (1) приказывает Python округлить значение stats.score до десятков и сохранить его в rounded_score.

примечание

В Python 2.7 функция round() всегда возвращает дробное значение, поэтому мы используем int(), чтобы гарантировать, что счет будет выводиться в виде целого числа. Если вы используете Python 3, вызов int() можно опустить.

Рис. 14.3. Округленный счет с разделителями групп

В точке (2) директива форматирования строки приказывает Python вставить запятые при преобразовании числового значения в строку — например, ­чтобы вместо 1000000 выводилась строка 1,000,000. Теперь при запуске игры ­всегда будет отображаться аккуратно отформатированный, округленный счет (рис. 14.3).

 

Рекорды

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

game_stats.py

def __init__(self, ai_settings):

...

. .# Рекорд не должен сбрасываться.

. .self.high_score = 0

Так как рекорд не должен сбрасываться при повторном запуске, значение high_score инициализируется в __init__(), а не в reset_stats().

Теперь изменим класс Scoreboard для отображения рекорда. Начнем с метода __init__():

scoreboard.py

def __init__(self, ai_settings, screen, stats):

...

. . . .# Подготовка изображений счетов.

self.prep_score()

(1) . . . .self.prep_high_score()

Рекорд должен отображаться отдельно от текущего счета, поэтому для подготовки его изображения понадобится новый метод prep_high_score():

scoreboard.py

def prep_high_score(self):

. ."""Преобразует рекордный счет в графическое изображение."""

(1) . .high_score = int(round(self.stats.high_score, -1))

(2) . . . .high_score_str = "{:,}".format(high_score)

(3) . . . .self.high_score_image = self.font.render(high_score_str, True,

. . . . . .self.text_color, self.ai_settings.bg_color)

. . . . . .

. . . .# Рекорд выравнивается по центру верхней стороны.

. . . .self.high_score_rect = self.high_score_image.get_rect()

(4) . . . .self.high_score_rect.centerx = self.screen_rect.centerx

(5) . . self.high_score_rect.top = self.score_rect.top

Рекорд округляется до десятков (1) и форматируется с запятыми (2). Затем для рекорда строится графическое изображение (3), выполняется горизонтальное выравнивание прямоугольника по центру экрана (4), а атрибут top прямоугольника приводится в соответствие с верхней стороной изображения счета (5).

Теперь метод 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)

Для обновления рекорда в файл game_functions.py добавляется новая функция check_high_score():

game_functions.py

def check_high_score(stats, sb):

. ."""Проверяет, появился ли новый рекорд."""

(1) . .if stats.score > stats.high_score:

. . . .stats.high_score = stats.score

. . . .sb.prep_high_score()

Рис. 14.4. Рекордный счет выводится в середине экрана

Функция check_high_score() получает два параметра, stats и sb. Параметр stats используется для проверки текущего счета и рекорда, а параметр sb необходим для изменения изображения рекорда при необходимости. В точке (1) программа сравнивает текущий счет с рекордом. Если текущий счет выше, мы обновляем значение high_score и вызываем prep_high_score() для обновления изображения рекорда.

Функция check_high_score() должна вызываться при каждом попадании в пришельца после обновления счета в check_bullet_alien_collisions():

game_functions.py

def check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship,

aliens, bullets):

...

if collisions:

for aliens in collisions.values():

stats.score += ai_settings.alien_points * len(aliens)

sb.prep_score()

. . . .check_high_score(stats, sb)

...

Когда вы впервые играете в Alien Invasion, текущий счет одновременно будет наивысшим, поэтому он будет отображаться и как текущий счет, и как рекорд. Но в начале второй игры ваш предыдущий рекорд должен отображаться в середине, а текущий счет справа, как показано на рис. 14.4.

 

Вывод уровня

Чтобы в игре выводился текущий уровень, сначала в класс GameStats следует включить атрибут для его представления. Чтобы уровень сбрасывался в начале каждой игры, инициализируйте его в reset_stats():

game_stats.py

def reset_stats(self):

. ."""Инициализирует статистику, изменяющуюся в ходе игры."""

. .self.ships_left = self.ai_settings.ship_limit

. .self.score = 0

. .self.level = 1

Чтобы класс Scoreboard выводил текущий уровень (сразу же под текущим счетом), мы вызываем новый метод prep_level() из __init__():

scoreboard.py

def __init__(self, ai_settings, screen, stats):

...

# Подготовка изображений счетов.

self.prep_score()

self.prep_high_score()

. .self.prep_level()

Метод prep_level() выглядит так:

scoreboard.py

. .def prep_level(self):

. . . ."""Преобразует уровень в графическое изображение."""

(1) . . . .self.level_image = self.font.render(str(self.stats.level), True,

. . . . . . . .self.text_color, self.ai_settings.bg_color)

. .

. . . .# Уровень выводится под текущим счетом.

. . . .self.level_rect = self.level_image.get_rect()

(2) . . . .self.level_rect.right = self.score_rect.right

(3) . . . .self.level_rect.top = self.score_rect.bottom + 10

Метод prep_level() создает изображение на базе значения, хранящегося в stats.level (1) , и приводит атрибут right изображения в соответствие с атрибутом right счета (2). Затем атрибут top сдвигается на 10 пикселов ниже нижнего края изображения текущего счета, чтобы между счетом и уровнем оставался пустой интервал (3).

В метод 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():

game_functions.py

def check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship,

aliens, bullets):

...

if len(aliens) == 0:

. . . .# Если весь флот уничтожен, начинается следующий уровень.

bullets.empty()

ai_settings.increase_speed()

. . . .

. . . .# Увеличение уровня.

(1) . . . .stats.level += 1

(2) . . . .sb.prep_level()

. . . .

create_fleet(ai_settings, screen, ship, aliens)

Если все пришельцы уничтожены, программа увеличивает значение stats.level (1) и вызывает prep_level() для обновления уровня (2).

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

game_functions.py

def check_play_button(ai_settings, screen, stats, sb, play_button, ship,

. . . .aliens, bullets, mouse_x, mouse_y):

"""Запускает новую игру при нажатии кнопки Play."""

button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)

if button_clicked and not stats.game_active:

...

# Сброс игровой статистики.

stats.reset_stats()

stats.game_active = True

. . . .

. . . .# Сброс изображений счетов и уровня.

(1) . . . .sb.prep_score()

. . . .sb.prep_high_score()

. . . .sb.prep_level()

. . . .

# Очистка списков пришельцев и пуль.

aliens.empty()

bullets.empty()

. .

...

Определению check_play_button() необходим объект sb. Чтобы сбросить изображения на экране, после сброса игровых настроек следуют вызовы prep_score(), prep_high_score() и prep_level().

Затем объект sb передается check_events(), чтобы объект Scoreboard был доступен для check_play_button():

game_functions.py

def check_events(ai_settings, screen, stats, sb, play_button, ship, aliens,

. . . .bullets):

"""Обрабатывает нажатия клавиш и события мыши."""

for event in pygame.event.get():

if event.type == pygame.QUIT:

...

elif event.type == pygame.MOUSEBUTTONDOWN:

mouse_x, mouse_y = pygame.mouse.get_pos()

(1) . . . . . .check_play_button(ai_settings, screen, stats, sb, play_ button,

. . . . . . . .ship, aliens, bullets, mouse_x, mouse_y)

Определение check_events() должно получать sb в параметре, чтобы при вызове check_play_button() можно было передать sb в аргументе (1) .

Остается обновить вызов check_events() в alien_invasion.py, чтобы в нем также передавался объект sb:

alien_invasion.py

# Запуск основного цикла игры.

while True:

. .gf.check_events(ai_settings, screen, stats, sb, play_button, ship,

. . . .aliens, bullets)

...

Теперь количество пройденных уровней отображается на экране (рис. 14.5).

примечание

В некоторых классических играх выводимая информация снабжается текстовыми метками: «Уровень», «Рекорд» и т.д. Мы эти метки опустили, потому что смысл каждого числа понятен каждому, кто сыграл в Alien Invasion. Если вы включите эти метки, добавьте их в строки непосредственно перед вызовами font.render() в Scoreboard.

Рис. 14.5. Текущий уровень выводится под текущим счетом

 

Вывод количества кораблей

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

Для начала нужно сделать так, чтобы класс Ship наследовал от Sprite, — это необходимо для создания группы кораблей:

ship.py

import pygame

from pygame.sprite import Sprite

(1) class Ship(Sprite):

. .

def __init__(self, ai_settings, screen):

"""Инициализирует корабль и задает его начальную позицию."""

(2) . . . .super(Ship, self).__init__()

...

Здесь мы импортируем Sprite, объявляем о наследовании Ship от Sprite (1) и вызываем super() в начале __init__() (2).

Далее необходимо изменить Scoreboard и создать группу кораблей для вывода на экран. Команды import и метод __init__() выглядят так:

scoreboard.py

import pygame.font

from pygame.sprite import Group

from ship import Ship

class Scoreboard():

"""Класс для вывода игровой информации."""

def __init__(self, ai_settings, screen, stats):

...

self.prep_level()

. . . .self.prep_ships()

...

Так как мы собираемся создать группу кораблей, программа импортирует классы Group и Ship. Метод prep_ships() будет вызываться после prep_level(). Он выглядит так:

scoreboard.py

. .def prep_ships(self):

. . . ."""Сообщает количество оставшихся кораблей."""

(1) . . . .self.ships = Group()

(2) . . . .for ship_number in range(self.stats.ships_left):

. . . . . .ship = Ship(self.ai_settings, self.screen)

(3) . . . . . .ship.rect.x = 10 + ship_number * ship.rect.width

(4) . . . . . .ship.rect.y = 10

(5) . . . . . .self.ships.add(ship)

Метод prep_ships() создает пустую группу self.ships для хранения экземпляров кораблей (1) . В ходе заполнения этой группы цикл выполняется по одному разу для каждого корабля, оставшегося у игрока (3). В цикле создается новый корабль, а координата x этого корабля задается так, чтобы корабли размещались рядом друг с другом, разделенные интервалами величиной 10 пикселов (3). Координата y задается так, чтобы корабли были смещены на 10 пикселов от верхнего края экрана и были выровнены по изображению текущего счета (4). Наконец, каждый корабль добавляется в группу ships (5).

Следующим шагом становится вывод кораблей на экран:

scoreboard.py

def show_score(self):

...

self.screen.blit(self.level_image, self.level_rect)

. .# Вывод кораблей.

. .self.ships.draw(self.screen)

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

Чтобы игрок видел, сколько попыток у него в начале игры, мы вызываем prep_ships() при запуске новой игры. Это происходит в функции check_play_button() из файла game_functions.py:

game_functions.py

def check_play_button(ai_settings, screen, stats, sb, play_button, ship,

aliens, bullets, mouse_x, mouse_y):

"""Запускает новую игру при нажатии кнопки Play."""

button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)

if button_clicked and not stats.game_active:

...

# Сброс изображений счетов и уровня.

sb.prep_score()

sb.prep_high_score()

sb.prep_level()

. . . .sb.prep_ships()

...

Метод prep_ships() также вызывается при столкновении пришельца с кораблем, чтобы изображение обновлялось при потере корабля:

game_functions.py

(1) def update_aliens(ai_settings, screen, stats, sb, ship, aliens, bullets):

...

# Проверка коллизий "пришелец-корабль".

if pygame.sprite.spritecollideany(ship, aliens):

(2) . . . .ship_hit(ai_settings, screen, stats, sb, ship, aliens, bullets)

. .

# Проверяет, добрались ли пришельцы до нижнего края экрана.

(3) . .check_aliens_bottom(ai_settings, screen, stats, sb, ship, aliens, bullets)

(4)def ship_hit(ai_settings, screen, stats, sb, ship, aliens, bullets):

"""Обрабатывает столкновение корабля с пришельцем."""

if stats.ships_left > 0:

# Уменьшение ships_left.

stats.ships_left -= 1

. . . .

. . . .# Обновление игровой информации.

(5) . . . .sb.prep_ships()

# Очистка списков пришельцев и пуль.

...

Сначала параметр sb добавляется в определение update_aliens() (1) . Затем программа передает sb функциям ship_hit() (2) и check_aliens_bottom(), чтобы эти функции имели доступ к объекту Scoreboard (3).

Затем определение ship_hit() изменяется с включением sb (4). Метод prep_ships() вызывается после уменьшения значения ships_left (5), так что при каждой потере корабля выводится правильное количество изображений.

Вызов ship_hit() также включен в check_aliens_bottom(), так что эту функцию тоже нужно обновить:

game_functions.py

def check_aliens_bottom(ai_settings, screen, stats, sb, ship, aliens,

. . . .bullets):

"""Проверяет, добрались ли пришельцы до нижнего края экрана."""

screen_rect = screen.get_rect()

for alien in aliens.sprites():

if alien.rect.bottom >= screen_rect.bottom:

# Происходит то же, что при столкновении с кораблем.

. . . . . .ship_hit(ai_settings, screen, stats, sb, ship, aliens, bullets)

break

Так как check_aliens_bottom() теперь получает параметр sb, мы добавляем аргумент sb в вызов ship_hit().

Остается добавить sb в вызов update_aliens() в файле alien_invasion.py:

alien_invasion.py

# Запуск основного цикла игры.

while True:

...

if stats.game_active:

ship.update()

gf.update_bullets(ai_settings, screen, stats, sb, ship, aliens,

bullets)

. . . .gf.update_aliens(ai_settings, screen, stats, sb, ship, aliens,

. . . . . .bullets)

...

На рис. 14.6 показана полная игровая информация на экране, с количеством оставшихся кораблей в левой верхней части экрана.

Рис. 14.6. Полная игровая информация в Alien Invasion

Упражнения

14-4. Исторический рекорд: в текущей версии рекорд сбрасывается каждый раз, когда игрок закрывает и перезапускает Alien Invasion. Чтобы этого не происходило, запишите рекорд в файл перед вызовом sys.exit() и загрузите его при инициализации значения в GameStats.

14-5. Рефакторинг: найдите функции и методы, которые решают более одной задачи, и проведите рефакторинг, улучшающий структуру и эффективность кода. Например, переместите часть кода функции check_bullet_alien_collisions(), которая запускает новый уровень при уничтожении флота, в функцию start_new_level(). Также переместите четыре метода, вызываемых в методе __init__() класса Scoreboard, в метод prep_images() для сокращения длины __init__(). Метод prep_images() также может оказать помощь check_play_button() или start_game(), если вы уже провели рефакторинг check_play_button().

примечание

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

14-6. Расширение Alien Invasion: подумайте над возможными расширениями Alien Invasion. Например, пришельцы тоже могут стрелять по кораблю, или же вы можете добавить укрытия, за которыми может скрываться корабль (укрытия могут разрушаться пулями с обеих сторон). Или добавьте звуковые эффекты (например, взрывы или звуки выстрелов) средствами модуля pygame.mixer.

 

Итоги

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