Нейрогенератор игровых миров: рассказ о моём интригующем пет проекте

Представьте себе игру с полностью открытым и бесконечным миром, этот мир живет своей жизнью, и игрок полностью свободен делать всё, что заблагорассудиться, а игра просимулирует результаты его действий. Такой open world со своей уникальной вселенной. Интересная такая идея для петпроекта, не правда ли? В этой статье я расскажу о своей попытке реализовать подобную игру, по крайней мере её фундамент. 

Визуализация наших мечтаний на этот счёт, но такое я видел только во сне

Визуализация наших мечтаний на этот счёт, но такое я видел только во сне

Вступление

Дайте угадаю, когда я спрашивал, наверное, вообразили что-то похожее на Minecraft, No Mans Sky, или Kenshi? Но мне кажется больше всего под описание такой свободы действий подходит AiDungeon и её аналоги. Она хоть и не имеет внутри себя симулятивную модель мира, но в текстовом виде показывает игроку реалистичную ответную реакцию на вмешательство в этот мир, и это вмешательство никак не ограничено. Так скажем, зачем нам симулировать всю вселенную изнутри с помощью алгоритмов, если игроку достаточно показать только то, что он может или хочет увидеть — один из методов оптимизации ресурсов. И эта оболочка мира, показываемая игроку, последовательность его действий и их ответных реакций симуляции и составляет геймплейный опыт, к тому же который для каждого игрока будет полностью уникальный.

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

Концепция продукта

Если к AiDungeon добавить визуализацию, то легко представляется жанр визуальной новеллы, но я считаю его достаточно скудным для демонстрации возможностей выстраивания взаимодействия между игроком и нейронкой, нужно что-то более комплексное. И я подумал, что 2D RPG с видом сверху, с элементами виз новеллы подойдёт лучше: в этот жанр можно внедрять множество интересных механик, и связывать их с результатами нейросетей. И если мы вспомним одинаковые игры на том же RPG Maker, которые отличаются только сюжетом и визуалом — как будто бы то что надо. 

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

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

Техническая реализация

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

В качестве инструмента генерации визуализаций результатов нашей симуляции будем использовать img2img и txt2img + апскейлеры и пикселизаторы. Базироваться всё это будет на Unity и C#, так как я Unity разработчик, и такой стек мне более удобен.

Причем модули генерации текста и картинок будут абстрактными, чтобы мы могли поставить к ним адаптеры разных нейронок. Из имплементаций для языка будем использовать GPT-4 и Dalai-LLaMA, для картинок SD 1.5 + ControlNet + Pixalization. Все эти имплементации будут подключены по их API.

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

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

Граф-генератор историй

Генератор представляет из себя настраиваемый граф, очень похоже на визуальное программирование, но его ноды — это ячейка с некоторыми данными истории, которые выдала нейросеть (например, цвет волос главного персонажа, или описание основного квеста истории). Смысл некоторых ячеек может быть заранее определен, некоторые могут генерироваться в процессе, как и связи между ними, а после генерации граф сбрасывается в изначальную структуру, наподобие Play Mode«а в Unity, и под конец работы графа мы сериализуем результат в файл с историей. Потом историю можно будет отправить на сервер, и выдавать игрокам в случайном порядке при запуске новой игры, или дать им выбрать что запустить. Суть в том, что эти истории прегенерированы заранее, и игра не способна редактировать их локально при помощи нейросетей, иначе бы нам не хватило вычислительных мощностей игроков (у некоторых хватило бы, но это не рентабельно).

Ноды имеют входные и выходные порты определенного типа, а также сами имеют свой функциональный тип. Пока сделаем 2 типа нод: генерирующие текст и картинку, соответственно тип портов такой же. У нод есть свои параметры генерации, которые надо заранее выставить, включая промпт. Если параметры не задаются вручную, то их можно вынести как входной порт, и тогда нода запуститься, как только на все входные порты поступят недостающие данные. Причем в контексте промпта можно применять множественную вставку в разные его места, по типу: «Жил был {0} в королевстве {1}, и делал {2}». Все 3 участка промпта под вставку выносятся как входные текстовые порты ноды. Также для облегчения проектирования истории сделаем специальные пресеты нод, назовём их шаблонами, для генерации какой-то конкретной структуры, например портрет персонажа. Также на выход к ноде может быть прикреплен парсер/постпроцессор выходных данных, который форматирует текст/обрезает картинку и т.д., в зависимости от нужд вашей ноды, там может быть любая логика, включая даже динамическое достраивание графа во время его работы.

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

Итоговый архив истории: ресурсы + основной файл истории

Итоговый архив истории: ресурсы + основной файл истории

Пример структуры файла с историей

Пример структуры файла с историей

Вы можете спросить: зачем нужен такой сложный генератор, основанный на нодах, постпроцессорах, графе, если можно составить шаблон json файла, взять guidance подготовить тот же json, и промпты к полям в нужных местах, и сгенерировать все в один заход? Отвечаю заранее, в этом генераторе-графе есть пара важных преимуществ:

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

  2. Даже если и использовать guidance под капотом, выстраивая заранее нужный порядок блоков для генерации, граф остается удобным инструментом редактирования шаблона истории человеком

  3. Снятие ограничений на размер истории. Мы не сможем сгенерировать всю историю разом, если её итоговый размер перелезет за макс кол-во токенов на входе/выходе

Схема тестового графа-генератора

Схема тестового графа-генератора

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

Пример готовой истории и немного визуала

И тут мы можем поговорить про генерацию визуала на примере портрета и иконки главных героев. Чем больше различных тэгов мы можем вытащить из описания персонажа, тем более приближена будет его визуализация к описанию. Скормить всё описание мы не можем по причине плохого качества генерации в таком случае, но я уверен что с текущими достижениями DALLE-3 (которой не существовало на момент создания прототипа) можно опускать этап вытаскивания тэгов и сократить граф в этом моменте. Приведу переведённый пример одной истории.

Начну с описания мира:
Мир «Chrono Nexus» представляет собой уникальную смесь научной фантастики и фэнтези. Действие происходит в далеком будущем, когда человечество освоило межзвездные путешествия и колонизировало бесчисленные миры. Но когда они распространились по галактике, они обнаружили, что они не одни. Были и другие разумные виды, некоторые дружелюбные, другие враждебные. Действие игры происходит на планете под названием Нексус, которая находится в центре загадочного явления, известного как Разлом Хроно. Этот разлом — разрыв в ткани пространства‑времени, позволяющий путешествовать между разными эпохами и измерениями. В результате на планете обитают разнообразные существа из разных времен и миров. Игрок берет на себя роль путешественника во времени, которого отправили на Нексус, чтобы исследовать Разлом Хроно и его влияние на планету. По пути они встретят самых разных персонажей: от средневековых рыцарей до космических пиратов и даже мифических существ, таких как драконы и единороги. Исследуя мир Chrono Nexus, игрок раскроет тайны Chrono Rift и древней цивилизации, создавшей его. Им также придется разобраться в сложной политике различных фракций на планете, каждая из которых имеет свои собственные планы и альянсы.

Описание главного квеста:
Основная проблема Chrono Nexus заключается в том, что Chrono Rift дестабилизируется и грозит коллапсом, что приведет к катастрофическим последствиям не только для Нексуса, но и для всей галактики. Игрок должен найти способ стабилизировать разлом и предотвратить его крах, а также иметь дело с различными фракциями, у которых могут быть свои собственные планы относительно разлома.

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

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

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

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

Портрет img2img + x2 upscale с оригинала пешки

Портрет img2img + x2 upscale с оригинала пешки

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

Генерация карты мира

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

Идея была в том, чтобы разделить карту на тайлы: вода, пляж, поле, лес, горы. В целях визуализации карта пока генерировалась просто шумом Перлина: в зависимости от значения в точке брался соответствующий тайл из списка. Базовые тайты имели референсную текстуру, которую я потом планировал с помощью img2img трансформировать под стиль игры. У меня было 2 стратегии текстутирования карты: микро и макро. Под микро я подразумеваю генерацию каждого тайла отдельно, под макро — обработка с помощью img2img сразу всей карты. Сначала я пытался сгенерировать отдельные тайлы, но нейронка их не хотела генерить бесшовными, и я не нашёл подходящей модели, которая смогла бы сгенерить что-то адекватное используя бесшовность, выглядело вырвиглазно.
Поэтому дальше я переключился на второй вариант: сгенерировал карту из дефолтных тайлов, и потом рендрил камерой разные её квадранты. Почему не всю карту разом? Она должна быть довольно большая, не влезла бы в память нейронки, поэтому даже тут пришлось делить всю карту на секции. Кстати промпт для картинок я тоже генерировал: алгоритм считал кол-во тайлов на секции карты, и менял вес тэгов от их кол-ва. Получилось что-то такое:

Сгенерированная карта мира со швами

Сгенерированная карта мира со швами

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

Карта мира после обработки швов с помощью масок и img2img

Карта мира после обработки швов с помощью масок и img2img

Окэй, визуал карты сгенерирован, у нас есть тайловая структура под капотом. Дальше я хотел привязать сюжет и квесты к этой карте, и в принципе начать генерировать основную её структуру. Главное генерировать необходимо честно — без шума Перлина или чего-то еще. Иначе опять получиться инверсия причинно-следственных связей, когда нам дают какие-то случайные декорации, а нам под них нужно подстроить сюжет нашей истории. Миры могут быть очень разнообразными, от обычных фентези с землей под ногами, до космических станций, летающих островов, огромных городов или подземных лабиринтов. Поэтому ответственность за определение списка тайлов и структуру карты (хотя бы базовой) нужно возложить на генеративную модель в контексте описания нашего мира. Как раз с этим и возникли некоторые сложности…

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

Chat GPT-4 сделал нам тайлы для карты, осталось только запарсить

Chat GPT-4 сделал нам тайлы для карты, осталось только запарсить

Chat GPT-4 не всегда умеет считать кол-во городов на своих картах

Chat GPT-4 не всегда умеет считать кол-во городов на своих картах

GPT-4 не всегда точна не только в цифрах, но и в счёте, направлениях и координатах. Зачастую она не способна сгенерировать правильное кол-во объектов на карте, особенно с указанием места или по координатам, и в обратном направлении тоже — не способна описать готовую карту, посчитать кол-во определенных объектов, описать как их самих, так и их месторасположение как координатно, так и относительно соседей или сторон света.

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

Заключение

Пока что это весь труд, проделанный над прототипом. Продолжать делать его с допущением, что карта генерировалась алгоритмически, не хотелось бы, поэтому придется или искать нужный промпт, надеясь что GPT-4 такое по итогу вывезет, или действительно дообучить какой-нибудь опенсорс LLM под эту задачу, или делать какие-либо уступки, например, генерировать только основные ориентиры. Всем спасибо, что дочитали мою первую статью. Надеюсь она оставила в вас какие-либо мысли на счёт этой идеи, которые вы можете высказать в комментариях!

P.S. Так же, если вас заинтересовала идея или мой пет проект, можете связаться со мной через тг в профиле, и обсудить какие-либо ваши предложения, если они имеются

© Habrahabr.ru