Война с 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
Причина по которой 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
Переход на 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
Разработчики 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
Curved World
Если мир бесконечный, встаёт вопрос что делать с линией горизонта.
Скайбокс не нарисуешь, реальную «прогрузку» далеко не сделаешь — это всё ресурсы которых не так много, учитывая что я мечу также в порт на мобильные устройства. Остаётся только добавить туман как в Silent Hill )
И как-то вдруг мне пришла в голову идея сделать эффект «круглой планеты» — загибать игровой мир за линию горизонта. Позже я узнал что оказывается «в народе» этот эффект больше всего ассоциируется с Animal Crossing и это мне позже пригодится.
Первые попытки реализовать этот эффект я принял ещё когда проект был ориентирован на Web-технологии. Да, я пытался собрать прототип на React+Three.js+Colyseus и оно в принципе работало — можно было открыть сайт хоть с телефона, хоть с компа, хоть через встроенный браузер VR-шлема и попасть в игру. Причем даже работал proximity-voice-chat посредством WebRTC. Но сейчас не об этом.
Так как в браузере ресурсов крайне мало, а я тогда был уж совсем не гейм-девелопер чтобы писать кастомные шейдеры — я подошел к решению «в лоб». Так как мир уже разделен на тайлы — мы можем просто эти тайлы смещать по Y-координате пропорционально расстоянию от игрока. И это работало!
Однако, при подъеме на высокие горы вся магия «рассыпалась». Становились видны эти поднимающиеся-опускающиеся «островки» и их резкие квадратные границы.
В целом, в этом тоже был свой шарм и игру можно было продолжать делать именно с этим эффектом.
Проблема крылась в принципе размещения пропов: из-за того что у меня была потребность привязывать каждый проп к родителю-тайлу чтобы двигать его по высоте вместе с ним — я не мог использовать GPU-инстанцирование, так как оно не поддерживает parent-transform, все координаты инстансов нужно передать «как есть».
К этому времени эксперименты с ускорением рендеринга увели меня в сторону собственного шейдера — я перешел на цвета запеченные в вершины, а стандартный Unity/Lit такое не умеет. В собственный шейдер можно было добавить «честный» curved-world — смещать именно вершины через vertex-shader.
Эффект превзошел все мои ожидания! Если раньше было видно как куски уровня на горизонте двигаются ввер-вниз, то теперь весь уровень искривзялся и загибался за горизонт максимально плавно. Деревья и здания «выползали» из-за горизонта давая ощущение исследования круглой планеты.
Следующий челлендж был в том как заставить облака огибать наш круглый мир.
Но перед этим нужно было в принципе как-то сделать интересные динамичный облака )
Об этом в другой раз.
#devblog
Если мир бесконечный, встаёт вопрос что делать с линией горизонта.
Скайбокс не нарисуешь, реальную «прогрузку» далеко не сделаешь — это всё ресурсы которых не так много, учитывая что я мечу также в порт на мобильные устройства. Остаётся только добавить туман как в Silent Hill )
И как-то вдруг мне пришла в голову идея сделать эффект «круглой планеты» — загибать игровой мир за линию горизонта. Позже я узнал что оказывается «в народе» этот эффект больше всего ассоциируется с Animal Crossing и это мне позже пригодится.
Первые попытки реализовать этот эффект я принял ещё когда проект был ориентирован на Web-технологии. Да, я пытался собрать прототип на React+Three.js+Colyseus и оно в принципе работало — можно было открыть сайт хоть с телефона, хоть с компа, хоть через встроенный браузер VR-шлема и попасть в игру. Причем даже работал proximity-voice-chat посредством WebRTC. Но сейчас не об этом.
Так как в браузере ресурсов крайне мало, а я тогда был уж совсем не гейм-девелопер чтобы писать кастомные шейдеры — я подошел к решению «в лоб». Так как мир уже разделен на тайлы — мы можем просто эти тайлы смещать по Y-координате пропорционально расстоянию от игрока. И это работало!
Однако, при подъеме на высокие горы вся магия «рассыпалась». Становились видны эти поднимающиеся-опускающиеся «островки» и их резкие квадратные границы.
В целом, в этом тоже был свой шарм и игру можно было продолжать делать именно с этим эффектом.
Проблема крылась в принципе размещения пропов: из-за того что у меня была потребность привязывать каждый проп к родителю-тайлу чтобы двигать его по высоте вместе с ним — я не мог использовать GPU-инстанцирование, так как оно не поддерживает parent-transform, все координаты инстансов нужно передать «как есть».
К этому времени эксперименты с ускорением рендеринга увели меня в сторону собственного шейдера — я перешел на цвета запеченные в вершины, а стандартный Unity/Lit такое не умеет. В собственный шейдер можно было добавить «честный» curved-world — смещать именно вершины через vertex-shader.
Эффект превзошел все мои ожидания! Если раньше было видно как куски уровня на горизонте двигаются ввер-вниз, то теперь весь уровень искривзялся и загибался за горизонт максимально плавно. Деревья и здания «выползали» из-за горизонта давая ощущение исследования круглой планеты.
Следующий челлендж был в том как заставить облака огибать наш круглый мир.
Но перед этим нужно было в принципе как-то сделать интересные динамичный облака )
Об этом в другой раз.
#devblog
🔥3