В больших проектах перед добавлением нового кода часто проводится рефакторинг уже написанного кода. Рефакторинг упрощает структуру существующего кода и дальнейшее развитие проекта. В этом разделе мы создадим новый модуль game_functions для хранения функций, обеспечивающих работу игры. Модуль game_functions предотвратит чрезмерное разрастание alien_invasion.py и сделает логику alien_invasion.py более простой и понятной.
Функция check_events()
Начнем с перемещения кода управления событиями в отдельную функцию check_events(). Тем самым вы упростите run_game() и изолируете цикл управления событиями от остального кода. Изоляция цикла событий позволит организовать управление событиями отдельно от других аспектов игры (например, обновления экрана).
Поместим check_events() в отдельный модуль с именем game_functions:
game_functions.py
import sys
import pygame
def check_events():
. ."""Обрабатывает нажатия клавиш и события мыши."""
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
Этот модуль импортирует модули sys и pygame, используемые в цикле обработки событий. На данный момент эта функция не получает параметров, а ее тело копируется из цикла событий в alien_invasion.py.
Теперь изменим код alien_invasion.py, чтобы он импортировал модуль game_functions, и мы заменим цикл событий вызовом check_events():
alien_invasion.py
import pygame
from settings import Settings
from ship import Ship
import game_functions as gf
def run_game():
...
# Запуск основного цикла игры.
while True:
. . . .gf.check_events()
. . . . . . . .
# При каждом проходе цикла перерисовывается экран.
...
Импортировать модуль sys прямо в главный файл в программы уже не нужно, потому что он сейчас используется только в модуле game_functions. Импортируемому модулю game_functions для удобства присваивается псевдоним gf.
Функция update_screen()
Для дальнейшего упрощения run_game() выделим код обновления экрана в отдельную функцию update_screen() в game_functions.py:
game_functions.py
...
def check_events():
...
def update_screen(ai_settings, screen, ship):
. ."""Обновляет изображения на экране и отображает новый экран."""
# При каждом проходе цикла перерисовывается экран.
screen.fill(ai_settings.bg_color)
ship.blitme()
# Отображение последнего прорисованного экрана.
pygame.display.flip()
Новая функция update_screen() получает три параметра: ai_settings, screen и ship. Теперь необходимо заменить цикл while из alien_invasion.py вызовом update_screen():
alien_invasion.py
...
# Запуск основного цикла игры.
while True:
gf.check_events()
. . . .gf.update_screen(ai_settings, screen, ship)
run_game()
Эти две функции упрощают цикл while и процесс дальнейшей разработки. Бульшая часть работы будет выполняться не в run_game(), а в модуле game_functions.
Так как мы решили начать работу с кодом c одного файла, мы не стали вводить модуль game_functions с самого начала. Эта последовательность дает представление о реальном процессе разработки: сначала вы пишете свой код в самом простом виде, а потом подвергаете его рефакторингу по мере роста сложности проекта.
Теперь, когда мы изменили структуру кода и упростили его расширение, можно переходить к динамическим аспектам игры!
Упражнения
12-1. Синее небо: создайте окно Pygame с синим фоном.
12-2. Игровой персонаж: найдите изображение игрового персонажа, который вам нравится, в формате .bmp (или преобразуйте существующее изображение). Создайте класс, который рисует персонажа в центре экрана, и приведите цвет фона изображения в соответствие с цветом фона экрана (или наоборот).
Управление кораблем
Реализуем возможность перемещения корабля по горизонтали. Для этого мы напишем код, реагирующий на нажатие клавиш или . Начнем с движения вправо, а потом применим те же принципы к движению влево. Заодно вы научитесь управлять перемещением изображений на экране.
Обработка нажатия клавиши
Каждый раз, когда пользователь нажимает клавишу, это нажатие регистрируется в Pygame как событие. Каждое событие идентифицируется методом pygame.event.get(), поэтому в функции check_events() необходимо указать, какие события должны отслеживаться. Каждое нажатие клавиши регистрируется как событие KEYDOWN.
При обнаружении события KEYDOWN необходимо проверить, была ли нажата клавиша, инициирующая некоторое игровое событие. Например, при нажатии клавиши значение rect.centerx корабля увеличивается для перемещения корабля вправо:
game_functions.py
def check_events(ship):
"""Обрабатывает нажатия клавиш и события мыши."""
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
(1) . . . .elif event.type == pygame.KEYDOWN:
(2) . . . . . .if event.key == pygame.K_RIGHT:
. . . . . . . .# Переместить корабль вправо.
(3) . . . . . . . .ship.rect.centerx += 1
Функции check_events() передается параметр ship, потому что корабль должен двигаться вправо при нажатии клавиши . Внутри check_events() в цикл событий добавляется блок elif для выполнения кода при обнаружении события KEYDOWN (1) . Чтобы проверить, является ли нажатая клавиша клавишей (pygame.K_RIGHT), мы читаем атрибут event.key (2). Если нажата клавиша , корабль перемещается вправо, для чего значение ship.rect.centerx увеличивается на 1 (3).
Вызов check_events() в alien_invasion.py необходимо изменить, чтобы в аргументе передавался объект ship:
alien_invasion.py
# Запуск основного цикла игры.
while True:
. . . .gf.check_events(ship)
gf.update_screen(ai_settings, screen, ship)
Если запустить программу alien_invasion.py сейчас, вы увидите, что корабль перемещается вправо на 1 пиксел при каждом нажатии клавиши . Неплохо для начала, но это не лучший способ управления кораблем. Чтобы управление было более удобным, следует реализовать возможность непрерывного перемещения.
Непрерывное перемещение
Если игрок удерживает клавишу , корабль должен двигаться вправо до тех пор, пока клавиша не будет отпущена. Чтобы узнать, когда клавиша будет отпущена, наша игра отслеживает событие pygame.KEYUP; таким образом, реализация непрерывного движения будет основана на отслеживании событий KEYDOWN и KEYUP в сочетании с флагом moving_right.
В неподвижном состоянии корабля флаг moving_right равен False. При нажатии клавиши флагу присваивается значение True, а когда клавиша будет отпущена, флаг возвращается в состояние False.
Класс Ship управляет всеми атрибутами корабля, и мы добавим в него атрибут с именем moving_right и метод update() для проверки состояния флага moving_right. Метод update() изменяет позицию корабля, если флаг содержит значение True. Этот метод будет вызываться каждый раз, когда вы хотите обновить позицию корабля.
Ниже приведены изменения в классе Ship:
ship.py
class Ship():
def __init__(self, screen):
...
# Каждый новый корабль появляется у нижнего края экрана.
self.rect.centerx = self.screen_rect.centerx
self.rect.bottom = self.screen_rect.bottom
. . . .# Флаг перемещения
(1) . . . .self.moving_right = False
(2) . .def update(self):
. . . ."""Обновляет позицию корабля с учетом флага."""
. . . .if self.moving_right:
. . . . . .self.rect.centerx += 1
. .def blitme(self):
. . . ....
Мы добавляем атрибут self.moving_right в методе __init__() и инициализируем его значением False (1) . Затем вызывается метод update(), который перемещает корабль вправо, если флаг равен True (2).
Теперь внесем изменения в check_events(), чтобы при нажатии клавиши moving_right присваивалось значение True, а при ее отпускании — False:
game_functions.py
def check_events(ship):
"""Обрабатывает нажатия клавиш и события мыши."""
for event in pygame.event.get():
...
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RIGHT:
(1) . . . . . . . .ship.moving_right = True
(2) . . . .elif event.type == pygame.KEYUP:
. . . . . .if event.key == pygame.K_RIGHT:
. . . . . . . .ship.moving_right = False
В точке (1) изменяется реакция игры при нажатии клавиши ; вместо непосредственного изменения позиции корабля программа просто присваивает moving_right значение True. В точке (2) добавляется новый блок elif, реагирующий на события KEYUP. Когда игрок отпускает клавишу (K_RIGHT), moving_right присваивается значение False.
Остается изменить цикл while в alien_invasion.py, чтобы при каждом проходе цикла вызывался метод update() корабля:
alien_invasion.py
# Запуск основного цикла игры.
while True:
gf.check_events(ship)
. . . .ship.update()
gf.update_screen(ai_settings, screen, ship)
Позиция корабля будет обновляться после проверки событий клавиатуры, но перед обновлением экрана. Таким образом, позиция корабля обновляется в ответ на действия пользователя и будет использоваться при перерисовке корабля на экране.
Если запустить alien_invasion.py и удерживать клавишу , корабль непрерывно двигается вправо, пока клавиша не будет отпущена.
Перемещение влево и вправо
Теперь, когда мы реализовали непрерывное движение вправо, добавить движение влево относительно несложно. Для этого нужно снова изменить класс Ship и функцию check_events(). Ниже приведены необходимые изменения в __init__() и update() в классе Ship:
ship.py
def __init__(self, screen):
...
. . . .# Флаги перемещения
self.moving_right = False
. . . .self.moving_left = False
. . . .
def update(self):
. . . ."""Обновляет позицию корабля с учетом флагов."""
if self.moving_right:
self.rect.centerx += 1
. . . .if self.moving_left:
. . . . . .self.rect.centerx -= 1
В методе __init__() добавляется флаг self.moving_left. В update() используются два отдельных блока if вместо elif, чтобы при нажатии обеих клавиш со стрелками атрибут rect.centerx сначала увеличивался, а потом уменьшался. В результате корабль остается на месте. Если бы для движения влево использовался блок elif, то клавиша всегда имела бы приоритет. Такая реализация повышает точность перемещения при переключении направления, когда игрок может ненадолго удерживать нажатыми обе клавиши.
В check_events() необходимо внести два изменения:
game_functions.py
def check_events(ship):
"""Обрабатывает нажатия клавиш и события мыши."""
for event in pygame.event.get():
...
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RIGHT:
ship.moving_right = True
. . . . . .elif event.key == pygame.K_LEFT:
. . . . . . . .ship.moving_left = True
elif event.type == pygame.KEYUP:
if event.key == pygame.K_RIGHT:
ship.moving_right = False
. . . . . .elif event.key == pygame.K_LEFT:
. . . . . . . .ship.moving_left = False
Если событие KEYDOWN происходит для события K_LEFT, то moving_left присваивается True. Если событие KEYUP происходит для события K_LEFT, то moving_left присваивается False. Здесь возможно использовать блоки elif, потому что каждое событие связано только с одной клавишей. Если же игрок нажимает обе клавиши одновременно, то программа обнаруживает два разных события.
Если вы запустите alien_invasion.py, то увидите, что корабль может непрерывно двигаться влево и вправо. Если же нажать обе клавиши, корабль останавливается.
Следующий шаг — доработка движения корабля. Внесем изменения в скорость и ограничим величину перемещения, чтобы корабль не выходил за края экрана.
Регулировка скорости корабля
В настоящий момент корабль смещается на один пиксел за каждый проход цикла while, но для повышения точности управления скоростью можно добавить в класс Settings атрибут ship_speed_factor. Этот атрибут определяет величину смещения корабля при каждом проходе цикла. Новый атрибут settings.py выглядит так:
settings.py
class Settings():
"""Класс для хранения всех настроек игры Alien Invasion."""
def __init__(self):
...
. . . .# Настройки корабля
Переменной ship_speed_factor присваивается значение 1.5. При перемещении корабля его позиция изменяется на 1,5 пиксела вместо 1. Дробные значения скорости позволят лучше управлять скоростью корабля при последующем повышении темпа игры. Однако атрибуты прямоугольников (такие, как centerx) принимают только целочисленные значения, поэтому в Ship необходимо внести ряд изменений:
ship.py
class Ship():
(1) . .def __init__(self, ai_settings, screen):
"""Инициализирует корабль и задает его начальную позицию."""
self.screen = screen
(2) . . . .self.ai_settings = ai_settings
...
# Каждый новый корабль появляется у нижнего края экрана
...
. . . .
. . . .# Сохранение вещественной координаты центра корабля.
(3) . . . .self.center = float(self.rect.centerx)
. . . .
# Флаги перемещения
self.moving_right = False
self.moving_left = False
. . . .def update(self):
"""Обновляет позицию корабля с учетом флагов."""
. . . .# Обновляется атрибут center, не rect.
if self.moving_right:
(4) . . . . . .self.center += self.ai_settings.ship_speed_factor
if self.moving_left:
. . . . . .self.center -= self.ai_settings.ship_speed_factor
. . . .
. . . .# Обновление атрибута rect на основании self.center.
(5) . . . .self.rect.centerx = self.center
def blitme(self):
...
В точке (1) в список параметров __init__() добавляется параметр ai_settings, чтобы для корабля была доступна величина его скорости. Затем параметр ai_settings преобразуется в атрибут для использования в update() (2). Так как позиция корабля изменяется с нецелым приращением пикселов, она должна храниться в переменной, способной хранить дробные значения. Формально атрибутам rect можно присвоить дробные значения, но rect сохранит только целую часть этого значения. Для точного хранения позиции корабля определяется новый атрибут self.center, способный хранить дробные значения (3). Функция float() используется для преобразования значения self.rect.centerx в вещественный формат и сохранения этого значения в self.center.
После изменения позиции корабля в update() значение self.center изменяется на величину, хранящуюся в ai_settings.ship_speed_factor (4). После обновления self.center новое значение используется для обновления атрибута self.rect.centerx, управляющего позицией корабля (5). В self.rect.centerx будет сохранена только целая часть self.center, но для отображения корабля этого достаточно.
Значение ai_settings должно передаваться в аргументе при создании экземпляра Ship в alien_invasion.py:
alien_invasion.py
...
def run_game():
...
# Создание корабля.
. .ship = Ship(ai_settings, screen)
...
Теперь с любым значением ship_speed_factor, бульшим 1, корабль будет двигаться быстрее. Эта возможность ускорит реакцию корабля на действия игрока, а также позволит нам изменить темп игры с течением времени.
Ограничение перемещений
Если удерживать какую-нибудь клавишу со стрелкой достаточно долго, корабль выйдет за край экрана. Давайте сделаем так, чтобы корабль останавливался при достижении края экрана. Задача решается изменением метода update() в классе Ship:
ship.py
def update(self):
"""Обновляет позицию корабля с учетом флагов."""
# Обновляется атрибут center, не rect.
(1) . . . .if self.moving_right and self.rect.right < self.screen_rect.right:
self.center += self.ai_settings.ship_speed_factor
(2) . . . .if self.moving_left and self.rect.left > 0:
self.center -= self.ai_settings.ship_speed_factor
. . . .
# Обновление атрибута rect на основании self.center
self.rect.centerx = self.center
Этот код проверяет позицию корабля перед изменением значения self.center. Выражение self.rect.right возвращает координату x правого края прямоугольника корабля. Если это значение меньше значения, возвращаемого self.screen_rect.right, значит, корабль еще не достиг правого края экрана (1) . То же относится и к левому краю: если координата x левой стороны прямоугольника больше 0, значит, корабль еще не достиг левого края экрана (2). Проверка гарантирует, что корабль будет оставаться в пределах экрана, перед изменением значения self.center.
Если вы запустите alien_invasion.py сейчас, то движение корабля будет останавливаться у края экрана.
Рефакторинг check_events()
В ходе разработки функция check_events() будет становиться все длиннее, поэтому мы выделим из check_events() еще две функции: для обработки событий KEYDOWN и для обработки событий KEYUP:
game_functions.py
def check_keydown_events(event, ship):
. ."""Реагирует на нажатие клавиш."""
if event.key == pygame.K_RIGHT:
ship.moving_right = True
elif event.key == pygame.K_LEFT:
ship.moving_left = True
def check_keyup_events(event, ship):
. ."""Реагирует на отпускание клавиш."""
if event.key == pygame.K_RIGHT:
ship.moving_right = False
elif event.key == pygame.K_LEFT:
ship.moving_left = False
def check_events(ship):
"""Обрабатывает нажатия клавиш и события мыши."""
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
. . . . . .check_keydown_events(event, ship)
elif event.type == pygame.KEYUP:
. . . . . .check_keyup_events(event, ship)
В программе появились две новые функции: check_keydown_events() и check_keyup_events(). Каждая функция получает параметр event и параметр ship. Тела двух функций скопированы из check_events(), а старый код заменен вызовами новых функций. Новая структура кода упрощает функцию check_events() и облегчает последующее программирование реакции на действия игрока.