SCRAP[b] {devlog}
26 subscribers
50 photos
14 videos
SCRAP Brigade | Web-based online game | Development blog
Download Telegram
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
Переход на ECS.

Разработчики Unity знали обо всех этих проблемах и уперевшись в потолок доступных инструментов ввели новый подход для разработки — ECS или Entity-Component-System.

ECS более не полагается на managed-природу C#. Здесь мы сами выделяем и освобождаем память, а функции-джобы компилируем с помощью специального компилятора чтобы они выполнялись на скоростях сравнимых с C++. Также мы получили возможность выполнять такие функции многопоточно и делать это очень эффективно в виду того как в ECS организуется работа с памятью.

Как я говорил ранее, в обычных managed-классах все данные инстанса выделяются в памяти хаотично. Мы не можем просто обработать все координаты 1000 зерлингов — под капотом с каждым из них будет индивидуальная работа. А значит мы не можем эффективно это распаралелить.

ECS вводит понятия Entity и Component — Entity это некая сущность, «якорь» для данных, а Component это собственно какие-то данные. Например Entity это будет наш зерлинг, и он будет иметь компоненты — Position, Rotation, Health, Attack и тд. В таком случае мы получим архетип — Enity который имеет конкретный перечень компонентов. И данные этих компонентов в памяти займут один конкретный блок, то есть все позиции всех зерлингов будут лежать одним непрерывным блоком! Не нужно будет отдельно искать где там runtime выделил место под очередную пачку координат! Всё в одном месте от начала и до конца.

И мы мы можем взять эту пачку данных, засунуть в процессор и запустить параллельную многопоточную обработку их всех сразу.
Для обработки и изменения данных в ECS используются системы — System, та самая «S» из ECS. Системы используют ограниченный API, позволяя применять Burst-компиляцию и превращать их в настоящих многопоточных монстров обработки.

Главная проблема ECS — вы теряете удобную иерархию сцены из GameObject’ов и в целом процесс разработки заметно усложняется. Также вам становятся недоступны многие API самого юнити. Однако, вы можете строить «мосты» между managed и ECS мирами. И такие мосты часто необходимы так как не всё можно сделать из ECS.

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

Ещё одна особенность ECS-мира — в нем не доступна классическая Unity-физика на PhysX.
А значит у вас нет удобного способа через инспектор расставлять коллайдеры, мы теряем все предзаготовленные инструменты вроде Vehicle-физики и CharacterController’а.

В замен мы получаем альтернативную ECS-Unity-физику которая работает на много потоков, но имеет множество нюансов. Инструменты вроде CharacterController’а и Vehicle-физики уже имеются, но находятся в ранних бетах и плохо описаны. А коллайдеры расставлять приходится программно или же добавить фазу «запекания» моделек. Мы с помощью редактора префаба расставляем обычные managed-коллайдеры в привычном UI, но при сохраненнии запускается специальный скрипт который заменяет все managed-обертки на настоящие ECS-Physics-Colliers.

Есть также опция использовать Havok, но с недавнего времени лицензию на него нужно покупать самостоятельно, а это 50 000 долларов )

В общем, резюмируя всё выше сказанное: в какой-то момент из-за статтеров я был готов отказаться от Unity и пересесть на Unreal Engine и даже рассматривал движки на Rust, но погружение в ECS решило мои проблемы и вернуло мне веру в успех оставаясь на Unity.
#devblog
👍1
Генерация зданий

#kitchensink
🔥2