Воспроизведение звука — ещё один важный аспект игр. В общем случае существует два типа звука: фоновая музыка и звуковые эффекты. В Breakout я реализую только звуковые эффекты, которые воспроизводятся при различных событиях.
Фоновая музыка — это просто музыка, постоянно играющая на фоне. В некоторых играх она не используется, а в некоторых меняется на каждом уровне.
Жизни, очки и уровни
В большинстве игр игрок имеет определённое количество жизней, и когда они заканчиваются, игра завершается. Также в играх часто присутствуют очки, позволяющие понять, насколько хорошо мы играем, и дающие мотивацию к самосовершенствованию или просто хвастаться друзьям своими рекордами. Во многих играх есть уровни, которые или совершенно отличаются, или постепенно увеличивают сложность.
Знакомство с Pygame
Прежде чем приступить к реализации игры, давайте немного узнаем о Pygame, который возьмёт на себя большую часть работы.
Что такое Pygame?
Pygame — это фреймворк языка Python для программирования игр. Он создан поверх SDL и обладает всем необходимым:
|
|
· зрелостью
· хорошим сообществом
· открытым исходным кодом
· кроссплатформенностью
· качественной документацией
· множеством примеров игр
· простотой изучения
Установка Pygame
Введите pip install pygame, чтобы установить фреймворк. Если вам нужно что-то ещё, то следуйте инструкциям из раздела Getting Started в Wiki проекта. Если у вас, как и у меня, macOS Sierra, то могут возникнуть проблемы. Мне удалось установить Pygame без сложностей, и код работает отлично, но окно игры никогда не появляется.
Это станет серьёзным препятствием при запуске игры. В конце концов мне пришлось запускать её в Windows внутри VirtualBox VM. Надеюсь, ко времени прочтения этой статьи проблема будет решена.
Архитектура игры
Играм нужно управлять кучей информации и выполнять почти одинаковые операции со множеством объектов. Breakout — это небольшая игра, однако попытка управлять всем в одном файле может оказаться слишком утомительной. Поэтому я решил создать файловую структуру и архитектуру, которая подойдёт и для гораздо более крупных игр.
Структура папок и файлов
├── Pipfile
├── Pipfile.lock
├── README.md
├── ball.py
├── breakout.py
├── brick.py
├── button.py
├── colors.py
├── config.py
├── game.py
├── game_object.py
├── images
│ └── background.jpg
├── paddle.py
├── sound_effects
│ ├── brick_hit.wav
│ ├── effect_done.wav
│ ├── level_complete.wav
│ └── paddle_hit.wav
└── text_object.py
Pipfile и Pipfile.lock — это современный способ управления зависимостями в Python. Папка images содержит изображения, используемые игрой (в нашей версии будет только фоновое изображение), а в папке sound_effects directory лежат короткие звуковые клипы, используемые (как можно догадаться) в качестве звуковых эффектов.
|
|
Файлы ball.py, paddle.py и brick.py содержат код, относящийся к каждому из этих объектов Breakout. Подробнее я рассмотрю их в следующих частях туториала. Файл text_object.py содержит код отображения текста на экране, а в файле background.py содержится игровая логика Breakout.
Однако существует несколько модулей, создающих произвольный «скелет» общего назначения. Определённые в них классы можно будет использовать в других играх на основе Pygame.
Класс GameObject
GameObject представляет собой визуальный объект, знающий о том, как себя рендерить, сохранять свои границы и перемещаться. В Pygame есть и класс Sprite, исполняющий похожую роль, но в этом туториале я хочу показать вам, как всё работает на низком уровне, а не полагаться слишком активно на готовую магию. Вот как выглядит класс GameObject:
from pygame.rect import Rect
class GameObject:
def __init__(self, x, y, w, h, speed=(0,0)):
self.bounds = Rect(x, y, w, h)
self.speed = speed
@property
def left(self):
return self.bounds.left
@property
def right(self):
return self.bounds.right
@property
def top(self):
return self.bounds.top
@property
def bottom(self):
return self.bounds.bottom
@property
def width(self):
return self.bounds.width
@property
def height(self):
return self.bounds.height
@property
def center(self):
return self.bounds.center
@property
def centerx(self):
return self.bounds.centerx
@property
def centery(self):
return self.bounds.centery
def draw(self, surface):
pass
def move(self, dx, dy):
self.bounds = self.bounds.move(dx, dy)
def update(self):
if self.speed == [0, 0]:
return
self.move(*self.speed)
GameObject предназначен для того, чтобы быть базовым классом для других объектов. Он непосредственно раскрывает множество свойств его прямоугольника self.bounds, а в своём методе update() он перемещает объект в соответствии с его текущей скоростью. Он ничего не делает в своём методе draw(), который должен быть переопределён подклассами.
Класс Game
Класс Game — это ядро игры. Он выполняется в основном цикле. В нём есть множество полезных возможностей. Давайте разберём его метод за методом.
Метод __init__() инициализирует сам Pygame, систему шрифтов и звуковой микшер. Три разных вызова нужны, так как не во всякой игре на Pygame используются все компоненты, поэтому можно контролировать подсистемы, которые мы используем, и инициализировать только нужные с соответствующими параметрами. Метод создаёт фоновое изображение, основную поверхность (на которой всё отрисовывается) и игровой таймер с правильной частотой кадров.
Элемент self.objects хранит все игровые объекты, которые должны рендериться и обновляться. Различные обработчики управляют списками функций-обработчиков, которые должны выполняться при определённых событиях.
import pygame
import sys
from collections import defaultdict
class Game:
def __init__(self,
caption,
width,
height,
back_image_filename,
frame_rate):
self.background_image = \
pygame.image.load(back_image_filename)
self.frame_rate = frame_rate
self.game_over = False
self.objects = []
pygame.mixer.pre_init(44100, 16, 2, 4096)
pygame.init()
pygame.font.init()
self.surface = pygame.display.set_mode((width, height))
pygame.display.set_caption(caption)
self.clock = pygame.time.Clock()
self.keydown_handlers = defaultdict(list)
self.keyup_handlers = defaultdict(list)
self.mouse_handlers = []
Методы update() и draw() очень просты. Они обходят все управляемые игровые объекты и вызывают соответствующие им методы. Если два объекта накладываются друг на друга на экране, то порядок списка объектов определяет, какой из них будет рендериться первым, а остальные будут частично или полностью его перекрывать.
def update(self):
for o in self.objects:
o.update()
def draw(self):
for o in self.objects:
o.draw(self.surface)
Метод handle_events() слушает события, генерируемые Pygame, такие как события клавиш и мыши. Для каждого события он вызывает все функции-обработчики, которые должны обрабатывать события соответствующих типов.
|
|
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.KEYDOWN:
for handler in self.keydown_handlers[event.key]:
handler(event.key)
elif event.type == pygame.KEYUP:
for handler in self.keydown_handlers[event.key]:
handler(event.key)
elif event.type in (pygame.MOUSEBUTTONDOWN,
pygame.MOUSEBUTTONUP,
pygame.MOUSEMOTION):
for handler in self.mouse_handlers:
handler(event.type, event.pos)
Наконец, метод run() выполняет основной цикл. Он выполняется до тех пор, пока элемент game_over не принимает значение True. В каждой итерации он рендерит фоновое изображение и вызывает по порядку методы handle_events(), update() и draw().
Затем он обновляет экран, то есть записывает на физический дисплей всё содержимое, которое было отрендерено на текущей итерации. И последнее, но не менее важное — он вызывает метод clock.tick() для управления тем, когда будет вызвана следующая итерация.
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)
Заключение
В этой части мы изучили основы программирования игр и все компоненты, участвующие в создании игр. Также мы рассмотрели сам Pygame и узнали, как его установить. Наконец, мы погрузились в архитектуру игры и изучили структуру папок, классы GameObject и Game.
Во второй части мы рассмотрим класс TextObject, используемый для рендеринга текста на экране. Мы создадим основное окно, в том числе и фоновое изображение, а затем узнаем, как отрисовывать объекты (мяч и ракетку).