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

Мэтиз Эрик

Проект 1. Инопланетное вторжение

 

12. Стреляющий корабль

Давайте создадим собственную игру! Мы воспользуемся Pygame — подборкой интересных, мощных модулей Python для управления графикой, анимацией и даже звуком, упрощающей построение сложных игр. Pygame берет на себя такие задачи, как перерисовка изображений на экране, что позволяет вам пропустить бульшую часть рутинного, сложного программирования и сосредоточиться на высокоуровневой логике игровой динамики.

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

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

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

примечание

Игра Alien Invasion состоит из множества файлов; создайте в своей системе новый каталог с именем alien_invasion. Чтобы команды import работали правильно, все файлы проекта должны находиться в этой папке.

 

Планирование проекта

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

Итак, напишем общее описание игрового процесса. Хотя это описание не затрагивает все аспекты игры, оно дает достаточно четкое представление о том, с чего начинать работу:

Каждый игрок управляет кораблем, который находится в середине нижнего края экрана. Игрок перемещает корабль вправо и влево клавишами управления или курсором; клавиша «пробел» используется для стрельбы. В начале игры флот пришельцев находится в верхней части экрана и постепенно опускается вниз, также смещаясь в сторону. Игрок выстрелами уничтожает пришельцев. Если ему удается сбить всех пришельцев, появляется новый флот, который движется быстрее предыдущего. Если пришелец сталкивается с кораблем игрока или доходит до нижнего края экрана, игрок теряет корабль. Если игрок теряет все три ­корабля, игра заканчивается.

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

 

Установка Pygame

Прежде чем браться за программирование, установите пакет Pygame. Ниже описан процесс установки в Linux, OS X и Microsoft Windows.

Если вы используете Python 3 в системе Linux или если вы работаете в OS X, для установки Pygame используется pip — программа, управляющая загрузкой и установкой пакетов Python. Процедура установки пакетов с использованием pip описана ниже.

Если вы используете Python 2.7 в системе Linux или если вы работаете в Windows, для установки Pygame программа pip вам не понадобится. Вместо этого перейдите к разделу «Установка Pygame в Linux» (с. 229) или «Установка Pygame в Windows» (с. 231).

примечание

Далее приводятся инструкции по установке pip во всех системах, потому что эта программа понадобится вам для визуализации данных и веб-приложений. Инструкции также доступны по адресу https://www.nostarch.com/pythoncrashcourse/. Если у вас возникнут проблемы с инструкциями, приведенными ниже, попробуйте загрузить инструкции с сайта — возможно, они сработают.

 

Установка пакетов Python с использованием pip

В последних версиях Python pip устанавливается автоматически, поэтому сначала проверьте, присутствует ли эта программа в вашей системе. В Python 3 программа pip иногда называется pip3.

 

Проверка pip в Linux и OS X

Откройте терминальное окно и введите следующую команду:

$ pip --version

(1) pip 7.0.3 from /usr/local/lib/python3.5/dist-packages (python 3.5)

$

Если в вашей системе установлена только одна версия Python и вы получили примерно такой результат, переходите к разделу «Установка Pygame в Linux» (с. 229) или «Установка Pygame в OS X» (с. 230). Если вы получите сообщение об ошибке, попробуйте ввести имя pip3 вместо pip. Если ни одна версия не установлена в вашей системе, обратитесь к разделу «Установка pip» (c. 228).

Если в вашей системе установлено несколько версий Python, проверьте, что программа pip связана с используемой версией — например, Python 3.5 (1) . Если программа pip связана с правильной версией, переходите к разделу «Установка Pygame в Linux» (с. 229) или «Установка Pygame в OS X» (с. 230). Если версия неправильная, попробуйте ввести имя pip3 вместо pip. Если ни одна команда не работает для вашей версии Python, обратитесь к разделу «Установка pip» (c. 228).

 

Проверка pip в Windows

Откройте окно командной строки и введите следующую команду:

$ python -m pip --version

(1) pip 7.0.3 from C:\Python35\lib\site-packages (python 3.5)

$

Если в вашей системе установлена только одна версия Python, и вы получили примерно такой результат, переходите к разделу «Установка Pygame в Windows» (с. 231). Если вы получите сообщение об ошибке, попробуйте ввести имя pip3 вместо pip. Если ни одна версия не установлена в вашей системе, обратитесь к разделу «Установка pip» (с. 228).

Если в вашей системе установлено несколько версий Python, проверьте, что программа pip связана с используемой версией, например Python 3.5 (1) . Если программа pip связана с правильной версией, переходите к разделу «Установка Pygame в Windows» (с. 231). Если версия неправильная, попробуйте ввести имя pip3 вместо pip. Если ни одна команда не работает для вашей версии Python, обратитесь к следующему разделу «Установка pip».

 

Установка pip

Чтобы установить pip, обратитесь по адресу https://bootstrap.pypa.io/get-pip.py. Сохраните файл, если вам будет предложено. Если код get-pip.py появится в ­браузере, скопируйте код в текстовый редактор и сохраните в файле с именем get-pip.py. После того как программа get-pip.py будет сохранена на вашем компьютере, ее необходимо будет запустить с административными привилегиями, потому что pip будет устанавливать новые пакеты в вашей системе.

примечание

Если вы не нашли программу get-pip.py, обратитесь по адресу https://pip.pypa.io/, щелкните на ссылке Installation на левой панели, а затем в разделе Install pip перейдите по ссылке для загрузки get-pip.py.

 

Установка pip в Linux и OS X

Чтобы запустить get-pip.py с административными привилегиями, введите следу­ющую команду:

$ sudo python get-pip.py

примечание

Если терминальный сеанс был запущен командой python3, используйте команду sudo python3 get-pip.py.

После выполнения программы введите команду pip --version (или pip3 --version), чтобы убедиться в том, что программа pip была установлена правильно.

 

Установка pip в Windows

Чтобы запустить get-pip.py, введите следующую команду:

$ python get-pip.py

Если для запуска Python в терминале использовалась другая команда, проследите за тем, чтобы программа get-pip.py запускалась этой же командой — например, python3 get-pip.py или C:\Python35\python get-pip.py.

После выполнения программы введите команду python -m pip --version, чтобы убедиться в том, что программа pip была установлена правильно.

 

Установка Pygame в Linux

Если вы используете Python 2.7, установите Pygame при помощи менеджера пакетов. Откройте терминальное окно и введите следующую команду, которая загрузит и установит Pygame в вашей системе:

$ sudo apt-get install python-pygame

Проверьте правильность установки в терминальном сеансе; для этого введите следующую команду:

$ python

>>> import pygame

>>>

Если никаких дополнительных сообщений нет, значит, импортирование Pygame прошло успешно, и вы можете переходить к разделу «Создание проекта игры» на с. 231.

Если вы используете Python 3, процесс состоит из двух шагов: установки библиотек, от которых зависит Pygame, и загрузки/установки Pygame.

Чтобы установить библиотеки, необходимые Pygame, введите следующую команду (если в вашей системе используется другая команда, например python3.5, замените python3-dev на python3.5-dev).

$ sudo apt-get install python3-dev mercurial

$ sudo apt-get install libsdl-image1.2-dev libsdl2-dev libsdl-ttf2.0-dev

Эти команды установят библиотеки, необходимые для успешного запуска игры Alien Invasion. Если вы хотите включить расширенную функциональность Pygame (например, возможность добавления звуков), добавьте следующие библиотеки:

$ sudo apt-get install libsdl-mixer1.2-dev libportmidi-dev

$ sudo apt-get install libswscale-dev libsmpeg-dev libavformat-dev libavcode-dev

$ sudo apt-get install python-numpy

Теперь установите Pygame следующей командой (используйте pip3, если эта команда соответствует вашей системе):

$ pip install --user hg+http://bitbucket.org/pygame/pygame

После небольшой паузы программа сообщает, какие библиотеки были найдены. Нажмите Enter, даже если некоторые библиотеки отсутствуют. Вы увидите сообщение об успешной установке Pygame.

Чтобы проверить правильность установки, откройте терминальный сеанс и попробуйте импортировать Pygame:

$ python3

>>> import pygame

>>>

Если импортирование прошло нормально, переходите к разделу «Создание проекта игры» на с. 231.

 

Установка Pygame в OS X

Для установки некоторых пакетов, от которых зависит Pygame, вам понадобится менеджер пакетов Homebrew. Если в вашей системе он еще не установлен, обращайтесь к приложению А за инструкциями.

Чтобы установить библиотеки, от которых зависит Pygame, введите следующую команду:

$ brew install hg sdl sdl_image sdl_ttf

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

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

$ brew install sdl_mixer portmidi

Установите Pygame следующей командой (используйте pip вместо pip3, если вы используете Python 2.7):

$ pip3 install --user hg+http://bitbucket.org/pygame/pygame

Чтобы проверить правильность установки, откройте терминальный сеанс и попробуйте импортировать Pygame (используйте python вместо python3, если вы используете Python 2.7):

$ python3

>>> import pygame

>>>

Если импортирование прошло нормально, переходите к разделу «Создание проекта игры».

 

Установка Pygame в Windows

Проект Pygame размещен на сайте совместного использования кода Bitbucket. Чтобы установить Pygame для вашей версии Windows, найдите на странице https://bitbucket.org/pygame/pygame/downloads/ программу установки для Windows, ­соответствующую вашей версии Python. Если вы не нашли подходящую программу установки на сайте Bitbucket, попробуйте поискать по адресу http://www.lfd.uci.edu/~gohlke/pythonlibs/#pygame.

Когда подходящий файл будет загружен, запустите программу установки, если это файл с расширением .exe.

Если файл имеет суффикс .whl, скопируйте его в каталог проекта. Откройте окно командной строки, перейдите в папку, в которую был скопирован установочный пакет, и воспользуйтесь программой pip для запуска установки:

> python -m pip install --user pygame-1.9.2a0-cp35-none-win32.whl

 

Создание проекта игры

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

 

Создание окна Pygame и обработка ввода

Начнем с создания пустого окна Pygame. Ниже приведена базовая структура игры, написанной с использованием Pygame:

alien_invasion.py

import sys

import pygame

def run_game():

. .# Инициализирует игру и создает объект экрана.

(1) . .pygame.init()

(2) . .screen = pygame.display.set_mode((1200, 800))

. .pygame.display.set_caption("Alien Invasion")

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

(3) . .while True:

. . . .# Отслеживание событий клавиатуры и мыши.

(4) . . . .for event in pygame.event.get():

(5) . . . . . .if event.type == pygame.QUIT:

. . . . . . . .sys.exit()

. . . . . . . .

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

? . . . .pygame.display.flip()

run_game()

Программа начинается с импортирования модуля sys и pygame. Модуль pygame содержит функциональность, необходимую для создания игры, а модуль sys ­завершает игру по команде игрока.

Игра Alien Invasion начинается с определения функции run_game(). Строка pygame.init() (1) инициализирует настройки, необходимые Pygame для нормальной работы. В точке (2) вызов pygame.display.set_mode() создает отображаемую область screen, на которой прорисовываются все графические элементы игры. Аргумент (1200, 800) представляет собой кортеж, определяющий размеры игрового окна. Передавая эти размеры pygame.display.set_mode(), мы создаем игровое окно с шириной 1200 пикселов и высотой 800 пикселов. (Вы можете изменить эти значения в соответствии с размерами своего монитора.)

Объект screen называется поверхностью (surface). Поверхность в Pygame представляет часть экрана, на которой отображается игровой элемент. Каждый элемент в игре (например, пришелец или корабль игрока) представлен поверхностью. Поверхность, возвращаемая display.set_mode(), представляет все игровое окно. При активизации игрового цикла анимации эта поверхность автоматически перерисовывается при каждом проходе цикла.

Процессом игры управляет цикл while (3), который содержит цикл событий и код, управляющий обновлениями экрана. Событием называется действие, выполняемое пользователем во время игры (например, нажатие клавиши или перемещение мыши). Чтобы наша программа реагировала на события, мы напишем цикл событий для прослушивания событий и выполнения соответствующей операции в зависимости от типа произошедшего события. Этим циклом событий является цикл for в точке (4).

Чтобы получить доступ к событиям, обнаруженным Pygame, мы используем метод pygame.event.get(). При любом событии клавиатуры или мыши отрабатывает цикл for. В этом цикле мы пишем серию команд if для обнаружения и обработки конкретных событий. Например, когда игрок щелкает на кнопке закрытия игрового окна, программа обнаруживает событие pygame.QUIT, и программа вызывает метод sys.exit() для выхода из игры (5).

Вызов pygame.display.flip() ? приказывает Pygame отобразить последний отрисованный экран. В данном случае при каждом выполнении цикла while будет отображаться пустой экран со стиранием старого экрана, так что виден будет только новый экран. При перемещении игровых элементов вызов pygame.display.flip() будет постоянно обновлять экран, отображая игровые элементы в новых позициях и скрывая старые изображения; таким образом создается иллюзия плавного движения.

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

Запустите этот код, и вы увидите пустое окно Pygame.

 

Назначение цвета фона

Pygame по умолчанию создает черный экран, но это банально. Выберем другой цвет фона:

alien_invasion.py

...

def run_game():

...

pygame.display.set_caption("Alien Invasion")

. .# Назначение цвета фона.

(1) . .bg_color = (230, 230, 230)

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

while True:

# Отслеживание событий клавиатуры и мыши.

...

. . . .# При каждом проходе цикла перерисовывается экран.

(2) . . . .screen.fill(bg_color)

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

pygame.display.flip()

run_game()

Сначала программа создает цвет фона и сохраняет его в переменной bg_color (1) . Цвет достаточно задать только один раз, поэтому его значение определяется до входа в основной цикл while.

Цвета в Pygame задаются в схеме RGB: тройками интенсивности красной, зеленой и синей составляющих цвета. Значение каждой составляющей лежит в диапазоне от 0 до 255. Цветовое значение (255, 0, 0) соответствует красному цвету, (0, 255, 0) — зеленому и (0, 0, 255) — синему. Разные сочетания составляющих RGB позволяют создать до 16 миллионов цветов. В цветовом значении (230, 230, 230) красная, синяя и зеленая составляющие смешиваются в равных долях, давая светло-серый цвет фона.

В точке (2) экран заполняется цветом фона. Для этого вызывается метод screen.fill(), получающий всего один аргумент: цвет фона.

 

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

Каждый раз, когда в нашу игру добавляется новая функциональность, также в нее обычно добавляются новые настройки (параметры конфигурации). Вместо того чтобы добавлять настройки в коде, мы напишем модуль с именем settings; этот модуль содержит класс с именем Settings для хранения всех настроек. Такое решение позволит передавать один объект вместо множества отдельных настроек. Кроме того, оно упрощает вызовы функций и упрощает изменение внешнего вида игры с ростом проекта. Чтобы внести изменения в игру, достаточно будет изменить некоторые значения в settings.py вместо того, чтобы искать разные настройки в файлах.

Исходная версия класса Settings выглядит так:

settings.py

class Settings():

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

. .def __init__(self):

. . . ."""Инициализирует настройки игры."""

. . . .# Параметры экрана

. . . .self.screen_width = 1200

. . . .self.screen_height = 800

. . . .self.bg_color = (230, 230, 230)

Чтобы создать экземпляр Settings и использовать его для обращения к настройкам, внесите изменения в alien_invasion.py:

alien_invasion.py

...

import pygame

from settings import Settings

def run_game():

. .# Инициализирует pygame, settings и объект экрана.

pygame.init()

(1) . .ai_settings = Settings()

(2) . .screen = pygame.display.set_mode(

. . . .(ai_settings.screen_width, ai_settings.screen_height))

pygame.display.set_caption("Alien Invasion")

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

while True:

...

# При каждом проходе цикла перерисовывается экран.

(3) . . . .screen.fill(ai_settings.bg_color)

. . . . . . . .

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

pygame.display.flip()

run_game()

Класс Settings импортируется в основной файл программы, после чего программа создает экземпляр Settings и сохраняет его в ai_settings после вызова pygame.init() (1) . При создании экрана (2) используются атрибуты screen_width и screen_height объекта ai_settings, после чего объект ai_settings также используется для получения цвета фона при заполнении экрана (3).

 

Добавление изображения корабля

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

Выбирая графику для своих игр, обязательно обращайте внимание на условия лицензирования. Самый безопасный и дешевый начальный вариант — использование бесплатной графики с таких сайтов, как http://pixabay.com/.

В игре можно использовать практически любые графические форматы, но проще всего использовать файлы в формате .bmp, потому что этот формат Pygame загружает по умолчанию. И хотя Pygame можно настроить для других типов файлов, некоторые типы зависят от установки на компьютере определенных графических библиотек. (Большинство изображений, которые вы найдете, имеют формат .jpg, .png или .gif, но их можно преобразовать в формат .bmp при помощи таких программ, как Photoshop, GIMP или Paint.)

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

В игре Alien Invasion используется файл ship.bmp (рис. 12.1), который можно загрузить в числе ресурсов книги по адресу https://www.nostarch.com/pythoncrashcourse/. Цвет фона файла соответствует настройкам, используемым в проекте. Создайте в главном каталоге проекта (alien_invasion) каталог с именем images. Сохраните файл ship.bmp в каталоге images.

Рис. 12.1. Корабль для игры Alien Invasion

 

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

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

ship.py

import pygame

class Ship():

. .def __init__(self, screen):

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

. . . .self.screen = screen

. . . .# Загрузка изображения корабля и получение прямоугольника.

(1) . . . .self.image = pygame.image.load('images/ship.bmp')

(2) . . . .self.rect = self.image.get_rect()

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

. . . .# Каждый новый корабль появляется у нижнего края экрана.

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

. . . .self.rect.bottom = self.screen_rect.bottom

(5) . .def blitme(self):

. . . ."""Рисует корабль в текущей позиции."""

. . . .self.screen.blit(self.image, self.rect)

Сначала программа импортирует модуль pygame. Метод __init__() класса Ship получает два параметра: ссылку self и объект screen, на котором выводится корабль. Загрузка изображения выполняется вызовом pygame.image.load() (1) . Функция возвращает поверхность, представляющую корабль; полученный объект сохраняется в self.image.

После того как изображение будет загружено, метод get_rect() используется для получения атрибута rect поверхности (2). Один из факторов эффективности Pygame заключается в том, что программист может выполнять операции с игровыми элементами как с прямоугольниками даже в том случае, если они имеют другую форму. Операции с прямоугольниками эффективны, потому что прямоугольник — простая геометрическая фигура. Обычно этот подход работает достаточно хорошо и игроки не замечают, что программа не отслеживает точную геометрическую форму каждого игрового элемента.

При работе с объектом rect для вас доступны координаты x и y верхней, нижней, левой и правой сторон, а также центра. Присваивая любые из этих значений, вы задаете текущую позицию прямоугольника.

Местонахождение центра игрового элемента определяется атрибутами center, centerx или centery прямоугольника. Стороны определяются атрибутами top, bottom, left и right. Для изменения горизонтального или вертикального расположения прямоугольника достаточно задать атрибуты x и y, содержащие координаты левого верхнего угла. Эти атрибуты избавляют вас от вычислений, которые раньше разработчикам игр приходилось выполнять вручную, притом достаточно часто.

примечание

В Pygame начало координат (0, 0) находится в левом верхнем углу экрана, а оси направлены сверху вниз и слева направо. На экране с размерами 1200 на 800 начало координат располагается в левом верхнем углу, а правый нижний угол имеет координаты (1200, 800).

Корабль будет расположен в середине нижней стороны экрана. Для этого мы сначала сохраняем прямоугольник экрана в self.screen_rect (3), а затем присваиваем self.rect.centerx (координата x центра корабля) значение атрибута centerx прямоугольника экрана (4). Атрибуту self.rect.bottom (координата y низа корабля) присваивается значение атрибута bottom прямоугольника экрана. Pygame использует эти атрибуты rect для позиционирования изображения, чтобы корабль был выровнен по центру, а его нижний край совпадал с нижним краем экрана.

В точке (5) определяется метод blitme(), который выводит изображение на экран в позиции, заданной self.rect.

 

Вывод корабля на экран

Изменим программу alien_invasion.py, чтобы в ней создавался корабль (экземпляр Ship) и вызывался метод blitme() класса Ship:

alien_invasion.py

...

from settings import Settings

from ship import Ship

def run_game():

...

pygame.display.set_caption("Alien Invasion")

. .# Создание корабля.

(1) . .ship = Ship(screen)

# Start the main loop for the game.

while True:

...

# При каждом проходе цикла перерисовывается экран.

screen.fill(ai_settings.bg_color)

(2) . . . .ship.blitme()

. . . . . . . .

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

pygame.display.flip()

run_game()

После создания экрана программа импортирует класс Ship и создает его экземпляр (с именем ship). Это должно происходить до начала основного цикла while (1) , чтобы при каждом проходе цикла не создавался новый экземпляр корабля. Чтобы перерисовать корабль на экране, мы вызываем ship.blitme() после заполнения фона, так что корабль выводится поверх фона (2).

Если вы запустите alien_invasion.py сейчас, вы увидите пустой игровой экран, в центре нижней стороны которого находится корабль (рис. 12.2).

Рис. 12.2. Корабль в середине нижней стороны экрана

 

Рефакторинг: модуль game_functions

В больших проектах перед добавлением нового кода часто проводится рефакторинг уже написанного кода. Рефакторинг упрощает структуру существующего кода и дальнейшее развитие проекта. В этом разделе мы создадим новый модуль 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() и облегчает последующее программирование реакции на действия игрока.

 

В двух словах

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

 

alien_invasion.py

Главный файл программы alien_invasion.py создает ряд важных объектов, используемых ходе игры: настройки хранятся в ai_settings, основная поверхность для вывода изображения хранится в screen, а экземпляр ship тоже создается в этом файле. Также в alien_invasion.py содержится главный цикл игры — цикл while с вызовами check_events(), ship.update() и update_screen().

Файл alien_invasion.py — единственный файл, который должен запускаться для игры в Alien Invasion. Все остальные файлы — settings.py, game_functions.py, ship.py — содержат код, который импортируется (прямо или косвенно) в этот файл.

 

settings.py

Файл settings.py содержит класс Settings. Этот класс содержит только метод __init__(), инициализирующий атрибуты, которые управляют внешним видом и скоростью игры.

 

game_functions.py

Файл game_functions.py содержит набор функций, выполняющих основную работу в игре. Функция check_events() обнаруживает события, представляющие интерес для игры (например, нажатия и отпускания клавиш), и обрабатывает все эти типы событий при помощи вспомогательных функций check_keydown_events() и check_keyup_events(). Пока эти функции управляют только движением корабля. Модуль game_functions также содержит функцию update_screen(), которая перерисовывает экран при каждом проходе основного цикла.

 

ship.py

Файл ship.py содержит класс Ship. В этом классе определен метод __init__(), метод update() для управления позицией корабля и метод blitme() для вывода изображения корабля на экран. Изображение корабля хранится в файле ship.bmp, который находится в папке images.

Упражнения

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

12-4. Клавиши: создайте файл Pygame, который создает пустой экран. В ­цикле событий выводите значение атрибута event.key при обнаружении события pygame.KEYDOWN. Запустите программу, нажимайте различные клавиши и понаблюдайте за реакцией Pygame.

 

Стрельба

А теперь добавим в игру возможность стрельбы. Мы напишем код, который выпускает пулю (маленький прямоугольник) при нажатии игроком клавиши «пробел». Пули летят вертикально вверх, пока не исчезнут у верхнего края экрана.

 

Добавление настроек

Сначала добавим в settings.py новые настройки для значений, управляющих поведением класса Bullet. Эти настройки добавляются в конец метода __init__():

settings.py

. . def __init__(self):

. . . ....

. . . .# Параметры пули

. . . .self.bullet_speed_factor = 1

. . . .self.bullet_width = 3

. . . .self.bullet_height = 15

. . . .self.bullet_color = 60, 60, 60

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

 

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

Теперь создадим файл bullet.py для хранения класса Bullet. Первая часть файла bullet.py выглядит так:

bullet.py

import pygame

from pygame.sprite import Sprite

class Bullet(Sprite):

. ."""Класс для управления пулями, выпущенными кораблем."""

. .def __init__(self, ai_settings, screen, ship):

. . . ."""Создает объект пули в текущей позиции корабля."""

. . . .super(Bullet, self).__init__()

. . . .self.screen = screen

. . . .# Создание пули в позиции (0,0) и назначение правильной позиции.

(1) . . . .self.rect = pygame.Rect(0, 0, ai_settings.bullet_width,

. . . . . .ai_settings.bullet_height)

(2) . . . .self.rect.centerx = ship.rect.centerx

(3) . . . .self.rect.top = ship.rect.top

. . . .

. . . .# Позиция пули хранится в вещественном формате.

(4) . . . .self.y = float(self.rect.y)

(5) . . . .self.color = ai_settings.bullet_color

. . . .self.speed_factor = ai_settings.bullet_speed_factor

Класс Bullet наследует от класса Sprite, импортируемого из модуля pygame.sprite. Работая со спрайтами (sprite), разработчик группирует связанные элементы в своей игре и выполняет операцию со всеми сгруппированными элементами одновременно. Чтобы создать экземпляр пули, методу __init__() необходимо передать экземпляры ai_settings, screen и ship, а вызов super() необходим для правильной реализации наследования от Sprite.

примечание

Вызов super(Bullet, self).__init__() использует синтаксис Python 2.7. В Python 3 этот синтаксис тоже работает, хотя вызов также можно записать в более простой форме super().__init__().

В точке (1) создается атрибут rect пули. Пуля не создается на основе готового изображения, поэтому прямоугольник приходится строить «с нуля» при помощи класса pygame.Rect(). При создании экземпляра этого класса необходимо задать координаты левого верхнего угла прямоугольника, его ширину и высоту. Прямоугольник инициализируется в точке (0, 0), но в следующих двух строках он перемещается в нужное место, так как позиция пули зависит от позиции корабля. Ширина и высота пули определяются значениями, хранящимися в ai_settings.

В точке (2) атрибуту centerx пули присваивается значение rect.centerx корабля. Пуля должна появляться у верхнего края корабля, поэтому верхний край пули совмещается с верхним краем прямоугольника корабля для имитации «выстрела» из корабля (3).

Координата y пули хранится в вещественной форме для внесения более точных изменений в скорость пули (4). В точке (5) настройки цвета и скорости пули сохраняются в self.color и self.speed_factor.

А вот как выглядит вторая часть bullet.py, update() и draw_bullet():

bullet.py

. .def update(self):

. . . ."""Перемещает пулю вверх по экрану."""

. . . .# Обновление позиции пули в вещественном формате.

(1) . . . .self.y -= self.speed_factor

. . . .# Обновление позиции прямоугольника.

(2) . . . .self.rect.y = self.y

. .def draw_bullet(self):

. . . ."""Вывод пули на экран."""

(3) . . . .pygame.draw.rect(self.screen, self.color, self.rect)

Метод update() управляет позицией пули. Когда происходит выстрел, пуля двигается вверх по экрану, что соответствует уменьшению координаты y; следовательно, для обновления позиции пули следует вычесть величину, хранящуюся в self.speed_factor, из self.y (1) . Затем значение self.y используется для изменения значения self.rect.y (2). Атрибут speed_factor позволяет увеличить скорость пуль по ходу игры или при изменении ее поведения. Координата x пули после выстрела не изменяется, поэтому пуля летит вертикально по прямой линии.

Для вывода пули на экран вызывается функция draw_bullet(). Она заполняет часть экрана, определяемую прямоугольником пули, цветом из self.color (3).

 

Группировка пуль

Класс Bullet и все необходимые настройки готовы; можно переходить к написанию кода, который будет выпускать пулю каждый раз, когда игрок нажимает клавишу «пробел». Сначала мы создадим в alien_invasion.py группу для хранения всех летящих пуль, чтобы программа могла управлять их полетом. Эта группа будет представлена экземпляром класса pygame.sprite.Group, который напоминает список с расширенной функциональностью, которая может быть полезна при построении игр. Мы воспользуемся группой для прорисовки пуль на экране при каждом проходе основного цикла и обновления текущей позиции каждой пули:

alien_invasion.py

import pygame

from pygame.sprite import Group

...

def run_game():

...

# Создание корабля.

ship = Ship(ai_settings, screen)

. .# Создание группы для хранения пуль.

(1) . .bullets = Group()

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

while True:

. . . .gf.check_events(ai_settings, screen, ship, bullets)

. . . .ship.update()

(2) . . . .bullets.update()

. . . .gf.update_screen(ai_settings, screen, ship, bullets)

run_game()

Класс Group импортируется из pygame.sprite. В точке (1) создается экземпляр Group с именем bullets. Эта группа создается за пределами цикла while, чтобы новая группа пуль не создавалась при каждом проходе цикла.

примечание

Если группа будет создаваться в цикле, в результате программа создает тысячи групп, и скорость игры упадет до минимума. Если ваша игра со временем начинает заметно «тормозить», внимательно проверьте, что происходит в основном цикле while.

Объект bullets передается методам check_events() и update_screen(). В check_events() он используется при обработке клавиши «пробел», а в update_screen() необходимо перерисовать выводимые на экран пули.

Вызов update() для группы (2) приводит к автоматическому вызову update() для каждого спрайта в группе. Строка bullets.update() вызывает bullet.update() для каждой пули, включенной в группу bullets.

 

Обработка выстрелов

В файле game_functions.py необходимо внести изменения в метод check_keydown_events(), чтобы при нажатии клавиши «пробел» происходил выстрел. Изменять check_keyup_events() не нужно, потому что при отпускании клавиши ничего не происходит. Также необходимо изменить update_screen() и вывести каждую пулю на экран перед вызовом flip(). Ниже представлены соответствующие ­изменения в game_functions.py:

game_functions.py

...

from bullet import Bullet

(1) def check_keydown_events(event, ai_settings, screen, ship, bullets):

...

(2) . .elif event.key == pygame.K_SPACE:

. . . .# Создание новой пули и включение ее в группу bullets.

. . . .new_bullet = Bullet(ai_settings, screen, ship)

. . . .bullets.add(new_bullet)

...

(3)def check_events(ai_settings, screen, ship, bullets):

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

for event in pygame.event.get():

...

elif event.type == pygame.KEYDOWN:

. . . . . .check_keydown_events(event, ai_settings, screen, ship, bullets)

...

(4)def update_screen(ai_settings, screen, ship, bullets):

...

. .# Все пули выводятся позади изображений корабля и пришельцев.

(5) . .for bullet in bullets.sprites():

. . . .bullet.draw_bullet()

ship.blitme()

...

Рис. 12.3. Экран игры после серии выстрелов

Группа bullets передается check_keydown_events() (1) . Когда игрок нажимает пробел, создается новая пуля (экземпляр Bullet с именем new_bullet), которая добавляется в группу bullets (2) методом add(); код bullets.add(new_bullet) сохраняет новую пулю в группе bullets.

Группу bullets необходимо добавить в число параметров в определении check_events() (3), а также передать в аргументе при вызове check_keydown_events().

Параметр bullets передается функции update_screen() (4), которая рисует пули на экране. Метод bullets.sprites() возвращает список всех спрайтов в группе bullets. Чтобы нарисовать все выпущенные пули на экране, программа перебирает спрайты в bullets и вызывает для каждого draw_bullet() (5).

Если запустить alien_invasion.py сейчас, вы сможете двигать корабль влево и вправо и выпускать сколько угодно пуль. Пули перемещаются вверх по экрану и исчезают при достижении верхнего края (рис. 12.3). Размер, цвет и скорость пуль можно изменить при помощи настроек в settings.py.

 

Удаление старых пуль

На данный момент пули исчезают по достижении верхнего края, но только потому, что Pygame не может нарисовать их выше края экрана. На самом деле пули продолжают существовать; их координата y продолжает уменьшаться. И это создает проблему, потому что пули продолжают потреблять память и вычислительные мощности.

От старых пуль необходимо избавиться, иначе игра замедлится из-за большого объема лишней работы. Для этого необходимо определить момент, когда атрибут bottom прямоугольника пули достигнет 0, — это означает, что пуля вышла за верхний край экрана:

alien_invasion.py

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

while True:

gf.check_events(ai_settings, screen, ship, bullets)

ship.update()

bullets.update()

. .

. . . .# Удаление пуль, вышедших за край экрана.

(1) . . . .for bullet in bullets.copy():

(2) . . . . . .if bullet.rect.bottom <= 0:

(3) . . . . . . . . bullets.remove(bullet)

(4) . . . .print(len(bullets))

gf.update_screen(ai_settings, screen, ship, bullets)

Удалять элементы из списка или группы в цикле for не следует, поэтому перебирать нужно копию группы. Метод copy() используется для создания цикла for (1) , в котором возможно изменять содержимое bullets. Программа проверяет каждую пулю и определяет, вышла ли она за верхний край экрана (2). Если пуля пересекла границу, она удаляется из bullets (3). В точке (4) добавляется команда print, которая сообщает, сколько пуль сейчас существует в игре; по выведенному значению можно убедиться в том, что пули действительно были удалены.

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

 

Ограничение количества пуль

Многие игры-«стрелялки» ограничивают количество пуль, одновременно находящихся на экране, чтобы у игроков появился стимул стрелять более метко. То же самое будет сделано и в игре Alien Invasion.

Сначала сохраним максимально допустимое количество пуль в settings.py:

settings.py

# Параметры пули

self.bullet_width = 3

self.bullet_height = 15

self.bullet_color = 60, 60, 60

self.bullets_allowed = 3

В любой момент времени на экране может находиться не более трех пуль. Эта настройка будет использоваться в game_functions.py для проверки количества существующих пуль перед созданием новой пули в check_keydown_events():

game_functions.py

def check_keydown_events(event, ai_settings, screen, ship, bullets):

...

elif event.key == pygame.K_SPACE:

# Создание новой пули и включение ее в группу bullets.

. . . .if len(bullets) < ai_settings.bullets_allowed:

new_bullet = Bullet(ai_settings, screen, ship)

bullets.add(new_bullet)

При нажатии клавиши «пробел» программа проверяет длину bullets. Если значение len(bullets) меньше трех, создается новая пуля. Но, если на экране уже находятся три активные пули, при нажатии пробела ничего не происходит. Если вы запустите игру сейчас, вы сможете выпускать пули только группами по три.

 

Создание функции update_bullets()

Мы хотим, чтобы главный файл программы alien_invasion.py был как можно более простым, поэтому после написания и проверки кода управления пулями этот код можно переместить в модуль game_functions. Мы создадим новую функцию update_bullets() и добавим ее в конец game_functions.py:

game_functions.py

def update_bullets(bullets):

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

. .# Обновление позиций пуль.

bullets.update()

# Удаление пуль, вышедших за край экрана.

for bullet in bullets.copy():

if bullet.rect.bottom <= 0:

bullets.remove(bullet)

Код update_bullets() вырезается и вставляется из alien_invasion.py; единственным необходимым параметром функции является группа bullets.

Цикл while в alien_invasion.py снова выглядит просто:

alien_invasion.py

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

while True:

(1) gf.check_events(ai_settings, screen, ship, bullets)

(2)ship.update()

(3) . . . .gf.update_bullets(bullets)

(4)gf.update_screen(ai_settings, screen, ship, bullets)

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

 

Создание функции fire_bullet()

Переместим код стрельбы в отдельную функцию, чтобы выстрел выполнялся всего одной строкой кода, а блок elif в check_keydown_events() оставался простым:

game_functions.py

def check_keydown_events(event, ai_settings, screen, ship, bullets):

"""Реагирует на нажатия клавиш."""

...

elif event.key == pygame.K_SPACE:

. . . .fire_bullet(ai_settings, screen, ship, bullets)

def fire_bullet(ai_settings, screen, ship, bullets):

. ."""Выпускает пулю, если максимум еще не достигнут."""

# Создание новой пули и включение ее в группу bullets.

if len(bullets) < ai_settings.bullets_allowed:

new_bullet = Bullet(ai_settings, screen, ship)

bullets.add(new_bullet)

Функция fire_bullet() просто содержит код, который использовался для выстрела при нажатии клавиши «пробел»; вызов fire_bullet() добавляется в check_keydown_events() при нажатии клавиши «пробел».

Запустите alien_invasion.py еще раз и убедитесь в том, что стрельба проходит без ошибок.

Упражнения

12-5. Боковая стрельба: напишите игру, в которой корабль размещается у левого края экрана, а игрок может перемещать корабль вверх и вниз. При нажатии клавиши «пробел» корабль стреляет, и пуля двигается вправо по экрану. Проследите за тем, чтобы пули удалялись при выходе за край экрана.

 

Итоги

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

В главе 13 в игру Alien Invasion будут добавлены пришельцы. К концу главы 13 игрок сможет сбивать корабли пришельцев — конечно, если они не доберутся до него первыми!