Программирование видеоигры Сапёр на C++

Введение в видеоигру «Сапёр» #

Принципы игры «Сапёр» #

«Сапёр» — это логическая головоломка, где есть интерактивная решётка из клеток, некоторые из которых мины, при нажатие на одну из клеток,

происходит её открытие. Клетка может быть:

1) закрытой(такие все клетки в начале игры);

2) пронумерованной, цифрами от 1 до 8, где цифра означает количество мин около данной клетки;

3) пустыми клетками (вокруг их мин нет);

4) помеченные флажком(перестают открываться на левый клик мыши).

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

В игре «Сапёр» обычно три уровня сложности:

1) простой, с размером поля 8 на 8 клеток с 10 минами;

2) средний, с размером поля 16 на 16 клеток с 40 минами;

3) трудный, с размером поля 30 на 16 клеток с 99 минами.

Уровни сложности отличаются только размерами полей и количеством мин.

История игры «Сапёр» #

Кто первый придумал игровой процесс игры «Сапёр» доподлинно неизвестно, но известно что известная нам игра появилась в наборе

«Microsoft Entertaiment Pack» 1992 года выпуска. Игра была предназначена как инструмент обучению использованию манипулятора

«Мышь» в игровой форме и была разработана Робертом Доннером и Куртом Джонсоном.

Выбор базы для разработки игры #

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

Среди движков для выбора был Godot, Unity, Unreal Engine, Cocos. Unity

и Unreal Engine были отброшены сразу из-за больших вычислительных требований для системы разработки. Cocos работает на TypeScript,

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

Поэтому игра будет писаться на GDExtension.

Но сначала стоит написать прототип игры, который полностью отладить, а потом уже переносить на C++.

НАПИСАНИЕ ВИДЕОИГРЫ #

Программирование рабочего прототипа #

Начать стоит с прототипа, в котором реализовать основные алгоритмы полной игры. Для прототипа я выбрал язык Python. Проговорю основные моменты. Начал с объявления класса Cell (рисунок 1), который представляет собой клетку и имеет все свойства нужные для её

функционирования на поле.

Рисунок 1 --- Класс «Cell»

Затем объявил одномерный массив клеток с названием field и инициализировал его при помощи цикла for и двух переменных width и height.

Следующим было написание функции которая переводит координаты x, y в индекс массива и функции, делающей всё в точности, да наоборот (рисунок 2).

Рисунок 2 --- Вспомогательные функции

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

Рисунок 3 --- Функция подготавливающая поле для игры

И теперь самая важная и сложная функция для нашей игры открытие клетки. В игре «Сапёр» если клетка, на которую нажал игрок, имеет число мин вокруг равное нулю, то открываются все пустые клетки соединённые вместе и клетки их окружающие, для этого был сделан простой алгоритм, показанный на рисунке 4. Рисунок 4 --- Алгоритм открытия соединённых пустых клеток

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

то она открывается и пропускается, убирается из массива, в противном случае — открывается и удаляется из массива.

Теперь реализуем простой алгоритм открытия клетки, если она вокруг нее есть мина (рисунок 5).

Рисунок 5 --- Алгоритм открытия клетки около которой есть мины

Полную функцию смотрите на рисунке 6.

Рисунок 6 --- Код полной функции открытия клетки

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

https://gist.github.com/Foxelyss/b76a759bd6f4f0e6f0f6f8b092641219.

Написание основы видеоигры #

Перевод игры на GDExtension и C++ #

Для начала переведём клетку в структуру и все функции из прототипа в класс GDExtension.

Первым нужно портировать наш прототип. Весь функционал прототипа,

кроме вывода в консоль будет находиться в классе Field. Класс,

хранящий в прототипе состояние клетки, под названием Cell станет структурой, потому что не будет содержать никаких методов и будет хранить только данные. Godot Engine предоставляет свои методы для генерации случайных чисел поэтому вместо random мы будем использовать класс RandomNumberGenerator. Вместо использования остатка от деления будет использована метод класса RandomNumberGenerator под названием

randi_range. Метод открытия клетки, принимавший координаты, был переработан для приёма индекса в массив-поле клеток. Были добавлены методы set и несколько методов get для получения свойств класса Field,

методы очищения поля и генерации новой задачи для решения.

Настройка и выбор узлов для реализации игрового процесса #

Для поля будем использовать узел GridContainer, а для клеток —

Button. Затем реализуем вспомогательный класс FieldGrid, который будет отвечать за создание поля клеток, проигрыш и выигрыш, за подсчёт времени. Он будет «фасадом» над нашим классом Field. Класс FieldGrid

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

Итоговое строение сцены показано на рисунке 7.

Рисунок 7 --- Итоговое строение сцены

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

Создание текстур для клеток #

Давайте создадим текстуры для каждого состояния клетки. Для этого будем использовать программу Inkscape. Каждую текстуру расположим на отдельном листе, начиная с пустой клетки с фиолетовым фоном, продолжая на синем фоне: клетки с цифрами от 1 до 9, иконками флажка и мины и текстурой не открытой клетки (рисунок 8).

Рисунок 8 --- Текстуры клеток

Повторим ещё раз, но с другим фоном (рисунок 9).

Рисунок 9 --- Добавленные текстуры клеток в другом цвете

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

Затем напишем в FieldGrid алгоритм загрузки текстур. После этого сделаем алгоритм создающий кнопки в данном полем количестве и реализуем метод update_grid, который будет обновлять состояние поля.

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

Реализация главного меню #

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

(рисунок 10).

Рисунок 10 --- Сцена главного меню

Код для главного меню считывает какая кнопка нажата и выполняет переход к сцене с основной игрой. Текст который показывается в текстовых полях не строго записан в программу. Он разный для разных языков и читается из файла «translations.csv». Таким образом не надо менять код, чтоб заменить текст где-либо.

Получения обратной связи #

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

Рисунок 11 --- первая итерация игры «Сапёр»

Совершенствование игры #

Оптимизация игры. #

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

В результате оптимизации были починены некоторые недочёты в игровом процессе. К примеру вместо полного очищения динамического массива

(рисунок 13 и 14) происходит обнуление каждого элемента (рисунок 12),

что быстрее использования системных вызовов для освобождения памяти, а затем для создания поля обратно.

Рисунок 12 --- Оптимизированный метод clear

Рисунок 13 --- Неоптимизированный участок программы

Рисунок 14 — Метод, вызывающийся после неоптимизированного участка программы

Унификация управления #

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

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

(рисунок 15).

Рисунок 15 --- Пример унификации управления

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

Это было контринтуитивно для игрока, который не разу не использовал профессиональное ПО для 3D моделирования или инженерных задач,

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

Добавление экрана рекордов #

Для добавления экрана рекордов были реализованы методы загрузки и выгрузки статистики в формате JSON. Для всех трёх категорий сложности было отведено по массиву чисел — количеству секунд. Код для создания первоначального файла рекордов расположен на рисунке 16, для загрузки и сохранения рекордов на рисунке 17 и 18 соответственно.

Рисунок 16 --- Метод создания файла статистики

Рисунок 17 --- Метод записи рекорда в файл

Рисунок 18 --- Метод показа экрана рекордов

Рисунок 19 --- Краткая структура экрана рекордов

Рисунок 20 --- Экран рекордов в редакторе Godot

Engine

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

Popup было реализовано меню отображения рекордов, где для каждой категории отведена отдельная вкладка, которые, благодаря узлу

TabContainer, организованы в вкладки (рисунок 20).

Стилизация #

Рассчитав силы, за стиль были выбраны простые фигуры в сочетании с яркими цветами. К текстовым полям в игре были применены синие фоны и скошенные, как у параллелепипеда, края, в соответствии с рисунком 21.

Это помогло игре точно соблюдать стиль. В качестве шрифта в игре был выбран IBM Plex Sans.

Рисунок 21 --- Новые стили пользовательского интерфейса

Создание финальной сцены #

Для создание финальной сцены был использован Blender. Финальная сцена состоит из плавающих в пространстве 3D моделей некоторых игровых элементов. На заднем плане простой движущийся орнамент (рисунок 22),

разработанный в Aseprite.

Рисунок 22 --- Движущийся в игре орнамент

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

AnimationPlayer.

Анимирование игрового процесса #

Была сделана анимация проигрыша — тряска пользовательского интерфейса. При открывании клетки теперь включается анимация чем-то похожая на лопание шарика (рисунок 23), разработанная тоже в программе Inkscape. Рисунок 23 --- Анимация лопания клетки

Усложнение игры #

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

Добавление звукового сопровождения #

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

«Googie — Imagenary Numbers (REMIX)» от Rolobi.

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

Ещё один AudioStreamPlayer в связке с AnimationPlayer был использован для проигрывания эффектов открытия клетки, проигрыша и выигрыша.

Мелкие правки #

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

Поэтому был написан алгоритм для форматирования времени игры (рисунок 24).

Рисунок 24 --- Метод форматирования времени

Итогом мелких и больших правок стала релизная версия игры «Сапёр»,

исходный код которой доступен на GitHub

https://github.com/Foxelyss/minesweeper. Игровой процесс приведён на рисунке 25.

Рисунок 25 --- Игровой процесс разработанной игры

«Сапёр»

РЕКОМЕНДАЦИИ ПО СОЗДАНИЮ ВИДЕОИГРЫ #

Действия при написании видеоигры:

1) Если это ваша первая видеоигра, то проект должен быть репликой чего-то простого и популярного.

2) Расписывайте самую главную часть игры — игровой процесс, а именно основные механики досконально. Основные механики в видеоигре должны быть весёлыми и интересными. Описывайте все их детали, чтоб потом не сидеть и не думать над их реализацией.

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

4) Дорабатывайте игру и давайте её на тест. Тестирование на реальных игроках всегда важно не только для ловли ошибок, но и получения важных отзывов. В игру будут играть люди, а не один разработчик!

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

ЗАКЛЮЧЕНИЕ #

В итоге проектной деятельности была разработана видеоигра «Сапёр» на языке C++, используя видеоигровой движок Godot Engine. При разработке игры было столкнуто со огромным количеством проблем, особенно проблем с доступом к памяти. В ходе работы, чем код становился комплекснее и связаннее, приходилось больше и больше думать наперёд, для того чтобы он оставался поддерживаемым. Были выяснены и починены многие изъяны игры, просмотрен процесс создания игры изнутри. И выяснено: гипотеза неверна. Программирование видеоигр не простой процесс, а комплексный,

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

СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ #

1) Как студент-интерн создал самую популярную в мире видеоигру, или история игр Windows. — Текст : электронный // Jimmy Maher : [сайт]. — URL: https://habr.com/ru/articles/428091/ (дата обращения 30.01.2024).

2) Cocos Creator. — Текст : электронный // Cocos : [сайт]. — URL: https://docs.cocos.com/creator/manual/en/getting-started/helloworld/ (дата обращения 30.01.2024).

3) Godot Docs. — Текст : электронный // Juan Linietsky, Ariel Manzur and the Godot community : [сайт]. — URL: https://docs.godotengine.org/en/stable/ (дата обращения 30.01.2024).

4) Godot Recipes. — Текст : электронный // KidsCanCode: [сайт]. — URL: https://kidscancode.org/godot_recipes/4.x/ (дата обращения 30.01.2024).

5) Minesweeper (video game). — Текст : электронный // Wikipedia : [сайт]. — URL: https://en.wikipedia.org/wiki/Minesweeper_(video_game) (дата обращения 30.01.2024).

6) Unity Manual. — Текст : электронный // Unity Technologies : [сайт]. — URL: https://docs.unity3d.com/Manual/index.html (дата обращения 30.01.2024).

7) Unreal Engine 5.3 Documentation. — Текст : электронный // Epic Games Inc. : [сайт]. — URL: https://docs.unrealengine.com/5.3/en-US/ (дата обращения 30.01.2024).