SCRAP[b] {devlog}
26 subscribers
50 photos
14 videos
SCRAP Brigade | Web-based online game | Development blog
Download Telegram
Дорожные знаки чтобы знать что ждет впереди

#kitchensink
👍3
Отладка связывания точек интереса в дорожную сеть

#kitchensink
👍1🔥1
Как сгенерировать интересный мир?

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

Зачем так усложнять? Основная цель — получить бесшовный бесконечный мир, изучение можно будет начать с любой области, и встретив игроков которые уже успели «пожить» в другой его части важно остаться в одной «системе координат».

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

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

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

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

Я перепробовал много разных алгоритмов размещения застроек, но остановился на диаграмме Вороного.
С помощью него можно разбивать пространство на разные равномерно распределенные многоугольники.
В центре каждого многоугольника расположим застройку.
Часть многоугольников обозначим парками, а часть выпадет из-за гористой местности и крутых спусков-подъемов.

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

Вот мы и получили бесконечный разнообразный мир, который будет интересно изучать.

Осталось научиться загружать его в юнити и делать это без статтеров )
#devblog
👍2🔥1
Media is too big
VIEW IN TELEGRAM
Видеоапдейт за 29 марта.

#kitchensink
🔥1👻1
Блин, пока не понимаю почему ffmpeg соотношение сторон меняет… вечером разберусь

Видосики должны быть шииииреееее)
👍2🙈1
Один из главных челленджей опен-ворлдов это стримминг игрового мира.

Все мы помним долгие загрузки при входе/выходе из городов в Скайриме (в Старфилд не играл, но говорят они и там присутствуют).
Дело в том что игровым движкам очень сложно динамически подгружать части локации. Обычно весь уровень в каком-то виде помещается в память и далее лишь с помощью culling’а ограничивается рендер видимой его части.

Загрузка новых частей уровня это в первую очередь выделение памяти. Но память бесконечно выделять невозможно, значит то что уже не нужно придется освобождать. Так как игровым движком я выбрал Unity — я погряз в «managed»-мире, где правит его величество Garbage Collector. Хотя лучше ему подойдет аналогия «старухи с косой» которая идет на игроком, вычищает более не нужные данные, создавая лаги и статтеры в моменты чистки.

Встала задача научиться ничего не создавать и не выгружать каким бы то ни было способом.

И такой способ нашелся: называется подход «infinite treadmill» — «бесконечная беговая дорожка». Весь игровой мир делится на тайлы (я выбрал размер 16х16 метров) и заранее создаётся то количество тайлов что будет видеть игрок. При движении к краю мы забираем тайлы с противоположной стороны, перекладываем их на сторону к которой игрок движется и меняем их геометрию. Profit!

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

А вот задача отображать пропы карты не так просто решалась. Каждый тайл мог иметь от 10 до 100 различных декор-элементов, деревьев, камней, кустов, пучков травы. Так как набор элементов сильно разнится от тайла к тайлу, заранее подготовить «100 ячеек» не помогло бы. Приходилось создавать пулы каждого типа пропов — отдельные буферы по несколько тысяч слотов, где заранее заготавливались пропы каждого типа, а затем использовались при формировании очередного тайла.

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

Следующее что я попробовал был merge геометрии — объединение всех объектов тайла в один большой blob. Меньше оверхед на каждый отдельный GameObject (а это в юнити очень чувствуется), но дополнительное «приседание» при самом мердже. Количество ререндеров сократилось, стоя на месте перфоманс был приемлемый, но при «кручении» моей бесконечной treadmill я получал неприятные статтеры. Особенно при движении на машине.

Решение нашлось в инстанцировании объектов — GPUBatchRendering. По сути мы храним один экземпляр объекта, а в видеокарту стримим лишь позиции на которых этот объект нужно отобразить. С таким подходом количество Render-вызовов в видеокарту снизилось до комфортного минимума и старуха-мусорщица перестала добавлять статтеры при перестройке тайлов.

Однако полностью от проблем связанных с managed-природой Unity и C# на этом моменте я не избавился и они ещё долго давали о себе знать, заставляя меня сомневаться в правильности выбора игрового движка под мои задачи.

Я уже практически был готов пересесть на Unreal Engine и даже рассматривал движки на Rust, но решение практически всех моих проблем нашлось — великий и могучий ECS. Про него расскажу в одном из следующих постов.

To be continued…
#devblog
👍1🔥1
Media is too big
VIEW IN TELEGRAM
Отладка генерации дороги

#kitchensink
🔥1
Продолжаем про стримиинг игрового мира.

Была ещё проблема с так называемым «floating precision». Дело в том, что Unity использует 32-битные float для координат мира, а у них есть «предел точности». Чем дальше мы уходим от начала координат, тем менее точными становятся значения. Условно, чем больше значение хранится в переменной — тем меньше остаётся места на его дробную часть. А дробная часть очень важна в вычислениях физики и даже рендеринге (олды могут вспомнить «шевеленку» текстур на первой плейстейшен — вот что бывает когда у вас проблемы с float’ами).

Если уйти от начала координат на 5 000 метров — всё будет ок, физика будет работать корректно, графика не потеряет своей точности.
Но стоит приблизиться к 10 000, как мы обнаружим заметный jitter — «шевеленку» у вершин 3д-моделек. Также игровая физика в этой точке станет нестабильна — объекты начнут проходить сквозь друг друга, застревать и тд и тп. И чем дальше, тем проблемы будут серьезнее.

Забавный факт: Godot использует float на 64 бита и у него эта проблема начнется при приближении к 4-5 триллионам километров от начала координат )
Годот я тоже рассматривал, но не решился. Комфорт «конструктора» Unity слишком манил своим удобством, простотой и количеством различных материалов в сети. Хоть мне и пришлось позже отказаться практически от всего того «комфорта» что юнити даёт из коробки, но об этом позже )

В общем для моего бесконечного игрового мира проблема была вполне себе актуальна. Пусть не каждый игрок во время игровой сессии пройдет 8-10 километров в одну сторону, но он же может начать свою игру в точке 20-30 километров от начала координат — и это нужно будет как-то обрабатывать.

Решение простое по сути, но несколько напряжное в реализации — floating origin.
Каждый раз когда игрок отходит на расстояние 5+ км от начала координат, мы превращаем его положение в то самое начало координат и начинаем обрабатывать его «локальную копию мира», будто он находится в точке 0:0:0. Главное не забывать приводить к его «локальному смещению» все значения что мы получаем с сервера и наоборот — разворачивать его координаты в глобальные при отправке на бэкенд.

Теперь игрок может бесконечно и бесшовно двигаться в любую сторону.
Осталось придумать что делать с линией горизонта — как скрыть визуальную подгрузку.
Об этом расскажу в следующий раз.
#devblog
👍1
Media is too big
VIEW IN TELEGRAM
Переехали с colyseusjs на собственный сервер. Golang + ark — ECS и на клиенте и на бэкенде

#kitchensink
Media is too big
VIEW IN TELEGRAM
Первые NPC. Обслуживаются собственным go-сервером, тоже с ECS. Общение с основным через NATS

#kitchensink
🔥1👻1
Война с Managed-подходом.

Причина по которой Unity считается более медленным и примитивным по сравнению с тем же Unreal Engine — его managed-природа и C# в его сердце.
Дело в том что C# выполняется виртуальной машиной (dot)NET и полагается на работу Garbage Collector.

Это безусловно очень удобно для разработчика — мы создаём «понятные» ООП-шные классы, не думаем о ручном выделении памяти.
Просто пишем логику пакуя её в MonoBehavior, живем в определенном уровне абстракции от железа и не лезем в дебри технических особенностей работы CPU/RAM/GPU.

И если вы делаете небольшую игру с конкретными картами-уровнями — Unity ваш друг. В нем полно оптимизаций и инструментов чтобы собрать увлекательный парк аттракционов и просто не париться по многим вопросам.

Проблемы начинаются когда вам нужно выйти за рамки проторенных схем.
Одной важной особенностью managed-GC-подхода является то что runtime очень не любит когда вы начинаете создавать и уничтожать объекты. Это создаёт «давление» на сборщик мусора и вы получаете статтеры. Это лекгко решается созданием пулов — заранее подготовленных объектов на все случаи жизни и переиспользование их.

Например, у вас бывает до 100 врагов в сцене: создаёте заранее эти 100 врагов, просто без надобности не показываете. Прячете за текстурами и активируете только когда этого требует ваш «парк развлечений». Тех кого игрок «деактивировал» просто убираете обратно в пул до следующей активации.

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

Другая проблема проявляющаяся при усложнении сцены это оверхед на обработку GameObject.
GO — удобная и простая абстракция для создания игровых сущностей. Вы можете наделять их любой логикой через MonoBehavoir, можете складывать один внутрь другого чтобы двигать и вращать из вместе — всё работает из коробки и понятно любому кто решил освоить «геймдев».

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

Ещё одной важной особенностью «классических классов» является то что managed-мир существует в основном потоке и не умеет параллелиться на несколько ядер. То есть основная логика вашей игры навсегда завязана на одно ядро процессора. Усугубляет ситуацию и то, что память выделяемая под инстансы классов и их свойства распределяется хаотично и если вам нужно обработать 1000 зерлингов — под капотом (dot)NET будет каждый раз, для каждого инстанса искать где там спрятался икс, а где игрек. О многопоточной обработке можно забыть.

В целом в Unity достаточно оптимизаций и готовых подсистем которые решают большую часть проблем для любой среднестатистической игры и разработчику можно просто творить и не париться. Проблемы начинаются когда вам нужно сильно выйти за рамки существующих ограничений, например вы делаете игру в открытом мире или вообще ММО.

To be continued…
#devblog
👍1
Много экспериментов с алгоритмами распределения POI

#kitchensink
🔥1🙈1