Roguera: как препод рогалик на Java делал. Часть 1
Хотите узнать, как я до такого докатился?
Вступление, которое можно пропустить
Так уж вышло, что я — преподаватель в одном из московских вузов и мой основной предмет — программирование на Java (официально он называется иначе, но разницы никакой), по которому я, как ни странно, веду практики. В середине сентября, всё таки встал вопрос — как научить студентов языку, когда ты сам практически не шаришь в нём, а из проектного опыта только кликер на C#?..
Уже думал, что опять два семестра гонять балду, а студентам давать незамысловатые задачи, как однажды, во время дрёмы, в голову пришла гениальная и дерзкая мысль:
А что если написать рогалик на джава и обучать студентов по нему?
И вот мы здесь спустя больше полугода с момента начала работы над игрой — 24 сентября 2020 года.
Небольшой дисклеймер
Я заметил, что во многих постах про разработку игры, авторами был упущен один важный, на мой взгляд, пункт, который был бы полезен новичкам: образцы игрового кода с пояснениями. Да, у нас здесь не курс Java, поэтому будет ещё полезнее знать хотя бы основы синтаксиса. Тем же, кто далёк от этого языка, будет хотя бы интересно прочитать об увлекательном процессе создания игры без движка почти с нуля.
А в чём вообще суть?
Roguera - это классический псевдографический рогалик, со всеми вытекающими особенностями: процедурная генерация уровней/монстров/предметов, перманентная смерть (при выходе из игры можно сохраниться), текстовый лог, описывающий происходящее, примитивные эффекты через ASCII цвет. В планах добавить очки за прохождение и "таблицу лидеров" со статистикой, которая будет висеть где-то на моём хостинге.
Как всё начиналось
Разумеется, с выбора «графической подсистемы». Мне довольно быстро повезло найти фреймворк Lanterna, который удовлетворял моим нуждам. По сути, это относительно простой эмулятор терминала, который позволял как выводить встроенный псевдографический GUI, аля Norton Commander, так и посимвольный вывод. Поигравшись с гайдами, я начал реализовывать простейший прототип игры: @, которая бегает внутри квадрата из "*".
К сожалению, самого старого фрагмента кода у меня не сохранилось (а жаль, можно было бы прочувствовать разницу). Зато есть код движения персонажа из чуть более поздней версии (примерно спустя пять часов работы).
Всё достаточно просто: метод MovePlayer(KeyStroke key) отвечает за обработку кнопок WASD и перемещает на одну координату в соответствующую сторону с помощью метода Move(int x, int y). Оный просто заменяет символ @ на пустой и перемещает собачку в нужную точку. Вот так просто и наивно.
Завод костылей
Теперь-то можно рассказать об особенностях устройства игры.
Например, начнём с того, как генерировались комнаты, для этого использовался вот такой метод:
Для любителей покопаться в легаси коде — ссылка на снапшот файла класса R_Dungeon.java от 26 сентября 2020.
Если вы ничего не поняли, не волнуйтесь, дальше я проиллюстрирую как же выглядело игровое поле. (спойлер, очень неоптимальное решение — это как если бы YandereDev делал рогалик)
Грубо говоря, у нас есть двумерный массив (аля таблица с столбцами и строками) символов для примера назовём roomStructure[y][x] (да ещё и координаты перепутаны). Строки обозначим как y, а столбцы как x.
Чтобы получить содержимое точки, на иллюстрации выше, где находится собачка, надо адресоваться к нужному элементу в массиве:
Наш "игрок" находится на координатах 1;1, его то отображение мы и получаем.
Это было безумно неэффективно, так как нам надо ещё и проверять является ли эта клетка игроком, чтобы как-то с ним взаимодействовать, например, правильно отрисовать в консоли:
Соединение комнат было на тот момент запилено адской непонятной вундервафлей через методы StreamAPI:
Проспойлерю и покажу как этот метод выглядит в текущей версии.
Думаю, лаконичность говорит сама за себя.
Собственно рендеринг или вывод на экран терминала здесь был представлен весьма специфично в классе T_View:
При нажатии на кнопку, сначала игрок «незримо» перемещался, после чего весь экран перерисовывался.
Основной метод здесь, конечно, DrawDungeon(TextGraphics textGraphics, char cell)
То есть, на каждой клетке мы проверяем:
1. Если есть игрок, рисуем игрока (зелёненьким).
2. Если не игрок, то берём из 2D массива комнаты то, что находится по координатам i, j и отображаем на том же месте на экране (относительно точки 0.0 от левого верхнего угла окна).
Причём мы не просто размещаем символ, мы размещаем строку из одного символа.
Можете сами догадаться, насколько мне было удобно наращивать игру фичами. В конце концов, как правильно сказал Юра turbojedi:
«…видеоигры это дым и зеркала: вам не обязательно иметь AI со сложным поведением, достаточно записать побольше строк диалога, уверяющих игрока, что это сложное поведение есть»
В тот момент я не догадывался, что не стоит создавать буквальное нахождение персонажа в каком-то двумерном массиве, достаточно просто выводить его наличие где-то на экране, а его действительное местоположение держать в объекте…однако мы забегаем вперёд.
Вождение костылями по джойстику
Через полторы недели я представил очередной прогресс:
Примитивнейший класс моба, у которого был такой же примитивный класс контроллера (да бы совсем не нагромождать статью кодом, оставлю по ссылке).
Сами битвы происходили по jRPG принципу: если в комнате были мобы, то игрок, задев хотя бы одного — бил всех. В данном случае с одного удара, а они даже не могли ответить…
Структура игры (в особенности вывод в консоль), выглядела как один большой референс к треку Оксимирона — Переплетено. Нужно было как-то разделить логику вывода и логику остального безобразия и при этом ничего не сломать. Ах да, следует добавить, что игра на тот момент была всё ещё однопоточной…
Я потратил ещё около десяти часов, чтобы расчистить объектно-ориентированные авгиевы конюшни. Теперь у меня появился класс MapEditor, который представляет собой простейший инструмент для «рисования» линий и прямоугольников. Это можно представить как вождение костылями по джойстику, чтобы использовать кисть. Однако, критичная часть логики не была переписана и необходимо было ломать всё дальше во имя прогресса. А КАДА ИГРА ТО?
На самом деле процесс создания чего-то с примитивных штук очень увлекает. Особенно когда начинаешь понимать где можно улучшить часть программы, где удалить старый код, где обернуть повторы в методы и так далее. Так что понимаю разработчиков, которые много лет делают свои проекты и почти не выпускают их, так как им доставляет кайф сама разработка.
Собственно, сам метод:
Ничего необычного, при каждом движении мы: стираем экран, обновляем позиции у элементов, «рисуем» блок информации и саму комнату, после чего обновляем экран.
Дальше можно посмотреть как я экспериментировал с тем, как будут генерироваться комнаты, хотел добиться разнообразия.
А вот и метод, который отвечает за генерацию всего безобразия выше.
Спустя три дня и несколько исписанных листов в блокноте, я разработал новый процедурный алгоритм генерации комнат по точкам, который по итогу позволил делать более разнообразные локации:
И на этом данная часть заканчивается. Мы прошли пока что только пару недель разработки, дальше больше и ещё интереснее!
В следующих частях вас ждут:
- Как придумать процедурную генерацию по точкам для двухмерного массива и не сойти с ума.
- Поиск пути это who? Как я научил мобов ходить по линеечке за игроком.
- Попытка сделать псевдо-SDK.
- Игра со шрифтами и ASCII кодами.
- Миллионы строк логов.
- Дырявый инвентарь.
- Как я написал свою систему Windows.
- И конечно же великий переход на новую архитектуру…