Хабрахабр

[Перевод] Создание игр на Python 3 и Pygame: Часть 5

image

Это последняя из пяти частей туториала, посвящённого созданию игр с помощью Python 3 и PyGame. В четвёртой части мы научились распознавать коллизии, реагировать на то, что мяч сталкивается с разными игровыми объектами и создали игровое меню с собственными кнопками.

(Остальные части туториала: первая, вторая, третья, четвёртая.)

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

Конец игры

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

Game Over!

Полю game_over класса Game присваивается значение False в методе __init__() класса Game. Основной цикл продолжается до тех пор, пока переменная game_over не изменит значение на True:

class Game: def __init__(self, caption, width, height, back_image_filename, frame_rate): ... self.game_over = False ... def run(self): while not self.game_over: self.surface.blit(self.background_image, (0, 0)) self.handle_events() self.update() self.draw() pygame.display.update() self.clock.tick(self.frame_rate)

Всё это происходит в классе Breakout в следующих случаях:

  • Игрок нажимает в меню на кнопку QUIT.
  • Игрок теряет последнюю жизнь.
  • Игрок разрушает все кирпичи.
def on_quit(button): self.game_over = True self.is_game_running = False def handle_ball_collisions(self): ... # Удар об пол if self.ball.top > c.screen_height: self.lives -= 1 if self.lives == 0: self.game_over = True if not self.bricks: self.show_message('YOU WIN!!!', centralized=True) self.is_game_running = False self.game_over = True return def update(self): ... if not self.bricks: self.show_message('YOU WIN!!!', centralized=True) self.is_game_running = False self.game_over = True return

Отображение сообщения о конце игры

Обычно при завершении игры мы не хотим, чтобы окно игры молча пропадало. Исключением является случай, когда мы нажимаем на кнопку QUIT в меню. Когда игрок теряет последнюю жизнь, Breakout показывает традиционное сообщение 'GAME OVER!', а когда игрок выигрывает, она показывает сообщение 'YOU WIN!'

В обоих случаях используется функция show_message(). Она отображает текст поверх текущего экрана (игра приостанавливается) и ждёт несколько секунд, прежде чем вернуться. В следующей итерации игрового цикла проверка поля game_over определит, что оно равно True, после чего программа завершится.

Вот как выглядит функция show_message():

def show_message(self, text, color=colors.WHITE, font_name='Arial', font_size=20, centralized=False): message = TextObject(c.screen_width // 2, c.screen_height // 2, lambda: text, color, font_name, font_size) self.draw() message.draw(self.surface, centralized) pygame.display.update() time.sleep(c.message_duration)

Сохранение рекордов между играми

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

Добавление звуковых эффектов и музыки

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

Звуковые эффекты

Для воспроизведения звуковых эффектов нам потребуются звуковые файлы (как и в случае с файлами изображений). Эти файлы могут иметь формат .wav, .mp3 или .ogg. Breakout хранит свои звуковые эффекты в папке sound_effects:

~/git/pygame-breakout > tree sound_effects/
sound_effects/
├── brick_hit.wav
├── effect_done.wav
├── level_complete.wav
└── paddle_hit.wav

Давайте посмотрим, как эти звуковые эффекты загружаются и воспроизводятся в нужное время. Во-первых, для воспроизведения звуковых эффектов (или фоновой музыки) нам нужно инициализировать звуковую систему Pygame. Это делается в классе Game: pygame.mixer.pre_init(44100, 16, 2, 4096)

Затем в классе Breakout все звуковые эффекты загружаются из config в объект pygame.mixer.Sound и хранятся в словаре:

# В config.py
sounds_effects = dict( brick_hit='sound_effects/brick_hit.wav', effect_done='sound_effects/effect_done.wav', paddle_hit='sound_effects/paddle_hit.wav', level_complete='sound_effects/level_complete.wav',
) # В breakout.py
class Breakout(Game): def __init__(self): ... self.sound_effects = { name: pygame.mixer.Sound(sound) for name, sound in c.sounds_effects.items()} ...

Теперь мы можем воспроизводить звуковые эффекты, когда происходит что-то интересное. Например, когда мяч ударяется об кирпич:

# Удар об кирпич
for brick in self.bricks: edge = intersect(brick, self.ball) if not edge: continue self.sound_effects['brick_hit'].play()

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

Запись собственных звуковых эффектов и сообщений

Запись собственных звуковых эффектов может быть простым и приносящим удовольствие занятием. В отличие от создания визуальных ресурсов, для него не требуется большого таланта. Кто угодно может сказать «Бум!» или «Прыг», или крикнуть «Вас убили. Повезёт в другой раз!»

Я часто прошу своих детей записывать звуковые эффекты и голосовые сообщения, сопровождающие текстовые сообщения, например 'YOU WIN!' или 'GAME OVER!' Единственное ограничение здесь — собственное воображение.

Воспроизведение фоновой музыки

Фоновая музыка должна воспроизводиться постоянно. Теоретически, можно создать очень долгий звуковой эффект, но чаще всего применяют зацикленное воспроизведение фоновой музыки. Музыкальные файлы могут иметь формат .wav, .mp3 или .midi. Вот как реализуется музыка:

music = pygame.mixer.music.load('background_music.mp3')
pygame.mixer.music.play(-1, 0.0)

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

Добавление расширенных возможностей

Давайте сделаем что-то любопытное. Разрушать кирпичи мячом интересно, но довольно быстро надоедает. Как насчёт общей системы спецэффектов? Мы разработаем расширяемую систему спецэффектов, связанную с некоторыми кирпичами, которая активируется при ударе мяча об кирпич.

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

Система спецэффектов

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

Наши спецэффекты определяются словарём из модуля breakout.py. Каждый эффект имеет имя (например, long_paddle) и значение, которое состоит из цвета кирпича, а также две функции. Функции задаются как лямбда-функции, берущие экземпляр Game, в который включается всё, что может изменять спецэффект в Breakout.

special_effects = dict( long_paddle=( colors.ORANGE, lambda g: g.paddle.bounds.inflate_ip( c.paddle_width // 2, 0), lambda g: g.paddle.bounds.inflate_ip( -c.paddle_width // 2, 0)), slow_ball=( colors.AQUAMARINE2, lambda g: g.change_ball_speed(-1), lambda g: g.change_ball_speed(1)), tripple_points=( colors.DARKSEAGREEN4, lambda g: g.set_points_per_brick(3), lambda g: g.set_points_per_brick(1)), extra_life=( colors.GOLD1, lambda g: g.add_life(), lambda g: None))

При создании кирпичей им может назначаться один из спецэффектов. Вот код:

def create_bricks(self): w = c.brick_width h = c.brick_height brick_count = c.screen_width // (w + 1) offset_x = (c.screen_width - brick_count * (w + 1)) // 2 bricks = [] for row in range(c.row_count): for col in range(brick_count): effect = None brick_color = c.brick_color index = random.randint(0, 10) if index < len(special_effects): x = list(special_effects.values())[index] brick_color = x[0] effect = x[1:] brick = Brick(offset_x + col * (w + 1), c.offset_y + row * (h + 1), w, h, brick_color, effect) bricks.append(brick) self.objects.append(brick) self.bricks = bricks

Класс Brick имеет поле effect, которое обычно имеет значение None, но может (с вероятностью в 30%) содержать один из определённых выше спецэффектов. Заметьте, что этот код не знает о том, какие эффекты существуют. Он просто получает эффект и цвет кирпича и при необходимости назначает их.

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

if brick.special_effect is not None: # Сброс предыдущего эффекта при его наличии if self.reset_effect is not None: self.reset_effect(self) # Срабатывание спецэффекта self.effect_start_time = datetime.now() brick.special_effect[0](self) # Задание текущей функции сброса эффекта self.reset_effect = brick.special_effect[1]

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

# Сброс спецэффекта при необходимости
if self.reset_effect: elapsed = datetime.now() - self.effect_start_time if elapsed >= timedelta(seconds=c.effect_duration): self.reset_effect(self) self.reset_effect = None

Увеличение ракетки

Эффект длинной ракетки заключается в увеличении ракетки на 50%. Её функция сброса возвращает ракетку к обычным размерам. Кирпич имеет цвет Orange:

long_paddle=( colors.ORANGE, lambda g: g.paddle.bounds.inflate_ip( c.paddle_width // 2, 0), lambda g: g.paddle.bounds.inflate_ip( -c.paddle_width // 2, 0)),

Замедление мяча

Ещё один эффект, помогающий в погоне за мячом, заключается замедлении мяча, то есть уменьшении его скорости на одну единицу. Кирпич имеет цвет Aquamarine.

slow_ball=(colors.AQUAMARINE2, lambda g: g.change_ball_speed(-1), lambda g: g.change_ball_speed(1)),

Больше очков

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

tripple_points=(colors.DARKSEAGREEN4, lambda g: g.set_points_per_brick(3), lambda g: g.set_points_per_brick(1)),

Дополнительные жизни

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

extra_life=(colors.GOLD1, lambda g: g.add_life(), lambda g: None))

Возможности, которые можно добавить в будущем

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

Переход на следующий уровень

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

Второй мяч

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

Сохраняющиеся рекорды

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

Бомбы и бонусы

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

Подведём итог

Разработка Breakout с помощью Python 3 и Pygame оказалась очень увлекательным занятием. Это очень мощное сочетание для создания 2D-игр (и для 3D-игр тоже). Если вы любите Python и хотите создавать собственные игры, то не колеблясь выбирайте Pygame.

Я совершенно точно буду создавать на Python и Pygame и другие игры.

Теги
Показать больше

Похожие статьи

Кнопка «Наверх»
Закрыть