Книга: Изучаем Python: программирование игр, визуализация данных, веб-приложения. 3-е изд. дополненное и переработанное
Назад: Часть II. Проекты
Дальше: 13. Осторожно, пришельцы!

12. Атакующий корабль

22725.png

 

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

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

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

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

ПРИМЕЧАНИЕ

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

Кроме того, если вы уверенно работаете с системами управления версиями — возможно, вам стоит использовать такую систему в этом проекте. Если ранее вы никогда не работали с подобными системами, обратитесь к краткому обзору в приложении Г.

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

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

Итак, опишем игровой процесс в целом. Это описание не затрагивает все аспекты игры, но дает достаточно четкое представление о том, с чего начинать работу.

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

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

Установка Pygame

Прежде чем браться за программирование, установите пакет Pygame. Он устанавливается так же, как и pytest в главе 11: с помощью инструмента pip. Если вы не читали главу 11 и не умеете работать с pip, то см. раздел «Установка pytest с помощью pip» данной главы.

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

$ python -m pip install --user pygame

Если для запуска программ или терминального сеанса вы используете команду, отличающуюся от python, например python3, то измените приведенную выше команду соответствующим образом.

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

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

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

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

alien_invasion.py

import sys

 

import pygame

 

class AlienInvasion:

    """Класс для управления ресурсами и поведением игры."""

 

    def __init__(self):

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

❶         pygame.init()

 

❷         self.screen = pygame.display.set_mode((1200, 800))

        pygame.display.set_caption("Alien Invasion")

 

    def run_game(self):

        """Запускает основной цикл игры."""

❸         while True:

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

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

❺                 if event.type == pygame.QUIT:

                    sys.exit()

 

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

❻             pygame.display.flip()

 

if __name__ == '__main__':

    # Создание экземпляра и запуск игры.

    ai = AlienInvasion()

    ai.run_game()

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

Игра «Инопланетное вторжение» начинается с класса ALienInvasion. В методе __init__() функция pygame.init() инициализирует настройки, необходимые Pygame для нормальной работы . Вызов pygame.display.set_mode() создает окно , в котором прорисовываются все графические элементы игры. Аргумент (1200, 800) представляет собой кортеж, определяющий размеры игрового окна размером 1200 пикселов в ширину и 800 пикселов в высоту. (Вы можете изменить эти значения в соответствии с размерами своего монитора.) Объект окна присваивается атрибуту self.screen, что позволяет работать с ним во всех методах класса.

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

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

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

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

В последней строке файла создается экземпляр игры, после чего вызывается метод run_game(). Вызов run_game() заключается в блок if, чтобы он выполнялся только при прямом вызове функции. Запустив файл alien_invasion.py, вы увидите пустое окно Pygame.

Управление частотой кадров

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

Мы определим объект отслеживания игрового времени в методе __init__():

alien_invasion.py

    def __init__(self):

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

        pygame.init()

        self.clock = pygame.time.Clock()

        --пропуск--

После инициализации pygame создадим экземпляр класса Clock из модуля pygame.time. Затем настраиваем скорость игры в конце цикла while в функции run_game():

def run_game(self):

        """Запускает основной цикл игры."""

        while True:

            --пропуск--

            pygame.display.flip()

            self.clock.tick(60)

Метод tick() принимает один аргумент: частоту кадров игры. Я указал значение 60, поэтому Pygame сделает все возможное, чтобы цикл повторялся ровно 60 раз в секунду.

ПРИМЕЧАНИЕ

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

Задание фонового цвета

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

alien_invasion.py

    def __init__(self):

        --пропуск--

        pygame.display.set_caption("Alien Invasion")

 

        # Задание цвета фона.

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

 

    def run_game(self):

        --пропуск--

        for event in pygame.event.get():

            if event.type == pygame.QUIT:

                sys.exit()

 

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

❷         self.screen.fill(self.bg_color)

 

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

        pygame.display.flip()

        self.clock.tick(60)

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

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

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

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

Создайте новый файл settings.py в папке alien_invasion и добавьте этот первоначальный класс Settings:

settings.py

class Settings:

    """Класс для хранения всех настроек игры "Инопланетное вторжение"."""

 

    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

 

class AlienInvasion:

    """Класс для управления ресурсами и поведением игры."""

 

    def __init__(self):

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

        pygame.init()

        self.clock = pygame.time.Clock()

❶         self.settings = Settings()

 

❷         self.screen = pygame.display.set_mode(

            (self.settings.screen_width, self.settings.screen_height))

        pygame.display.set_caption("Alien Invasion")

 

    def run_game(self):

        --пропуск--

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

❸         self.screen.fill(self.settings.bg_color)

 

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

        pygame.display.flip()

        self.clock.tick(60)

--пропуск--

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

Запустив файл alien_invasion.py, вы не заметите никаких изменений, поскольку в этом разделе мы всего лишь переместили настройки, уже использованные в другом месте. Теперь можно переходить к добавлению новых элементов на экран.

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

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

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

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

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

В игре «Инопланетное вторжение» используется файл ship.bmp (рис. 12.1), который можно скачать из дополнительных материалов книги на https://ehmatthes.github.io/pcc_3e. Цвет фона файла соответствует настройкам, используемым в проекте. Создайте в главной папке проекта (alien_invasion) папку images. Сохраните файл ship.bmp в папке images.

12_01.tif 

Рис. 12.1. Корабль для игры «Инопланетное вторжение»

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

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

ship.py

import pygame

 

class Ship:

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

 

    def __init__(self, ai_game):

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

❶         self.screen = ai_game.screen

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

 

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

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

        self.rect = self.image.get_rect()

 

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

❹         self.rect.midbottom = self.screen_rect.midbottom

 

❺     def blitme(self):

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

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

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

Перед определением класса программа импортирует модуль pygame. Метод __init__() класса Ship получает два параметра: ссылку self и ссылку на текущий экземпляр класса AlienInvasion. Так класс Ship получает доступ ко всем игровым ресурсам, определенным в AlienInvasion. Экран присваивается атрибуту Ship , чтобы к нему можно было легко обращаться во всех модулях класса. Программа обращается к атрибуту rect объекта экрана с помощью метода get_rect() и присваивает его self.screen_rect . Это позволяет поместить корабль в нужной позиции экрана.

Чтобы загрузить изображение, мы вызываем метод pygame.image.load() и передаем ему местоположение изображения корабля. Функция возвращает поверхность, представляющую корабль, которая присваивается self.image. Когда изображение будет загружено, программа вызывает get_rect() для получения атрибута rect поверхности корабля, чтобы позднее использовать ее для позиционирования корабля.

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

ПРИМЕЧАНИЕ

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

Корабль будет расположен в середине нижней стороны экрана. Для этого значение self.rect.midbottom выравнивается по атрибуту midbottom прямоугольника экрана . Pygame использует эти атрибуты rect для позиционирования изображения, чтобы корабль был выровнен по центру, а его нижний край совпадал с нижним краем экрана.

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

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

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

alien_invasion.py

--пропуск--

from settings import Settings

from ship import Ship

 

class AlienInvasion:

    """Класс для управления ресурсами и поведением игры."""

 

    def __init__(self):

        --пропуск--

        pygame.display.set_caption("Alien Invasion")

 

❶         self.ship = Ship(screen)

 

    def run_game(self):

        --пропуск--

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

        self.screen.fill(self.settings.bg_color)

❷         self.ship.blitme()

 

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

        pygame.display.flip()

        self.clock.tick(60)

--пропуск--

После создания экрана программа импортирует класс Ship и создает его экземпляр . При вызове Ship передается один аргумент — экземпляр AlienInvasion. Аргумент self относится к текущему экземпляру AlienInvasion. Этот параметр предоставляет Ship доступ к ресурсам игры — например, к объекту screen. Экземпляр Ship присваивается self.ship.

После заполнения фона корабль рисуется на экране с помощью вызова ship.blitme(), так что корабль выводится поверх фона .

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

12_02.tif 

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

Рефакторинг: методы _check_events() и _update_screen()

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

Метод _check_events()

Начнем с перемещения кода управления событиями в отдельный метод _check_events(). Тем самым вы упростите метод run_game() и изолируете цикл управления событиями от остального кода. Изоляция цикла событий позволит организовать управление событиями отдельно от других аспектов игры (например, обновления экрана).

Ниже приведен класс AlienInvasion с новым методом _check_events(), который используется только в коде метода run_game():

alien_invasion.py

    def run_game(self):

        """Запускает основной цикл игры."""

        while True:

❶             self._check_events()

 

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

            --пропуск--

 

❷     def _check_events(self):

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

        for event in pygame.event.get():

            if event.type == pygame.QUIT:

                sys.exit()

Мы определяем новый метод _check_events() и перемещаем строки, которые проверяют, не закрыл ли игрок окно щелчком кнопки мыши, в этот новый метод.

Для вызова метода внутри класса используется точечная запись с переменной self и именем метода . Затем метод вызывается в цикле while метода run_game().

Метод _update_screen()

Чтобы еще больше упростить метод run_game(), выделим код обновления экрана в отдельный метод _update_screen():

alien_invasion.py

    def run_game(self):

        """Запускает основной цикл игры."""

        while True:

            self._check_events()

            self._update_screen()

            self.clock.tick(60)

 

    def _check_events(self):

        --пропуск--

 

    def _update_screen(self):

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

        self.screen.fill(self.settings.bg_color)

        self.ship.blitme()

 

        pygame.display.flip()

Код прорисовки фона и переключения экрана перемещен в метод _update_screen(). Тело основного цикла в методе run_game() серьезно упростилось. С первого взгляда видно, что программа отслеживает новые события и обновляет экран при каждом проходе цикла.

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

Теперь, когда мы изменили структуру кода и упростили его расширение, можно переходить к динамическим аспектам игры!

Упражнения

12.1. Синее небо. Создайте окно Pygame с синим фоном.

12.2. Игровой персонаж. Найдите изображение игрового персонажа, который вам нравится, в формате .bmp (или преобразуйте существующее изображение). Создайте класс, который рисует персонажа в центре экрана, и приведите цвет фона изображения в соответствие с цветом фона экрана (или наоборот).

Управление кораблем

Реализуем возможность перемещения корабля по горизонтали. Для этого мы напишем код, реагирующий на нажатие клавиш или . Начнем с движения вправо, а затем применим те же принципы к движению влево. Заодно вы научитесь управлять перемещением изображений на экране.

Обработка нажатия клавиши

Каждый раз, когда пользователь нажимает клавишу, это нажатие регистрируется в Pygame как событие. Каждое событие идентифицируется методом pyga­me.event.get(), поэтому в методе _check_events() необходимо указать, какие события должны отслеживаться. Каждое нажатие клавиши регистрируется как событие KEYDOWN.

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

alien_invasion.py

    def _check_events(self):

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

        for event in pygame.event.get():

            if event.type == pygame.QUIT:

                sys.exit()

❶             elif event.type == pygame.KEYDOWN:

❷                 if event.key == pygame.K_RIGHT:

                    # Переместить корабль вправо.

❸                     self.ship.rect.x += 1

Внутри _check_events() в цикл событий добавляется блок elif для выполнения кода при обнаружении события KEYDOWN . Чтобы проверить, является ли нажатая клавиша клавишей (pygame.K_RIGHT), мы читаем атрибут event.key . Если нажата клавиша , то корабль перемещается вправо, для чего значение self.ship.rect.x увеличивается на 1 .

Если вы запустите программу 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, ai_game):

        --пропуск--

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

        self.rect.midbottom = self.screen_rect.midbottom

 

        # Флаг перемещения: начинаем с неподвижного корабля.

❶         self.moving_right = False

 

❷     def update(self):

        """Обновляет позицию корабля с учетом флага."""

        if self.moving_right:

            self.rect.x += 1

 

    def blitme(self):

        --пропуск--

Мы добавляем атрибут self.moving_right в метод __init__() и инициализируем его значением False . Затем вызываем метод update(), который перемещает корабль вправо, если флаг равен True . Метод update() будет вызываться вне класса, поэтому не считается вспомогательным методом.

Теперь внесем изменения в метод run_game(), чтобы при нажатии клавиши флагу moving_right присваивалось значение True, а при ее отпускании — False:

alien_invasion.py

    def check_events(self):

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

        for event in pygame.event.get():

            --пропуск--

            elif event.type == pygame.KEYDOWN:

                if event.key == pygame.K_RIGHT:

❶                 self.ship.moving_right = True

❷             elif event.type == pygame.KEYUP:

                if event.key == pygame.K_RIGHT:

                    self.ship.moving_right = False

Теперь реакция игры при нажатии клавиши изменяется; вместо непосредственного изменения позиции корабля программа просто присваивает флагу moving_right значение True . Затем добавляется новый блок elif, реагирующий на события KEYUP . Когда игрок отпускает клавишу (K_RIGHT), флагу moving_right присваивается значение False.

Остается изменить цикл while в файле alien_invasion.py, чтобы при каждом проходе цикла вызывался метод update() корабля:

alien_invasion.py

    def run_game(self):

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

        while True:

            self._check_events()

            self.ship.update()

            self._update_screen()

            self.clock.tick(60)

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

Если запустить файл alien_invasion.py и удерживать клавишу , то корабль непрерывно двигается вправо, пока она не будет отпущена.

Перемещение влево и вправо

Теперь, когда мы реализовали непрерывное движение вправо, добавить движение влево относительно несложно. Для этого нужно снова изменить класс Ship и метод _check_events(). Ниже приведены необходимые изменения в методах __init__() и update() в классе Ship:

ship.py

    def __init__(self, ai_game):

        --пропуск--

        # Флаги перемещения: начинаем с неподвижного корабля

        self.moving_right = False

        self.moving_left = False

 

    def update(self):

        """Обновляет позицию корабля с учетом флагов."""

        if self.moving_right:

            self.rect.x += 1

        if self.moving_left:

            self.rect.x -= 1

В методе __init__() добавляется флаг self.moving_left. В update() используются два отдельных блока if вместо elif, чтобы при нажатии обеих клавиш со стрелками атрибут rect.x сначала увеличивался, а потом уменьшался. В результате корабль остается на месте. Если бы для движения влево использовался блок elif, то клавиша всегда имела бы приоритет. Два блока if повышают точность перемещения при переключении направления, когда игрок может ненадолго удерживать нажатыми обе клавиши.

В метод _check_events() необходимо внести два изменения:

alien_invasion.py

    def _check_events(self):

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

        for event in pygame.event.get():

            --пропуск--

            elif event.type == pygame.KEYDOWN:

                if event.key == pygame.K_RIGHT:

                    self.ship.moving_right = True

                elif event.key == pygame.K_LEFT:

                    self.ship.moving_left = True

 

            elif event.type == pygame.KEYUP:

                if event.key == pygame.K_RIGHT:

                    self.ship.moving_right = False

                elif event.key == pygame.K_LEFT:

                    self.ship.moving_left = False

Если событие KEYDOWN происходит для события K_LEFT, то флагу moving_left присваивается True. Если событие KEYUP происходит для события K_LEFT, то moving_left присваивается False. Здесь возможно использовать блоки elif, поскольку каждое событие связано только с одной клавишей. Если же игрок нажимает обе клавиши одновременно, то программа обнаруживает два разных события.

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

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

Управление скоростью корабля

В настоящий момент корабль смещается на один пиксел за каждый проход цикла while, но для повышения точности управления скоростью можно добавить в класс Settings атрибут ship_speed. Он определяет величину смещения корабля при каждом проходе цикла. Новый атрибут в файле settings.py выглядит так:

settings.py

class Settings:

    """Класс для хранения всех настроек игры "Инопланетное вторжение"."""

 

    def __init__(self):

        --пропуск--

 

        # Настройки корабля

        self.ship_speed = 1.5

Переменной ship_speed присваивается значение 1.5. При перемещении корабля его позиция изменяется на 1,5 пиксела вместо 1.

Вещественные значения скорости позволят лучше управлять скоростью корабля при последующем повышении темпа игры. Однако атрибуты прямоугольников (такие как x) принимают только целочисленные значения, поэтому в класс Ship необходимо внести ряд изменений:

ship.py

class Ship:

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

 

    def __init__(self, ai_game):

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

        self.screen = ai_game.screen

❶         self.settings = ai_game.settings

        --пропуск--

 

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

        self.rect.midbottom = self.screen_rect.midbottom

 

        # Сохранение вещественной координаты центра корабля.

❷         self.x = float(self.rect.x)

 

        # Флаги перемещения: начинаем с неподвижного корабля

        self.moving_right = False

        self.moving_left = False

 

    def update(self):

        """Обновляет позицию корабля с учетом флагов."""

        # Обновляется атрибут x, не rect.

        if self.moving_right:

❸             self.x += self.settings.ship_speed

        if self.moving_left:

            self.x -= self.settings.ship_speed

 

        # Обновление атрибута rect на основании self.x.

❹         self.rect.x = self.x

 

    def blitme(self):

        --пропуск--

В классе Ship создается атрибут settings, чтобы он мог использоваться в методе update() . Позиция корабля изменяется с нецелым приращением пикселов, поэтому должна храниться в переменной, способной содержать вещественные значения. Формально атрибутам rect можно присвоить вещественные значения, но rect сохранит только целую часть этого значения. Для точного отслеживания позиции корабля определяется новый атрибут self.x . Функция float() используется для преобразования значения self.rect.x в вещественный формат, затем мы присваиваем это значение переменной self.x.

После изменения позиции корабля в update() значение self.x изменяется на величину, хранящуюся в settings.ship_speed . После обновления self.x новое значение используется для обновления атрибута self.rect.x, управляющего позицией корабля . В self.rect.x будет сохранена только целая часть self.x, но для отображения корабля этого достаточно.

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

Ограничение перемещений корабля

Если удерживать какую-нибудь клавишу со стрелкой достаточно долго, то корабль выйдет за край экрана. Сделаем так, чтобы корабль останавливался при достижении края экрана. Задача решается путем изменения метода update() в классе Ship:

ship.py

    def update(self):

        """Обновляет позицию корабля с учетом флагов."""

        # Обновляется атрибут x объекта ship, не rect.

❶         if self.moving_right and self.rect.right < self.screen_rect.right:

            self.x += self.settings.ship_speed

❷         if self.moving_left and self.rect.left > 0:

            self.x -= self.settings.ship_speed

 

        # Обновление атрибута rect на основании self.x.

        self.rect.x = self.x

Этот код проверяет позицию корабля перед изменением значения self.x. Выражение self.rect.right возвращает координату x правого края прямоугольника ко­рабля. Если это значение меньше значения, возвращаемого self.screen_rect.right, значит, корабль еще не достиг правого края экрана . То же относится и к левому краю: если координата x левой стороны прямоугольника больше 0, значит, корабль еще не достиг левого края экрана . Проверка гарантирует, что корабль будет оставаться в пределах экрана перед изменением значения self.x.

Если вы запустите программу alien_invasion.py сейчас, то движение корабля будет останавливаться у края экрана. Согласитесь, эффектно: мы всего лишь добавили условную проверку в оператор if, но все выглядит так, словно у края экрана корабль наталкивается на невидимую стену или силовое поле!

Рефакторинг метода _check_events()

В ходе разработки метод _check_events() будет становиться все длиннее, поэтому мы выделим из него еще два отдельных метода для обработки событий KEYDOWN и KEYUP:

alien_invasion.py

    def _check_events(self):

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

        for event in pygame.event.get():

            if event.type == pygame.QUIT:

                sys.exit()

            elif event.type == pygame.KEYDOWN:

                self._check_keydown_events(event)

            elif event.type == pygame.KEYUP:

                self._check_keyup_events(event)

 

    def _check_keydown_events(self, event):

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

        if event.key == pygame.K_RIGHT:

            self.ship.moving_right = True

        elif event.key == pygame.K_LEFT:

            self.ship.moving_left = True

 

    def _check_keyup_events(self, event):

        """Реагирует на отпускание клавиш."""

        if event.key == pygame.K_RIGHT:

            self.ship.moving_right = False

        elif event.key == pygame.K_LEFT:

            self.ship.moving_left = False

В программе появились два вспомогательных метода: _check_keydown_events() и _check_keyup_events(). Каждый метод получает параметры self и event. Тела двух методов скопированы из метода _check_events(), а старый код заменен вызовами новых методов. Новая структура кода упрощает метод _check_events() и облегчает последующее программирование реакции на действия игрока.

Нажатие клавиши Q для завершения

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

alien_invasion.py

    def _check_keydown_events(self, event):

        --пропуск--

        elif event.key == pygame.K_LEFT:

            self.ship.moving_left = True

        elif event.key == pygame.K_q:

            sys.exit()

В код метода _check_keydown_events() добавляется новый блок. Теперь в процессе тестирования можно закрыть игру путем нажатия клавиши Q вместо того, чтобы пользоваться кнопкой закрытия окна.

Запуск игры в полноэкранном режиме

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

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

alien_invasion.py

    def __init__(self):

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

        pygame.init()

        self.settings = Settings()

 

❶         self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)

❷         self.settings.screen_width = self.screen.get_rect().width

        self.settings.screen_height = self.screen.get_rect().height

        pygame.display.set_caption("Alien Invasion")

При создании экранной поверхности передается размер (0, 0) и параметр pyga­me.FULLSCREEN . Благодаря этим значениям Pygame получает указание вычислить размер окна, заполняющего весь экран. Так как ширина и высота экрана неизвестны заранее, эти настройки обновляются после создания экрана . Атрибуты width и height прямоугольника экрана используются для обновления объекта settings.

Если вам понравится, как игра выглядит или работает в полноэкранном режиме, то оставьте новые настройки. Если вы предпочитаете, чтобы игра работала в отдельном окне, — вернитесь к исходной реализации, где назначались конкретные размеры экрана.

ПРИМЕЧАНИЕ

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

Обобщим

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

Файл alien_invasion.py

Главный файл программы alien_invasion.py содержит класс AlienInvasion, в котором находится ряд важных атрибутов, используемых в процессе игры: настройки хранятся в settings, основная поверхность для вывода изображения — в screen, а экземпляр ship тоже создается в этом файле. Кроме того, в alien_invasion.py содержится главный цикл игры — while с вызовами методов _check_events(), ship.update() и _update_screen(). Вдобавок при каждой итерации цикла происходит отсчет времени.

Метод _check_events() обнаруживает важные события (например, нажатия и отпускания клавиш) и обрабатывает все эти типы событий с помощью методов _check_keydown_events() и _check_keyup_events(). На данный момент эти методы управляют движением корабля. Класс AlienInvasion также содержит метод _update_screen(), который перерисовывает экран при каждом проходе основ­ного цикла.

Файл alien_invasion.py — единственный файл, который должен запускаться для игры «Инопланетное вторжение». Остальные файлы — settings.py и ship.py — содержат код, который импортируется в этот файл.

Файл settings.py

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

Файл ship.py

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

Упражнения

12.3. Документация Pygame. Разработка игры зашла уже достаточно далеко, и вам стоит просмотреть документацию Pygame. Главная страница Pygame находится по адресу https://www.pygame.org/, а главная страница документации — по адресу https://www.pygame.org/docs/. В данный момент вы можете ограничиться простым просмотром документации. Она не понадобится вам для завершения этого проекта, но пригодится, если вы захотите внести изменения в игру или займетесь созданием собственной игры.

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

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

Стрельба

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

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

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

settings.py

    def __init__(self):

        --пропуск--

        # Параметры снаряда

        self.bullet_speed = 2.0

        self.bullet_width = 3

        self.bullet_height = 15

        self.bullet_color = (60, 60, 60)

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

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

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

bullet.py

import pygame

from pygame.sprite import Sprite

 

class Bullet(Sprite):

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

 

    def __init__(self, ai_game):

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

        super().__init__()

        self.screen = ai_game.screen

        self.settings = ai_game.settings

        self.color = self.settings.bullet_color

 

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

❶         self.rect = pygame.Rect(0, 0, self.settings.bullet_width,

            self.settings.bullet_height)

❷         self.rect.midtop = ai_game.ship.rect.midtop

 

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

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

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

Затем создается атрибут rect снаряда . Снаряд не создается на основе готового изображения, поэтому прямоугольник приходится рисовать с нуля с помощью класса pygame.Rect(). При создании экземпляра этого класса необходимо задать координаты левого верхнего угла прямоугольника, его ширину и высоту. Прямо­угольник инициализируется в строке (0, 0), но в следующих двух строках перемещается в нужное место, так как позиция снаряда зависит от позиции корабля. Ширина и высота снаряда определяются значениями, хранящимися в self.settings.

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

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

bullet.py

    def update(self):

        """Перемещает снаряд вверх по экрану."""

        # Обновление точной позиции снаряда.

❶         self.y -= self.settings.bullet_speed

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

❷         self.rect.y = self.y

 

    def draw_bullet(self):

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

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

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

Атрибут bullet_speed позволяет увеличить скорость снарядов по ходу игры или при изменении ее поведения. Координата x снаряда после выстрела не изменяется, поэтому снаряд летит вертикально по прямой линии.

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

Группировка снарядов

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

Сначала мы импортируем новый класс Bullet:

alien_invasion.py

--пропуск--

from ship import Ship

from bullet import Bullet

Группа будет создаваться в методе __init__():

    def __init__(self):

        --пропуск--

        self.ship = Ship(self)

        self.bullets = pygame.sprite.Group()

Позиция снаряда будет обновляться при каждом проходе цикла while:

    def run_game(self):

        """Запускает основной цикл игры."""

        while True:

            self._check_events()

            self.ship.update()

            self.bullets.update()

            self._update_screen()

            self.clock.tick(60)

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

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

В классе AlienInvasion необходимо внести изменения в метод _check_keydown_events(), чтобы при нажатии клавиши Пробел происходил выстрел. Изменять метод _check_keyup_events() не нужно, поскольку при отпускании клавиши ничего не происходит. Необходимо также изменить _update_screen() и вывести каждый снаряд на экран перед вызовом flip().

При обработке выстрела придется выполнить довольно большую работу, для которой мы напишем новый метод fire_bullet():

alien_invasion.py

    def _check_keydown_events(self, event):

        --пропуск--

        elif event.key == pygame.K_q:

            sys.exit()

❶         elif event.key == pygame.K_SPACE:

            self._fire_bullet()

 

    def _check_keyup_events(self, event):

        --пропуск--

 

    def _fire_bullet(self):

        """Создает новый снаряд и добавляет его в группу bullets."""

❷         new_bullet = Bullet(self)

❸         self.bullets.add(new_bullet)

 

    def _update_screen(self):

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

        self.screen.fill(self.settings.bg_color)

❹         for bullet in self.bullets.sprites():

            bullet.draw_bullet()

        self.ship.blitme()

 

        pygame.display.flip()

--пропуск--

При нажатии клавиши Пробел вызывается _fire_bullet() . В коде _fire_bullet() мы создаем экземпляр Bullet, которому присваивается имя new_bullet . Он добавляется в группу bullets путем вызова метода add(). Метод add() похож на append(), но написан специально для групп Pygame.

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

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

12_03.tif 

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

Удаление выпущенных снарядов

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

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

alien_invasion.py

    def run_game(self):

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

        while True:

            self._check_events()

            self.ship.update()

            self.bullets.update()

 

        # Удаление снарядов, вышедших за край экрана.

❶         for bullet in self.bullets.copy():

❷             if bullet.rect.bottom <= 0:

❸                  self.bullets.remove(bullet)

❹         print(len(self.bullets))

 

        self._update_screen()

        self.clock.tick(60)

При использовании цикла for со списком (или группой в Pygame) Python ожидает, что длина списка будет оставаться прежней во время выполнения цикла. Таким образом, вы не можете удалять элементы из списка или группы в цикле for, поэтому перебирать нужно копию группы. Метод copy() используется для создания цикла for , в котором можно изменять группу снарядов. Программа проверяет каждый снаряд и определяет, не покинул ли он пределы экрана . Если да, то снаряд удаляется из bullets . Затем добавляется вызов функции print(), чтобы увидеть, сколько снарядов сейчас существует в игре; по выведенному значению можно убедиться в том, что снаряды действительно удаляются при достижении верхнего края экрана .

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

Ограничение количества снарядов

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

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

settings.py

        # Параметры снаряда

        --пропуск--

        self.bullet_color = (60, 60, 60)

        self.bullets_allowed = 3

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

alien_invasion.py

    def _fire_bullet(self):

        """Создает новый снаряд и добавляет его в группу bullets."""

        if len(self.bullets) < self.settings.bullets_allowed:

            new_bullet = Bullet(self)

            self.bullets.add(new_bullet)

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

Создание метода _update_bullets()

Мы хотим, чтобы класс AlienInvasion был как можно более простым, поэтому после написания и проверки кода управления снарядами его можно переместить в отдельный метод. Мы создадим новый метод _update_bullets() и добавим его непосредственно перед _update_screen():

alien_invasion.py

    def _update_bullets(self):

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

        # Обновление позиций снарядов.

        self.bullets.update()

 

        # Удаление снарядов, вышедших за край экрана.

        for bullet in self.bullets.copy():

            if bullet.rect.bottom <= 0:

                self.bullets.remove(bullet)

Код _update_bullets() вырезается и вставляется из run_game(); мы всего лишь немного уточнили комментарии.

Цикл while в run_game() снова выглядит просто:

alien_invasion.py

        while True:

            self._check_events()

            self.ship.update()

            self._update_bullets()

            self._update_screen()

            self.clock.tick(60)

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

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

Упражнения

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

Резюме

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

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

Назад: Часть II. Проекты
Дальше: 13. Осторожно, пришельцы!