Автор текста: Александр Нилов
Архитектор информационных систем департамента «Логистика» КОРУС Консалтинг
Всем привет! Меня зовут Александр Нилов, я архитектор департамента «Логистика» КОРУС Консалтинг. Но сегодня речь пойдет не о работе, а о моем личном проекте – 3D компьютерной игре.
Я довольно давно занимаюсь программированием. Около 10 лет назад у меня возникла идея написать игру именно на Java, поскольку я использую этот язык в работе. Это был своего рода челлендж. Хотел попробовать себя, посмотреть, возможно ли это. И спойлер – возможно. Но проект дал мне больше, чем я мог рассчитывать.
В этой статье я погружусь в детали геймдева и расскажу о том, почему вообще стоит заниматься подобными проектами.
Ссылка на репозиторий: https://github.com/Arifolth/jme3rpg.
На Java написано не так уж много игр. Как правило, на Java создаются так называемые инди-игры. В последнее время это направление довольно популярно. Например, есть многопользовательская игра Wurm Online с миллионами игроков. Но для мира Java это скорее исключение, чем правило.
В целом Java – более универсальный язык – и для кофеварок, и для мейнфреймов. Но игры, как пет-проект, интересны тем, что они, как это называется, on the cutting edge, т.е. здесь используются самые передовые технологии, потому что работать все должно максимально быстро. В игре не должно быть багов, потому что исправить первое впечатление нового игрока будет скорее всего невозможно. Если человек скачает игру и наткнется на баги, он просто удалит ее. То есть здесь требования к разработке довольно высоки.
Примерно с такими соображениями 10 лет назад я начал проект. Прямо в процессе выкристаллизовалась идея RPG-игры по аналогии с Gothic или Oblivion. Первое время развитие шло довольно вяло, лет пять практически ничего не делал. Но потом у меня появилось свободное время, и я взялся за проект активнее.
Движок
В основе моей игры опенсорсный движок jMonkeyEngine 3. По условиям лицензии его можно использовать даже в коммерческих проектах. На нем писались игры, выложенные в Steam за небольшие деньги (порядка 160 рублей). Однако суперизвестных коммерческих применений этого движка я не знаю. Он не особо распространен.
Существуют гораздо более популярные игровые движки, например Unreal Engine или Unity Real-Time Development Platform. У них огромные комьюнити и хорошая документация, но работают они на других языках – C++ и .NET. На Java ничего такого нет. jMonkeyEngine – фактически единственный развивающийся из некоммерческих. Возможно, есть еще проприетарные, но их я изначально не рассматривал.
У jMonkeyEngine есть достаточно большое комьюнити – он поддерживается, есть активный форум. Однако, документация на несколько версий опаздывает за развитием кода (документацию обычно писать не так интересно, как код). Поэтому форум, по сути, и является основным источником информации. Проще посмотреть код и поговорить там. Почти каждый день на форуме всплывают темы: «А как сделать это…» или «Почему у меня трава мигает». В свое время я был удивлен тому, что старожилы очень подробно отвечают на эти вопросы, так что их ответы действительно можно использовать вместо документации.
Одно из основных преимуществ движка в том, что он позволяет писать под несколько платформ сразу. Поскольку это Java, весь код кроссплатформенный. Разрабатывая игру под Linux, можно автоматически получить работающую игру под Windows и MacOS, а также под планшеты и смартфоны Android, но с некоторыми оговорками, касающимися тестирования – подробнее на этом остановлюсь позже.
Честно говоря, мне не очень нравится курс развития этого движка. Команда core-девелоперов часто отметает классные идеи, с которыми к ним приходят участники сообщества. На мой взгляд, с учетом того, что это опенсорс без финансирования, если не будет развития, проект может потерять пользователей (если появится другой движок на Java, который предложит тот же функционал, он может оттянуть пользователей). Однако в целом, если говорить только про мой проект, к движку вопросов нет. Быть может, хотелось бы больше фич, которые позволили бы быстрее генерировать фреймы. Но в целом все, что надо, уже реализовано. Этим просто надо учиться пользоваться.
Погружаемся в технические детали
Я постепенно изучал движок – начал с простых туториалов, потом перешел к решению интересных мне задач. Движок отвечает за рендеринг содержимого графа объектов и передачу звука. Кроме того, в нем есть библиотека, которая обрабатывает физику столкновения тел и обнаруживает коллизии.
Все остальные задачи надо решать своими руками. Если движки на других платформах позволяют заложить какую-то базу – с чего-то начать, используя их готовые примитивы, то здесь этого нет. Это как конструктор Lego – делай, что хочешь и как хочешь.
Одна из библиотек движка позволяет процедурно сгенерировать ландшафт, состоящий из квадратов – так называемых тайлов. Когда камера приближается к крайнему тайлу, генерируются новые, находящиеся на стыке с ним, а наиболее отдаленные выгружаются. Но это только иллюзия – чтобы по такой земле можно было перемещаться нужно добавить невидимую физическую поверхность, повторяющую изгибы ландшафта, и наделить игроков телами присутствующими в мире физики (Collision shape). То есть как бы и сам игровой мир, и персонажи многомерны – существуют сразу в нескольких параллельных измерениях.
Сами по себе игры работают по одному и тому же принципу – в бесконечном цикле while=1 они рендерят так называемые фреймы в секунду (frames per second, FPS). Сама игра – как кукольный театр из графа объектов. Существует только то, что в данный момент снимает камера. Создается впечатление, что там есть земля, персонажи, деревья, небо и облака с солнцем. То, что находится вне ее поля зрения, движок не рендерит.
Добавление объекта в поле зрения осуществляется манипуляциями с графом объектов. В конце каждой итерации картинка отрисовывается. И если какие-то операции отрабатывают медленно (алгоритмы неудачные или объектов слишком много), вы получаете низкий FPS. Это относится к любым играм – что трехмерным, как в моем случае, что к двумерным, как на старых приставках.
Существуют разные способы реализовывать объекты так, чтобы это было оптимально с точки зрения производительности. Например, можно рисовать трехмерные картинки, а можно двухмерные. Глазом разница заметна не очень, но на производительности это сказывается в лучшую сторону.
В интернете есть целое сообщество game developer, для которых это хобби. Возможно, кто-то из них и получает за это деньги, но в свое свободное время они продолжают подбирать подходы и лайфхаки. Некоторое время назад я начал делиться скриншотами проекта на профильном форуме движка, и народ начал давать интересные советы, в том числе по части оптимизации.
Растительность
Движок позволяет задавать озера и землю с определенным рельефов (неровностями почвы). Я подобрал параметры, которые делают лес похожим на природу под Питером.
Довольно долго я бился над задачей реалистично сгенерировать траву. Звучит просто – любой детсадовец на бумаге нарисует траву. Но в рамках игры это оказалось экстремально сложной темой, по которой даже написано несколько диссертаций.
В целом есть множество подходов – как сложные, так и простые – прямоугольник с текстурой.
В современных играх, как я понял, используют комбинированный подход, чтобы трава выглядела правдоподобнее – создается как бы куст из нескольких перекрещенных под разными углами прямоугольников.
Я также использую именно его. Возможно, в будущем перейду на что-то более сложное. Пример сложного подхода – отрисовка травы полностью на GLSL-шейдерах.
Что я подразумеваю под простым комбинированным подходом?
Для используемого движка в интернете было выложено несколько примеров реализации травы. Я пытался использовать эти библиотеки, но они оказались слишком сильно завязаны на собственную внутрянку, т.е. там была своя генерация земли, деревьев и т.п. Оттуда нельзя было взять только траву, а перетаскивать всю библиотеку к себе не хотелось.
Библиотеки были плохо задокументированы, поэтому подробно изучать их можно было только через реверс-инжиниринг алгоритмов. Компилируешь, подключаешь к коду дебаггер и смотришь, как и что работает. Изучив таким образом несколько библиотек, я понял, что проще сделать самому.
Генерация травы осуществляется следующим образом. На высоте 150 местных единиц от нулевой точки (земли) я беру плоскость, на ней генератором случайных чисел выбираю точки. Из каждой такой точки вниз направляю луч — это техника называется ray cast. Если точка пересечения луча и земли выше уровня воды, т.е. не в озере, то я сажаю в нее пучок травы. Для этого один из стандартных пучков я подкручиваю в трех измерениях и масштабирую, чтобы вся трава не выглядела одинаково.
Аналогично сажаются деревья, только их на карте гораздо меньше. Я использовал модели деревьев из одной бесплатно распространяемой библиотеки. У меня применяется всего три модели дерева, но, как и траву, каждый раз я ее подкручиваю в трех измерениях и масштабирую. В итоге в кадре мы видим разнообразие.
Уровень детализации
С точки зрения перформанса детализация – очень важный момент. Если мы будем отрисовывать все со 100% детализацией, игра просто сожрет вычислительные ресурсы видеокарты и процессора. Но на самом деле 100% детализированы должны быть только те объекты, которые находятся вблизи. Когда мы отодвигаемся, допустим, на 100 игровых метров, детализация может быть уже 50%, а еще дальше – 25% и так далее. Установка правильного уровня детализации очень хорошо влияет на производительность. С этим тоже пришлось повозиться.
Кстати, для скорости обработки леса и травы важно, чтобы все материалы были одинаковые. Поэтому после посадки растительности нужно средствами движка сливать текстуры – это еще один лайфхак с форумов, который здорово экономит производительность. Грубо говоря, вся трава должна представлять собой единый материал, тогда движок сможет отрисовывать ее за один проход.
Погода
Довольно долго работал над тем, чтобы добавить дождь. Лайфхаки тоже вычитал на форумах.
В играх дождь и снег никогда не просчитываются для всей карты – от такой задачи любое современное железо просто умрет. Осадки создаются в полусфере, привязанной к невидимому узлу над игроком. Когда игрок идет по карте, полусфера следует за ним. Камера как бы крутится внутри этой сферы. Таким образом создается впечатление, что на карте везде есть осадки (хотя они рендерятся на маленьком ее кусочке). Гипотетически, если сильно отдалить камеру, будет видно, как над игроком перемещается полусфера.
Модели и лицензии
Существует большое количество открытых опенсорсных моделей, которые можно использовать в своих разработках. Но есть нюанс – надо следить за лицензиями. Моя игра разрабатывается под GPL v3. Эта лицензия требует открытия кода проекта в случае использования исходников. Но существует множество других лицензий. Некоторые из них позволяют распространять код, пригодный для коммерческого использования, другие же имеют определенные ограничения, несовместимые с GPL.
При использовании любых чужих разработок приходится следить за совместимостью лицензий. При этом я стараюсь избегать самописных лицензий, потому что они мутные. Непонятно, какие могут быть последствия использования моделей или текстур с такими ограничениями.
Я обратил внимание, что несмотря на то, что есть множество объектов, которые можно использовать, просто указав авторство (а иногда и не указывая его), в мире опенсорсных игр принято сохранять источник. Когда ты берешь текстуры, модели, звуки, скачанные с какого-либо открытого источника, всегда указывается ссылка на исходник, а также на лицензию. Вероятно, это делается, чтобы потом не было никаких вопросов. Например, если использовать в игре свободно распространяемые качественные звуки, а потом прохождение этой игры выложить на YouTube, сервис скорее всего определит использование мелодии и может даже заблокировать ролик в публичном доступе без нужных ссылок.
Последние изменения
Фактически, я закончил с генерацией леса. Можно бесконечно бродить по вымышленному лесу по типу Карелии под красивую музыку. Недавно я подключил библиотеку, которая позволила отыграть смену дня и ночи со всеми красивыми цветами рассветов и закатов. Иногда в этом лесу идет дождь.
Сейчас доделываю часть, касающуюся добавления звуков природы. Для звука можно было использовать библиотеку, которая идет с движком. А можно использовать другие библиотеки, позволяющие на устройстве отрендерить объемный звук (эхо в коридоре, например). Третий путь – делать самостоятельно с нуля. Конкретно в случае со звуком у меня вопросов к движку не было – все прекрасно работало.
В игре уже есть простенький искусственный интеллект. Иногда появляется так называемый NPC (Non Player Character) – противник, который бегает за игроком и пытается его побить. Он определяет, где находится игрок, и пытается его настигнуть, потом начинается бой. Боевая система состоит из ударов, которые можно заблокировать либо избежать (уклониться). ИИ блокирует входящие удары с определенной вероятностью.
Дальше планирую заняться моделями зданий, возможно даже целых поселков, чтобы наполнить игру разнообразием.
Тестирование
Поскольку для ускорения рендеринга в играх используется множество сложных технологий – та же многопоточность – при наличии ошибок или при неправильном подходе игра ведет себя по-разному на различном железе. И все это надо тестировать. Приходится делать это не только у себя, но и у друзей или на компьютере ребенка. Только так начинают вскрываться баги, которые в целом не так видны.
Выше я говорил, что движок позволяет разрабатывать параллельно под несколько платформ сразу. Так вот тестировать тоже приходится на нескольких платформах, потому что в теории все кроссплатформенное, но на практике поведение на Windows и на Android несколько отличается.
На данный момент я заканчиваю отладку – близится тот момент, когда я смогу сказать, что в игре нет блокирующих и критических багов. Фактически, на носу первый релиз.
Зачем это все
Изначально идея была в том, чтобы попробовать в принципе сделать игру на Java с бесконечным процедурно-генерируемым миром. И это действительно возможно, все работает, не уступая по скорости аналогичным проектам, написанным, допустим, на С++. Но сейчас я вижу, что у этого направления огромный потенциал – он вышел далеко за первоначальные рамки.
Меня привлекают несколько моментов:
при работе с такими проектами приходится принимать много архитектурных решений, и это интересно. Требования к качеству таких проектов намного выше, чем в коммерческих играх. Как правило, у бизнеса все-таки предусмотрен жизненный цикл игры – когда есть баги, они сортируются по критичности, как-то исправляются. Здесь же zero bug tolerance. Такое встречается и на некоторых коммерческих проектах, но не часто. А в пет-проектах поправить потом не получится, это приходится учитывать.
Мне пришлось обкатать много разных идей и технологий, исследовать много тем, напрямую или косвенно связанных с геймдизайном. Например, о том, как наделить NPC интеллектом, написаны целые талмуды – можно применять кучу разных решений, от очень простеньких скриптов, до сложных – LLM. И я продолжаю искать и изучать доступные варианты. Конечно, мне хотелось бы сделать больше. Но на пет-проекты не всегда есть время: вам приходится балансировать между работой, личной жизнью и пет-проектом, и игра в этом случае не в приоритете.
Однако пет-проект положительно сказывается на работе. Ее вполне можно прикладывать к резюме. Когда приходишь на собеседование, достаточно дать ссылку на репозиторий. Вместо того, чтобы задавать тебе какие-то вопросы, интервьюер может пойти и посмотреть в код – понять, нравится ему то, что ты пишешь, или нет.
Кроме того, игра получается «технологией двойного назначения». С одной стороны я развлекаюсь, а с другой – получаю определенные полезные навыки. На своем проекте я отлаживаю некоторые идеи, которые потом применяю в работе. Например, так я экспериментировал с модуляризацией – игра побита на модули, связанные между собой интерфейсами. Это дает хорошую структуризацию кода. Код не перемешивается и не сплетается, лучше читаем. А кроме того отдельные части проекта можно легко заменить – т.е. переписать модуль или его часть, исправив кусок логики. Впоследствии этот подход я успешно применил в коммерческой разработке.
Аналогично я экспериментировал с многопоточностью. Игра требует значительных вычислительных ресурсов, которые сильно загружают процессор. Та же рассадка травы – достаточно «тяжелое» вычисление. До этого у меня были определенные навыки работы с многопоточностью в Java, но здесь появилась возможность их отточить – в многопоточном режиме рассадка травы и деревьев происходит гораздо быстрее. Большой кусок работы можно разбить на секции, каждую из которых выполнять в отдельном потоке. Это позволяет ускорить обработку в несколько раз.
За 10 лет, которые прошли с момента начала проекта, мои навыки сильно выросли. Сейчас многие вещи я бы сделал по-другому. Есть ограничение по доступному времени. Это время можно использовать на переписывание того, что было написано криво, но работает, либо на добавление новых фич.
В целом работа над игрой продолжается. Исходники выложены, периодически я получаю комментарии от сообщества. Если вам интересно, заходите познакомиться с проектом. Для меня это будет ценный фидбек. Репозиторий можно скачать и собрать из исходников игру на своем железе. https://github.com/Arifolth/jme3rpg