Boundless City — город, бесконечный в трех измерениях

Boundless City — город, бесконечный в трех измерениях

Boundless City — эксперимент в процедурной генерации бесконечного по вертикали (и в других измерениях) футуристического города и игра, которая этот алгоритм использует.

Красками разными: белыми, красными...

Был сделан для джема Game Off 2019, ссылка на «джемовскую версию» — Boundless City

Исходный код:

Основы алгоритма генерации

Пространство разбивается на кубические слоты. На каждый слот выделяется 36 вершин, т.е. 12 треугольников = 6 четырехугольников = 1 параллелепипед. После этого для каждого слота независимо определяется то, что в нем рисуется, исходя из вышеназванного бюджета вершин. Всё это делается в вершинном шейдере city‑v.glsl, что имеет некоторые достоинства, но также и существенные недостатки, но об этом позже.

Расставляем «блоки»

Первый шаг — определить, есть ли в данном слоте крупный блок, занимающий весь слот целиком. Если есть — рисуем.

Начинал я с более разноцветного города
Начинал я с более разноцветного города

Сначала я использовал для определения наличия блока градиентный шум, потом просто заменил его на случайное число. Следующим шагом было добавление вертикальных столбов в ячейки сетки 6*6 слотов. И, наконец, горизонтальных «проспектов» — почти пустых от блоков через каждые 50 слотов. В конечном итоге формула выглядит так:

bool isPillar(vec3 slot){ return mod(slot.x, 6.) == 0. && mod(slot.y, 6.) == 0.; } bool hasBlockIn(vec3 slot){ return isPillar(slot) || (rand3(slot) > .99 - fract(slot.x/50.)*0.15); }

Тут mod — деление по модулю, fract — дробная часть, обе встроенный функции GLSL. rand3 — псевдослучайное число от 0 до 1, а slot — целочисленный вектор, определяющий позицию слота в сетке.

«Шевелим» границы слотов

Чтобы добавить разнообразия и «реалистичности», границы слотов сдвигаются на случайное значение в диапазоне +- четверть слота.

Boundless City — город, бесконечный в трех измерениях

Окна

Для определения того, какие будут окна на здании, берутся три пары случайных чисел - отступы сетки окон от края блока, количество столбцов и рядов в сетки и размер окна относительно ячейки сетки. Собственно, это всё.

Также, хотя это и не попало в финальную версию, хочу отметить то, как рисовались «стекла» до того, как стиль стал черно-белым.

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

Sin City style

В какой-то момент при отладке шейдера стекла я «попросил» программу показать уровень «блестящести». Т.е. окна — белым (блестит), остальное — черным (не блестит). Результат неожиданно получился довольно стильным

Boundless City — город, бесконечный в трех измерениях

Не сразу, но в конечном счете я переключился именно на этот стиль. Кроме «стильности» это также ускорило и упростило рендеринг — например, не надо больше было считать нормали. В дальнейшем я погасил часть окон и добавил белые линии по краям зданий (точнее, там, где был переход «глубины» в экранном пространстве больше 10 метров).

Крыши, коридоры и другие «пристройки»

Простые параллелепипеды выглядели скучновато, поэтому я добавил к ним «пристройки». На первом этапе это были просто четырехскатные крыши в каждой пустой ячейке над или под блоком.

Boundless City — город, бесконечный в трех измерениях

Далее алгоритм несколько усложнился и стал выглядеть так. Для каждой пустой ячейки проверяем наличие блоков во всех шести направлениях. Если есть блоки двух противоположных направлениях, рисуем "коридор" - вытянутый параллелепипед с одним окном на стороне, иногда повернутый на 45 градусов. Если коридор не нужен, но если есть занятый блок сверху или снизу, рисуем один из четырех вариантов крыши - четырехскатную, двускатную, односкатную или в виде параллелепипеда.

Вверху справа - "коридоры​". Слева на стене можно заметить лифты, которые, к сожалению, не попали в финальную версию.
Вверху справа - "коридоры​". Слева на стене можно заметить лифты, которые, к сожалению, не попали в финальную версию.

Движущиеся объекты

Boundless City — город, бесконечный в трех измерениях

Через город проходит множество транспортных потоков. По горизонталям перемещаются "флайеры" (в виде урезанных конусов), а по вертикалям движутся целые здания.

Первый шаг - определить, движется ли данная ячейка, и если да, то в каком направлении. Используется такое правило:

Если x и y четные, то ряд может двигаться вертикально (z является вертикальной осью).

Если y и z нечетные, то ряд может двигаться вдоль оси x.

Если x нечетно, а z четно, то ряд может двигаться вдоль оси y.

Это дает гарантию, что ряды не пересекаются.

После этого "кидается кубик", определяющий действительно ли ряд движется (вероятность порядка 1/10), с какой скоростью и в каком направлении.

Далее определяется "сдвиг" объекта относительно первоначального положения и то, из какой именно ячейки этот объект берётся по очень простой формуле.

shift = speed * u_time; slot = slot - floor(shift / u_blockSize);

Тут speed - вектор скорости, u_time - текущее время, shift - положение объекта относительно первоначального, u_blockSize - размер ячейки, а slot - та ячейка, из которой объект берется. Наличие блока считается по обычной формуле, но уже для "сдвинутого" блока. Если поток горизонтальный, то вместо пустых слотов рисуются флайеры, а вместо занятных - уменьшенные блоки. Для вертикальных потоков алгоритм аналогичен статичным слотам.

Баннеры

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

Рисуются они, как и окна, во фрагментном шейдере city-f.glsl

Вот все 4 анимации:

bool animationSinus(vec2 uv, float time, float variant){ return fract(sin(uv.x * 13.)/sin(uv.y * 11.) * (.5 + rand(variant * 300.)) + 0.1 * time * (1. + variant)) > variant + 0.1; } bool animationCircles(vec2 uv, float time, float variant){ return fract(sin(uv.x * 45.) * cos(uv.y *53. + variant * sin(time)) + time) * 0.8 > variant; } bool animationBars(vec2 uv, float time, float variant){ return ((rand(floor(10. * (variant + uv.x - sin(time*0.1))))*.5 + .4 - uv.y) * (1.5 + sin(time *0.1) * rand(variant)) > variant) || fract(uv.x * 200.) > fract(uv.y * 200.) * 5.; } bool animationGridscape(vec2 uv, float time, float variant){ float d = tan(uv.y + 0.3); return mod((uv.x - 0.5) * d,0.2)<0.01 || mod(d + time*0.5,0.2)<0.01; }

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

vec2 scaleUV(vec2 uv, float variant){ return uv * vec2(rand(variant + 1.)+0.5, rand(variant + 2.)+0.5); }
Баннеры по краям и "проспект" посередине.​
Баннеры по краям и "проспект" посередине.​

Геймплей - красные кластеры и зеленые чекпойнты

Boundless City — город, бесконечный в трех измерениях

Игровой процесс укладывается в одно предложение. Красные кластеры дают монетки и ускорение, зеленые чекпойнты - продление лимита времени и увеличение множителя монеток, получаемых с кластеров.

Теперь подробности.

Полет происходит по "планерным" правилам. Т.е. без полноценного двигателя, с постепенно падающей скоростью и ограниченной возможностью ей повысить - только с помощью бустеров, снижения высоты и ограниченного количество ускорений, которое обновляется при прохождении чекпойнта (или респавне).

Кластеров много, генерируются они "броском кубика" для каждого пустого (совсем пустого, т.е. даже без "пристроек") слота независимо.

Чекпойнт в каждый момент один и направление на него показывается зелёной стрелочкой. Следующий чекпойнт генерируется в момент взятия предыдущего, и всегда примерно по ходу движения, чтобы не вертеть "головой" в поисках стрелочки. Расстояние до и высота следующего чекпойнта постепенно возрастает, так что лететь бесконечно (т.е. бесконечно увеличивать множитель монеток) гарантировано невозможно. Пространство в определенном радиусе вокруг чекпойнта расчищается от препятствий.

За собранные монеты можно купить апгрейды - полный список на скриншоте сверху.

Так образом, мы имеем краткосрочные (собрать кластеры, не врезаться), среднесрочные (долететь до чекпойнта) и долгосрочные (насобирать на апгрейд) цели, что теоретически должно добавлять аддиктивности.

Музыка

Музыка для этой игры (и для Marshmallow Sky) была написана Ashley Thorpe (aka The Soundsmith, aka Ashley T.)

Что интересно, если бы не он, то ни эта игра, ни Marshmallow Sky, скорее всего, не были бы написаны. Я обсуждал идею "gliding sim" в дискорде Procjam, но в конце концов решил тогда сделать что-то более соответствующее теме. Уже после джема Ashley T. постучался в личку и предложил написать для такой игры музыку. Это вдохновило меня копать в этом направлении и написать эти две мини-игры, и, быть может, развивать эту тему дальше.

Кстати, для усиления эффекта музыки я слегка меняю освещённость окон в такт

UX

Интерфейс функционален, но сбивает с толку поначалу (а местами, и не только поначалу).

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

И это (наверное) работает, но мне не стоило вываливать список апгрейдов на людей сразу, так как многие воспринимали это как монолитную стену текста и сильно пугались.

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

Быстродействие

Генерация геометрии выполняется полностью в вершинном шейдере city-v.glsl. Т.е. CPU передаёт в GPU только самые общие параметры (например, положение камеры и количество вершин), а шейдер по порядковому номеру вершины определяет, к какому слоту она относится и всё остальное.

Достоинством этого метода является то, что практически вся работа выполняется внутри GPU, не загружая CPU, шину между GPU и CPU и т.д. Также, это несколько упрощает алгоритм.

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

При всём при этом, удалось получить довольно интересную и "реалистичную" картинку даже при очень простом алгоритме. И достичь 60 FPS (почти стабильно) на уже довольно стареньком GTX 760.

Но всё же, при более сложном алгоритме генерации (и/или для более стабильного FPS или более слабых GPU) от такого подхода придется отказаться.

Перспективы

Мне лично проект кажется имеющим потенциал дальнейшего развития и превращения в коммерчский продукт. Учитывая, например, что Superflight оценен довольно хорошо, несмотря на простоту геймплея и уровней, спрос на этот жанр наличествует. А судя по отзывам с джема, такой подход к реализации этого жанра людям нравится.

Возможные пути развития:

  • Оптимизация.
  • Улучшение UX.
  • Портировать на другие платформы. Если получится - в том числе на мобильные.
  • Добавить разнообразия архитектуры, топологии и визуальных стилей. Разбиение города на различные "биомы"/"районы" или даже "миры".
  • Сделать кампанию, построенную, например, наподобие Saints Row и/или NFS: Underground. Т.е. открытый мир с разбросанными по нему "миссиями". Случайно генерируемыми и со сложностью и наградами, постепенно возрастающими с их географической высотой.
  • Возможно, добавить "медитативный"/"исследовательский" режим полета без цели и ограничений.
Рефренсы возможных стилей для дальнейшей разработки - по большей части найдены на <a href="https://api.dtf.ru/v2.8/redirect?to=https%3A%2F%2Fwww.reddit.com%2Fr%2FImaginaryColorscapes%2F&postId=87687" rel="nofollow noreferrer noopener" target="_blank">https://www.reddit.com/r/ImaginaryColorscapes/</a>
Рефренсы возможных стилей для дальнейшей разработки - по большей части найдены на https://www.reddit.com/r/ImaginaryColorscapes/
146146
28 комментариев

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

16
Ответить

Наконец-то можно будет поискать сетевые гены

8
Ответить

Либо гигахрущ)

2
Ответить

Blame смотрел, много общего, но там более замкнутые структуры, так что архитектуры натырить мне оттуда особо не удалось.

Ответить

От 3го лица управляя такой же машинкой наверное лучше бы смотрелось, и еще инерцию ей добавить чтобы какой-то челлендж и геймплей появился. Думаю это совсем не сложно. А то так больше демонстрация генерации на рнд чем собственно игра :)

10
Ответить

Всё верно, в общем.


Насчет инерции - тут есть разные варианты баланса на разных скоростях. Supeflight любит высокие скорости и широкий радиус разворота, мне больше нравится более точный полет в стиле Saints Row: Gat Out of Hell. В Аркхамах что-то среднее.

Ответить

Повсюду баннеры "Добро пожаловать в Омск"

Так и знал, что из него не выбраться

4
Ответить