() translation by (you can also view the original English article)
Обзор
Многие разработчики приходят в разработку ПО, потому что хотят создавать игры. Не все могут стать профессиональными разработчиками игр, но любой может создавать собственные игры из интереса (а может быть, и с выгодой). В этом туториале, состоящем из пяти частей, я расскажу вам, как создавать двухмерные однопользовательские игры с помощью Python 3 и замечательного фреймворка PyGame.
Мы создадим версию классической игры Breakout. Освоив этот туториал, вы будете чётко понимать, что необходимо для создания игры, познакомитесь с возможностями Pygame и напишете собственный пример игры.
Мы реализуем следующие функции и возможности:
- простые стандартные GameObject и TextObject
- простой стандартный Game object
- простая стандартная кнопка
- файл конфигурации
- обработка событий клавиатуры и мыши
- кирпичи, ракетка и мяч
- управление движением ракетки
- обработка коллизий мяча с объектами игры
- фоновое изображение
- звуковые эффекты
- расширяемая система спецэффектов
Не стоит ожидать, что игра будет очень красива графически. Я программист, а не художник, меня больше интересует эстетика кода. Созданный мной дизайн может неприятно удивить. С другой стороны, у вас будут почти неограниченные возможности по улучшению графики этой версии Breakout. Если вы отважитесь повторять за мной, посмотрите на скриншот:



Готовый исходный код выложен здесь.
Краткое введение в программирование игр
Главное в играх — перемещение пикселей на экране и издаваемый шум. Почти во всех видеоиграх есть эти элементы. В этой статье мы не будем рассматривать клиент-серверные и многопользовательские игры, для которых требуется много сетевого программирования.
Основной цикл
Основной цикл (main loop) игры выполняется и обновляет экран через фиксированные интервалы времени. Они называются частотой кадров и определяют плавность перемещения. Обычно игры обновляют экран 30-60 раз в секунду. Если частота будет меньше, то покажется, что объекты на экране дёргаются.
Внутри основного цикла есть три основных операции: обработка событий, обновление состояния игры и отрисовка текущего состояния на экране.
Обработка событий
События в игре состоят из всего, что происходит за пределами управления кода игры, но относится к выполнению игры. Например, если в Breakout игрок нажимает клавишу «стрелка влево», то игре нужно переместить ракетку влево. Стандартными событиями являются нажатия (и отжатия) клавиш, движение мыши, нажатия кнопок мыши (особенно в меню) и события таймера (например, действие спецэффекта может длиться 10 секунд).
Обновление состояния
Сердце любой игры — это её состояние: всё то, что она отслеживает и отрисовывает на экране. В случае Breakout к состоянию относятся положение всех кирпичей, позиция и скорость мяча, положение ракетки, а также жизни и очки.
Существует также вспомогательное состояние, позволяющее управлять игрой:
- Отображается ли сейчас меню?
- Закончена ли игра?
- Победил ли игрок?
Отрисовка
Игре нужно отображать своё состояние на экране, в том числе отрисовывать геометрические фигуры, изображения и текст.
Игровая физика
В большинстве игр симулируется физическое окружение. В Breakout мяч отскакивает от объектов и имеет очень приблизительную систему физики твёрдого тела (если это можно так назвать).
В более сложных играх могут использоваться более изощрённые и реалистичные физические системы (особенно в 3D-играх). Стоит также отметить, что в некоторых играх, например, в карточных, физики почти нет, и это совершенно нормально.
ИИ (искусственный интеллект)
Во многих играх мы сражаемся с компьютерными противниками, или в них есть враги, пытающиеся нас убить. Часто они ведут себя в игровом мире так, как будто обладают разумом.
Например, враги преследуют игрока и знают о его местоположении. В Breakout нет никакого ИИ. Игрок сражается с холодными и твёрдыми кирпичами. Однако ИИ в играх часто очень прост и всего лишь следует простым (или сложным) правилам, обеспечивающим псевдоразумные результаты.
Воспроизведение звука
Воспроизведение звука — ещё один важный аспект игр. В общем случае существует два типа звука: фоновая музыка и звуковые эффекты. В Breakout я реализую только звуковые эффекты, которые воспроизводятся при различных событиях.
Фоновая музыка — это просто музыка, постоянно играющая на фоне. В некоторых играх она не используется, а в некоторых меняется на каждом уровне.
Жизни, очки и уровни
В большинстве игр игрок имеет определённое количество жизней, и когда они заканчиваются, игра завершается. Также в играх часто присутствуют очки, позволяющие понять, насколько хорошо мы играем, и дающие мотивацию к самосовершенствованию или просто хвастаться друзьям своими рекордами. Во многих играх есть уровни, которые или совершенно отличаются, или постепенно увеличивают сложность.
Знакомство с Pygame
Прежде чем приступить к реализации игры, давайте немного узнаем о Pygame, который возьмёт на себя большую часть работы.
Что такое Pygame?
Pygame — это фреймворк языка Python для программирования игр. Он создан поверх SDL и обладает всем необходимым:
- зрелостью
- хорошим сообществом
- открытым исходным кодом
- кроссплатформенностью
- качественной документацией
- множеством примеров игр
простотой изучения
Установка Pygame
Введите pip install pygame
, чтобы установить фреймворк. Если вам нужно что-то ещё, то следуйте инструкциям из раздела Getting Started в Wiki проекта. Если у вас, как и у меня, macOS Sierra, то могут возникнуть проблемы. Мне удалось установить Pygame без сложностей, и код работает отлично, но окно игры никогда не появляется.
Это станет серьёзным препятствием при запуске игры. В конце концов мне пришлось запускать её в Windows внутри VirtualBox VM. Надеюсь, ко времени прочтения этой статьи проблема будет решена.
Архитектура игры
Играм нужно управлять кучей информации и выполнять почти одинаковые операции со множеством объектов. Breakout — это небольшая игра, однако попытка управлять всем в одном файле может оказаться слишком утомительной. Поэтому я решил создать файловую структуру и архитектуру, которая подойдёт и для гораздо более крупных игр.
Структура папок и файлов
1 |
├── Pipfile |
2 |
├── Pipfile.lock |
3 |
├── README.md |
4 |
├── ball.py |
5 |
├── breakout.py |
6 |
├── brick.py |
7 |
├── button.py |
8 |
├── colors.py |
9 |
├── config.py |
10 |
├── game.py |
11 |
├── game_object.py |
12 |
├── images |
13 |
│ └── background.jpg |
14 |
├── paddle.py |
15 |
├── sound_effects |
16 |
│ ├── brick_hit.wav |
17 |
│ ├── effect_done.wav |
18 |
│ ├── level_complete.wav |
19 |
│ └── paddle_hit.wav |
20 |
└── 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:
1 |
from pygame.rect import Rect |
2 |
|
3 |
|
4 |
class GameObject: |
5 |
def __init__(self, x, y, w, h, speed=(0,0)): |
6 |
self.bounds = Rect(x, y, w, h) |
7 |
self.speed = speed |
8 |
|
9 |
@property |
10 |
def left(self): |
11 |
return self.bounds.left |
12 |
|
13 |
@property |
14 |
def right(self): |
15 |
return self.bounds.right |
16 |
|
17 |
@property |
18 |
def top(self): |
19 |
return self.bounds.top |
20 |
|
21 |
@property |
22 |
def bottom(self): |
23 |
return self.bounds.bottom |
24 |
|
25 |
@property |
26 |
def width(self): |
27 |
return self.bounds.width |
28 |
|
29 |
@property |
30 |
def height(self): |
31 |
return self.bounds.height |
32 |
|
33 |
@property |
34 |
def center(self): |
35 |
return self.bounds.center |
36 |
|
37 |
@property |
38 |
def centerx(self): |
39 |
return self.bounds.centerx |
40 |
|
41 |
@property |
42 |
def centery(self): |
43 |
return self.bounds.centery |
44 |
|
45 |
def draw(self, surface): |
46 |
pass
|
47 |
|
48 |
def move(self, dx, dy): |
49 |
self.bounds = self.bounds.move(dx, dy) |
50 |
|
51 |
def update(self): |
52 |
if self.speed == [0, 0]: |
53 |
return
|
54 |
|
55 |
self.move(*self.speed) |
GameObject предназначен для того, чтобы быть базовым классом для других объектов. Он непосредственно раскрывает множество свойств его прямоугольника self.bounds, а в своём методе update()
он перемещает объект в соответствии с его текущей скоростью. Он ничего не делает в своём методе draw()
, который должен быть переопределён подклассами.
Класс Game
Класс Game — это ядро игры. Он выполняется в основном цикле. В нём есть множество полезных возможностей. Давайте разберём его метод за методом.
Метод __init__()
инициализирует сам Pygame, систему шрифтов и звуковой микшер. Три разных вызова нужны, так как не во всякой игре на Pygame используются все компоненты, поэтому можно контролировать подсистемы, которые мы используем, и инициализировать только нужные с соответствующими параметрами. Метод создаёт фоновое изображение, основную поверхность (на которой всё отрисовывается) и игровой таймер с правильной частотой кадров.
Элемент self.objects хранит все игровые объекты, которые должны рендериться и обновляться. Различные обработчики управляют списками функций-обработчиков, которые должны выполняться при определённых событиях.
1 |
import pygame |
2 |
import sys |
3 |
|
4 |
from collections import defaultdict |
5 |
|
6 |
|
7 |
class Game: |
8 |
def __init__(self, |
9 |
caption, |
10 |
width, |
11 |
height, |
12 |
back_image_filename, |
13 |
frame_rate): |
14 |
self.background_image = \ |
15 |
pygame.image.load(back_image_filename) |
16 |
self.frame_rate = frame_rate |
17 |
self.game_over = False |
18 |
self.objects = [] |
19 |
pygame.mixer.pre_init(44100, 16, 2, 4096) |
20 |
pygame.init() |
21 |
pygame.font.init() |
22 |
self.surface = pygame.display.set_mode((width, height)) |
23 |
pygame.display.set_caption(caption) |
24 |
self.clock = pygame.time.Clock() |
25 |
self.keydown_handlers = defaultdict(list) |
26 |
self.keyup_handlers = defaultdict(list) |
27 |
self.mouse_handlers = [] |
Методы update()
и draw()
очень просты. Они обходят все управляемые игровые объекты и вызывают соответствующие им методы. Если два объекта накладываются друг на друга на экране, то порядок списка объектов определяет, какой из них будет рендериться первым, а остальные будут частично или полностью его перекрывать.
1 |
def update(self): |
2 |
for o in self.objects: |
3 |
o.update() |
4 |
|
5 |
def draw(self): |
6 |
for o in self.objects: |
7 |
o.draw(self.surface) |
Метод handle_events()
слушает события, генерируемые Pygame, такие как события клавиш и мыши. Для каждого события он вызывает все функции-обработчики, которые должны обрабатывать события соответствующих типов.
1 |
def handle_events(self): |
2 |
for event in pygame.event.get(): |
3 |
if event.type == pygame.QUIT: |
4 |
pygame.quit() |
5 |
sys.exit() |
6 |
elif event.type == pygame.KEYDOWN: |
7 |
for handler in self.keydown_handlers[event.key]: |
8 |
handler(event.key) |
9 |
elif event.type == pygame.KEYUP: |
10 |
for handler in self.keydown_handlers[event.key]: |
11 |
handler(event.key) |
12 |
elif event.type in (pygame.MOUSEBUTTONDOWN, |
13 |
pygame.MOUSEBUTTONUP, |
14 |
pygame.MOUSEMOTION): |
15 |
for handler in self.mouse_handlers: |
16 |
handler(event.type, event.pos) |
Наконец, метод run()
выполняет основной цикл. Он выполняется до тех пор, пока элемент game_overне
принимает значение True. В каждой итерации он рендерит фоновое изображение и вызывает по порядку методы handle_events()
, update()
и draw()
.
Затем он обновляет экран, то есть записывает на физический дисплей всё содержимое, которое было отрендерено на текущей итерации. И последнее, но не менее важное — он вызывает метод clock.tick()
для управления тем, когда будет вызвана следующая итерация.
1 |
def run(self): |
2 |
while not self.game_over: |
3 |
self.surface.blit(self.background_image, (0, 0)) |
4 |
|
5 |
self.handle_events() |
6 |
self.update() |
7 |
self.draw() |
8 |
|
9 |
pygame.display.update() |
10 |
self.clock.tick(self.frame_rate) |
Заключение
В этой части мы изучили основы программирования игр и все компоненты, участвующие в создании игр. Также мы рассмотрели сам Pygame и узнали, как его установить. Наконец, мы погрузились в архитектуру игры и изучили структуру папок, классы GameObject
и Game.
Во второй части мы рассмотрим класс TextObject
, используемый для рендеринга текста на экране. Мы создадим основное окно, в том числе и фоновое изображение, а затем узнаем, как отрисовывать объекты (мяч и ракетку).
Кроме того, посмотрите, что у нас есть для продажи и обучения на Envato Market, и не стесняйтесь задавать любые вопросы и предоставлять свои ценные отзывы, используя приведенную ниже форму.