Представьте себе игру с полностью открытым и бесконечным миром, этот мир живет своей жизнью, и игрок полностью свободен делать всё, что заблагорассудиться, а игра просимулирует результаты его действий. Такой 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. Так же, если вас заинтересовала идея или мой пет проект, можете связаться со мной через тг в профиле, и обсудить какие‑либо ваши предложения, если они имеются.

Комментарии (8)


  1. SUNsung
    22.03.2024 14:34
    +3

    Идея хорошая - реализация так себе.

    Генерацию карты/мира можно спереть из концепции minecraft/minetest только вместо алгоритмов использовать chatGPT (автор что то подобное начал но как то оочень однобоко) а вместо соли вставки в промт

    .

    Описания нужно генерить матрично. То есть сначала описываем ноду. Потому "увеличиваем" и описываем каждую ячейку ноды. И так опускаемся буквально до камня на земле. Причем привязка по именам собственным и chatGPT это прекрасно запомнит и сможет с этим работать.

    Например нода у нас великая пустыня. В ней есть 4 ячейки (в идеале минимум квадрат 9х9 но лень описывать) и эти ячейки - интересный оазис, унылые пески, странная равнина и поле кактусов.

    Далее chatGPT описывывает все по сужающейся спирали с расстановкой приритетов.

    Что бы итоге был "текстовый обзор" как книги-описания в моровинде и машино-понятный (великая пустыня >> странная равнина >> 10х10 >> { раненый авантюрист, .... иное описание }

    .

    Вся сложность интеграции нейроных сетей в безшовности. Что что бы сделать npс которые взаимодейсвуют с реальным миром нужно или весь мир описать руками (промты) или уже сам мир строить на основании промтов.

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

    (У автора пока получилось то что можно добится обычными алгоритмами псевдосоучайности для создания зон/карт)

    .

    Много думал на эту тему, но в плане мира ничего лучше [генерим картинку >> распознаем и описываем картинку >> смешаем курсор и повторяем] я не смог придумать.

    Для простых вещей и как "умный гугл" нейронки сейчас на коне, но для сложных вещей у них очень узкое окно [размер загружаемых данных] из за чего нужно в ручном режиме контролировать процесс так как нейрока может сфокусироватся совсем не на том


    1. Minebot Автор
      22.03.2024 14:34
      +1

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

      По поводу построения мира на основании промптом - это основное направление, делать инверсивно как будто бы смысла особого нет, ибо мы сильно сужаем вариации миров, тоже самое относиться к идеи генерации карты в виде картинки или алгоритмически, и последующее её описание. Так что следующие эксперементы будут направлены в эту сторону: каким образом адекватно генерировать карту на основации контекста истории мира


      1. SUNsung
        22.03.2024 14:34

        Как по мне мир нужно именно алгоритмически генерить, с нейронки только получать соль по чанкам для соответсвия истории.

        Дороги и города так же алгоритм расставит легко (есть куча уже готового в инете)

        А вот история чанков, городов и локаций как раз задача для нейронки.

        Из личного опыта chatGPT очень плохо работает с большой картиной где может быть множество деталей. А вот с маленьким куском большой картины он работает отлично.

        .

        К слову даже без геймлея такой "генератор миров" очень пригодится всем творческим. От писателей до игроделов и прочих творцов. Как раз единая связность всего мира и внимание к деталям самая большая вава авторов


        1. Minebot Автор
          22.03.2024 14:34

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


      1. MaxKitsch
        22.03.2024 14:34

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