Вы находитесь на странице: 1из 108

Разработка игр Playdate на языке С

Разработка и документация видеоигры для консоли Playdate

Автор
Alberto Benavent Ramón

Преподаватель
Francisco José Gallego Duran
Ciencia de la Computación e Inteligencia Artificial
Аннотация
Playdate — это новая портативная консоль, разработанная Panic, которая будет выпущена
в 2021 году. Её цель — предложить уникальный и удивительный опыт для любителей
видеоигр, и по этой причине она обладает нетрадиционными характеристиками: в
дополнение к обычным кнопкам управления и действия, она имеет отражающий
монохромный черно-белый экран, акселерометр и рукоятку (crank) на боковине, которая
тоже действует как контроллер.
В этой бакалаврской диссертации будут рассмотрены возможности дизайна видеоигр,
для такого своеобразного оборудования, посредством создания нескольких прототипов,
охватывающих все поддерживаемые языки программирования и их оценки. С точки
зрения производительности Playdate достаточно скромен; по этой причине основное
внимание будет уделяться низкоуровневому программированию для достижения
максимально возможной производительности. Знания, полученные на первом этапе,
будут применены при разработке полноценной игры «TinySeconds» на языке C.
«TinySeconds» — это 2D-платформер, в котором каждый уровень необходимо пройти за
2,5 секунды. В дополнение к этому лимиту, чтобы перейти в следующий мир, игрок
должен последовательно пройти все последующие уровни в течение общего ограничения
по времени. Это делает игру захватывающей с отличной возможностью повторного
прохождения, поскольку она предлагает игроку попрактиковаться и улучшить время
прохождения. Помимо ограничения по времени, различные типы препятствий
добавляют разнообразия уровням, используя уникальные для консоли характеристики,
например, такие как рукоятка.
В дополнение к документированию разработки этих проектов будет включено
руководство по программированию на C для Playdate, в котором обучаются основным
принципам настройки среды программирования в Windows и разрабатывается пример
программы. Цель этой главы — восполнить недостаток документации по
программированию на C для Playdate на платформе Windows, поскольку официальное
руководство сосредоточено на языке Lua в средах Mac.
Resumen
Playdate es una nueva consola portátil desarrollada por Panic que será lanzada al Mercado en
2021. Su objetivo es ofrecer una experiencia distinta y sorprendente a entusiastas de los
videojuegos, y por ello, presenta características poco convencionales: además de los habituales
botones direccionales y de acción, tiene una pantalla monocroma reflectante en blanco y negro
puros, acelerómetro, y una manivela en el lado que sirve como controlador.
En esta memoria, se explorarán las posibilidades de diseño de videojuegos que ofrece un
hardware tan peculiar mediante la creación de diversos prototipos, cubriendo los diferentes
lenguajes de programación que soporta y realizando una evaluación de los mismos. A nivel de
hardware es una consola de potencia modesta, por lo que se optará por la programación a bajo
nivel para buscar el mejor rendimiento posible. Este conocimiento adquirido será después
aplicado al desarrollo de un juego completo en C, “TinySeconds”.
“TinySeconds” es un videojuego de plataformas en vista lateral donde cada nivel debe ser
completado en menos de 2,5 segundos. Además, para poder progresar de un mundo al siguiente,
los niveles de un mismo mundo deben ser superados consecutivamente en un tiempo limitado.
Esto dota al juego de un ritmo frenético y de gran rejugabilidad al inviter a los jugadores a
practicar para mejorar sus tiempos. Además de la limitación temporal, diversos tipos de
obstáculos añaden variedad a los niveles utilizando características propias de la consola como la
manivela.
Además de documentar el desarrollo de estos proyectos, se desarrollará un tutorial de
programación en C para Playdate, instruyendo los principios básicos de configuración del
entorno de programación en Windows, y desarrollando un programa de ejemplo. Este capítulo
nace para suplir la falta de documentación oficial sobre programación en C para la consola en un
entorno Windows, ya que los recursos oficiales se centran en el lenguaje Lua y entornos Mac.
Благодарность
Эта работа была бы невозможна без поддержки, которую я получил от своего окружения
во время её разработки.
Я хотел бы поблагодарить моего научного руководителя Франциско Хосе Гальего за его
руководство при разработке этой бакалаврской диссертации, а также за то, что он
поделился страстью и знаниями, которые он хранит в области видеоигр, со своими
студентами.
Друзьям, которых я приобрёл во время учебы в университете, и тем, кто всегда со мной
рядом: спасибо за то, что вы заставили меня насладиться этими прошедшими пятью
годами моей жизни. Вы все были постоянным источником радости и поддержки, и мне не
терпится пережить ещё множество приключений вместе с вами.
Спасибо моей семье за то, что выслушали мои рассуждения о разработке этой
диссертации, за то, что они были самыми любящими и поддерживали меня, а также за то,
что они сыграли главную роль в моих самых счастливых воспоминаниях; вы превратили
меня в того, кем я являюсь сегодня.
Итак, занимательные истории, весёлые игровые системы... Они уже существуют в
этом мире.
Я хочу увидеть, что находится за этой стеной.
Как бы вы это ни называли, это пространство,
куда ещё никто не заходил.

Йоко Таро
Содержание
1. Введение 1
2. Основные цели 2
3. Теоретическая основа 5
3.1. Playdate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
3.1.1. Характеристика оборудования . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3.2. Художественная составляющая . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
3.2.1. Игры Playdate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
3.2.1.1. Crankin’s Time Travel Adventure . . . . . . . . . . . . . . . . . . . . . . . . . . 7
3.2.1.2. Daily Driver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3.2.1.3. PlayMaker . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3.2.2. Другие игры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
3.2.2.1. Super Mario 3D World . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
3.2.2.2. Rhithm Heaven . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
3.2.2.3. BOXBOY! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.2.2.4. Minit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.2.3. Заключение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
4. Методология 15
5. Работа с Playdate на языке C 17
5.1. Настройка среды . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
5.1.1. Создание шаблона . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
5.1.2. Структура проекта Playdate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
5.2. Hello, World! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
5.2.1. Некоторые улучшения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
5.2.2. По частоте кадров . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
5.2.3. Подпрыгивания по кругу . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
5.2.4. Прокрутите рукоятку . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
5.2.5. Дополнительные шаги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
6. Разработка 27
6.1. Итерация 0 – Знакомство с Playdate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
6.1.1. Итерация 0.1 - язык Lua . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
6.1.2. Итерация 0.2 - язык С и С++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
6.1.3. Итерация 0.3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
6.2. Игра: TinySeconds . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
6.2.1 Концепция . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31

xii
xiv Содержание

6.3. Итерация 1 - Установка фундамента . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30


6.3.1. Введение в систему компонента сущности (Entity Component System) . . . . . . . . 31
6.3.2. Упрощенная версия системы Entity Component (ECS) . . . . . . . . . . . . . . . . . 32
6.3.3. Полная реализация ECS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
6.3.4. Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
6.4. Итерация 2 - Тайловые карты и движение . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
6.4.1. Tilemaps (Тайловые карты) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
6.4.2. Обозначение объектов JavaScript (JSON) . . . . . . . . . . . . . . . . . . . . . . . . 36
6.4.2.1. Ошибка декодера JSON . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
6.4.3. Рисование тайловой карты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
6.4.3.1. Ошибка ClipRect . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
6.4.4. Движение игрока . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
6.4.5. Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
6.5. Итерация 3 - Коллизии (Столкновения) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
6.5.1. Столкновение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
6.5.2. Дельта времени . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
6.5.3. Обновление движения игрока . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
6.5.4. Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
6.6. Итерация 4 - Вход в игровой цикл . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
6.6.1. Система запуска . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
6.6.2. Лимит времени . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
6.6.3. Чтение объектов из тайловой карты . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
6.6.4. Перезапуск уровня . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
6.6.5. Изменение уровня . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
6.6.6. Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
6.7. Итерация 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
6.7.1. Переключить блоки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
6.7.2. Функции преобразования . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
6.7.3. Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
6.8. Итерация 6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
6.8.1. Улучшенная система столкновений . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
6.8.2. Управление состоянием игры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
6.8.3. Улучшена физика игрока . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
6.8.4. Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
6.9. Итерация 7 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
6.9.1. Вектор (Vector2f) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
6.9.2. Бамперы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
6.9.3. Вывод . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
6.10. Итерация 8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
6.10.1. Улучшенные бамперы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
6.10.2. Новое состояние автомата . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
6.10.2.1. Меню состояний . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
6.10.2.2. Состояние в игре . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
6.10.2.3. Состояние мира в игре . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
6.10.2.4. Состояние победы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Содержание xv

6.10.3. Система точек доступа меню . . . . . . . . . . . . . . . . . . . . . . . . . . . 54


6.10.4. Тестирование пользователями и изменение дизайна . . . . . . . . . . . . . . 55
6.10.5. Система летающих часов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
6.10.6. Линейная интерполяция . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
6.10.7. Различные тайлы в мире . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
6.10.8. Вывод . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
6.11. Итерация 9 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
6.11.1. Сохранение прогресса . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
6.11.2. Рисуем внешний мир . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
6.11.3. Добавление музыки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
6.11.4. Применение мирового таймера . . . . . . . . . . . . . . . . . . . . . . . . . 60
6.11.4.1. Система ограждений . . . . . . . . . . . . . . . . . . . . . . . . . . 61
6.11.5. Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
7. Заключение 63
7.1. Состояние игры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
7.2. Улучшения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
7.3. Извлечённые уроки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
7.4. Личные выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65

Источники 67

Список сокращений и аббревиатур 69

A. Проведённые эксперименты 71
A.1. Lua . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
A.1.1. Hello world . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
A.1.2. Макет Dr. Mario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
A.1.3. Устройте сюрприз . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
A.1.4. Наклонная мини-игра . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
A.1.5. Ритмическая игра . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
A.2. C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
A.2.1. Hello World . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
A.2.2. Упрощённый ECS Starfield эффект . . . . . . . . . . . . . . . . . . . . . . . . 77
A.2.3. Полный ECS Starfield эффект . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
A.3. C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
A.3.1. Hello World . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
A.4. Pulp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
A.4.1. Приключенческая игра . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79

B. Отчёты об ошибках 81
B.1. Ошибка пропуска JSON . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
B.1.1. Ошибка при пропуске пары JSON в методе shouldDecodeTableValueForKey() 81
B.1.1.1. Настройка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
B.1.1.2. Шаги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
B.1.1.3. Ожидаемые результаты . . . . . . . . . . . . . . . . . . . . . . . . . 82
xvi Содержание

B.1.1.4. Фактические результаты . . . . . . . . . . . . . . . . . . . . . . . . . 82


B.1.1.5. Частота . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
B.1.1.6. Строгость . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
B.1.1.7. Обходной путь . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
B.1.2. Вывод . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
B.2. Ошибка обрезки прямоугольника . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
B.2.1. Обрезка ширины/высоты прямоугольника в зависимости от положения . . . 83
B.2.1.1. Настройка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
B.2.1.2. Шаги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
B.2.1.3. Ожидаемые результаты . . . . . . . . . . . . . . . . . . . . . . . . . 83
B.2.1.4. Фактические результаты . . . . . . . . . . . . . . . . . . . . . . . . . 84
B.2.1.5. Частота . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
B.2.1.6. Строгость . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
B.2.1.7. Обходной путь . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
B.2.2. Вывод . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84

C. Программа Tiled 85

D. Простой конечный автомат 87


Указатель иллюстраций
3.1. Модель консоли Playdate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
3.2. Схема аппаратных элементов (Panic, 2020a) . . . . . . . . . . . . . . . . . . . . . . . . 6
3.3. Скриншоты Crankin’s time-travelling adventure . . . . . . . . . . . . . . . . . . . . . . 7
3.4. Скриншоты Daily Driver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3.5. Скриншоты PlayMaker . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
3.6. Звуковые блоки в Super Mario 3D World . . . . . . . . . . . . . . . . . . . . . . . . . . 10
3.7. Изменение Красно-синих панелей посередине прыжка . . . . . . . . . . . . . . . . . . 10
3.8. Игроки прыгают на грибных батутах . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.9. Урок по мини-игре . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.10. Экран выбора мини-игры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.11. Монохромный художественный стиль BOXBOY! . . . . . . . . . . . . . . . . . . . . 12
3.12. Скриншоты Minit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13

5.1. Hello World! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22


5.2. Hello World! прыгает по экрану . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

6.1. Скриншоты всех разработанных прототипов . . . . . . . . . . . . . . . . . . . . . . . . 27


6.2. Прототип Unity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
6.3. Эффект ECS Starfield . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
6.4. Пример тайловой карты в Super Mario Bros . . . . . . . . . . . . . . . . . . . . . . . . 35
6.5. Деление тайловой карты по слоям . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
6.6. Нумерация и распределение тайлов в тайлсете и тайловой карте . . . . . . . . . . . . 38
6.7. Пример листа спрайта игрока . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
6.8. Складывание рукоятки устройства для создания взаимодействия с переключателем . 46
6.9. Головоломка с участием противоположных блоков переключения . . . . . . . . . . . 46
6.10. Головоломка со скрытыми структурами . . . . . . . . . . . . . . . . . . . . . . . . . . 47
6.11. Головоломка, требующая быстрой координации включения и отключения блоков . . 47
6.12. Старый метод 6.12a приводил к чрезмерной коррекции перекрытия оси Y . . . . . . 49
6.13. Изображение, отображаемое в состоянии победы в игре . . . . . . . . . . . . . . . . 50
6.14. Уровни бампера . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
6.15. Прыжок после отскока от бампера . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
6.16. Различные плитки для мира 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
6.17. Программное рисование внешнего мира . . . . . . . . . . . . . . . . . . . . . . . . . 59
A.1. Hello world Lua . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
A.2. Макет Dr. Mario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
A.3. Приготовьте сюрприз . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
A.4. Наклонная микроигра . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
A.5. Ритмическая игра . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
xvii
xviii Указатель иллюстраций

A.6. Hello World на языке C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77


A.7. Приключенческая игра на Pulp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79

B.1. Демо-проект по устранению ошибки обрезки прямоугольника . . . . . . . . . . . . . 83

C.1. Тайловый (плиточный) интерфейс . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85


Строки
5.1. arm_patched.cmake . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
5.2. CMakeLists.txt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
5.3. cmake-kits.json . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
5.4. tasks.json . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
5.5. Основной Hello World main.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
5.6. Hello World с улучшениями, main.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
5.7. Hello World переменные движения, main.c . . . . . . . . . . . . . . . . . . . . . . . . . 24
5.8. Прыгающий Hello World, main.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
5.9. Добавление управления рукояткой, main.c . . . . . . . . . . . . . . . . . . . . . . . . . 26

6.1. Определения класса игрока в объектно-ориентированном программ-нии (ООП) . . . 31


6.2. Пример создания сущности игрока в архитектуре ECS . . . . . . . . . . . . . . . . . . 31
6.3. main.c: основной цикл игры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
6.4. Заголовочный файл менеджера объектов . . . . . . . . . . . . . . . . . . . . . . . . . . 32
6.5. Пример системы: Физическая система . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
6.6. Инициализация объекта json_decoder с использованием инициализаторов C99 . . . . . 37
6.7. Открытие файла с помощью Playdate Software Development Kit (SDK) и передача его в
декодер . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
6.8. Рисование плиток . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39

A.1. Класс component.h, где определены структуры компонента . . . . . . . . . . . . . . . 78


A.2. В классе entity.h, сущности теперь имеют массив указателей на свои компоненты . . 79

B.1. Пропуск пары JSON . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82

D.1. Состояние машины . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87

xix
1. Введение
«Разработка для Playdate» — это введение в разработку программного обеспечения для
будущей портативной консоли Playdate, написанное до её публичного запуска во время
предварительной версии для разработчиков.
Содержание этой бакалаврской диссертации призвано стать ориентиром для будущих
разработчиков, заинтересованных в этом оборудовании, а также стать хроникой моих
прототипов, экспериментов и процесса обучения, кульминацией которых станет разработка
полноценной игры.
Большая часть содержания сосредоточена на программировании на языке C, с целью
получения базовых знаний об аппаратном обеспечении, осознанного развития с точки
зрения максимизации производительности и передачи знаний, извлеченных из этого опыта,
читателю. Оно также призвано охватить менее документированную область разработки
языка C на Windows для консоли, поскольку большая часть доступных ресурсов
сосредоточена на программировании Lua и среде Mac.
Каждый из созданных прототипов и демоверсий будет стремиться изучить сильные и
слабые стороны устройства, найти в них новые дизайнерские возможности и включить их
в игровой процесс. «TinySeconds», основная игра, разработанная в рамках этой
бакалаврской диссертации, будет использовать опыт, полученный на этапе прототипов, для
разработки увлекательного игрового процесса и инновационных взаимодействий,
адаптированных к функциям оборудования.
«TinySeconds» — это 2D-платформер с элементами головоломки, ориентированными на
прохождение уровней за короткое время. Эта механика требует от игрока быстрой реакции
и повышает возможность повторного прохождения, заставляя его проходить уровни и миры
за наименьшее количество перезапусков.
В дополнение к этой хронике, диссертация включает главу, написанную в виде обычного
учебника, которая проведет новичков в консоли через первые шаги разработки C для
Playdate в Windows. В этой главе подчеркиваются основные способы достижения
производительности на устройстве и включены упражнения для отработки и расширения
изложенных в ней концепций.
В этой диссертации также рассказывается об опыте создания игр на этапе производства
оборудования, процессе, который включал раскрытие или изменение функций и
спецификаций во время разработки, а также отчеты об ошибках и ошибках,
способствующие обеспечению качества (QA) консоли.

1
2. Основные цели
Когда в мае 2019 года была анонсирована консоль Playdate, я сразу же был очарован
простотой и свежестью её предложения; зачастую творчество стимулируется
ограничениями, и хотя Playdate — это консоль с современными характеристиками, её
аппаратное обеспечение по-прежнему ограничено по сравнению с современными
консолями и ПК. Его способность программировать на низкоуровневом языке C дала
возможность применить знания, полученные при изучении мультимедийной инженерии,
что заставило меня посчитать его идеально подходящим для моей бакалаврской
диссертации.
Летом 2020 года у меня появилась возможность принять участие в Playdate Developers
Preview — программе, которая предоставила мне доступ к консоли и SDK перед запуском.
Я понял, что документации по C API очень мало, и решил, что моя бакалаврская
диссертация может стать полезным ресурсом для других разработчиков после меня.
Итак, я решил разработать свою бакалаврскую диссертацию, посвященную
исследованиям и разработкам для Playdate, а также написанию полезной документации для
разработчиков, заинтересованных в программировании на C для этой новой консоли.
Перечень задач дипломной работы следующий:

 Проанализировать консоль Playdate на предмет программного обеспечения, железа,


SDK и документации.

 Создать небольшие прототипы видеоигр, одновременно учась разработке для этой


консоли.

 Разработать и реализовать полноценную игру, использующую характеристики


Playdate.

 Протестировать игру на реальных пользователях и повторить её на основе


полученных отзывов.

 Разработка учебных ресурсов по программированию на C для Playdate.

3
3. Теоритическая основа

3.1. Playdate
Playdate (3.1) — грядущая портативная консоль, созданная Panic; компания по разработке
программного обеспечения, специализирующаяся на приложениях для Mac и имеющая
опыт работы в индустрии видеоигр в качестве издателя игр «Firewatch» и «Untitled Goose
Game». Впервые об этом было объявлено 22 мая 2019 года*1, одновременно с запуском
официального сайта (https://play.date).

Рис. 3.1: модель консоли Playdate


Вместо того, чтобы конкурировать за внимание общественности, Playdate ориентирован
на независимых разработчиков и энтузиастов. В комплект поставки входит коллекция из
более чем 24 игр, созданных выдающимися деятелями игровой индустрии (такими как
Кейта Такахаши, Беннетт Фодди и Чак Джордан). Участие известных авторов, а также то,
что устройство является открытой платформой для разработки и публикации игр, вызвали
значительный интерес среди целевой аудитории*2.
Шведская фирма Teenage Engineering разработала аппаратную часть консоли, и некоторые
из ее выдающихся характеристик — это 1-битный черно-белый экран и рукоятка на правой
стороне устройства, которая выполняет функцию контроллера.
_______________________
*1. Playdate открывает твит, https://twitter.com/playdate/status/1131307504116174848
*2. Более 70 000 подписок за первые 24 часа и тысячи писем от разработчиков (@playdate, 2019)

5
6 Теоритическая основа

3.1.1. Характеристика оборудования

Используемый экран представляет собой ЖК-дисплей SHARP Memory, который сочетает


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

Рис. 3.2: Схема аппаратных элементов (Panic, 2020a)


Что касается ввода, Playdate имеет восьмипозиционный D-Pad, две кнопки с
обозначениями A и B, кнопку меню паузы, кнопку блокировки, акселерометр, микрофон и,
самое главное, рукоятку. Рукоятка прикреплена к поворотному энкодеру, и во время игры
ее можно запрашивать, чтобы узнать ее текущий угол и ускорение. Он также является
складным и использует магнитный переключатель, чтобы определить, находится ли он в
сложенном состоянии (Лунь, 2020).
3.2. Художественная составляющая 7

Полный список технических характеристик:

 Размеры: 76 x 74 x 9 мм.
 Дисплей: 2,7-дюймовый ЖК-дисплей Sharp Memory с разрешением 400×240 (173
точки на дюйм).
 Частота обновления: до 50 Гц для полноэкранного рисования и выше при рисовании
с меньшим количеством строк пикселей.
 Процессор: Cortex M7 180 МГц
 Память: 16 МБ внешней оперативной памяти плюс 320 КБ встроенной оперативной
памяти.
 Память: 4 ГБ.
 Возможности подключения: Wi-Fi (b/g/n) @ 2,4 ГГц, Bluetooth 4.2, USB-C, разъем
для наушников.
 Вес: 86 грамм.

3.2. Художественная составляющая

3.2.1 Игры Playdate


3.2.1.1. Crankin’s Time Travel Adventure
Crankin’s Time Travel Adventure — игра, разработанная Кейтой Такахаши, создателем саги
о Катамари, в сотрудничестве с Panic. Это была первая игра, представленная при первом
анонсе Playdate, и та, которая использовалась в ранних демоверсиях, рекламных материалах
и на веб-сайте.
Эта игра управляется исключительно с помощью рукоятки, которая перемещает или
перематывает время. Главный герой засыпает и опаздывает на свидание, и игрок должен
защитить его по дороге на встречу. На некоторые опасности изменение течения времени не
влияет, поэтому игрок должен избежать вреда, перемотав назад к моменту, когда они не
смогут поразить главного героя. Существует также ограничение по времени, независимое
от перемотки, что не позволяет пользователям играть слишком осторожно.

(а) Предыстория рассказа (б) Обход препятствий


Рис. 3.3: Скриншоты игры Crankin’s Time Travel Adventure
8 Теоритическая основа

3.2.1.2. Daily Driver

Daily Driver — это гоночная игра с видом сверху, созданная разработчиком Мэттом
Септоном. В нем представлен широкий выбор автомобилей и подобных транспортных
средств с разной физикой и внешним видом.
Автомобили представляют собой предварительно визуализированные изображения 3D-
объектов, созданные в OpenSCAD, программе компьютерного проектирования (САПР),
которая позволяет создавать модели с помощью сценариев с использованием собственного
языка описания. Частям модели присваиваются чистые красные, зеленые или синие цвета,
а затем они визуализируются под 32 углами вокруг них, чтобы получить выборочный вид
на 360°. Затем полученные изображения подвергаются пакетной обработке с
использованием ImageMagick, библиотеки обработки изображений с открытым исходным
кодом, разделяя их на каналы RGB и присваивая каждому каналу черный, белый цвет или
шаблон размывания.
В ходе разработки для каждого автомобиля были добавлены дополнительные рендеры,
отражающие поворот колес и смещение веса автомобиля. Тени реализуются путем
выравнивания 3D-моделей транспортных средств по вертикальной оси и их рендеринга для
каждого из спрайтов транспортных средств.
В игровом процессе автомобили управляются кнопкой A или стрелкой вверх для
ускорения, кнопкой B или стрелкой вниз для тормозов и рукояткой для дрифта. На уровнях
представлены разнообразные задачи, в которых игрок должен мчаться по трассе, быть
осторожным, чтобы не столкнуться с препятствиями, играть в футбол или собирать монеты,
а также выполнять другие условия победы.

(а) Режим гоночной трассы (б) Режим футбола


Рис. 3.4: Скриншоты игры Daily Driver

3.2.1.3. PlayMaker
PlayMaker — это набор игрушек для творчества, разработанный Дастином Миро. В нем
представлены режимы музыки, рисования, блоков и танца, а возможные дополнительные
режимы еще не раскрыты.
Музыкальный режим работает аналогично музыкальной шкатулке, где игрок может
размещать ноты на пентаграмме, выбирая тембр и высоту звука, а затем воспроизводить
музыку, поворачивая ручку. Темп зависит от того, насколько быстро игрок поворачивает
рукоятку, что также позволяет воспроизводить песни задом наперёд.
Режим рисования представляет собой простой редактор растровых изображений с
несколькими инструментами рисования, такими как кисть с динамикой обводки, карандаш
для мелких деталей, ведро для заливки цветом, ластик и инструмент рисования
3.2. Художественная составляющая 9

распылением. Он поддерживает импорт и экспорт файлов, позволяя пользователю


загружать изображения в формате .gif на устройство и использовать их в игре.
Режим блоков позволяет игроку строить конструкции из блоков различной формы,
например коробки и крыши. Тогда конструкцию можно обрушить взрывом.
Наконец, танцевальный режим состоит из фигурки тряпичной куклы, которая реагирует
на движение акселерометра и поворот рукоятки, заставляя ее дергаться и комично
«танцевать». Он реализован с помощью библиотеки Box2D, использующей твердые тела,
соединенные суставами для создания каждой части персонажа. С помощью навигационной
панели куклу можно перемещать из стороны в сторону экрана, а во время ее движения за
ней будет следить театральный прожектор.

(а) Музыкальный режим (б) Режим рисования

(в) Режим бллоков (г) Танцевальный режим


Рис. 3.5: Скриншоты игры PlayMaker

3.2.2. Другие игры


Поскольку на момент разработки концепции на Playdate не было выпущено ни одной игры,
TinySeconds черпает вдохновение из игр, выпущенных до неё в других системах. Вот
список игр, которые так или иначе сформировали нашу игру:

3.2.2.1. Super Mario 3D World


Super Mario 3D World — это 3D-платформер, разработанный Nintendo и выпущенный для
Wii U в ноябре 2013 года. Это вторая часть серии Super Mario 3D, которая переводит
философию дизайна уровней классических 2D-игр Super Mario в 3D. перспектива. Эта игра
послужила источником вдохновения для создания некоторых специальных блоков в нашей
игре, а именно блоков-переключателей и пружинных блоков.
Блоки-переключатели в TinySeconds ведут себя аналогично «блокам звуковых сигналов»
(рис. 3.6) из Super Mario 3D World, поскольку они имеют два состояния: твердое и
неосязаемое, и часто находятся на одном уровне с блоками в противоположном состоянии.
10 Теоритическая основа

В игре Nintendo эти блоки меняют состояние с фиксированным ритмом, а в нашей игре
игрок управляет их состоянием с помощью рукоятки. Такое поведение, когда игрок
управляет этим типом блока, можно сравнить с «Красно-синими панелями» (рис. 3.7) из той
же игры, которые меняют свое состояние каждый раз, когда игрок прыгает.

(а) Розовые блоки - твёрдые (б) Синие блоки твёрдые


Рис. 3.6: Звуковые блоки в Super Mario 3D World

Рис. 3.7: Красно-синие панели меняются в середине прыжка.


Пружинные блоки, реализованные в TinySeconds, являются распространенной механикой
в платформерах, а также появляются в Super Mario 3D World как блоки «Грибной батут»
(рис. 3.8). Эти блоки продвигают игрока в направлении, на которое указывает блок.
Все упомянутые механики впервые появились в Super Mario Galaxy 2 и продолжили
использоваться в саге Super Mario 3D.

3.2.2.2. Rhythm Heaven


Rhythm Heaven — это ритмическая игра о небесной саге, разработанная Nintendo и
созданная японским музыкальным продюсером Мицуо Терада, более известным под своим
сценическим псевдонимом Цунку. Игры состоят из множества различных мини-игр, в
которых игрок выполняет ритмичные действия, синхронизированные с музыкальной
дорожкой.
В начале каждой мини-игры в учебнике (3.9) объясняется ее основная механика и ритм-
паттерн, на котором она будет основана; в некоторых мини-играх используются
синкопированные ритмы, в некоторых — звуковые сигналы, обозначающие действия, а
некоторые, среди прочих вариаций, основаны на повторении. Затем механика применяется
на практике в песне, а результативность игрока оценивается в конце мини-игры.
Мини-игры сгруппированы в столбцы главного меню (рис. 3.10) и разблокируются
последовательно после прохождения предыдущих с рейтингом «ОК» или выше. Затем в
конце каждой группы разблокируется специальный этап «Ремикс». На этом этапе нет
3.2. Художественная составляющая 11

Рис. 3.8: Игроки прыгают на грибных батутах

обучения, и он сочетает в себе механику предыдущих мини-игр в одной песне повышенной


сложности. Этапы «Ремикса» дают игроку возможность бросить вызов самому себе и
применить знания, полученные до этого момента.

Рис. 3.9: Учебное пособие по мини-игре

Уровни «ремикс» послужили основным источником вдохновения для структуры


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

3.2.2.3 BOXBOY!
БОКСБОЙ! — это серия игр-платформеров-головоломок, разработанная HAL Laboratory
и изданная Nintendo для системы Nintendo 3DS. Игрок управляет персонажем, который
может создавать коробки и использовать их для решения головоломок. Коробки
прикрепляются к игроку, поэтому их можно свешивать с выступов или использовать в
качестве щита, а затем можно бросить на землю, что может активировать переключатели и
другие виды механики.
БОКСБОЙ! был главным источником вдохновения для художественного стиля TinySe-
12 Теоритическая основа

Рис. 3.10: Экран выбора мини-игры

conds с его преимущественно черно-белой эстетикой, в которой читаемость ценится


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

Рис. 3.11: Монохромный художественный стиль BOXBOY!

3.2.2.4. Minit

Minit — независимая видеоигра, разработанная Домиником Йоханном, Юкио Каллио,


Яном Виллемом Нейманом и Китти Калис. По своей сути игра представляет собой ретро-
приключенческую ролевую игру (РПГ), но ее главная загвоздка в том, что по истечении
таймера в одну минуту игрок возвращается к последней посещенной контрольной точке.
Прогресс зависит от поиска коротких путей, понимания мира и выполнения квестов, а
также от достижения новых контрольных точек в разных областях.
Эта игра стала важным ориентиром при разработке концепции TinySeconds из-за ее
ограничения по времени, хотя игры относятся к разным жанрам. Он также отличается 1-
битным художественным стилем, похожим на возможности Playdate, что сразу же приходит
на ум при поиске вдохновения.
3.2. Художественная составляющая 13

Рис. 3.12: Скриншоты игры Minit

3.2.2. Заключение
Наша игра TinySeconds является инновационной на рынке Playdate, поскольку не
существует анонсированных игр с сопоставимой механикой, которые могли бы составить
конкуренцию в категории динамичных аркадных платформеров. Он также представляет
новый способ использования рукоятки: она складывается за устройством, ограничивая ее
диапазон задней стороной консоли. Это положение позволяет использовать рукоятку одним
из самых быстрых способов, поскольку ее можно щелкнуть, как переключатель, не выходя
за пределы досягаемости игрока, что устраняет распространенную проблему, когда
рукоятку и кнопки трудно использовать одновременно.
Его особенность еще и в том, что он запрограммирован на C, в то время как более широко
распространенный язык программирования для консоли — Lua. C — один из самых
низкоуровневых языков, в котором память управляется вручную, а код компилируется
непосредственно в ассемблер. Lua-игры, наоборот, собирают мусор и запускаются на
виртуальной машине. Эта разница значительно повышает производительность игры на C,
например, при чтении файлов JSON по сравнению с той же операцией на Lua. Даже если
конечный пользователь не обращает внимания на используемый язык программирования,
хорошая производительность всегда будет приветствоваться, а хроника разработки игры
будет ценна для будущих программистов Playdate C.
4. Методология
Этот проект следует итеративной методологии, основанной на прототипах. Время
разработки разделено на итерации, основанные на предыдущих, что означает, что основная
реализация всех функций будет быстро готова, а улучшения и доработки будут добавляться
к ней волнами. На первых этапах проекта целью итераций будет не продвижение основной
игры, а создание быстрых демоверсий как способа изучения и документирования
использования Playdate SDK.
Каждая итерация делится на три этапа:
1. Планирование. Первым шагом в каждой итерации является определение целей,
которые будут преследоваться на протяжении всей итерации. Это должны быть
краткосрочные, конкретные цели, достижимые за одну итерацию, что в нашем
случае означает четыре недели разработки. Задачи, длина которых превышает одну
итерацию, следует разбить на более мелкие цели, указав, какая часть из них будет
выполнена в текущем сроке.

2. Разработка. Естественно, это самая длинная часть каждой итерации, в ходе которой
работа направлена на достижение целей, определенных на этапе планирования. Это
включает в себя программирование демо-версий или игры и часто натыкается на
ошибки или препятствия, которые могут замедлить или изменить проклятие
разработки. Хотя эти отклонения и нежелательны, они могут быть ценными с точки
зрения обучения и будут собираться и анализироваться на третьем этапе
итеративного процесса.

3. Анализ и документация. По завершении итерации некоторое время будет


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

За пределами этой структуры лежит создание главы 5, которая служит руководством для
новых разработчиков Playdate, заинтересованных в кодировании на C, и не соответствует
циклам разработки основного проекта.

15
5. Работа с Playdate на языке С
Эта глава представляет собой руководство для начинающих по разработке для Playdate на
C. Мы рассмотрим каждый шаг: от настройки инструментов C для кодирования и
компиляции в Windows до создания простой игры об астероидах.
Данное руководство предполагает среднее понимание языка программирования C.
Большинство концепций будет легко понять при наличии общих знаний
программирования, но мы будем использовать некоторые характеристики, специфичные
для C, например указатели.
В этом руководстве, будет использоваться Playdate SDK версии 1.0.8, которую можно
загрузить из официальных источников (на момент написания — форумы разработчиков
Playdate*1).

5.1. Настройка среды


Прежде чем мы начнем, необходимо выполнить некоторую настройку для разработки
Playdate в Windows. Мы будем использовать бесплатный многоцелевой текстовый редактор
Visual Studio Code, разработанный Microsoft, благодаря его многочисленным расширениям,
простоте использования и поддержке задач.
Загрузите Visual Studio для Windows*2. Затем откройте его и на боковой панели выберите
панель расширений. Найдите следующие расширения и установите их:
 Расширение C/C++ от Microsoft: обеспечивает поддержку языка C и автодополнение
кода.
 Расширение CMake Tools от Microsoft: интегрирует конвейер компиляции, который
мы будем использовать, в редактор.
Как только это будет сделано, загрузите и установите CMake*3. CMake — это набор
инструментов сборки, которые генерируют файлы, необходимые системе сборки для
компиляции наших игр. Кстати говоря, загрузите Ninja*4 и распакуйте zip-файл, запомнив
каталог, в который вы его распаковали. Ninja — это небольшая низкоуровневая система
сборки, ориентированная на быстрое время сборки. Он использует CMake для создания
файлов сборки.
Playdate оснащен центральным процессором (ЦП) ARM, поэтому нам потребуется
установить подходящий компилятор C для этой архитектуры. Загрузите GCC ARM
Toolchain*5 и извлеките файлы, как мы это делали с Ninja, записав путь к ним.
После того, как все будет установлено, мы создадим переменные пользовательской среды,
чтобы легко ссылаться на необходимые пути к этим инструментам. Важно отметить: при
написании путей в переменных среды используйте косую черту (/) или экранированную
обратную косую черту, но не одиночную обратную косую черту.
_______________________
*1. Страница загрузки SDK 1.0.8: https://devforum.play.date/t/playdate-sdk-1-0-8/1468.
*2. Страница загрузки Visual Studio: https://code.visualstudio.com/Download.
*3. Страница загрузки CMake: https://cmake.org/download/
*4. Страница загрузки Ninja: https://github.com/ninja-build/ninja/releases
Страница загрузки GCC ARM Toolchain: https://developer.arm.com/tools-and-software/open-source-
software/developer-tools/gnu-toolchain/gnu-rm/downloads/9-2019-q4-major
17
18 Работа с Playdate на языке С

Откройте панель управления и найдите параметр «Изменить переменные среды».


Нажмите на него и в разделе «Пользовательские переменные» нажмите кнопку «Создать».
Таким образом, создайте переменную с именем PLAYDATE_SDK, в которой будет
храниться путь к распакованной папке Playdate SDK. Создайте еще одну переменную с
именем PLAYDATE_ARM_GCC и установите для нее путь к GCC ARM Toolchain.
Наконец, создайте или добавьте к переменной PATH путь к инструментам сборки Ninja.
Далее мы собираемся адаптировать файл конфигурации CMake, входящий в состав
Playdate SDK для Windows. В папке Playdate SDK перейдите в C_API/buildsupport и
создайте файл Arm_patched.cmake. Откройте его с помощью текстового редактора и
вставьте содержимое листинга 5.1.*6

5.1.1. Создание шаблона


Давайте создадим шаблон, который мы сможем повторно использовать для создания наших
будущих проектов. Для этого мы собираемся продублировать пример Hello World, который
поставляется в комплекте с SDK, и модифицировать его для Windows и кода Visual Studio.
В каталоге Playdate SDK перейдите в C_API/Examples и продублируйте папку «Hello
World».
_______________________
*6. Обучение CMake выходит за рамки этого руководства, оно сосредоточено на разработке, специфичной
для Playdate.
5.1. Настройка среды 19

Откройте папку, которую мы только что скопировали, и удалите файлы .nova, .xcodeproj и
Makefile, поскольку они относятся к другим редакторам и системам сборки, которые мы не
будем использовать. Мы должны изменить содержимое файла CMakeLists.txt, чтобы
адаптировать его к платформе Windows. Этот файл сообщает CMake расположение наших
исходных файлов, имя исполняемого файла, который мы хотим создать, версию CMake,
которую мы хотим использовать, и где найти файлы CMake, предоставленные Panic с SDK.
Замените содержимое файла следующим:

Создайте новую папку в корне проекта с именем .vscode. В этом каталоге будут храниться
файлы конфигурации, которые Visual Studio Code будет читать и использовать. Внутри него
создайте файл cmakekits.json и заполните его следующим:

Это определяет новую цель CMake, которая использует файл Arm_patched.cmake, который
мы создали в предыдущем разделе.
20 Работа с Playdate на языке С

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

Здесь определяется задача «Запуск на Playdate», которая устанавливает и запускает игру


на устройстве нажатием Ctrl+Shift+B, задача «Развертывание на Playdate», которая
устанавливает исполняемый файл в консоль, но не запускает его, и «Mount Playdate»,
которая откроет файловую систему консоли в проводнике Windows*7.

5.1.2. Структура проекта Playdate

Взгляните на проект шаблона, который мы настроили в предыдущем разделе. Типичный


проект Playdate C будет иметь следующую структуру:

_______________________
*7. Обратите внимание, что для работы первых двух задач корневая папка проекта должна иметь имя, точно
соответствующее значению переменной PLAYDATE_GAME_NAME в файле CMakeLists.txt, и не содержать
пробелов.
5.2. Hello World 21

 Каталог build (папка сборки), в котором хранятся промежуточные файлы сборки


CMake и Ninja. Обычно нет необходимости редактировать или добавлять файлы в
эту папку вручную.

 Каталог Source (папка исходного кода), содержащий файлы, которые будут


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

 Каталог src, папка в которой мы создаем исходные файлы, содержащие код нашей
игры. Именно здесь происходит большая часть разработки. Каждый проект Playdate
будет иметь в этой папке файл main.c, который содержит цикл обновления, который
будет выполнять каждый кадр, и функцию eventHandler, которая позволяет нам
реагировать на различные типы обратных вызовов, таких как запуск игры,
блокировка или разблокировка консоли. или откроется меню паузы.

 Файл CMakeLists.txt, который настраивает CMake для нашего проекта в котором мы


укажем его имя и имя самого пакета.

Когда мы скомпилируем нашу игру, в корневом каталоге появится дополнительная папка:


файл .pdx. Это пакет, который устанавливается на устройство и содержит встроенные
двоичные файлы для всех ресурсов и кода.
Откройте папку проекта в Visual Studio Code. Если расширение CMake включено, должно
появиться всплывающее окно с вопросом, хотим ли мы настроить CMake с помощью файла
CMakeLists.txt. Выберите «Да», а затем опцию набора инструментов «Playdate Device» в
следующем раскрывающемся списке. Теперь вы можете открыть файл CMakeLists.txt и
изменить переменные PLAYDATE_GAME_NAME и PLAYDATE_GAME_DEVICE на имя,
которое вы хотите, чтобы ваш проект имел; файлы конфигурации будут автоматически
обновляться после сохранения.
На этом последнем шаге мы успешно настроили среду программирования.

5.2. Hello World

Давайте рассмотрим простейший код Hello World.


22 Работа с Playdate на языке С

Здесь мы видим две обязательные функции: update() и eventHandler(). Когда игра


запускается, kEventInit принимается в eventHandler(), и мы используем его для выполнения
любых необходимых действий по инициализации. Прежде всего, мы указываем интерфейсу
прикладного программирования (API) Playdate функцию, которую мы собираемся
использовать в качестве функции обновления. Затем очищаем экран белым цветом и пишем
текст «Hello World!» в позиции х = 100, у = 100.
Метод обновления просто возвращает 1: важно знать, что если функция обновления
возвращает 0, в этом кадре рисование не выполняется. Нам нужно, чтобы метод обновления
возвращал 1, иначе текст не будет отрисован.
Дублируйте проект шаблона, который мы создали в разделе 5.1.2, и замените содержимое
файла main.c кодом из листинга 5.5. Скомпилируйте проект, используя сочетание клавиш
CMake F7. Вы также можете скомпилировать, перейдя к значку CMake на левой боковой
панели и щелкнув значок «Построить все проекты». Теперь подключите устройство
Playdate и разверните .pdx, используя сочетание клавиш Ctrl+Shift+B, симулятор Playdate
или команды pdutil.exe, последние две включены в загрузку Playdate SDK.
Как только игра запустится на вашем Playdate, вы должны увидеть такой результат:

Рис. 5.1: Hello World!

5.2.1. Некоторые улучшения

Начнем с определения псевдонима для Playdate API: мы создадим статический указатель


типа PlaydateAPI и назовем его pd. Это не повлияет на поведение кода, но это обычная
практика в разработке Playdate, которая позволяет нам писать меньше. Указатель pd должен
быть присвоен значению playdate в событии kEventInit. Теперь мы можем заменить все
ссылки на переменную playdate этим сокращенным псевдонимом.
Теперь давайте переместим вызовы функций рисования в метод обновления. Несмотря на
то, что их вызов в kEventInit работает, это событие следует зарезервировать для целей
инициализации, тогда как рисование обычно выполняется в конце метода обновления. Мы
также можем добавить индикатор кадров в секунду (fps) с помощью одной строки кода,
используя функцию pd->system->drawFPS( x, y).
Как видно из тестирования на устройстве, текстовый шрифт по умолчанию очень тонкий,
его толщина составляет всего 1 пиксель. Мы можем изменить шрифт на жирный, используя
pd->graphics->loadFont() и pd->graphics->setFont().
Все эти изменения вместе оставляют нам следующий файл main.c:
5.2. Hello World 23

5.2.2. По частоте кадров


Как вы, возможно, заметили, счетчик кадров в секунду, который мы добавили в последнем
разделе, не превышает 30 кадров в секунду, хотя мы показываем только строку текста без
каких-либо дополнительных вычислений. Это связано с тем, что частота обновления экрана
по умолчанию ограничена 30 Гц, но этот предел можно изменить с помощью функции pd-
>display->setRefreshRate(floatrate). Установка параметра скорости на 0 дает нам
разблокированную частоту кадров, благодаря чему экран обновляется с максимально
возможной частотой.
Добавьте pd->display->setRefreshRate(0) в раздел kEventInit eventHandler, скомпилируйте и
протестируйте на устройстве, чтобы увидеть, сколько кадров в секунду мы получим.
Теперь вы будете получать около 50 кадров в секунду, что может показаться высоким, но
все же это не максимальная мощность Playdate. Только что мы столкнулись с аппаратным
ограничением: полноэкранную перерисовку дисплея невозможно выполнить с частотой
выше 50 Гц. Рисование на дисплее выполняется построчно, то есть обновляются только
затронутые строки экрана. Если вы посмотрите на наш код, вы заметите, что мы выполняем
pd->graphics->clear() в каждом кадре, заполняя каждую строку пикселей белым цветом
перед перерисовкой текста. Удалите эту строку, скомпилируйте и проверьте частоту кадров
на устройстве.
Теперь вы должны увидеть индикатор со скоростью 99 кадров в секунду, что является
максимальным значением, которое он может отображать, а это означает, что фактическое
значение может быть еще выше. Фактически, частота кадров выше 100 Гц возможна на
Playdate с использованием методов выборочной прорисовки.
Итогом этого эксперимента должна стать важность оптимизации вызовов отрисовки и
рендеринга только необходимых областей экрана. Несмотря на то, что устройство способно
поддерживать такую высокую частоту кадров, за это приходится платить электроэнергией,
что в портативной консоли означает сокращение времени автономной работы. В
большинстве случаев частоты кадров 30 кадров в секунду будет достаточно для хорошего
впечатления, а режим 50 кадров в секунду будет хорошим вариантом для определенных
эффектов или динамичных типов игр.
24 Работа с Playdate на языке С

5.2.3. Подпрыгивания по кругу

Повторно добавьте строку pd->graphics->clear() в начале функции обновления. Мы


собираемся сделать наш hello world более интересным, заставив текст прыгать по экрану,
как в примере на языке C, включенном в SDK.
Объявите следующие глобальные переменные перед функцией обновления:

Во-первых, нам нужно знать размеры «Hello World!» text, чтобы определить, когда одна
из его сторон касается границы экрана, и инвертировать ощущение его движения. Мы знаем
высоту текста из шрифта, который мы указали в строке loadFont(), это «Asheville-Sans14-
Bold.pft», то есть его высота составляет 14 пикселей. Для расчета ширины в Playdate SDK
есть собственный метод pd->graphics->getTextWidth(). Зная это, инициализируйте
переменные textWidth и textHeight сразу после метода setFont() в eventHandler.
Переменные x и y хранят положение текста. Мы хотим, чтобы он начинался в центре
экрана, поэтому на каждой оси позиция должна быть равна размеру экрана минус размер
текста, а затем разделена на два. API Playdate имеет две константы для ширины и высоты
экрана: LCD_COLUMNS и LCD_ROWS. Это просто значения 400 и 240 соответственно,
что соответствует разрешению дисплея, но использование стандартных констант делает
код более читабельным.
Теперь нам нужно обновлять положение текста в каждом кадре, добавляя шаг X к
переменной x и шаг Y к переменной y. Наконец, если текст выходит за пределы экрана, нам
нужно переключить направление движения по каждой оси: это происходит, когда значение
позиции для этой оси меньше 0 или больше, чем размер экрана минус размер текста для
этой оси.
Добавление этих изменений в код приводит к следующему файлу main.c:
5.2. Hello World 25

Скомпилируйте и разверните программу на консоли и наблюдайте, как текст прыгает по


экрану, как в классических заставках DVD-плеера. Ностальгия!

Рис. 5.2: Hello World! летает по экрану.

5.2.4. Прокрутите рукоятку

Как вы знаете, одной из определяющих характеристик Playdate является рукоятка. Давайте


включим её в наш пример, используя для ускоренной перемотки текста вперед или назад.
Нам нужно изменить всего две строки нашего кода, чтобы добавить эту функцию; но
сначала давайте разберёмся, как Playdate SDK обрабатывает вводимые данные.
API Playdate имеет три метода, связанных с запуском:
26 Работа с Playdate на языке С

 int pd->system->isCrankDocked(), который возвращает 1, если рукоятка закреплена, и


0, если она отстыкована.
 float pd->system->getCrankAngle(), который дает нам текущий угол, под которым
находится рукоятка, начиная с 0° вверху и увеличиваясь по часовой стрелке, если
смотреть с правой стороны устройства.
 float pd->system->getCrankChange(), который возвращает изменение угла рукоятки с
момента последнего вызова функции. Это лучший вариант для того, что мы хотим
сделать, поскольку нам нужно знать только скорость и направление вращения между
вызовами обновления.

Мы хотим, чтобы текст подпрыгивал, как и раньше, и увеличивал или уменьшал скорость
в зависимости от скорости рукоятки. Чтобы добиться этого, при добавлении переменных
шага к позиции текста мы также добавим переменные шага, умноженные на значение,
возвращаемое pd->system-> getCrankChange().

Скомпилируйте и разверните проект, проверьте результат: при перемещении рукоятки


вперед появится сообщение «Hello World!» текст движется быстрее, а поворот в
противоположном направлении заставляет его двигаться назад. Гораздо более
интерактивно!

5.2.5. Дополнительные шаги

Работая над примером, который мы создали на основе этой главы, попробуйте реализовать
некоторые из следующих улучшений или бросьте вызов любым модификациям, которые
только можете себе представить:
 В разделе 5.2.2 мы видели, как очистка экрана в каждом кадре ограничивает частоту
обновления до 50 кадров в секунду. Можете ли вы изменить наш процесс
рендеринга, чтобы стереть только необходимую часть экрана? Ознакомьтесь с
функциями рисования геометрии в официальном руководстве Inside Playdate with C
от Panic (2020b) или придумайте собственное решение.

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

 Поэкспериментируйте с режимами рисования, такими как XOR, OR, и тем, как они
влияют на текст при наведении на фоновое изображение.

 Добавьте немного фоновой музыки.


6. Разработка

6.1. Итерация 0 - Знакомство с Playdate

Первые пару месяцев владения оборудованием были посвящены изучению и пониманию


консоли, а также структуры и философии SDK. В то же время, чтобы расширить
диссертацию и охватить все основные способы разработки для Playdate, прототипы были
созданы на Lua, C, C++ и инструменте создания игр Pulp. Благодаря этому
исследовательскому процессу было получено широкое понимание плюсов и минусов
каждого языка, что помогло закрепить C в качестве предпочтительного языка для
разработки основной игры.
Полную информацию о каждом прототипе, созданном на этом этапе, можно найти в конце
книги, в Приложении А.

Рис. 6.1: Скриншоты всех разработанных прототипов.

6.1.1. Итерация 0.1 - язык Lua

Одна из первых рекомендаций, которую разработчик Playdate Panic дал во время прямой
трансляции, посвященной программированию, заключалась в том, чтобы опытные
программисты «сначала проверили интерфейс Lua, вы можете получить от него приличную
производительность, и это намного проще, чем писать в интерфейс C» (Фрэнк, 2020, мин.
4:02). Это предложение показалось разумным, и поэтому первые прототипы были написаны
с использованием Lua SDK.
Даже без предыдущего опыта работы с языком, кривая обучения была умеренной.

27
28 Разработка

Интерфейс Lua оказался понятным и обширным, выходя за рамки основ благодаря готовой
реализации многих распространенных игровых функций, таких как тайловые карты,
эффекты изображения, z-буферизация и обнаружение столкновений.
Hello World. Первый эксперимент представлял собой модификацию примера кода из
официального руководства Inside Playdate от Panic (2020a) и служил для понимания
процесса рисования изображений на дисплее с использованием функций спрайтов,
включенных в SDK, простой обработки ввода и воспроизведение звука.
Макет Dr. Mario. На основе этого первого проекта я быстро реализовал макет того, как
классическая игра Dr. Mario от Nintendo Entertainment System (NES) будет выглядеть на
Playdate. Здесь таблетка свободно перемещается с помощью направляющей и вращается
поворотом рукоятки. Акселерометр используется для определения того, находится ли
устройство боком, и в этом случае переключается на вертикальное расположение.
Приготовьте сюрприз. В третьем эксперименте использовалась та же концепция
определения ориентации устройства с помощью акселерометра, чтобы показать
анимированную картинку собаки, когда дисплей обращен к земле. Здесь изучалась
анимация спрайтов с помощью встроенных функций спрайтов и применение некоторых
эффектов изображения в реальном времени, включенных в SDK.
Наклонная микроигра. После этих демонстраций снова был разработан более длинный
прототип, основанный на входе акселерометра. Результатом стала мини-игра, целью
которой было провести коробку через случайно сгенерированный лабиринт, наклоняя
устройство из стороны в сторону. В коробке была простая физика, реализованная с
помощью уравнений прямолинейного ускоренного движения. В этой демонстрации
использовалась структура состояния игры, предоставленная другим разработчиком
Playdate на официальных форумах, Ником Манье.

6.1.2. Итерация 0.2 - язык C и C++

После знакомства с Lua SDK разработка снова перешла к изучению интерфейса C путем
создания различных прототипов.
Hello World на языке C. Первый проект представлял собой модификацию примера
проекта Hello World на языке C, который распространяется вместе с SDK. В нём текстовая
строка «Hello World» перемещается по экрану подобно логотипам старых DVD-плееров.
Развивая эту простую демонстрацию, я включил фоновое изображение и выполнил
рендеринг текста в режиме рисования NXOR (это означает, что пиксели текста,
перекрывающие черные, инвертируют свой цвет). Текст стирается путем рисования поверх
него только необходимого прямоугольника фонового изображения, что повышает
производительность за счет исключения полноэкранных вызовов отрисовки.
Hello World на языке C++. Тот же пример был реализован на C++. В этой версии целью
эксперимента была компиляция и запуск кода C++ на Playdate, поскольку этот язык не
является официально поддерживаемым. Изучив пример, включенный в SDK, и изменив
конфигурации CMake, демо-версия была успешно скомпилирована и запущена на
устройстве.
В ходе этого процесса стало очевидным большое ограничение, которое умаляет
преимущества, которые C++ может дать при разработке Playdate: в консоли отсутствует
реализация стандартной библиотеки C++. Тем не менее, в языке есть полезные функции,
для работы которых не требуется стандартная библиотека, например классы, наследование
или шаблоны.
6.1. Знакомство с Playdate 29

Некоторое время было потрачено на понимание этой проблемы и изучение возможных


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

6.1.3. Итерация 0.3

Ритмическая игра. После недель разработки на C и C++ я вернулся на Lua, чтобы быстро
создать прототип ритм-игры. В духе классических музыкальных игр, таких как серия Guitar
Hero, Osu! или японские аркадные автоматы. Эта игра состоит из серии падающих нот,
синхронизированных с песней, которую игрок должен ударять в такт. Этот отход от языка
C был сделан для того, чтобы отдать приоритет скорости и простоте разработки, а также
сосредоточить внимание прототипа на игровом дизайне, дизайне взаимодействия и
наличии закрытого продукта.
Ни один из прототипов, начиная с раннего доказательства концепции Доктора Марио,
вообще не использовал рукоятку, что, возможно, является самой знаковой особенностью
консоли. Исследование и использование характеристик, которые делают Playdate
уникальным, является одной из основных целей данной диссертации; пришло время чудаку
сыграть центральную роль в игровом процессе, поэтому концепция этой игры была
задумана вокруг него.
Геймплей следующий: на заднем плане играет песня, центр экрана занимает круг, а игроки
управляют дугой, которая движется по нему в соответствии с текущим углом поворота
рукоятки. Используя эту дугу, игрок должен ловить точки, обозначающие «ноты»,
падающие к центру круга. Чтобы игра приносила удовлетворение, эти ноты должны быть
синхронизированы с музыкой и аранжированы, отражая ее характеристики, такие как ритм,
голоса и общая энергия.
Я реализовал простой конечный автомат для переключения между меню и игровым
процессом игры. Это было сделано с помощью класса GameManager, который содержит
таблицу Lua, ссылающуюся на логику и функции рендеринга текущего состояния.
Изменение состояний осуществляется путем вызова GameManager.changeState() с
функциями обновления и рендеринга, а также дополнительной функцией инициализации в
качестве параметров. Когда этот метод вызывается, он сохраняет функции в таблице
GameManager, а затем один раз выполняет функцию инициализации.
В этой прототипной версии игры есть только три игровых состояния. Первый — это
состояние загрузки, которое в полной версии будет использоваться для загрузки ресурсов
при открытии игры. В настоящее время все, что делает это состояние, — это мгновенно
переходит в следующее состояние — состояние меню. В состоянии меню игроков встречает
титульный экран и музыка. В полной версии появятся другие параметры меню,
реализованные в отдельных состояниях игры; но на данный момент простое нажатие
кнопки A в меню переключает во внутриигровое состояние, в котором начинается игровой
процесс.
Шаблоны нот необходимо было создавать вручную, и этот процесс выиграл бы от
воспроизведения звука, временной шкалы и визуализации формы волны. Audacity,
30 Разработка

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

6.2. Игра: TinySeconds

Первые месяцы, посвящённые маленьким прототипам, оказались очень полезными для


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

6.2.1. Концепция

TinySeconds будет платформером с видом сбоку, в котором игрок должен добраться до цели
за одну секунду или меньше, уделяя особое внимание высокоскоростному игровому
процессу, четкому управлению и быстрой реакции. Уровни будут одноэкранными и
нарисованы с использованием тайловых карт.
На всех уровнях несколько препятствий и специальные механики бросят вызов игроку и
добавят разнообразия в игровой процесс. Поскольку уровни очень короткие по
продолжительности, игрокам придется последовательно проходить их серию, не
проигрывая, чтобы перейти к следующей группе.
Перед разработкой, прототип, был создан на фирменном игровом движке Unity3d, в
который можно поиграть в браузере*1. См. рис. 6.2.

(a) Геймплей (б) Поражение

6.3. Итерация 1 - Установка фундамента

Задачи, запланированные на эту итерацию, заключались в следующем:


 Реализация движка ECS на языке C в качестве базовой структуры игры. Затем
_______________________
*1. Прототип можно воспроизвести по ссылке: https://abramaran.itch.io/one-second.
6.3. Итерация 1 - Установка фундамента 31

используйте его для реализации простой демонстрации эффекта звездного поля.

 Расширение этого движка за счет возможности отделения компонентов от объектов.


 Исследование Tup как возможной системы сборки игр Playdate.

6.3.1. Введение в систему компонента сущности (ECS)

Система Entity Component (ECS) — это архитектурный шаблон, ориентированный на


простоту проектирования и оптимизацию доступа к кэшу. Это достигается за счет
модульности его частей и отделения функциональности от данных, группируя последние в
сущности с простыми идентификаторами. С точки зрения проектирования это приводит к
гораздо более простой и удобной в обслуживании структуре, чем иерархические структуры,
присутствующие в большинстве шаблонов игровых движков объектно-ориентированного
программирования (ООП).
Компоненты — это одна или несколько переменных, сгруппированных по принципу
концептуального сходства; например, положение, скорость и ускорение могут быть
сгруппированы в физический компонент. Они являются строительными блоками, из
которых строятся сущности.
Сущность обычно представляется идентификатором, обычно уникальным номером,
присваиваемым при создании. Их цель — пометить ряд компонентов как принадлежащих
одному и тому же владельцу; например, экземпляр врага, игрока или игровой камеры.
Системы — это методы, которые предоставляют определенные функции, такие как
физическое моделирование, чтение ввода игрока или проверка столкновений. Они
используют и изменяют данные, содержащиеся в компонентах, для выполнения своих
функций. Зачастую для работы системы требуется более одного типа компонентов,
относящихся к одному и тому же объекту.
Представьте себе типичный ООП-подход к созданию класса «Игрок» для игры: мы
определяем новый класс, добавляем переменные-члены для хранения его данных, таких как
положение, скорость или изображение спрайта, а затем добавляем методы для обеспечения
функциональности с использованием этих переменных.

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

6.3.2. Упрощенная версия ECS

Шаблон ECS был выбранной архитектурой для структурирования игрового движка


TinySeconds; его внимание к скорости и оптимизации кэша важно на ограниченной машине,
как и Playdate, а простота конструкции является долгожданной характеристикой. Теперь
задача заключалась в разработке такого типа механизма с использованием C — языка, в
котором отсутствуют такие функции, как шаблоны или интерфейсы, которые обычно
используются в реализациях ECS.
Из-за этих трудностей было решено упростить структуру ECS для первой версии движка:
внутри каждой сущности будет храниться компонент каждого типа, тогда как в полной
реализации компоненты должны быть отделены от сущностей и храниться отдельно от них.
Тогда вместо того, чтобы системы перебирали все компоненты требуемых типов, они
будут перебирать все сущности. При обычной настройке это может снизить эффективность
доступа к кешу, но поскольку данные, содержащиеся в наших компонентах, очень малы,
все объекты полностью помещаются в кеш. Эта упрощенная версия структуры ECS была
смоделирована на основе серии обучающих прямых трансляций Дюрана (2020).
Код проекта разделен между основным циклом, менеджером сущностей и системами.
Файл main.c выполняет необходимые операции инициализации и содержит основной цикл
приложения, который вызывается каждый кадр. Во-первых, обновляются все логические
системы, в том числе те, которые могут создавать новые сущности; затем вызывается
система рендеринга, рисующая элементы игры на экране; наконец, вызывается менеджер
сущностей для уничтожения сущностей, помеченных для удаления. См. рис. 6.3.

Менеджер сущностей в файле entity.c определяет структуру сущности, типы сущностей и


все компоненты. Он также управляет созданием и уничтожением объектов и запускает
системы для всех них. Обзор класса сущности с комментариями можно прочитать в разделе
Строки 6.4.
6.3. Итерация 1 - Установка фундамента 33

Каждая система определяет свою функцию обновления и дополнительный метод


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

После завершения разработки первой версии движка пришло время испытать её. Для
34 Разработка

этого я реализовал графический эффект, аналогичный тому, что есть в обучающих


видеороликах Дюрана (2020), состоящий из серии звезд, движущихся по экрану. Иллюзия
глубины создается за счет уменьшения размера и скорости звезд по мере удаления от
камеры. Запуск этой демонстрации на консоли показал отличную производительность:
средняя скорость составила 43 кадра в секунду для 1000 одновременных объектов.
Скриншот на рис. 6.3

Рис. 6.3: Эффект ECS Starfield

6.3.3. Полная реализация ECS

Несмотря на то, что упрощенная версия ECS была функциональной и производительной, я


решил превратить её в более полную реализацию ECS с компонентами, отделёнными от
сущностей. Как вы уже догадались, компоненты по-прежнему должны были располагаться
в памяти последовательно для оптимизации кэша, а для работы функции
man_compent_forall(ComponentComponent) требовался своего рода полиморфизм. О
процессе эволюции ECS можно прочитать в приложении А.
Функционирующий полноценный движок ECS был успешно реализован, и пришло время
протестировать его на том же примере, что и раньше: графический эффект звездного поля.
Результаты были разочаровывающими; производительность была значительно снижена:
средняя частота кадров составила 12 кадров в секунду. Инструменты профилирования игр
C на Playdate на момент написания ограничены простыми отпечатками консоли, что
затрудняет поиск виновника. Тем не менее, источником этой потери производительности
можно считать небольшой размер кода Playdate и кэшей данных.
В этом простом примере есть только два типа компонентов: «Физика» и «Размер». Каждый
из них заключен в более общую структуру Component вместе с перечислением типов для
обеспечения полиморфизма и идентификатором объекта их владельца. Сложив размер в
битах его членов, мы видим, что каждый экземпляр Компонента занимает 112 бит, если
считать целые числа 32-битными. Кэш данных консоли может содержать до 4096 байт
информации, что соответствует 32768 битам. Отсюда мы видим, что в кэш данных
поместится только 292 компонента. Для работы систем обычно требуется более одного типа
компонентов, а массивы компонентов разных типов хранятся в памяти последовательно.
Каждый массив компонентов выделяет достаточно памяти для максимального количества
компонентов, в данном случае 1000. Зная все это, легко видеть, что два компонента разных
типов почти никогда не будут располагаться достаточно близко в памяти, чтобы их можно
было загрузить в кэш на в то же время. При отделении компонентов от их сущности движок
потерял прирост скорости кэша, который приносил пользу его упрощенной версии.
6.4. Итерация 2 - Тайловые карты и движение 35

6.3.4. Выводы

После сравнения производительности элементарной и полной версий движка ECS первая


была выбрана в качестве основы для игры. Извлеченный урок состоит в том, что наиболее
ортодоксальное решение не всегда является лучшим; Выбор дизайна должен быть
мотивирован не догмой или теоретической правильностью, а потребностями и
характеристиками каждого конкретного проекта. Чрезмерное проектирование и
преждевременная оптимизация — распространенные ошибки среди разработчиков
программного обеспечения, поэтому необходимо найти баланс между правильностью и
простотой.
Как упоминалось в разделе планирования, были проведены некоторые тесты системы
сборки Tup. Хотя это хорошо продуманная и инновационная система сборки,
отличающаяся быстрым временем компиляции и интуитивно понятным использованием,
она не очень хорошо подходит для проекта данной дипломной работы. Официально игры
Playdate создаются с использованием CMake и make, поэтому затраты времени,
необходимые для перевода скриптов, правил CMake и make-файлов в конфигурации Tup,
перевешивают выгоду.

6.4. Тайловые карты и движение

Цели, поставленные перед этой итерацией, были следующими:

 Реализация загрузки уровней через тайловые карты.


 Реализовать механику платформера: движение игрока.
 Разработать минимально жизнеспособный продукт с состоянием победы которую
игрок сможет достичь.

6.4.1. Tilemaps (Тайловые карты)


Тайловые карты — это метод создания карт и уровней видеоигр с использованием
небольших плиток, образующих стены, полы и углы, вместо уникальных рисунков для
всего уровня. Это был самый популярный подход на заре существования среды, поскольку
объем памяти был ограничен, и в игру можно было включить не так много графики. Яркими
примерами тайловых игр той эпохи являются Super Mario Bros. или The Legend of Zelda,
обе для NES.

(а) Скриншот Super Mario Bros. (б) Повторяющиеся плитки одного цвета
Рис. 6.4: Пример тайловой карты в Super Mario Bros.
36 Разработка

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

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

 Размеры квадратных плиток, закрывающих экран: 1, 2, 4, 5, 8, 10, 16, 20, 40 и 80 px.


 Размеры, соответствующие ширине экрана, но оставляющие запас по высоте: 25, 50,
100 и 200 px.
 Размеры, соответствующие высоте экрана, но оставляющие запас по ширине: 3, 6,
12, 15, 24, 30, 48, 60, и 120 px.

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

6.4.2. Обозначение объектов JavaScript (JSON)

После создания или изменения карты тайлов экспортируются из Tiled в формате JSON и
сохраняются вместе с остальными файлами игры. Playdate SDK предоставляет анализатор
и средство записи JSON, которые будут использоваться для загрузки уровней во время
выполнения: класс json_decoder внутри pd_api.h.
6.4. Итерация 2 - Тайловые карты и движение 37

Класс json_decoder работает, устанавливая обработчики для каждого действия, которым


мы хотим управлять; это указатели на функции, которые можно установить при создании с
помощью назначенных в C99 инициализаторов, как в фрагменте кода 6.6. При создании
json_decoder обязательно реализовать обработчик decodeError, остальное необязательно.
Неиспользуемые обработчики в json_decoder должны быть явно инициализированы
значением NULL.

(а) Передний план (б) Задний план


Рис. 6.5: Разделение слоёв тайловой карты

Распределение плиток, формирующее уровень, представлено в файле JSON в виде массива


идентификаторов (ID) плиток. Обработчик didDecodeArrayValue, который срабатывает
после анализа массива JSON, реализован для хранения этих данных в массиве уровней.
Перед чтением этого или любого другого значения вызывается обработчик
shouldDecodeTableValueForKey; здесь это реализовано для увеличения номера слоя
тайловой карты. didDecodeTableValue обрабатывает другие переменные, которые
упакованы в файл JSON вместе с данными о распределении плиток, например размеры
карты тайлов и набора плиток, размер плиток в пикселях или имя слоя карты тайлов,
который будет считан.
После создания json_decoder файл JSON открывается с помощью класса SDFile,
включенного в Playdate SDK, а затем передается в декодер.
38 Разработка

6.4.2.1. Ошибка декодера JSON

При разработке загрузки файла JSON, в версии 0.12.0 Playdate SDK была обнаружена
ошибка: return 0 в shouldDecodeTableValueForKey и shouldDecodeArrayValueAtIndex,
методы должны пропускать чтение значения в паре с текущим ключом, но использование
этой возможности приводило к сбою в работе приложения. Отчет об ошибке был
отправлен в официальные репозитории GitLab и исправлен в следующем выпуске SDK.
Более подробную информацию по этому вопросу можно найти в приложении B.1.

6.4.3. Рисование тайловой карты

(а) Набор плиток с присвоенным ID


(б) Представление тайловой карты в виде
одномерного массива идентификаторов

Рис. 6.6: Нумерация и распределение тайлов в наборе плиток и на карте тайлов.

Как только карта тайлов прочитана и сохранена в виде идентификаторов тайлов в массиве
(рис. 6.6 б), наступает время рендеринга уровня. Для каждого слоя тайловой карты,
упорядоченного сзади вперед, мы перебираем идентификаторы тайловой карты,
определяем часть текстуры набора тайлов, которая соответствует этому тайлу, и рисуем ее
в соответствующей строке и столбце экрана.
Для определения части набора плиток, которая будет отрисована, мы исходим из того, что
Tiled присваивает плиткам их идентификаторы на основе их положения в наборе плиток,
начиная с 1 в левом верхнем углу плитки и идя слева направо (6.6a). Зная ширину тайла в
пикселях и количество столбцов в наборе тайлов, мы можем получить смещение в пикселях
по координатам x и y, по соглашению называемое (u, v) соответственно, используя
следующие уравнения*2:

u = tile_width × ((tile_id − 1) mod tileset_columns) (6.1 a)


v = tile_width × (⌊(tile_id − 1) ÷ tileset_columns⌋) (6.1 б)
Далее функция Playdate playdate->graphics->setClipRect() используется для выбора области
экрана, в которой мы будем рисовать; в данном случае это квадрат размером с плитку в
позиции, соответствующей текущей строке и столбцу карты листов. Наконец, изображение
набора плиток рисуется со смещением его позиции на вычисленное нами смещение (u, v),
так что часть изображения, соответствующая рисуемому фрагменту, заполняет
прямоугольник обрезки. Вы можете представить этот процесс как вырезание окна на листе
бумаги и размещение под ним изображения так, чтобы желаемая его часть была видна через
окно.

*1. Операцию нижнего деления в 6.1б можно опустить из-за поведения целочисленного деления C по
умолчанию, которое отбрасывает десятичные дроби, уравнивая результат. Реализации на других языках или
использование других типов данных должны включать его, чтобы формула работала.
6.4. Итерация 2 - Тайловые карты и движение 39

6.4.2.1. Ошибка ClipRect

Версия 0.12.0 SDK представила ошибку при создании прямоугольников обрезки, на


которую я наткнулся на этом этапе разработки. На размер прямоугольника влияло его
положение: результирующие размеры представляли собой указанный размер плюс
значение положения на той же оси. Например, прямоугольник с позицией (3, 6) и размером
(10, 20) в конечном итоге будет иметь размеры (13, 26). Я подал отчет об ошибке с
демонстрационной программой и исходным кодом, и вскоре он был исправлен. Более
подробную информацию об этом процессе можно найти в приложении B.2.

6.4.4. Движение игрока

В этой итерации также были реализованы основные движения игрока и чтение ввода.
Движение обрабатывается в системе физики: позиция игрока увеличивается при нажатии
правой кнопки на D-Pad или уменьшается при нажатии левой кнопки. При запуске прыжка
переменной сущности vy (скорость по оси Y) присваивается большое значение, так как в
играх-платформерах прыжки ощущаются лучше, если они взрывные, а не ускоренные;
затем каждый кадр на протяжении всего прыжка игрок перемещает вы пикселей и
переменная уменьшается. Прыжок заканчивается, когда игрок возвращается в позицию y, с
которой он начал.
Тот же метод, который использовался для обрезки плиток из набора плиток, использовался
для изменения спрайта игрока в зависимости от выполняемого действия (движение влево
или вправо, стояние на месте и прыжок). Спрайты хранятся в листе спрайтов, который
представляет собой одно изображение, содержащее разные кадры анимации, а не
отдельные изображения. См. рис. 6.7.

Рис. 6.7: Пример листа спрайтов игрока

6.4.5. Выводы

Большинство целей, поставленных перед этой итерацией, были достигнуты, за


исключением последней (получение первого закрытого продукта с состоянием победы).
Это произошло из-за задержек, вызванных ошибками, возникшими при разработке загрузки
и отрисовки уровней. Тем не менее, реализованные функции составляют большую часть
игры, поэтому эта итерация оказалась плодотворной.
Помимо задач, описанных в этом разделе, прототип был создан с помощью Pulp —
инструмента для создания визуальных игр, разработанного Panic. Это было сделано с целью
получить полное представление о возможностях разработки игр Playdate — цель, которая
40 Разработка

неоднократно излагалась в этой диссертации. Более подробную информацию об этом


прототипе можно найти в приложении А.4.

6.5. Итерация 3 – Коллизии (Столкновения)

Цели, поставленные перед этой итерацией, были следующими:


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

6.5.1. Столкновение

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

(column, row) = (⌊x ÷ tile_width⌋ , ⌊y ÷ tile_height⌋) (6.2)

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


размеров, рассчитывается путем деления размера спрайта игрока на размер плитки по
каждой оси и округления в большую сторону в тех случаях, когда это деление может иметь
десятичные дроби.
Большую часть времени игрок не будет выровнен по сетке тайловой карты, поскольку его
движение не зависит от нее. Это необходимо учитывать, проверяя столкновение в
дополнительной строке или столбце плиток для смещенной оси. Определить это можно с
помощью модуля деления в формуле 6.2: если модуль равен 0, игрок идеально выровнен по
плиткам; в противном случае количество перекрывающихся плиток для этой оси
увеличивается на 1. Таким образом, количество плиток, которые необходимо проверить на
предмет коллизий, рассчитывается по формуле 6.3.

(tilesx, tilesy) = (⌈sprite_width ÷ tile_width⌉ + x mod tile_width,


⌈sprite_width ÷ tile_height⌉ + y mod tile_height) (6.3)
6.5. Итерация 3 - Коллизии (Столкновения) 41

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


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

(overlapx, overlapy) = (player_width − |playerx − tilex|,


player_height − |playery − tiley|) (6.4)

Для отмены столкновения игрок будет отталкиваться от плитки только по оси с


наименьшим перекрытием:

 В случае горизонтальной оси: если позиция x игрока меньше позиции плитки, игрок
сталкивается с левой стороны и перемещается на overlapx пикселей в этом
направлении; в противном случае игрок входит с правой стороны и перемещается на
такую же величину вправо.

 Для вертикальной оси: если позиция y игрока меньше позиции плитки, игрок идет
сверху и перемещается на overlapy пиксели вверх; в противном случае игрок
сталкивается снизу и перемещается на такую же величину вниз.

6.5.2. Дельта времени

До сих пор скорость игрока была привязана к частоте кадров в игре, поскольку его позиция
увеличивалась при каждом вызове обновления на фиксированную величину. В некоторых
случаях этот подход может сработать, но это не лучшее решение, поскольку возможное
падение частоты кадров приведет к заметному замедлению действия. Вместо этого
большинство функций, связанных со временем, таких как движение игрока, анимация или
недуги, наносящие урон с течением времени, должны основываться на таймерах,
независимых от частоты кадров.
Обычный способ сделать это — использовать так называемое дельта-время: время,
прошедшее между каждым вызовом обновления. Он состоит из простой системы,
называемой первым делом в цикле обновления, которая хранит две переменные: DeltaTime
и last_time. В Playdate SDK есть функция для получения текущего времени в
миллисекундах, измеренного с произвольного момента времени: playdate->system-
>getCurrentTimeMilliсекунды(). При каждом обновлении система сохраняет в переменной
DeltaTime текущее время минус last_time, в котором хранится отметка времени последнего
вызова системы. Таким образом, система получает время, прошедшее между вызовами
обновления. Наконец, он обновляет last_time текущим временем, подготавливая его к
следующему обновлению. DeltaTime — это общедоступная глобальная переменная,
которую другие системы могут использовать для своих нужд.

6.5.3. Обновление движения игрока

До сих пор игрок мог двигаться и прыгать только на одной высоте, как если бы он стоял на
ровной земле, потому что не было системы обнаружения столкновений, которая могла бы
сообщить ему, приземлился ли он на платформу. Таким образом, все прыжки заканчивались
на одной высоте. Реализовав столкновение, необходимо было обновить систему физики,
управляющую движением игрока, чтобы позволить игроку стоять на разных уровнях земли.
42 Разработка

Это реализуется путём добавления «гравитации»: если игрок не прыгает, его положение
по оси Y увеличивается каждый кадр*3, что позволяет ему упасть с уступов. Возможность
стоять на более высоких платформах появляется в результате того, что система
столкновений корректирует перекрытия с платформами, поэтому для этого не требуется
никакого дополнительного программирования.
Еще одним заметным изменением стало использование DeltaTime (преобразованного в
секунды) для определения количества, необходимого игроку для перемещения каждого
кадра, в результате чего скорость определялась в пикселях в секунду, а не как
фиксированная величина за обновление.

6.5.4. Выводы

Несмотря на то, что внедрение первой системы столкновений является важной вехой для
этого проекта, тестирование показало, что текущий способ устранения перекрытий дает
плохие результаты в определенных ситуациях. Платформы могут состоять из более чем
одной плитки, но используемый метод рассматривает каждую плитку, как если бы это была
отдельная платформа, что приводит к разрешению столкновений путем толкания игрока
внутрь соседней плитки (что, в свою очередь, толкает его дальше, что приводит к странному
телепортация). Особенно это происходит при столкновении с платформой снизу.
Тем не менее, основная механика игры присутствует в своей базовой форме, что имеет
основополагающее значение для дальнейшего развития. Две цели этой итерации не были
достигнуты: реализация изменения уровня и способ завершения уровня. Это замедление
можно объяснить недооцененной сложностью обнаружения столкновений, а также
отсутствием инструментов отладки или моделирования для игр C Playdate в Windows на
момент написания статьи. Эти невыполненные цели и проблемы с системой столкновений
будут решены в будущей итерации.

6.6. Итерация 4 - Вход в игровой цикл

Цели, поставленные перед этой итерацией, заключались в следующем:


 Реализуйте состояния победы и поражения.

 Переключение между уровнями.

 Добавьте статическую опасность, например шипы или лаву, которая перезапускает


уровень, когда игрок к ним прикасается.

 Добавьте простого патрулирующего врага.

6.6.1. Система запуска

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

*1. По умолчанию в играх Playdate начало системы координат расположено в верхнем левом углу экрана,
поэтому значения y увеличиваются по направлению к нижнему краю экрана.
6.6. Итерация 4 - Вход в игровой цикл 43

форменных игр, поскольку их можно использовать для определения того, когда игрок
достигает цели уровня, собирает предметы или касается опасностей или врагов, наносящих
урон, а также многих других приёмов и механик.
В этой итерации были реализованы триггеры, которые использовались для изменения
уровня при касании цели и перезапуска уровня, если игрок касается плитки опасности. Для
этого была создана триггерная система и добавлена ее функция обновления в основной
цикл. Эту систему необходимо обновить после системыboundingTiles, поскольку она
зависит от рассчитанных в ней координат тайла и ограничивающей рамки.
Система триггеров вызывается для каждой сущности, но действует только на те из типов,
которые должны реагировать при контакте с игроком, в нашем случае на goal_type (тип
цели) и enemy_type (тип врага). Во-первых, система должна определить, перекрываются ли
игрок и триггерный объект, что будет верно, если выполняется следующее условие: для
каждой оси позиция объекта больше или равна позиции игрока, но меньше, чем позиция
игрока плюс размер ограничивающей рамки на этой оси.
Если игрок и объект-триггер перекрываются, система возвращает значение перечисления
на основе требуемого ответа: trigger_none, trigged_goal или trigged_death. В основном
цикле обновления для этого возвращаемого значения выполняется оператор переключения,
и необходимые действия выполняются для каждого случая.

6.6.2. Лимит времени

В игре была одна определяющая механика, давшая ей название «Крошечные секунды»,


которая еще не была реализована: ограничение по времени. В Tiny Seconds цель состоит в
том, чтобы достичь конца каждого уровня за очень короткий промежуток времени,
первоначально за одну секунду, сосредоточив игровой процесс на гибком и быстром
платформере.
Чтобы эта механика была эффективной, лимит времени должен был быть достаточным
для достижения цели, но не более того, чтобы дать небольшую погрешность, но сделать
игру напряженной и дать игроку прилив адреналина. Первоначально запланированный
лимит в одну секунду оказался слишком коротким, и после некоторых тестов было выбрано
значение таймера 2,5 секунды. Также должен был быть очень читаемый способ передачи
оставшегося времени: вместо отображения числового счетчика использовался датчик в
виде вертикальной полосы в правой части экрана. На этом дисплее используются
контрастные черно-белые цвета для панели и фона, так что, даже не глядя на него
напрямую, игрок может краем глаза почувствовать, сколько времени осталось. Кроме того,
хотя это и менее важно, поскольку полоса находится в правой части экрана, где обычно
находится цель, играющий человек будет следовать за спрайтом игрока, когда он движется
к ней, а таймер будет в фокусе в самый важный момент. десятые доли секунды, когда игрок
собирается закончить уровень, а время истекает.
Система таймера — одна из самых простых в игре: она вычитает значение DeltaTime из
ограничения по времени при каждом обновлении и возвращает true, если время осталось.
Если таймер меньше 0, вместо этого он возвращает false, сообщая основному циклу
обновления, что уровень необходимо сбросить. Он также имеет метод sys_timer_reset(),
который присваивает максимальное значение ограничению времени, который вызывается
при сбросе уровня в цикле обновления.

6.6.3. Чтение объектов из тайловой карты


44 Разработка

Теперь, когда в игре появились цели, опасности и место появления игроков, а в будущем
появится больше элементов, стало ясно, что необходимо найти лучший способ размещения
этих объектов.
В качестве распространённого решения этой проблемы было выбрано: размещение этих
объектов в виде тайлов в редакторе тайлов, а в момент загрузки уровня идентификация этих
тайлов и выполнение необходимых действий (например, создание соответствующих
сущностей или установка цели и позиции появления игрока).
Идентификаторы этих специальных тайлов были сохранены в константах. Для тех, кому
требовалось создание или установка положения уникального объекта, в данном случае цели
и плиток появления, в файле tilemap.h была создана глобальная переменная для хранения
их положения и координат плиток.
В средстве чтения JSON обработчик didDecodeArrayValue() был изменён для выполнения
оператора переключения идентификатора считываемого фрагмента, выполняя
необходимые операции в случае специальных фрагментов. Хотя это может показаться
дорогостоящим, компилятор очень оптимизирует операторы переключения, особенно по
сравнению с операторами if-else, поскольку случаи внутри оператора переключателя не
зависят от предыдущих. Добавление этого шага не привело к заметному увеличению
времени загрузки уровня.
Для статических плиток опасностей и целей их индекс плитки в массиве карты тайлов
преобразуется в координаты плитки по следующей формуле:

(column, row) = (index mod tilemap_columns, ⌊index ÷ tilemap_columns⌋) (6.5)

Затем для появления игрока его индекс тайла переводится в положение в пиксельных
координатах по следующей формуле:

(playerx, playery) = (index mod tilemap_columns × tile_width,


⌊index ÷ tilemap_columns⌋ × tile_width − tile_width) (6.6)

6.6.4. Перезапуск уровня

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

6.6.5. Изменение уровня

Реализация загрузки следующего уровня, когда игрок касается цели, была довольно
простой: как объяснялось в подразделе, посвящённом системе триггеров, если игрок
перекрывает цель, система триггеров возвращает значение перечисления trigger_goal в
основной цикл, который, в свою очередь, вызывает метод loadAndDrawMap(). Этот метод
просит менеджера объектов удалить все объекты, помеченные тегом enemy_type,
поскольку опасности уникальны для каждого уровня; вызывает util_tilemap_loadLevel(),
передавая путь к следующему файлу карты тайлов в качестве параметра; преобразует его в
новое полностью белое растровое изображение; и, наконец, вызывает метод restart() для
сброса таймера и положения цели и появления игрока.
6.7. Итерация 5 45

Путь к файлу JSON каждого уровня хранится в массиве в файле main.c в том порядке, в
котором они должны появляться. Затем индекс в массиве текущего уровня сохраняется в
переменной-счетчике. При загрузке следующей карты тайлов счетчик увеличивается, и
путь к следующему индексу массива передается загрузчику карты тайлов.
Единственная трудность, обнаруженная во время разработки этой функции, заключалась
в манипуляциях со строками в C. До сих пор путь к изображению набора тайлов получался
путем чтения его из файла тайлмапов, где он фигурировал под полем «image». Программа
«Tiled» экспортирует этот путь как относительный маршрут, а это означает, что перед ним
должна быть добавлена строка «/media/», чтобы оборудование Playdate могло найти файл.
Это работало хорошо, когда функция загрузки тайловой карты вызывалась только один
раз, но при последовательных вызовах путь к изображению набора тайлов добавлялся к
самому себе, что делало маршрут неверным. Присвоение значения перед вызовом функции
конкатенации strcat() в качестве попытки сброса переменной не привело к каким-либо
изменениям. Некоторое время было посвящено исследованию этой проблемы, но, зная, что
все тайловые карты имеют один и тот же набор тайлов, было решено статически задать
маршрут к изображению и решить эту проблему в будущем, если возникнет необходимость.

6.6.6. Выводы

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

6.7. Итерация 5

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

6.7.1. Переключить блоки

Разработка интересной механики, использующей преимущества оборудования Playdate,


была сложной задачей из-за динамичного характера этой игры. Каждый уровень
необходимо пройти менее чем за 2,5 секунды, а это означает, что игрок почти наверняка
будет постоянно нажимать клавиши со стрелками, чтобы вовремя добраться до цели. Это
влияет на механику использования рукоятки, поскольку для ее захвата обычно требуется
сменить рукоятку. Точные движения рукояткой также затруднительны за столь короткое
время, а быстрое проворачивание слишком сильно трясет устройство, что одновременно
затрудняет перемещение персонажа.
Сдерживает акселерометр еще и то, что встряхивание устройства размывает экран, из-за
чего игрок не может уследить за происходящим. Этот эффект усугубляется технологией
SHARP без подсветки, которая обеспечивает хорошую видимость за счет отражения света
от экрана.
46 Разработка

После некоторых экспериментов было обнаружено необычное взаимодействие: рукоятку


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

Рис. 6.8: Складывание рукоятки за устройством чтобы переключить взаимодействие

Это взаимодействие используется для нового типа плитки, которая становится твердой или
неосязаемой в зависимости от положения рукоятки: тумблера. Диапазон поворота рукоятки
разделен на две части посередине, и если угол поворота рукоятки меняет область, блоки
переключателей переходят в противоположное состояние, работая как переключатель.

(a) Рукоятка под углом более 270° (б) Рукоятка под углом менее 270°
Рис. 6.9: Головоломка с противоположными блоками переключения

Состояние блоков переключения не привязано ни к одному из этих двух регионов: при


загрузке уровня они начинаются в состоянии, назначенном им в редакторе уровней, и
переключаются, когда рукоятка меняет регион.
Блоки переключателей реализованы как новый тип объекта: toggle_type, который имеет
логическую переменную toggle_on для хранения его начального состояния. При создании
уровней в программе Tiled (редакторе тайловых карт), исходное состояние представляется
с использованием двух разных типов тайлов: один для блоков, когда включен старт, и
другой для противоположного случая.
Для работы с этой механикой был создан новый системный класс: система переключения.
Он содержит логическое значение для сохранения исходного положения рукоятки при
каждом изменении уровня. В методе обновления он выполняет следующую проверку: если
исходное положение рукоятки равно текущему положению рукоятки и исходное состояние
объекта было включено, блок будет ВКЛ и будет присутствовать; в противном случае блок
ВЫКЛ и нематериален.
Подход, использованный для включения или отключения столкновения с
переключаемыми блоками, был прост: изменение слоя «Ground» тайловой карты, в котором
6.7. Итерация 5 47

есть столкновение, путём добавления или удаления сплошного блока под объектом. Для
этого была реализована вспомогательная функция getTilePointer(tileCoords, LayerName). Эта
функция выбирает слой карты тайлов с указанным именем и возвращает указатель на
позицию в массиве тайлов, которая соответствует координатам, переданным параметром.
Для изменения типа плитки достаточно просто записать в этой позиции другой ID плитки.
Рендеринг этих блоков был простым; Функция render_update_one_entity() в системе
рендеринга была изменена и теперь имеет оператор переключения, который обрабатывает
рисование игрока или блоков переключения. Система переключения устанавливает
координаты листа спрайтов объекта в положение ВКЛ или ВЫКЛ при изменении его
состояния, поэтому их рендеринг так же прост, как рисование этой области листа спрайтов.
В результате получилась увлекательная игровая механика, достаточно простая, чтобы её
можно было понять с первого взгляда, но позволяющая решить множество задач по
дизайну. Блоки могут иметь противоположные состояния переключения, как показано на
рис. 6.9, позволяющая игроку переключаться между состояниями, чтобы открывать разные
планировки уровней. В других случаях, игрок должен быстро открыть строение, чтобы
добраться до высокого места, как на рис. 6.10. На рис. 6.11, игрок должен раскрыть
переключатели, чтобы перепрыгнуть через шипы, но затем быстро отключить их, чтобы
достичь цели.

(а) Тумблеры начинают отключаться и (б) При включении блоков открывается


нет возможности подняться к цели лестница.
Рис. 6.10: Головоломка со скрытыми структурами

(a) Игрок должен позволить блокам (б) Цель находится под блоками, поэто-
пересекаться му их нужно снова отключить
Рис. 6.11: Головоломка, требующая быстрой координации ВКЛ и ВЫКЛ блоков.

6.7.2. Функции преобразования

В этом проекте есть три пространственных представления: пиксельные координаты,


координаты тайлов и массивы тайловых карт. До этого момента необходимые преобразова-
48 Разработка

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

6.7.3. Выводы

Эта итерация оказалась плодотворной благодаря реализации переключающих блоков,


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

6.8. Итерация 6

Целями этой итерации были улучшение системы столкновений и физики, добавление


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

6.8.1. Улучшенная система столкновений

На этом этапе разработки в системе столкновений было несколько критических ошибок,


связанных с исправлением перекрытий между игроком и окружающей средой. Самая
заметная проблема возникла при столкновении с платформой снизу, из-за чего игрока
выталкивало за пределы экрана за несколько кадров из-за того, что он застревал внутри
сплошных плиток.
Первым шагом в решении этой проблемы было отслеживание того, как текущая система
обрабатывает коллизии, в поисках ошибки, которая могла вызвать проблему.
Внутриигровое тестирование показало, что столкновения с плиткой над игроком всегда
исправлялись по горизонтали, а не по вертикали. При просмотре формулы 6.4, которая
вычисляет перекрытие между игроком и плиткой, проблема стала очевидной:
использование высоты спрайта для расчета коррекции перекрытия давало неправильные
результаты, когда проверяемая плитка находилась в верхнем ряду ограничивающей рамки.
Это связано с тем, что спрайт игрока имеет высоту в два тайла, а поправка в этом случае
должна быть в пределах высоты одного тайла. Из-за этого поправка по оси x всегда была
меньше, чем по оси y, и поскольку система столкновений отменяет наименьшую из двух,
ось y никогда не выбиралась. См. визуальное представление этой проблемы на рис. 6.12.
Чтобы решить эту проблему, формула коррекции была изменена, чтобы использовать
минимум между ростом игрока и строкой плитки в ограничивающей рамке, умноженный
на высоту плитки. Обновленная формула коррекции (или перекрытия) выглядит
следующим образом:

(overlapx, overlapy) = (min (playerwidth, bbox row · tile size) − |playerx − tilex|,
min (playerheight, bbox row · tile size) − |playery − tiley|) (6.7)
6.8. Итерация 6 49

(а) Старая формула (б) Новая формула

Рис. 6.12: Старый метод 6.12a приводил к чрезмерной коррекции перекрытия оси у.
Зеленый: коррекция оси Y Красный: коррекция оси X
Синий: размеры спрайта Фиолетовый: ограничительная рамка.

Ещё одной проблемой было столкновение за пределами уровня. Это происходило,


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

6.8.2. Управление состоянием игры

Большинство видеоигр во время своего выполнения проходят через ряд различных


состояний, таких как меню, экраны победы или завершения игры, разделы игрового
процесса, экраны выбора уровня и т. д. Каждая из этих частей игры обычно автономна,
ведет себя совершенно иначе, чем другие (например, ввод и доступные действия на экране
меню отделены от тех, которые управляют игроком во время игры) и могут переходить
между друг друга. Из-за этих характеристик наиболее распространенным способом
решения этой проблемы является реализация конечного автомата (FSM).
Общие характеристики шаблона FSM таковы: «У вас есть фиксированный набор
состояний, в которых может находиться машина. [...] Машина может находиться только в
одном состоянии одновременно. [...] На машину отправляется последовательность входных
данных или событий. [...] Каждое состояние имеет набор переходов, каждый из которых
связан с входом и указывает на состояние». Нистром (2014).
Существует множество сложных способов реализации этого шаблона, но важно помнить
о текущих потребностях и ограничениях проекта и консоли. Язык C не имеет возможностей
ООП, на которые опирается большинство реализаций, таких как интерфейсы, наследование
и полиморфизм. Кроме того, разработка Playdate предполагает определение приоритетов
производительности и работу с ограниченным объемом кэша кода. По этим причинам
выбранная реализация FSM очень проста: использование оператора переключения для
переменной currentState, которая содержит одно из серии состояний, определенных в
перечислении. Код реализации, этого проекта, можно найти в приложении D.
В этой итерации игра имела два состояния: первое для прохождения уровней и второе
50 Разработка

Рис. 6.13: Изображение, отображаемое в состоянии победы в игре

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

6.8.3. Улучшена физика игрока

До этого момента физика прыжков была очень простой: в отличие от горизонтального


движения, при прыжке не использовалось дельта-времени, а это означало, что его скорость
зависела от частоты обновления. Она состояла из счетчика, установленного на начальную
скорость прыжка, которая в каждом кадре использовалась для перемещения игрока на
указанное количество пикселей и уменьшалась на одну единицу. Также существовала
грубая реализация гравитации, которая просто перемещала игрока вниз на три пикселя
каждый кадр.
В этой итерации была создана новая реализация, которая включает дельту времени, чтобы
отделить физику от частоты обновления, и использует аппроксимацию уравнений
линейного равномерно ускоренного движения. В этих уравнениях используется переменная
сущности vy, которая хранит скорость игрока по вертикальной оси.
Во-первых, в сущности было создано новое поле airborne_time, которое подсчитывает
время, прошедшее с момента контакта игрока с землей. Этот таймер и vy игрока
сбрасываются, когда столкновение по оси Y отменяется. Если столкновение происходит с
платформой над игроком, таймер устанавливается на значение немного большее, чем 0,
чтобы дать небольшой отскок от удара перед падением обратно на землю. Сброс этих
значений также устранил существующую проблему с предыдущей реализацией, которая
заключалась в том, что при приземлении на более высокую платформу прыжок не мог быть
инициирован до тех пор, пока счетчик прыжков не достиг своего конечного значения, что
создавало некоторые кадры зависания.
Затем в физической системе гравитация применяется к вертикальной скорости игрока с
помощью уравнения 6.8. Наконец, позиция игрока рассчитывается по формуле 6.9,
преобразуя единицы измерения из метров в пиксели.

Прыжок начинается, когда игрок нажимает кнопку A и таймер airborne_time составляет


менее 0,2 секунды. Этот небольшой промежуток времени, в течение которого пользователь
может прыгать, находясь в воздухе, известен в игровом дизайне как «время койота» и
делает элементы управления более отзывчивыми, будучи немного снисходительным к реф-
6.9. Итерация 7 51

лексам пользователя. Способ инициирования прыжка прост: переменная vy игрока


устанавливается в начальную скорость прыжка.

6.8.4. Выводы

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

6.9. Итерация 7

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

6.9.1. Вектор (Vector2f)

Был создан новый тип, имитирующий структуру Vector2i, уже определенную в нашем
проекте. Как упоминалось ранее, структуры Vector2 содержат два числа, сохраненные в
полях x и y. Определение структуры такого типа является обычной практикой при
разработке игр, поскольку многие переменные идут парами, например положение в
системах координат, координаты текстуры или физические значения в 2D-средах. Новый
тип отличается от существующего тем, что его значения хранятся в виде чисел с плавающей
запятой, а не целых чисел.

6.9.2. Бамперы

Уровни, созданные до этого момента, были ограничены расстоянием, которое игрок может
пройти за 2,5 секунды, а механика была сосредоточена на управлении окружающей средой
с помощью блоков переключения. Чтобы преодолеть это ограничение, следующая
механика должна была влиять на движение игрока, позволяя создавать уровни, на которых
игрок преодолевал большие расстояния или достигал более высокого уровня, чем то, что
дает ему прыжок.
Механика, разработанная с учетом этого, - это бамперы, особый тип плитки, который
придает игроку мгновенную скорость в том направлении, на которое он указывает.
Некоторые из применений этой механики — это пружинные платформы, которые толкают
игрока вверх, заставляя его подпрыгивать; турбопэды, ускоряющие игрока вперед или
назад по горизонтали; ловушки, загоняющие игрока в тупик; или диагональные бамперы,
которые одновременно толкают игрока вперед и вверх. См. рис. 6.14 для скриншотов
уровней.
Был создан новый тип объекта, названный buger_type, а также новая переменная Vector2f,
называемая bugerForces, которая хранит вектор скорости, который будет применен к игроку
при контакте. Кроме того, в плеер была добавлена новая переменная для хранения его
скорости по оси x.
Всего имеется восемь плиток, по одной для каждого направления, которое может иметь
бампер (влево, вправо, вверх, вниз и по диагонали). Они нарисованы в виде стрелки,
указывающей туда, куда будет приложена сила.
52 Разработка

(а) Использование бампера, чтобы под- (б) Попадание в ловушку, наступая на


няться на более высокую платформу. направленные вниз бамперы

(в) Прыжки через шипы. (г) Путешествие на большие расстоя-


ния во времени благодаря бамперу.

Рис. 6.14: Уровни бампера

При загрузке карты в зависимости от ID тайла направление в виде нормализованного


вектора умножается на модуль скорости бампера, получая его bumperForces.
Взаимодействие с бамперами начинается в системе триггеров: как объяснялось ранее, эта
система обнаруживает перекрытие между игроком и специальными плитками (например,
воротами или шипами) и выполняет необходимые действия. Здесь был добавлен новый
случай: когда игрок перекрывает бампер, его переменная bumperForces добавляется к
скорости игрока с использованием векторной арифметики (сложение компонентов одной
оси).
Затем, в конце физической системы, положение игрока по оси x изменяется с учётом его
скорости по этой оси, добавленной бампером. Если пользователь нажимает клавишу со
стрелкой, а бампер влияет на скорость оси х, противоположную этому движению,
мгновенная скорость из входных данных вычитается из переменной vx игрока, которая
хранит только скорость, создаваемую бамперами.
Наконец, чтобы учесть трение, в системе столкновений vx игрока уменьшается каждый
раз, когда столкновение отменяется по направлению к верху платформы, то есть игрок
стоит на земле.

6.9.3. Вывод

Внедрение бамперов предоставило универсальный инструмент, расширяющий


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

6.10. Итерация 8
6.10. Итерация 8 53

Целями этой итерации были разработка карты мира, улучшение заставок, улучшение
конечного автомата и добавление главного меню.

6.10.1. Улучшенные бамперы

В своей первой итерации бамперы добавляли игроку скорость в каждом кадре, который они
перекрывали. Иногда это приводило к слишком большим движениям, что не приносило
желаемого эффекта. Чтобы исправить это, к сущностям была добавлена новая логическая
переменная под названием bumperTouchedPlayer: когда игрок не перекрывает бампер,
переменная имеет значение false. Если они начинают перекрываться, система триггеров
применяет скорость, а затем устанавливает для неё значение true, чтобы не применять её
снова при следующем обновлении. Как только игрок перестаёт касаться плитки, система
триггеров возвращает логическое значение false. Это означает, что скорость будет
добавлена только после начала перекрытия с бампером.
Чтобы добавить механике больше глубины, нажатие на бампер теперь сбрасывает таймер
прыжка, позволяя игроку инициировать еще один прыжок и получить больше импульса,
чем просто падение на него. Для этого необходимо точно рассчитать время нажатия кнопки
прыжка, что повышает потолок навыков в игре. Сравнение можно увидеть на рис. 6.15.

(а) Обычный отскок (б) Отскок плюс прыжок

Рис. 6.15: Прыжок после отскока от бампера

6.10.2. Новое состояние автомата

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


сложность игровых состояний в этой итерации выросла с добавлением меню и обзора
окружающего мира. По этой причине была создана новая версия государственной машины.
Во-первых, внутри каталога utils был добавлен новый класс. Этот файл определяет
структуру State, в которой хранятся указатели на функцию state_update и на функцию
state_init. Он также предоставляет глобальную переменную этого типа с именем
currentState, которая будет содержать указатели на активные функции обновления и
инициализации. Наконец, он предоставляет функцию под названием changeToState(
State newState), которая получает параметр State, копирует его в переменную currentState
и, если указатель на функцию инициализации не равен NULL, вызывает её.
Была создана новая папка с названием state для хранения классов, представляющих каждое
состояние. Каждый из них должен иметь как минимум функцию обновления и может иметь
дополнительную функцию инициализации. Для каждого из состояний необходимо создать
переменную в файле State.h, чтобы избежать циклических зависимостей между файлами
при изменении состояний между ними. В этой итерации существовали следующие
состояния:
54 Разработка

6.10.2.1. Меню состояний

Показывает титульный экран с возможностью запустить игру и перейти к настройкам.


Настройки пока не реализованы, но точка расширения создана. Пользователь может
переключаться между опциями меню, нажимая стрелки вправо или вверх на D-Pad,
переходить к предыдущим с помощью стрелок влево и вниз и выбирать один вариант с
помощью кнопки A.

6.10.2.2. Состояние в игре

Вся логика, связанная с состоянием игрового процесса, была перенесена из файла main.c в
отдельный файл состояния. Одним из дополнений была опция меню паузы, позволяющая
игроку выйти с уровня в внешний мир в любой момент; Playdate SDK позволяет добавлять
пользовательские параметры в меню паузы с помощью функции playdate->system-
>addMenuItem(). Выбор выхода из уровня с помощью этой опции вызывает функцию
changeToState(), передающую состояние overworld (внешний мир) в качестве параметра.
Когда игрок проходит последний уровень мира, состояние меняется на состояние победы.

6.10.2.3. Состояние мира в игре

В этом состоянии отображается карта с уровнями, доступными в одном мире, аналогично


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

6.10.2.4. Состояние победы

Демонстрирует экран победы, поздравляющий игрока с прохождением всех уровней (см.


рис. 6.13). Нажатие кнопки A, сменит текущее состояние на обычное состояние мира.

6.10.3. Система точек доступа меню

Состояния, которые работают как меню, например, внешний мир и состояние меню, имеют
схожий набор функций. Их можно резюмировать как возможность циклически
переключаться между наборами опций с помощью навигационной панели, отображать
растровое изображение для обозначения активной опции и выполнять действие при
нажатии кнопки A на одной из них.
Эта общая функциональность была реализована в виде системы, позволяющей повторно
использовать ее между состояниями меню: система menu_hotspot. Помимо обязательной
функции обновления, эта система имеет функцию конфигурации для инициализации
переменных-членов при изменении состояния.
В этом файле был объявлен новый тип структуры под названием Hotspot, который
представляет собой опцию меню. В его состав входят переменная Vector2i, обозначающая
положение курсора выбора, когда эта горячая точка активна, и функция, вызываемая при
её выборе.
6.10. Итерация 8 55

При входе в состояние, использующее эту систему, оно должно вызвать функцию
sys_menu_hotspot_config() со следующими параметрами:
 Массив со структурами Hotspot, присутствующими в этом меню.
 Длина массива горячих точек.
 Указатель на изображение (с использованием типа LCDBitmap Playdate SDK),
которое будет отображаться в качестве фона. В текущей реализации эта система не
отображает текст или любую другую графику для параметров, и они должны быть
встроены в это фоновое изображение или отрисованы состоянием каким-либо
другим способом.
 Указатель на изображение, которое будет отображаться в качестве курсора.
 Vector2i со смещением поворота изображения курсора, чтобы его центр имел
относительную координату, отличную от верхнего левого угла по умолчанию.

Все эти параметры хранятся в статических переменных внутри класса и используются для
обеспечения функциональности системы. Индекс активной в данный момент опции из
массива также сохраняется в переменной.
В функции обновления системы pd->system->getButtonState() используется для запроса
ввода устройства. Эта функция возвращает кнопки, которые в данный момент нажаты, либо
нажаты и отпущены в течение предыдущего цикла обновления. Затем на основе нажатых
кнопок выполняется комплекс операций. Если нажата кнопка A и установлен указатель
текущей горячей точки на функцию, эта функция вызывается. В противном случае, если
нажата стрелка вправо или вверх, текущий индекс горячей точки увеличивается или
устанавливается на ноль, если после текущего индекса больше нет опций. Наконец, если
оба этих условия не выполняются и нажата стрелка влево или вниз, текущий индекс горячей
точки уменьшается или устанавливается на последнее значение массива, если активный
индекс находится в первой позиции.
Метод обновления заканчивается рисованием фонового изображения, а затем
изображения курсора в позиции текущей активной горячей точки за вычетом смещения
поворота.

6.10.4. Тестирование пользователями и изменение дизайна

Во время этой итерации было проведено неофициальное пользовательское тестирование с


участием семьи и друзей. Реакция была положительной: игроки сочли игру сложной, но
увлекательной. Менее опытным игрокам поначалу было трудно проходить уровни, но они
наслаждались игрой и старались до конца.
Стало очевидно, что первоначальный план по блокировке перехода в следующий мир до
тех пор, пока игрок не завершит все уровни текущего мира, ни разу не проиграв, был
слишком суровым условием победы и не позволял менее опытным игрокам играть в игру.
Одним из решений было бы снизить сложность уровней, но это не было проблемой,
поскольку тестерам нравилась эта задача.
В конце концов, было решено позволить игроку проиграть либо из-за истечения таймера,
либо из-за столкновения с опасностями, и сосредоточить прогрессирующее состояние во
всеобъемлющем таймере от первого уровня до последнего. Таким образом, у игрока по-
прежнему остаётся мотивация совершенствовать свои навыки в игре, не отвлекаясь на
менее опытных пользователей. Таймер был представлен с использованием новой системы:
системы летающих часов.
56 Разработка

6.10.5. Система летающих часов

Эта недавно добавленная система заключена в том, чтобы демонстрировать таймер, в


течение которого игроки должны пройти каждый мир, чтобы перейти к следующему.
Должно было быть чёткое различие между таймером одного уровня и всеобъемлющим
таймером мира. Добавление большего количества HUD (Head-Up Display) может сбить с
толку игроков и загромождать экран информацией, поскольку в этой игре необходимы
сфокусированные и читаемые визуальные эффекты.
Вместо шкалы было найдено решение добавить «призрак» противника, который летает по
уровням мира, побуждая игроков участвовать в гонках или пытаться не отставать от него.
Достигнув последнего уровня, призрак подождёт несколько циклов, а затем исчезнет; если
игрок достигнет его до того, как это произойдёт, он очистит мир, и будет разблокирован
следующий. В противном случае им придется повторить попытку с первого уровня.
В этой системе есть таймер, отдельный от системы таймеров, поскольку игрок может
коснуться опасности и перезапустить уровень, не влияя на ход летающих часов. Этот
таймер накапливает значение deltaTime до тех пор, пока оно не достигнет 2,5 секунд, что
соответствует продолжительности ограничения времени одного уровня, а затем
сбрасывается.
Основное действие летающих часов — это линейная интерполяция от места появления
игрока на уровне до его цели. Эта информация получается из класса tilemap utils, который
считывает её из файла тайловой карты уровня. Был добавлен новый служебный класс с
функциями для выполнения интерполяции, которые обсуждаются далее в подразделе
6.10.6. Чтобы добавить изюминку движению, значение таймера делится пополам и
преобразуется в радианы, передаётся в синусоидальную функцию, получающую число от 0
до 1, умножается на 5, чтобы получить значение в диапазоне от 5 до -5, и добавляется к
значению положения оси y часов. Это простое дополнение заставляет часы зависать по
синусоиде, что значительно улучшает визуальный результат.
Часы рисуются в режиме NXOR, который представляет собой отрицательную версию
логического элемента XOR и может быть резюмирован следующим образом: если оба входа
одинаковы, элемент возвращает 1; в противном случае возвращается 0. В графиках Playdate,
пиксели представлены битами, так как они могут принимать только два значения: значение
1 означает белый пиксель, а значение 0 — черный пиксель. Таким образом, рисование в
режиме NXOR означает, что каждый пиксель изображения сравнивается с тем, что
находится под ним на дисплее; если базовый пиксель белый, цвет пикселя сохраняется, но
если фоновый пиксель черный, цвет пикселя инвертируется. В результате цвет часов
инвертируется при прохождении сквозь стены и плитки.
Чтобы имитировать движение часов по уровням, переменная clockLevel отслеживает
уровень, на котором часы находятся в данный момент. Эта переменная увеличивается
каждый раз, когда таймер часов сбрасывается, что совпадает с интерполированным
положением часов, достигающим цели. Тогда часы отображаются только в том случае, если
они находятся на том же уровне, что и игрок.

6.10.6. Линейная интерполяция

Одной из наиболее распространенных операций при разработке игр является линейная


интерполяция, которая позволяет получить промежуточные значения между начальным и
конечным числом с помощью линейной функции.
Он использует формулу 6.10, где a — начальное значение, b — конечное значение, и t —
число от 0 до 1, означающее прогресс между двумя значениями, где 0 возвращает начальное
6.11. Итерация 9 57

значение, 1 — конечное значение, а 0,5 — точку посередине между a и b.

x = a + (b − a) × t (6.10)

Эта функция была реализована в новом служебном файле под названием lerp, который
является широко распространенной квазиаббревиатурой линейной интерполяции.

6.10.7. Различные тайлы в мире

Плитка земли теперь меняется в соответствии с текущим миром, усиливая ощущение прог-
ресса, когда игрок видит изменение окружающей среды. Это реализовано в классе утилиты
tilemap путём установки пути к изображению набора тайлов мира при загрузке карты
тайлов в зависимости от папки, к которой она относится.

Рис. 6.16: Различные плитки для мира 2

6.10.8. Вывод

Это была одна из самых плодотворных итераций на данный момент. Поток приложения
наконец-то дополнен представлением общего мира и улучшенным конечным автоматом.
Благодаря системе часов возможность повторного прохождения игры значительно
улучшилась, что позволяет игрокам практиковаться на уровнях и улучшать свои
результаты. Наконец, было много других дополнений, которые добавляют совершенство
игровому опыту. Минимально жизнеспособный продукт (MVP) уже разработан, и все, что
осталось, — это доработать продукт, чтобы закрыть его.

6.11. Итерация 9

Девятая и последняя итерация была посвящена добавлению последних механик и доработке


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

6.11.1. Сохранение прогресса

До этого момента прогресс игрока в разблокировке уровней не сохранялся, поэтому каждый


раз, когда игра открывалась, игроку приходилось начинать с самого начала. Чтобы решить
эту проблему, количество разблокированных уровней необходимо было хранить в
файловой системе консоли.
Данные игры в системе Playdate разделены между двумя каталогами: файлом .pdx в
58 Разработка

каталоге Games и папкой, названной в честь имени пакета в каталоге Data.


Расширение .pdx — это формат пакета Playdate, который содержит двоичный файл игры
(.bin), файл .info, в котором хранятся метаданные, такие как версия SDK, с помощью
которой он был создан, и, наконец, содержимое папки Source проекта, сохраняющее его
файловую структуру. При доступе к файлам из кода папка «Исходный код» выступает в
качестве корневого каталога.
Файлы, созданные во время выполнения, хранятся в каталоге Data внутри папки игры. При
доступе к пути из кода сканируются как этот каталог, так и исходный каталог файла .pdx.
Обычный подход к сохранению данных — создать файл на устройстве, сохранить в нем
информацию, которую необходимо сохранить, а затем прочитать или изменить ее во время
выполнения. В случае с Playdate файл сохранения должен быть создан во время
выполнения, поскольку файлы, включенные в пакет .pdx, не могут быть изменены.
В ситуации, когда требуется сохранение и загрузка большого количества переменных,
синтаксический анализатор и запись JSON из Playdate SDK может подойти хорошо,
поскольку он аккуратно обрабатывает форматирование файла и обеспечивает приемлемую
производительность в программах на C. Этот инструмент используется в TinySeconds для
загрузки информации о тайловой карте из файлов, как описано в главе 6.4.2. Однако
требования к этому проекту гораздо проще: единственное значение, необходимое для
восстановления прогресса игрока, — это количество разблокированных уровней.
Выбранный подход представляет собой нетрадиционное решение, которое, тем не менее,
охватывает варианты использования этого проекта: создание каталога /saved/ внутри папки
Data игры и запись пустого файла с указанием количества разблокированных уровней в
качестве имени файла. Если бы данные хранились внутри файла, потребовалось бы
выполнить дополнительную обработку и системные вызовы, поскольку файл нужно было
бы перечислить, затем открыть, а затем проанализировать его содержимое. Используя имя
файла для хранения переменной, мы можем получить ее значение только с первого шага,
перечислив файлы в папке /saved/.
Чтобы записать файл сохранения, путь сначала создается с помощью pd->system-
>formatString(), аналога Playdate SDK функции sprintf() языка C, которая выделяет и
форматирует строку, позволяя легко объединять текст и другие типы значений. В данном
случае используется формат «/saved/%d», где %d заменяется переменной, содержащей
количество разблокированных уровней. Затем каталог /saved/ удаляется с помощью pd-
>system->unlink(), который удаляет файлы по указанному пути. Наконец, новый файл
сохранения создается с помощью pd->system->mkdir(), передавая в качестве параметра
форматированную строку пути.
Чтение файла сохранения ещё проще: функция pd->file->listfiles() получает путь, указатель
функции в качестве обратного вызова и пустой указатель на любые данные, к которым нам
нужно получить доступ внутри обратного вызова. Функция вызывает обратный вызов для
каждого файла по указанному пути, передавая его имя файла и пустой указатель в качестве
параметров. Таким образом, была создана функция обратного вызова, которая преобразует
имя файла в целое число с помощью функции стандартной библиотеки C atoi(), а затем
сохраняет его внутри переменной, подсчитывающей количество разблокированных
уровней. Эта переменная передаётся функции через параметр пустого указателя и при-
водится к целочисленному указателю внутри неё.

6.11.2. Рисуем внешний мир

Несмотря на то, что состояние внешнего мира было реализовано в прошлой итерации,
ресурс, используемый в качестве фона на экране выбора уровня, представлял собой карту-
6.11. Итерация 9 59

заполнитель, без визуализации того, какие уровни были разблокированы, а какие нет.
Была создана новая система под названием drawOverworld, которая получает из состояния
верхнего мира массив горячих точек, представляющих уровни и их размер, количество
разблокированных уровней в текущем мире и индекс отображаемого мира. В системе есть
функция рендеринга, которая рисует эллипс в положении каждой горячей точки, а затем
соединяет их линиями, выделяя линии между разблокированными уровнями.
Сначала создаются два растровых изображения: одно для хранения полученного изоб-
ражения, а другое для однократного рисования маркера уровня и последующего его пов-
торного использования для каждой горячей точки. Маркер уровня изображается в виде
короткого цилиндра в ортогональной перспективе. Он визуализируется путем рисования
двух эллипсов с помощью playdate->graphics->fillEllipse() для верха и основания и соеди-
нения их главной оси с прямоугольником той же ширины с помощью playdate->graphics-
>drawLine(). Верхний эллипс рисуется последним и окрашен в другой цвет, чтобы улучшить
иллюзию искусственного трёхмерного изображения.
Функции рисования Playdate SDK, которые получают LCDColor, также вместо сплошного
цвета принимают LCDPattern; они определяются как массив из 16 байтов, первые восемь
представляют цвета строки по 8 пикселей каждая, а последние восемь — значение маски
тех же строк. Чтобы понять представление строки в виде байта, нужно взглянуть на её
двоичную форму: число 11110000 будет означать строку, в которой первые четыре пикселя
белые, а последние четыре чёрные. Если байт представляет маску, значение бита 1 означает
сплошной пиксель, а значение 0 — прозрачный пиксель.*4
Первый и последний элементы массива имеют линию, идущую от их позиции до крайнего
левого или крайнего правого края экрана соответственно, чтобы обозначить, что прогресс
начинается в прошлых мирах и продолжается в следующих. В зависимости от
визуализируемого мира для путей между разблокированными уровнями используется
другой шаблон.

а) Первый мир; б) Второй мир.


Рис. 6.17: Программное рисование внешнего мира

6.11.3. Добавление музыки

Во время этой итерации была создана музыкальная дорожка, которая служила как фоновой
музыкой, так и звуковым индикатором производительности проигрывателя. Продол-
жительность трека составляет тридцать пять секунд, что соответствует продолжительности
таймеров всех уровней мира вместе взятых (2,5 секунд на уровень × 14 уровней = 35 секунд).
Музыкальный трек используется в системе flyingClock следующим образом: если плеер
находится на одном уровне или выше часов, музыка воспроизводится на полную громкость.
Если они отстают, громкость музыки снижается линейно в зависимости от того, на сколько
*4. Эту концепцию можно визуализировать с помощью инструмента https://ivansergeev.com/gfxp/, который
позволяет графически создавать шаблоны для использования при разработке на Playdate.
60 Разработка

уровней разница между плеером и часами. Изменение громкости осуществляется плавно с


помощью линейного уравнения и таймера часов: сначала значение x рассчитывается как
разница между уровнями игрока и часов плюс расширенный процент таймера. Затем он
подставляется в упрощённое уравнение линии, проходящей через координаты (1, 1) и (3, 0),
что означает, что при одном уровне разницы и без прошедшего времени объём будет иметь
полную величину, и затем опускайтесь до тех пор, пока музыка не перестанет быть слышна
на трёх уровнях разницы. См. уравнение 6.11.

6.11.4. Применение мирового таймера

Последней важной недостающей функцией было то, что если общий таймер мира истёк, то
часы блокировали переход на следующий уровень. Также должен быть некий запас тай-
мера, когда часы достигнут крайнего предела с демонстрацией обратного отсчёта.
Чтобы включить эти функции, система flyingClock была изменена. Теперь, начиная с
функции инициализации, она получает в качестве параметра уровень, выбранный из внеш-
него мира. Если этот уровень не является первым в мире, система ничего не делает, так как
часы должны пройти от первого уровня к последнему. В класс была добавлена логическая
переменная sys_flyingClock_isGoalOpen. Если она равна true, это означает, что игрок может
разблокировать следующий мир после достижения последней цели; если false, переход в
следующий мир заблокирован. Это логическое значение истинно только в том случае, если
игрок начал на первом уровне мира и таймер часов не истёк.
Самое большое изменение коснулось функции обновления системы. Здесь был создан
новый метод для расчёта положения часов в зависимости от ситуации, в которой они
находятся. Если часы находятся на уровне, предшествующем последнему, их движение
представляет собой интерполяцию между позицией появления игрока и целью, добавляя
смещение по высоте на 1/4 высоты спрайта игрока, чтобы он плавал над плитками, а не
позиционировал себя поверх них. Если часы находятся на последнем уровне, это смещение
увеличивается для цели, чтобы она плавала выше над ней. Затем, если счётчик часов уровня
превысил максимальное значение часов мира, в течение некоторого времени часы будут
плавать над целью и отображать обратный отсчёт до нуля. После прохождения итераций
заданного поля часы вылетают за правую границу экрана, устанавливая цель в качестве
начальной позиции и точку за пределами экрана в качестве конечной позиции. Для
логического значения sys_flyingClock_isGoalOpen устанавливается значение false, и в
последствии они не рисуются снова.
Текст обратного отсчёта рисуется с помощью функции pd->graphics->drawText() в позиции
часов минус двойное вышеупомянутое смещение высоты. В отличие от часов, обратный
отсчёт виден с любого уровня, чтобы игрок знал, сколько времени ему осталось, чтобы
пройти мир.
Во внутриигровом состоянии было добавлено условие для загрузки следующего уровня,
поэтому оно происходит только в том случае, если значение sys_flyingClock_isGoalOpen
истинно.

6.11.4.1. Система ограждений


6.11. Итерация 9 61

Поскольку условие для блокировки перехода в следующий мир было установлено, пришло
время графически представить его игроку. Была создана новая система под названием
«fence», которая рисует изображение забора, если sys_flyingClock_isGoalOpen имеет зна-
чение false. Забор представляет собой растровое изображение высотой в три плитки,
которое рисуется перед воротами; прыжок игрока достигает лишь чуть выше двух плиток,
что делает это препятствие непреодолимым. Как и в случае с системой переключения
плиток, динамические столкновения реализуются путем изменения массива тайловой
карты, чтобы пометить плитки под забором как сплошные. Это делается с помощью ранее
реализованной функции util_tilemap_getTilePointer(), которая, учитывая имя слоя, строку и
столбец, возвращает указатель на элемент массива плитки, которая находится на этом слое
и в этой позиции. Чтобы упростить систему, координаты плитки жёстко закодированы в
месте, которое занимает забор на экране. Это можно улучшить, преобразуя положение
забора в координаты плитки и увеличивая в два раза значение строки, или добавляя тип
плитки в карту тайлов и анализируя её в функциях загрузки карты тайлов. Текущая реали-
зация быстрая и понятная, поэтому она была выбрана вместо более сложного подхода.

6.11.5. Выводы

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

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

7.1. Состояние игры

После девяти итераций разработки TinySeconds достигла состояния, которое можно считать
первой законченной версией, со всей установленной базовой механикой, двадцатью
восемью уровнями, разделенными между двумя мирами, оригинальной музыкальной
дорожкой и тремя универсальными типами специальных препятствий, которые
обеспечивают множество возможностей дизайна уровней.
Большая часть запланированных механик была включена в конечный продукт, хотя
некоторые из них были переработаны в процессе разработки, чтобы улучшить их, заменить
ими не лучшие подходы или сбалансировать сложность игры. Использование итеративной
методологии обеспечило проекту такой высокий уровень манёвренности: поскольку каждая
итерация основывается на предыдущих, вскоре определяются способы улучшения
запланированных функций, и разработку можно легко развернуть.
Разработка TinySeconds дала мне возможность понять и улучшить свои навыки во всех
областях разработки игр: от написания игрового движка до проектирования уровней и
механики, программирования физики и игровой логики, создания 2D-графики и, наконец,
написания музыки для неё. Работа над новым, ограниченным оборудованием также
оказалась очень познавательным опытом и улучшила мои способности решать проблемы
как инженера. Этот опыт, а также полная разработка игры и прототипов
задокументированы в данной бакалаврской диссертации, которая послужит справочной
информацией для будущих разработчиков.

7.2. Улучшения

Есть несколько областей, в которых TinySeconds можно улучшить и расширить. Некоторые


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

 Улучшение изображения спрайтов, плиток, меню и фона. Поскольку разработка


ведется одним человеком, 2D-графика игры функциональна, но далека от того
уровня отшлифовки, который мог бы привнести в нее профессиональный художник.
Состояние внешнего мира и уровни имеют белый фон вместо изображения, и хотя
это повышает читаемость в такой динамичной игре, ненавязчивый рисунок может
добавить им визуального изящества.
 Добавление спрайтовой анимации. На данный момент игрок меняет свой внешний
вид в зависимости от действия, которое он выполняет (например, стоит на месте или
идет в том или ином направлении), рисуя соответствующую часть изображения
своего спрайт-листа. Улучшением могла бы стать реализация системы анимации,
которая циклически перебирает серию изображений с выбранной частотой кадров,

63
64 Заключение

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

 Создание новых механик и миров, которые их используют. Среди недобавленных


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

 Сочинение новой музыки и добавление звуковых эффектов. Для всех миров


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

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


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

 Настройки сложности и доступности. Существует ряд опций, которые можно


добавить, чтобы позволить менее опытным игрокам или игрокам с ограниченными
возможностями настроить свою сложность игры. Одним из таких вариантов было
бы увеличить общий таймер миров, чтобы снизить сложность. Другой вариант —
позволить игроку пропустить уровень после нескольких попыток. Наконец,
механика блока переключения может использовать кнопку B, если игроку неудобно
или он не может использовать рукоятку в заданной конфигурации. Это позволит
избежать выхода игроков из игры из-за разочарования или неспособности
прогрессировать.

7.3. Извлеченные уроки

Работа над этой бакалаврской диссертацией, над TinySeconds и над небольшими играми
стала очень познавательным опытом, охватывающим все области разработки игр и
обучение разработке для новой платформы.
Во-первых, многое было изучено об архитектуре и возможностях Playdate. Создание и
разработка игры на языке низкого уровня включало понимание характеристик, сильных и
слабых сторон консоли, а также их анализ для выявления лучших практик. Эти
соображения были учтены при разработке игры для ее оптимизации и получения хорошей
производительности.
Фаза создания прототипов проекта обеспечила понимание различных способов создания
игр для консоли, их преимуществ и недостатков. Программирование на Lua требовало
умеренного обучения благодаря более высокому уровню абстракции, управлению памятью
со сборкой мусора, а также более богатому набору функций Playdate SDK. Язык C,
напротив, оказался менее дружелюбным к новичкам, но преуспел в плане
производительности, позволяя играм Playdate достигать более высокой частоты кадров
благодаря более низкому уровню абстракции, ручному управлению системной памятью и
отказу от запуска. на виртуальной машине. Все эти знания, полученные на этапе создания
прототипа, были затем применены при разработке TinySeconds, что значительно облегчило
решение проблем.
Разработка TinySeconds также оказалась полезной в плане процессов планирования, анали-
7.4. Личные выводы 65

за прогресса и текущего состояния проекта на каждой итерации, а также адаптации объёма


проекта к доступному времени. Это также была возможность значительно расширить мои
знания языка C и подхода к низкоуровневой разработке.

7.4. Личные выводы

Лично я очень доволен работой, проделанной в рамках этого проекта, поскольку Playdate
была для меня неизведанной территорией, когда я впервые начал писать бакалаврскую
диссертацию, и теперь я могу сказать, что хорошо понимаю консоль и способы разработки
для неё.
Участие в программе Developer Preview было потрясающим опытом: наблюдать за
расцветом сообщества, развитием консоли и ростом ожиданий публики в течение года было
действительно захватывающе. Это также позволило мне взглянуть на процессы компании
Panic, пока они работали над подготовкой консоли к выпуску, и я рад, что внёс свой вклад,
сообщая об ошибках и помогая протестировать SDK и оборудование.
Кроме того, возможность завершить первую версию игры TinySeconds, которой я могу
гордиться, вселила в меня уверенность в будущих разработках и заставила задуматься о
возможности дальнейшей доработки продукта и подготовки его версии к выпуску. это как
название запуска.
Источники
Карр, Р. (2007, март). Тест скорости: Switch против if-else-if. Взят 21/03/2021 г. с http://
www.blackwasp.co.uk/speedtestifelseswitch.aspx

помощники, SMW (2021a). Мигающий блок. Взят 16/06/2021 с


www.mariowiki.com/Blinking_Block

помощники, SMW (2021b). Грибной батут. Взят 16/06/2021 с https://


www.mariowiki.com/Mushroom_Trampoline

помощники, SMW (2021c). Супер Марио 3D мир. Взят 16/06/2021 с https://


www.mariowiki.com/Super_Mario_3D_World

помощники, В. (2021a). Боксбой! (видеоигра) — википедия, бесплатная энциклопедия. Взят


16/06/2021 с https://en.wikipedia.org/wiki/BoxBoy!_(video_game)

помощники, В. (2021b). Инверсия контроля. Взят 01/02/2021 с https://


en.wikipedia.org/w/index.php?title=Inversion_of_control&oldid=998512915

помощники, В. (2021c). Минит (видеоигра). Взят 16/06/2021 с https://


en.wikipedia.org/wiki/Minit_(video_game)

помощники, В. (2021d). Ритмический рай. Взят 16/06/2021 с https://


en.wikipedia.org/wiki/Rhythm_Heaven

Дюран, FJG (август 2020). Gameengine ecs: Эффект звёздного поля. Взят 11/11/2020 с
https://www.youtube.com/watch?v=ighkMUM9-Ww

Франк, С. (август 2020). Прямая трансляция программирования Playdate. Взят 02/01/2021 с


https://www.twitch.tv/videos/608372277

Ли, Н. (август 2019). Очаровательная крошечная портативная консоль Playdate с рукоят-


кой. Взят 16/06/2021, с https://www.engadget.com/2019-08-28-playdate-hands-on.html

Линдейер, Т. (2019). Редактор тайлмапов. Взят 07/02/2021 с https://www.mapeditor.org/

Лунь С. (август 2020). Пролог Шан Луня. Взят 05/11/2020 с


https://devforum.play.date/t/shang-luns-proglog/1194/3

Мирау, Д. (март 2021). Журнал прогресса плеймейкера. Взят 16/06/2021 с https://


twitter.com/dmierau/status/1372321742828412936

Ништром, Р. (2014). Паттерны программирования игр. США: Женевер Беннинг

67
68 Источники

Panic. (2020a, Август). Inside playdate [Руководство девайса и Lua SDK].

Panic. (2020b, Август). Inside playdate with [Руководство по SDK на языке C].

Panic. (2021, Июнь). Официальные характеристики оборудования. Взят 06/10/2021, с


https://play.date/#the_specs

@playdate. (2019, Май). Отчёт о первичном приёме Playdate. Взят 11/05/2020, с


https://twitter.com/playdate/status/1131733213083136001?s=20

Septhon, M. (2021a, June). Daily driver: преобразование RGB в 1 бит. Взят 06/16/2021, с
https://blog.gingerbeardman.com/2021/06/05/channelling-rgb-into-1bit/

Septhon, M. (2021b, May). Daily driver: пред-рендеринг рейнджера. Взят 06/16/2021, с


https://blog.gingerbeardman.com/2021/05/18/prerendering-ranger/

SHARP. (n.d.). ЖК-технология Sharp Memory. Взят 11/03/2020, с


https://www.sharpsma.com/sharp-memory-lcd-technology
Список сокращений и аббревиатур
API Интерфейс прикладного программирования.
CAD Системы автоматизированного проектирования.
CPU Центральное процессорное устройство.
ECS Система компонентов сущности.
fps Кадры в секунду.
FSM Состояние конечного автомата.
GUI Графический интерфейс пользователя.
HUD Проекционный дисплей.
ID Идентификатор.
JSON Обозначение объектов JavaScript.
MVP Минимально жизнеспособный продукт.
NES Развлекательная система Nintendo.
OOP Объектно-ориентированное программирование.
QA Гарантия качества.
RPG Ролевая игра.
SDK Комплект разработки программного обеспечения.

69
A. Проведённые эксперименты

A.1. Lua

A.1.1. Hello world

Этот прототип состоит из рисования фонового изображения, спрайта, который можно


перемещать с помощью навигационной панели, и добавления фоновой музыки. Из-за своей
простоты код полностью находится в файле main.lua, который, как и его аналог на языке C,
является обязательным в каждом проекте. Отрисовка спрайта игрока и фонового
изображения выполняется с помощью спрайт-функций Playdate SDK. См. рис. А.1.
Изображение проигрывателя загружается с помощью playdate.graphics.image.new(). Затем
он добавляется в новый спрайт, его ось перемещается из верхнего левого угла в центр и
вызывается функция спрайта add(). Это важный шаг, поскольку он указывает модулю
спрайтов Playdate SDK, что этот спрайт необходимо обновить и отрисовать.
Далее таким же образом загружается фоновое изображение, и в модуле спрайтов
регистрируется функция обратного вызова, чтобы установить его в качестве фона сцены.
Это выполняется путём вызова playdate.graphics.sprite.setBackgroundDrawingCallback().
Функция обратного вызова получает положение и размер спрайта, которые каждый раз
используются для отрисовки только необходимой части фона, что является важной
оптимизацией в играх Playdate.
После этого фоновая музыка загружается с помощью playdate.sound.fileplayer.new() и
вызывается её метод play().
Последняя часть демонстрации — это функция playdate.update(), где ввод обрабатывается
путём вызова playdate.buttonIsPressed() для каждой клавиши D-Pad (крестовины), а игрок
перемещается в направлении нажатой клавиши.

Рис. A.1: Привет, мир! на Lua

71
72 Проведённые эксперименты

A.1.2. Макет Dr. Mario

Это была модификация первого прототипа, но с двумя разными макетами в зависимости от


того, в альбомной или портретной ориентации устройство.
Итак, в функции playdate.update() акселерометр нашего устройства запрашивается с
помощью playdate.readAccelerometer(). Используя приложение тестового ввода,
предустановленное в Playdate, можно увидеть, что значение 1,0 по оси Y акселерометра
соответствует удержанию консоли в ландшафтном режиме, а удержание её в портретном
режиме даёт значение -1,0 по оси X. Используя эти числа в качестве ссылки, фоновое
изображение изменяется в методе обновления, чтобы соответствовать ориентации. См. рис.
А.2.
Как и в предыдущем прототипе, игрок может перемещать спрайт по экрану с добавленной
возможностью поворачивать его с шагом 90° с помощью рукоятки. Эта функция
реализована с помощью обратного вызова playdate.cranked, который срабатывает каждый
раз, когда изменяется угол поворота рукоятки. Внутри этого обратного вызова мы
запрашиваем абсолютный угол рукоятки с помощью playdate.getCrankPosition(), и в
зависимости от того, в каком квадранте окружности она находится в данный момент,
таблетка поворачивается.

Рис. A.2: Макет доктора Марио

A.1.3. Устройте сюрприз

В этой демонстрации был представлен первый тест анимации и продолжено изучение


использования акселерометра для определения ориентации устройства. Когда пользователь
открывает приложение, экран таинственным образом предлагает ему лечь на спину, держа
устройство над головой. При этом воспроизводится анимация мопса, облизывающего
экран, и юмористическая песня.
Улучшением по сравнению с демо-версией «Доктора Марио» является использование
таймера для отмены точечных всплесков акселерометра, из-за которых ориентация мерцала
при небольших движениях. Таким образом, определение ориентации устройства является
стабильным. С каждым из двух состояний (изображение инструкции и анимация собаки)
связан таймер. Когда акселерометр входит в ориентацию состояния, таймер увеличивается
на одну единицу за кадр. Если таймер увеличивается на десять последовательных кадров,
он переходит в новое состояние; в противном случае, если значение акселерометра изме-
А.1. LUA 73

нится до того, как это произойдет, таймер сбрасывается на ноль, избегая мерцания.
Lua Playdate SDK предоставляет некоторые эффекты обработки изображений, которые
можно использовать во время выполнения. В этой демонстрации эффект
playdate.graphics.image:drawBlurred использовался через случайные промежутки времени,
чтобы придать изюминку изображению инструкции.
Анимация была реализована путём инициализации таблицы playdate.graphics.imagetable из
файла .gif и создания из неё playdate.graphics.animation.loop. Эти классы являются
стандартным решением для анимации серии изображений в SDK, с возможностью указать
задержку между кадрами в конструкторе. Анимация обновляется автоматически при
вызове функции draw().
Наконец, были добавлены обложка, звук запуска и анимация. Эти ресурсы отображаются
в меню Playdate при выборе игры, а анимация воспроизводится в полноэкранном режиме
вместе со звуковым эффектом. Эти элементы устанавливаются путём изменения файла
pdxinfo в корне каждого проекта. В нём в поле imagePath необходимо указать папку внутри
исходного каталога Source, в которой хранятся ресурсы для меню. Внутри этого пути папка
с именем launchImages содержит кадры анимации запуска, названные по номеру кадра,
начиная с «1.png». Другое поле в файле pdxinfo, называемое launchSoundPath, хранит путь
из исходной папки Source к пользовательскому звуковому эффекту запуска.
Скриншоты на рис. А.3

(a) Любая ориентация (б) Устройство обращено к земле

Рис. A.3: Сюрприз в лежачем положении

A.1.4. Наклонная мини-игра

Коллега-разработчик Playdate Ник Манье предложил в сообщении на форуме идею


создания краудсорсинговой коллекции микроигр в стиле серии WarioWare от Nintendo. Он
разработал структуру, охватывающую микроигры, предоставляющую таймеры, случайный
выбор микроигр и состояния выигрыша/проигрыша. Для этого я разработал простую игру,
в которой вы должны провести коробку до последнего этажа, наклоняя консоль из стороны
в сторону. Проломы в полу генерируются случайным образом в одной из четырех позиций,
никогда не повторяясь, поэтому ящик проваливается только по одному. Полы нарисованы
с использованием примитивов, а простую физику я реализовал с помощью уравнений
прямолинейного ускоренного движения.
Для создания дырок на каждом этаже экран разделен на пять столбцов. Массив чисел от
одного до пяти в начале игры перемешивается, и каждому этажу присваивается элемент из
него. Если этаж вытягивает цифру 1, ее пробел будет расположен в первом столбце экрана
и так далее. Таким образом, никакие зазоры не могут находиться поверх других, что
гарантирует, что ящик провалится только через один этаж за раз.
74 Проведённые эксперименты

Рис. A.4: Микроигра с наклоном

В каждом кадре фреймворк вызывает функцию update() микроигры, которая разделена на


три части: во-первых, метод проверяет расстояние от коробки до щели на её полу, и
находится ли оно ниже небольшого порога (5 пикселей), он перемещает коробку на
следующий этаж. Затем вызывается функция physics_update(), которая обновляет расчёты
движения коробки. Наконец, функция render() рисует все элементы на экране.
Функция render() рисует спрайт земли для каждого столбца без зазора и двух
вертикальных линий по бокам зазора, чтобы закрыть пол, поскольку спрайт создан для
плавного горизонтального соединения. Он также рисует ящик в его текущем положении.
Что касается физики, в верхней части программы были созданы три глобальные
переменные: масса — значение, используемое в качестве массы ящика в физических
расчётах (установлено в 1); гравитация, которая представляет ускорение силы тяжести
(установлено на 98); и сила, которая рассчитывается по формуле А.1а и представляет собой
полную силу, действующую на ящик. Функция physics_update() начинается с запроса
акселерометра Playdate с помощью playdate.readAccelerometer(). Эта функция возвращает
значения x, y и z от -1 до 1, которые являются компонентами единичного вектора ускорения
консоли на этой оси. Значение по оси x умножается на переменную силы, получая
горизонтальную величину силы. Затем ускорение ящика рассчитывается по формуле A.1б,
его скорость — по формуле A.1в *1 и, наконец, положение ящика рассчитывается по
формуле A.1г. Последнее значение представляет собой множитель для приблизительной
настройки масштаба моделирования, его значение выбирается в результате тестирования и
настройки.

force = mass × gravity (A.1a)


acceleration = forcex ÷ mass (A.1б)
speed = acceleration × deltaTime (A.1в)
positionx = positionx + speed × deltaTime × 200 (A.1г)

A.1.5. Ритмическая игра

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

*1. deltaTime — время, прошедшее с момента последнего вызова обновления.


А.2. C 75

В этом проекте представлена наша первая реализация на Lua конечного автомата игры. В
нём состояния игры должны иметь одну функцию для обновления логики, другую для
рендеринга и дополнительную функцию инициализации, которая будет вызываться при
переходе в это состояние. Затем в таблице GameManager сохраняются ссылки на функции
активного состояния, которые затем используются для независимого вызова функций
обновления и рендеринга из основного цикла приложения. См. рисунки A.5в, A.5a и A.5б.
Поскольку этот тип игры требует точных движений и восприятия, целью оптимизации
было достижение скорости 50 кадров в секунду. Учитывая это, подход к рендерингу был
разделен на два этапа: во-первых, отрисовка всех элементов в том виде, в каком они были
на предыдущем кадре, с инвертированными цветами для выборочной очистки экрана, и, во-
вторых, отрисовка текущего кадра. Как упоминалось в главе 3.1.1, этот тип рендеринга на
основе областей рекомендуется для приложений Playdate вместо полноэкранного подхода,
что позволяет нам достичь более высокой частоты кадров и продлить срок службы батареи
консоли.
Чтобы ритм-игра приносила удовольствие, действие должно быть точно синхро-
низировано с сопровождающей её музыкой. Как правило, этого лучше достичь с помощью
контента, созданного вручную, поэтому было важно иметь возможность легко написать
сценарий, о том в какое время и под каким углом «заметки» будут иметь влияние.
Выбранный путь заключался в использовании Audacity*2, бесплатного звукового редактора
с открытым исходным кодом, в качестве графического интерфейса пользователя (GUI).
Таким образом, дорожка меток может использоваться для представления воздействий нот,
определяя угол в качестве текста метки, а также использовать представление формы волны
и инструмент меток регулярных интервалов для синхронизации их с музыкальной
дорожкой*3 (см. рисунок A.5д). Наконец, был написан простой анализатор текста для
преобразования меток, экспортированных из проекта Audacity, в их игровое представление.
Файлы, экспортированные из Audacity, структурированы следующим образом: для
каждого тега указано время их начала, время окончания, их текст и символ новой строки.
Обратите внимание, что теги в Audacity могут иметь длительность, действуя как маркер
региона, хотя для наших целей эта функция не используется. Анализатор открывает файл с
помощью playdate.file.open(), а затем разделяет каждую строку, используя пробел в
качестве разделителя, сохраняя начальное значение как временную метку новой заметки и
текстовое значение как её угол.
Чтобы это приложение выглядело как родное, были использованы настраиваемые поля
системного меню для выхода в главное меню из песни, а также оповещение о включении
по умолчанию, если оно было убрано во время игры. В какой-то момент была добавлена
частичная поддержка перевёрнутой ориентации системы — экспериментальная функция
для игроков-левшей, но в итоге от неё отказались.

A.2. C

A.2.1. Hello World

После настройки среды разработки C, как описано в главе 5, я модифицировал пример


проекта C, который рисует по экрану прыгающий текст «Hello World». В своей версии я
добавил фоновое изображение и изменил рендеринг, рисуя только ту часть изображения,

*2. Официальный вебсайт: https://www.audacityteam.org


*3. Из руководства Audacity: https://manual.audacityteam.org/man/regular_interval_labels.html
76 Проведённые эксперименты

(a) Состояние меню (б) Состояние в игре

(в) Конечный автомат игры

(г) Оптимизированный рендеринг (отрисо-


вываются только выделенные области)

(д) Создание шаблона заметок в Audacity

Рис. А.5: Ритмическая игра


А.2. C 77

которая находилась под текстом в каждом кадре. Я также использовал для текста режим
рисования NXOR, чтобы выделить его на фоне. См. скриншот A.6.

Рис. А.6: Hello World на C

A.2.2. Упрощённый ECS Starfield эффект

Обычно игры, построенные на основе ООП, используют наследование для специализации


общих классов, таких как «Актёр» или «Враг», например, такие как определённые враги,
предметы или игровые персонажи. Таким образом, верхние классы содержат переменные и
методы, общие для всех производных классов, что позволяет обобщать такие методы, как
рендеринг или физика. Подход ECS, напротив, представляет собой архитектурный шаблон,
в котором вместо наследования используется композиция. Это означает, что сущности не
содержат переменных напрямую; вместо этого они представляют собой просто
идентификаторы, связанные с компонентами, которые представляют собой группы
связанных данных, таких как переменные физики, здоровья или преобразования. Затем
логика игры реализуется с помощью систем, которые представляют собой функции,
принимающие на вход один или несколько типов компонентов.
Целью этого проекта была реализация простой архитектуры ECS, которая могла бы
послужить основой для будущих игр на языке C. С этой целью я проследил за серией
обучающих прямых трансляций Durán (2020), изначально созданных для Amstrad CPC Z80,
и адаптировал их для консоли Playdate. Помимо создания игрового движка, в этой серии
видеороликов рассказывается, как создать эффект звёздного поля, который состоит из
частиц, движущихся справа налево с разной скоростью, чтобы создать ощущение глубины.
Я ещё больше улучшил этот эффект, привязав размер частиц к их скорости, усилив
ощущение приближения быстро движущихся частиц к камере.
Полученная архитектура не является полноценной ECS, поскольку компоненты связаны с
сущностями. Это означает, что с каждой сущностью связан компонент каждого типа. Тем
не менее, это полезная отправная точка для разработки игр на языке C и хороший трамплин
для будущих проектов движка ECS. См. рис. 6.3.

A.2.3. Полный ECS Starfield эффект

После последнего проекта я решил внедрить полноценный движок ECS и посмотреть,


приведет ли это к повышению производительности. Компоненты теперь были отделены от
сущностей, специализированы по типам и соответственно хранятся в массивах. Поскольку
C не является языком ООП, я эмулировал полиморфизм, определив структуру C для
каждого типа компонента, а затем общую структуру Component как объединение C всех
76 Проведённые эксперименты

возможных компонентов вместе с идентификатором типа. Объединение в C — это тип


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

Разделение компонентов на массивы одного типа позволяет обновлять системы путём


итерации этих массивов компонентов вместо сущностей, что обычно улучшает
кэширование ЦП. Это улучшение будет происходить за счёт того, что данные, к которым
осуществляется доступ, будут последовательно располагаться в памяти, что позволит ЦП
загружать эту часть памяти в кэш с гораздо более быстрым доступом к памяти.
У сущностей теперь есть массив указателей на их компоненты, так что одна система может
воздействовать более чем на один компонент сущности. Поток будет следующим: система
вызывается для каждого типа компонента, она обращается к родительской сущности из
А.3. C++ 79

этого компонента и ищет остальные необходимые компоненты в этой сущности.

К сожалению, после тестирования полученного эффекта звёздного поля Starfield, частота


кадров снизилась примерно на 25%, снизившись со значений в среднем 43 кадров в секунду
в упрощённой версии ECS, до в среднем 12 кадров в секунду.

A.3. C++

A.3.1. Hello World

То же, что и A.2.1, но реализовано на C++. Целью этого эксперимента было запуск кода
C++ на Playdate, поскольку этот язык не является официально поддерживаемым, но мне
нужен был ООП-подход. Используя настройки C и проверив пример проекта C++,
включённый в SDK, я изменил конфигурации CMake, успешно скомпилировал и запустил
демо-версию.

A.4. Pulp

A.4.1. Приключенческая игра

Стремясь открыть разработку игр для новичков, разработчик Panic создал веб-инструмент
под названием Pulp для создания игр для Playdate. Pulp позволяет пользователям любого
уровня навыков быстро создавать простые игры на основе плиток в стиле RPG.
Заинтересовавшись охватить все возможности разработки Playdate в этой бакалаврской
диссертации, я принял участие в предварительном просмотре бета-версии Pulp и в качестве
теста создал небольшую игру. См. рис. А.7.

(a) Система диалогов (б) Тайловая карта

Рис. A.7: Криминальная приключенческая игра


B. Отчеты об ошибках
Получить новую игровую систему до её выпуска для широкой публики — прекрасная
возможность, но следует ожидать некоторых недочётов в прошивке, SDK и инструментах
разработки. Одна из основных целей Playdate Developers Preview заключалась в том, чтобы
разработчики выявляли баги и ошибки до запуска, и чтобы команда Playdate в Panic могла
их вовремя исправить.
Для этой цели Panic размещает на своём сервере GitLab*1 систему отслеживания проблем,
где пользователи могут сообщать об ошибках или запрашивать новые функции.
При разработке дипломной работы были обнаружены следующие ошибки:

B.1. Ошибка пропуска JSON

Playdate C SDK реализует собственный анализатор JSON для чтения и записи файлов,
написанных на этом языке разметки. В документации к этой функции упоминается
возможность пропускать пары ключ-значение JSON по отдельности вместо их анализа.
При написании кода загрузки тайловых карт в главе 6.4 я пытался использовать эту
возможность, чтобы ускорить чтение файлов карт за счёт отказа от обработки ненужных
полей JSON; но затем каждый раз, когда он пытался прочитать файл, программа вылетала
с ожидаемой ошибкой decode_table expected ','.
Из-за отсутствия отладки Playdate игр на C в Windows, источник ошибки было трудно
отследить; файл JSON был правильным, и запятая не пропущена, поэтому сообщение об
ошибке не помогло. Через некоторое время стало очевидно, что парсер JSON
преждевременно прерывает чтение файла и приводит к сбою всего приложения.
Для проверки правильности этого предположения была разработана упрощённая
демонстрация, в которой приложение пыталось открыть файл, прочитать его содержимое и
вывести его на консоль отладки. Дальнейший анализ этой демонстрации доказал, что
теория верна, и поэтому в трекере проблем Playdate GitLab была зарегистрирована ошибка.
Вот полный отчет об ошибке:

B.1.1. Ошибка при пропуске пары JSON в shouldDecodeTableValueForKey()

B.1.1.1. Настройка
 Версия — обнаружена в 0.11.1, присутствует в 0.12.0.
 OС — Windows

B.1.1.2. Шаги

Простой исходный код для репликации этой ошибки включен в BugJSON.zip*2.

*1. GitLab — онлайн-платформа для управления проектами и исходным кодом Git https://about.gitlab.com/
*2. </uploads/1351873859769340764f58bb4a29d5c9/BugJSON.zip>

81
82 Отчёты об ошибках

Согласно документации, возврат 0 в методе shouldDecodeTableValueForKey () пропускает


текущую пару ключ-значение, но это приводит к появлению ошибки и останавливает
json_decoder от дальнейшего анализа файла.

Это было протестировано с файлами JSON, сгенерированными Tiled, а также с простыми


рукописными файлами. Изменение окончаний строк Windows и Unix не влияет на
результат.

B.1.1.3. Ожидаемые результаты

При возврате 0 из метода shouldDecodeTableValueForKey() эта пара ключ-значение будет


пропущена, и json_decoder продолжит анализ JSON.

B.1.1.4. Фактические результаты

В строке, которую нужно было пропустить (согласно параметру linenum в decodeError()),


возврат 0 приводит к ожидаемой ошибке decode_table ','.

B.1.1.5. Частота

 Постоянная

B.1.1.6. Строгость

 Незначительная

B.1.1.7. Обходной путь

Не пропускать строки и просто игнорировать те ключи, которые не нужны.

B.1.2. Вывод

После публикации отчёта, член команды Playdate подробно остановился на этом вопросе,
предоставив ещё один пример кода, и проблема была решена в выпуске Playdate 1.0.0 SDK.
В то же время был добавлен рабочий пример использования парсера C JSON и
дополнительная документация по этой функции.

B.2. Ошибка обрезки прямоугольника

На той же неделе произошла предыдущая ошибка, ещё во время главы 6.4, был выпущен
Playdate 12.0.0 SDK и обновление прошивки. Было важно принять эту версию, поскольку
В.2. Ошибка обрезки прямоугольника 83

она изменила способ рисования графики и растровых изображений из C SDK, поэтому пре-
дыдущие функции рисования устарели и не работали в последней версии прошивки (1.0.0).
К сожалению, в этом обновлении появилась ошибка в создании прямоугольных сечений.
Это нарушило отрисовку тайловых карт, что ещё больше замедлило ход выполнения этой
итерации.
Прямоугольные сечения используются в моей игре для выбора части листа плитки, соот-
ветствующей рисуемой плитке. После некоторого тестирования стало ясно, что положение
тайла влияет на размер обрезки. Значение координаты x добавлялось к ширине
прямоугольного обрезка, и то же самое происходило с координатой y и высотой. К счастью,
оказалось, что это можно легко обойти: вычесть положение прямоугольного сечения из его
масштаба.
Для наглядной демонстрации этого эффекта я разработал небольшое тестовое приложение
(рис. В.1), сравнивающее результаты рисования обрезанного изображения с устаревшими
функциями, новыми сломанными функциями и обходным путём. Он состоит из
полноэкранного изображения с прямоугольным обрезком, который перемещается по
экрану, демонстрируя, как его положение влияет на его размеры. Был дополнительный
пошаговый режим, чтобы было легче увидеть эту зависимость.

(a) Ожидаемый результат (b) Ошибка выдает неправильные размеры

Рис. B.1: Демо-проект для устранения ошибки с отсечением прямоугольника

Полный отчёт об ошибке, опубликованный на GitLab:

B.2.1. Обрезка ширины/высоты прямоугольника исходя из положения

B.2.1.1. Настройка

 Версия — 12.0.0
 ОС — Windows

B.2.1.2. Шаги

При использовании playdate->graphics->setClipRect(x, y, width, height) ширина и высота


ClipRect увеличиваются на x и y соответственно. Этого не происходило в предыдущих
версиях SDK или со старыми функциями рисования.

B.2.1.3. Ожидаемые результаты

playdate->graphics->setClipRect(x, y, width, height) должен установить ClipRect (ширину,


высоту) размеров.
84 Отчёты об ошибках

B.2.1.4. Фактические результаты

ClipRect в размерах (ширина + x, высота + y).

B.2.1.5. Частота

 Повторяющаяся
 Постоянная

B.2.1.6. Строгость

 Высокая

B.2.1.7. Обходной путь

Вычитание позиции x из параметра ширины и позиции y из параметра высоты.


c playdate->graphics->setClip Rect(x, y, ширина - x, высота - y);

B.2.2. Вывод

На этот отчёт об ошибке не было сделано ни одной рецензии. Та же ошибка была поднята
в разговоре на официальном сервере Playdate Discord другим разработчиком, и было
выпущено исправление для версии SDK 1.0.0.
C. Программа Tiled
Как следует из описания на веб-сайте, «Tiled — это редактор тайловых карт общего
назначения для всех тайловых игр, таких как ролевые игры, платформеры или клоны
Breakout» (Lindeijer, 2019). Это бесплатная программа с открытым исходным кодом,
позволяющая сохранять и загружать карты тайлов в формате JSON. В этом проекте в
качестве редактора уровней будет использоваться Tiled.

Рис. C.1: Интерфейс программы Tiled

85
D. Простой конечный автомат

Минималистичный конечный автомат, применённый в TinySeconds, использует


следующий код:

Переход в другое состояние игры осуществляется путём присвоения другого значения


переменной currentState внутри методов обновления.

87
(гугл перевод от С.Айрата)

Вам также может понравиться