«Сапёр» на движке Doom
Мое увлечение разработкой игр началось с создания карт для Doom и Heretic. В то время я ничего не смыслил в программировании, поэтому просто рисовал уровни и раскидывал по ним несопоставимое количество оружия и монстров. Выходило сносно, но некоторые карты было очень сложно пройти.
В этой заметке я расскажу как создать игру «Сапёр» на движке первых частей Doom (id Tech 1), а точнее — на модифицированной для порта GZDoom версии движка.
Инструментарий
- GZDoom. Этот порт предназначен для запуска всех игр, написанных на id Tech 1 (Doom, Doom 2, Heretic: Shadow of the Serpent Riders, HeXen: Beyond Heretic и Strife: Quest of the Sigil). Поддерживает много функций, недоступных в «ванильной» версии.
- GZDoom Builder. Модификация классического редактора уровней для игр, написанных на id Tech 1 — Doom Builder. Поддерживает все возможности GZDoom, в том числе динамическое освещение, наклонные поверхности и 3D полы.
- Slade3. Программа для распаковки и просмотра игровых архивов с расширением WAD (и не только). Помимо прочего может выступать в качестве редактора уровней, графики и текста.
- ZDoom Wiki. На этом сайте можно найти всю необходимую документацию.
- Любой графический редактор. Я использовал свой любимый Aseprite.
Графика
Изначально я хотел реализовать «Сапер» на основе уже имеющейся в Doom графики. Создал игровое поле и раскрасил ячейки в разные цвета, но потом решил все сделать по правилам.
Нам потребуется тайл с бомбой, тайл с флагом, два пустых тайла (для разных положений кнопки) и восемь тайлов с цифрами.
Подумав, я нарисовал еще 12 кастомных тайлов для органов управления «Сапером». Эти кнопки будут переключателями, поэтому потребовалось нарисовать по два тайла на одну кнопку.
Создание карты
Открываем GZDoom Builder и создаем новую карту. В зависимости от игры, на основе которой будет создаваться модификация, выбираем нужную конфигурацию. Советую выбирать формат UDMF, так как он поддерживает все функции GZDoom.
Далее нужно выбрать базовый WAD файл. Так как модификация создается на основе Doom 2, то выбираем DOOM2.WAD.
Код будем писать на ACS — скриптовом языке, предназначенном для управления логикой происходящего на уровне.
Создание уровня
Не буду рассказывать подробно о процессе создания карт, ведь об этом уже написал товарищ Теплый Рыс:
Скажу только, что нам понадобится один большой сектор для игрового поля. Этот сектор надо разбить на 100 секторов поменьше (игровое поле 10х10).
Каждому маленькому сектору надо присвоить уникальный тэг (от 1 до 100). Это понадобится для управления игровым полем.
Далее надо нарисовать сектор, прилегающий к основному, и немного поднять пол. Этот сектор будет играть роль «командного пункта». Здесь же создаём 12 маленьких секторов, из которых получится 6 органов управления «Сапером».
Импорт графики и создание переключателей
Подробно эту тему разобрал Теплый Рыс в свежей статье:
Для импорта графики понадобится Slade3. Открываем WAD архив с единственной картой. Внутри будут лежать несколько файлов, в том числе файл, содержащий данные карты. Переносим в архив новые тайлы в любом формате (я использовал png).
В оригинальной игре название каждого графического файла состояло из восьми символов, поэтому я постарался придерживаться этого правила, особенно для текстур переключателей.
Далее надо сконвертировать новую графику в формат Doom, иначе текстуры будут выглядеть как каша из пикселей. Для этого выделяем все графические файлы, нажимаем Convert to, ищем нужный формат и конвертируем. Скорее всего в окне предпросмотра все изображения будут черно-белыми, но это нормально.
После этого нужно сформировать текстовый файл, который называется TEXTURES и содержит параметры каждого нового тайла. Выделяем все файлы, нажимаем Add to TEXTUREx и выбираем TEXTURES (формат, который поддерживает GZDoom).
Позднее я узнал, что если использовать формат png и модифицировать игру для GZDoom, то текстуры конвертировать не обязательно. При этом обязательно нужно добавить их в TEXTURES.
Новую графику можно использовать в игре.
Переходим к созданию переключателей.
Конфигурация переключателей находится в файле ANIMDEFS. Создаём новый файл в архиве, переименовываем его и пишем:
Каждая строка содержит информацию о названии двух текстур переключателя и длительности их задержки на экране. С помощью файла ANIMDEFS можно создавать более сложные анимационные переключатели, а также любые последовательности анимации для текстур стен, пола и потолка.
Осталось применить новые текстуры в редакторе уровней.
Пишем код. Много кода
Немного об ACS
ACS — скриптовый язык программирования. Писать код на нем можно не выходя из GZDoom Builder, а встроенная среда разработки даже будет подсказывать названия функций, ключевых слов и выражений. Об отладке, конечно, приходится мечтать, но все решается обычной функцией Print().
Язык очень похож на C/C++, но с ограниченным функционалом. Я никогда не писал на этих языках, но синтаксис всех Си-подобных языков примерно одинаковый, поэтому особых проблем не возникло (кроме нежелания ставить кавычку после каждой строки). Тем не менее обращаться к ZDoom Wiki пришлось чуть ли не каждые 5 минут.
Глобальные переменные и константы
Первым делом нужно создать игровое поле, то есть двухмерный массив размером 10х10 элементов. Таких массивов нам понадобится два: первый — для определения положения мин, пустых ячеек и ячеек с цифрами, а второй — для определения состояния каждой ячейки (открыта/закрыта/поставлен флаг).
Константы определяются с помощью ключевой фразы define, а не const.
Помимо этого нужно определить:
- Переменные указателей на ячейку игрового поля по X и Y (а также в одномерном массиве, что потребуется для активации необходимых тэгов).
- Переменную, содержащую информацию о максимальном количестве мин.
- Константы направлений.
- Массив с наименованиями текстур с цифрами (чтобы не писать лишний код и не обращаться постоянно к разным константам).
- Константы с наименованиями прочих текстур.
Размещение мин и цифр с их количеством
Мины будут размещаться в случайном порядке равномерно по площади всего поля. Для этого надо создать скрипт, который до тех пор, пока не будут размещены все мины, отмечает случайную точку на поле и помещает туда мину, если эта точка еще не занята.
Пара слов про скрипты и функции.
Скрипты в ACS привязываются к какому-либо объекту на уровне и запускаются в момент совершения какого-либо действия. Вышеуказанный скрипт имеет тип (void). Этот скрипт можно привязать к линии или предмету на карте и определить условия его запуска. Существуют и другие типы скриптов. Например, скрипт с типом OPEN активируется при первой загрузке уровня.
Функции нельзя привязать к объекту на карте. Это вспомогательный элемент, который работает как обычная функция в любом языке программирования, но с некоторыми ограничениями. Функции могут возвращать какие-либо значения.
Далее надо поместить в ячейки, вокруг которых есть какие-либо мины, количество этих мин. Пишем функцию:
Ее надо вызвать из предыдущего скрипта. Функция проверяет каждую пустую ячейку на карте, а потом — восемь ячеек вокруг изначальной. Если в этих ячейках есть мины, то функция считает их количество и сохраняет в первоначальном массиве.
Для того, чтобы поле было сформировано в игре, привязываем первый скрипт к какой-либо линии на уровне. Я привязал ее к линии перед «командным центром» и установил, что скрипт активируется при пересечении этой линии игроком (When player walks over).
Выбор ячейки игрового поля
Я долго думал, как отмечать выбранный сектор игрового поля. Остановился на варианте с изменением освещения сектора. При выборе сектора его освещенность вырастает со 168 до 256, и наоборот.
Чтобы была возможность перемещать указатель на ячейку игрового поля, я написал такой скрипт:
Здесь все просто. Этот скрипт присоединяется ко всем кнопкам управления с помощью команды Script Execute с указанием аргумента, который отвечает за направление. Помимо этого надо указать, что скрипт будет срабатывать при нажатии игроком на кнопку (When player presses use), а также то, что действие можно производить более одного раза (Repeatable action).
Заметили, что скрипт взаимодействует с переменными указателя по X и Y, но при этом свет меняется по тэгу от 1 до 100. Для того, чтобы перевести двухмерные указатели в одномерный, нужно написать следующую функцию:
Эта функция преобразовывает координаты X и Y в одномерную координату по формуле: ширина поля * Y + X.
Размещение флагов и условия победы
Игра будет пройдена, если разместить все флаги на ячейки с минами. Для того, чтобы это сработало, нужно написать такую функцию:
Почему-то я решил поместить эту функцию внутрь скрипта, хотя можно было обойтись одним только скриптом. Видимо, новогодние праздники дали о себе знать.
Эта функция проверяет, открыта ли ячейка игрового поля под указателем (определяется массивом cells_state), а потом проверяет, установлен ли на закрытой ячейке флаг. Если установлен, то убирает его, если нет — ставит.
После каждой установки флага игра проверяет условия победы:
Если каждая ячейка с миной (в массиве bombs) соответствует ячейке с флагом (в массиве cells_state), то игрок побеждает.
Открытие игрового поля
Помимо установки флагов надо и поле открывать. Думаю, что все играли в «Сапер», и объяснять правила не надо.
Следующая функция получилась довольно сложной, и запросила больше всего драгоценного времени.
Разберем по пунктам:
- Если вы попытаетесь открыть ячейку с цифрой, то откроется только эта ячейка.
- Иначе программа проверит все прилегающие ячейки.
- Если прилегающая ячейка закрыта и без флага, то эта ячейка откроется.
- Если прилегающая ячейка пустая, то функция запустится рекурсивно с координатами прилегающей ячейки.
Последние штрихи
Во-первых, нужно написать скрипт, который будет запускаться при старте уровня. В этом скрипте надо лишить игрока всего оружия (зачем оно в «Сапере»?) и поместить указатель на первую ячейку игрового поля.
Во-вторых, нужно создать скрипт, который будет проверять, какую ячейку открыл игрок: с миной или пустую.
Вы можете заметить, что здесь очень много лишних строк. И сейчас я объясню, зачем они нужны.
Этот скрипт нужно привязать к той кнопке, которая открывает игровое поле.
Если игрок открывает пустую ячейку или ячейку с цифрой, то запускается функция open_area. Если же он открывает ячейку с бомбой, то начинается самое интересное.
В редакторе я поместил на уровень два объекта, которые называются Map Spot. Эти объекты позволяют телепортировать в соответствующие координаты различные объекты. Одному Map Spot я присвоил тэг 1001. В этой позиции будет появляться кибердемон. Другому Map Spot я присвоил тэг 1002. В эту позицию будет телепортироваться игрок. Объекту игрока я присвоил тэг 1000.
При открытии ячейки с бомбой игроку выдаются кулаки и ракетница с полным боекомплектом. На минном поле появляется кибердемон. Туда же телепортируется игрок. Если кибердемон умирает, игра заканчивается победой.
Для того, чтобы игра завершалась при смерти босса, нужно присвоить объекту босса тэг, а также привязать к нему скрипт. Поэтому прошу обратить внимание на следующие две строчки:
Первая строчка создает кибердемона, а вторая привязывает к этому объекту следующий скрипт:
Создание игры на этом закончилось, однако остался один момент. Когда я вызываю кибердемона, то использую название «CyberdemonSweeper», а не «Cyberdemon». Если говорить проще, то я создал нового монстра на основе имеющегося.
За все объекты в игре отвечает файл DECORATE. Если в архиве создать такой файл, скопировать конфигурацию кибердемона из оригинальной игры и поменять некоторые значения, то получится немного другой монстр.
Например, новому кибердемону я на 50% снизил количество здоровья, но при этом значительно увеличил его скорость. Из-за этого его легче убить, но попасть в него будет очень сложно.
А потом я узнал, что можно было обойтись пятью строчками. Надо просто наследовать новый класс из существующего. Получается следующее:
Итак, проект завершен. Можно полюбоваться на получившийся результат.
Чтобы поиграть, можно скачать WAD. Для запуска потребуется GZDoom (желательно, последней версии).
Признаюсь, я как будто вернулся в прошлое, пока реализовывал этот проект. Как будто я все еще подросток, который пишет диплом (создает карты для Doom), а не офисный планктон.
Надеюсь, что на этом я не остановлюсь, и вас ждут такие «шедевры» как «Змейка на движке Doom», «Wolfenstein 3D на движке Doom» и «Doom на движке Doom».