У Unity всё плохо

image-loader.svg

На просторах интернета, и в частности хабра, очень трудно встретить статьи с критикой игрового движка Unity. Я решил это исправить, и приготовил вам текст о переходе на DOTS, насилию над C#, знаменитых UI пакетах, MonoBehaviour, универсальности и о многом другом.

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

DOTS

Кадр из презентации DOTS. На картинке изображен спальный район Москвы через 20 лет.Кадр из презентации DOTS. На картинке изображен спальный район Москвы через 20 лет.

В данный момент Unity активно переходит на новый стек технологий под названием DOTS. Транзит сопровождается лозунгом «Производительность по умолчанию» и идеей того, что С# может быть таким же быстрым как C++ у UnrealEngine и других движков. Главный фронтмены данного действа это Entities, Jobsи Burst.

Теория

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

Entities представляет из себя реализацию популярного в игрвой индсутрии паттерна ECS. ECS это паттерн, где код делится на сущности к которым добавляются компоненты с данными, состоянием компонентов у сущностей управляют системы, которые эффективно перебирают сущности с нужными им компонентами и изменяют данные компонентов по надобности.

К примеру, у вас игра — шутер, в которой снаряд это сущность с компонентом Bullet в котором хранятся данные о расположении снаряда. Есть система движения снарядов, которая перебирает все сущности с компонентом Bullet и двигает их вперед, а есть система рендера снарядов, которая перебирает все сущности со снарядом Bullet и рендерит их.

Реализаций ECS есть великое множество, но в основном, используют следующие способы оптимизации перебора сущностей:

  • Хранение компонентов эффективно в памяти в соответствии с архитипом существа к которому они привязаны. Архетип это уникальное сочетание компонентов у существа. В случае Unity, компоненты хранятся в чанках по 16 кб в соответствии с архетипом. Выходит, что чанки с компонентами хранятся друг за другом в памяти.

  • Распараллеливание обработки существ системой. Тут всё очевидно, двигать 50 тысяч снарядов в каком-то шутере быстрее в 32х потоках одновременно. От программиста не требуется особых телодвижений для управления многопоточностью, потенциально ECS может эффективно управлять потоками самостоятельно. В Unity роль управления потоками делегирована фреймворку Jobs.

Такой паттерн хорошо подходит сложным играм, в которых есть очень много сущностей которые надоочень частоперебирать (к примеру ММО). Вдобавок, ECS управляет зависимостями, позволяя игре становиться все больше и больше без последствий.Но есть и обратная сторона медали, ECS плохо подходит играм, в которых мало сущностей и их не надо часто перебирать (например 99% мобильного рынка).

Jobs это реализация безопасной мультипоточности путем создания экземпляра «работы». Работу можно запланировать, получить ее обработчик и выполнить ее. Также работу можно запланировать после другой работы, или вместо с другими работами. Таким образом создаются целые цепи последовательных задач. Сами задачи выполняются сразу в нескольких потоках. Например, вам надо умножить все числа массива А с вместительностью в 100 элементов и положить их в массив B. Это можно сделать в десяти потоках, в каждом из них задача будет обрабатывать по 10 элементов.

Burst это компилятор, который транслирует IL байткод в оптимизированный нативный код с помощью LLVM. В основном, используется для компиляции работ с Jobs.

Имплементация

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

Начнем с того, как же команда Unity хочет сделать C# таким же быстрым, как C++, а именно:

  • Создали новый компилятор для Работ с векторизацией и интринсиктами (разве что без блекджека)

  • Создали нессылочные альтернативы массиву, списку, очереди и т.д которые дружат с Burst’ом. Особенность в том, что эти контейнеры надо аллоцировать и после их жизненного цикла освобождать, иначе утечка. Причем, аллоцировать надо не абы как, а с правильными лейблами (Persistent/Temp/TempJob).

  • Ввела в использование указатели на списки и указатели на функции. Также вам может понадобиться использовать Malloc, MemMove, MemCpy и подобные brand-new функции.

Я думаю, вы уже поняли к чему я клоню. Они решили, что лучший способ сделать сишарп таким же быстрым как и плюсы, это сделать из сишарпа плюсы! *занавес, бурные овации, публика в шоке*

Ладно указатели, ладно Dispose у контейнеров, но когда вы начали писать новый компилятор для языка, можно же было что-то заподозрить! Пытаться уже третий год реализовать стек технологий на языке, который не подходит вашему стеку, и при этом переделывать* язык, это же сумасшествие!

Еще один пример несовместимости DOTS и C# это Jobs с Работой IJobChunk и производными. Для того, что бы проитерировать 3 компонента c помощью IJobChunk, вам надо 43 строки кода. Ситуацию могли бы спасти макросы, но они в C# предательски отсутствуют!

Следующая проблема нового стека Unity это недостаток многих функций, которые, иногда, могут вам пригодиться в создании игры, например:

  • Звук. Версия пакета для звука 0.1.0-preview17 и за 3 года он так и не обновлялся.

  • Интерфейс. Тут всё проще, пакета для интерфейса с поддержкой Entities просто нет.

  • 2д. Существует com.unity.2d.entities, но он создан для Project Tiny и работает вообщем-то только с ним, о Project Tiny тоже позже.

  • Анимации. Пакет тоже существует, версия 0.9.0-preview.6, в теории использование возможно, на практике не очень.

  • Всё элементы движка, которые не требуют очевидного отдельного пакета. Свет, камера, партиклы и т.д, всеми ими нельзя управлять через Entities, что угодно делайте. Можете создавать псевдо-сущность с компонентом, к примеру, камеры, и отдельной системой в главном потоке синхронизировать поля между MonoBehaviour камерой и вашей псевдо-камерой, можете другие костыли городить. За 3 года решения от Unity так и нет.

*под переделыванием языка я подразумеваю минимизирование использования ссылочных объектов и, как следствие, кучи путем создания кастомных аллокаторов, компиляторов которые не поддерживают кучу и т.п.

Проблемы

Предположим, что отсутствующие пакеты можно дописать, а текущую имплементацию можно еще починить (в конце концов, можно переписать Roslyn!). Но проблемы самой DOTS как концепции, быстро решить не выйдет.

DOTS по умолчанию сложен. ECS требует полного понимания, как он работает, программисту надо знать на что влияет Read/Write доступ к компонентам, что такое Sync point и как сократить их количество, зачем нужен ECB, WriteGroup, гибридные компоненты и многое, многое другое. Также есть Jobs со своими контейнерами на указателях и такой же морокой с Read/Write доступом, Burst с миллиардом приспособлений для оптимизиации и примерами в докумментации из ассемблера.

Такая сложность явно не идёт на руку движку, одна из основных преимуществ которого простота. Unity занял свое место на рынке как «движок для дизайнеров, а не программистов», на нём делают Cuphead, Ori, Hollow Knight и другие игры которые не Factorio по сложности исполнения и требуемой оптимизации. У Unity занят рынок мобильных игр, маленьких кросс-платформенных инди игр, игр уровня «мы с парнями другие движки не знали, пришлось на этом пилить» (таким образом получаются Rust и Tarkov), вообщем, идиллия. ААА игры не будут делать на Unity даже с полностью реализованным DOTS, ведь для них уже пишут отдельные движки на основе опенсоурса (UE) или вообще с нуля, а сложные инди игры делают энтузиасты на своих микро-движках, фреймворках или на том же UE.

Вообщем, плюсы от перехода на DOTS не очевидны, а минусы из-за нехватки ресурсов компании на поддержку и развития старых решений из уже уходящей эры MonoBehaviour присутствуют.

Project Tiny

В дополнение к выше написанному, хочу упомянуть Project Tiny. Это этакий набор мини-фреймворков для DOTS, которые должны помочь в разработке «Instant games» или, если выражаться русским языком, сделанные за 2 недели Егором из 9Б гипер казуальные игры типа тривряд. Также для Project Tiny команда Unity написала новую среду выполнения DOTS Runtime, вот вам документация (кстати в гугл доках, такого я не видел). Опять пересказывать то, что они пытаются переделать C# смысла я не вижу, просто обозначу, что факт существования данного набора пакетов подтверждает то, что они хотят именно переделать движок с новым стеком DOTS, что в свою очередь, делает проблемы описанные выше еще более значимыми.

Unity, но без DOTS

Скриншот редактора Unity. Если вы думаете, что автор не умеет удобно расставлять окна - вы правы.Скриншот редактора Unity. Если вы думаете, что автор не умеет удобно расставлять окна — вы правы.

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

MonoBehaviour

Начнем с основного старого архитектурного решения — MonoBehaviour. Mono потому что на C#, раньше ещё был, к примеру, JSBehaviour, но все языки кроме C# в Unity бесславно умерли, так и остался один единственный MonoBehaviour.

Если кратко излагаться, MonoBehaviour представляет из себя базовый класс для скриптов в Unity, унаследованные от него классы можно добавить на любой объект в сцене как компоненты, а публичные поля таких классов красивенько отображаются в редакторе. Код пишется в методах со спец. именем, которые ядро движка на С++ вызывает самостоятельно, такие методы называются Messages, их у MonoBehaviour целая куча, от обычных Start и Update до OnCollisionEnter и OnApplicationQuit.

Такое решение довольно простое в понимании и не требует вообще никаких навыков у программиста, у вас все под рукой. Для больших проектов это, естественно, никуда не годиться. Тут нет какой-то внятной архитектуры, у вас логика приложения находиться сразу же в структуре с данными (компоненте). MonoBehaviour почти не предоставляет никаких абстракций, не дает инструмента для управления зависимостями, методы для поиска объектов с MonoBehvaiour на сцене де факто антипаттерн из-за производительности, методы для вызова Message-методов у других MonoBehaviour ещё больше антипаттерн из-за еще худшей производительности.

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

Команда Unity давным давно забила на попытки хоть как-то развить эту систему и пофиксить проблемы с производительностью.

Editor и UI

Разработчики Unity не умеет в UI. За 16 лет они не смогли создать пакет для адекватного интерфейса с xml/css/js и имплементировать редактор своего движка на нем. Для игр они сделали какой-то хлам с Canvas-ом и перетаскиванием панелей по иерархии сцены, а для редактора они предоставили что-то невообразимо ужасное с IMGUI на C# и рефлексией.

Спустя 16 лет они поняли, что шутки про использование штатного пакета для интерфейса Unity на форумах больше не смешные, и решили сделать новый пакет для интерфейса — UIElements (UIToolkit), с xml и css, но без js. Новый пакет для интерфейса непригоден для работы из-за отсутствия функционала у стилей. В нем нет анимаций, нет масок, нет полей для настроек выставления фона (только background-color и background-image), нет настроек шрифта и теней. Можете сравнить набор свойств у обычного CSS, и USS Unity.

Можно было бы сказать, что реализовать полный набор функций CSS движка довольно сложно, но вот Valve создали свой фреймворк Panorama с JS’ом, анимациями, масками, фоном, партиклами, сценами и многим другим. Благодаря ему, у игр Valve одни из самых лучших интерфейсов в индустрии. Понятно, что сравнивать Valve с Unity не совсем корректно, но это пример успешной реализации xml/css/js интерфейса в движке.

Декларируемая универсальность

Уже не совсем техническая, но огромная проблема Unity прошлого, настоящего и будущего. Unity старается быть универсальным, но игровой движок по природе своей не может быть универсальным. Нельзя сделать движок, который одинаково подходил бы сандбокс игре с процедурной генерацией мира, кооперативному FPS шутеру и мобильному кликеру. У разных игр разные требования к архитектуре движка, и если ваш движок это не каркас для плагинов как vim, то сделать его одинаково подходящим для всех не выйдет.

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

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

Другое

Руководствуясь больше долгом о написании всех недочетов Unity, чем здравым смыслом, хочу отметить ещё немного мелких проблем которые не заслуживают отдельного параграфа, но заслуживают упоминание:

  • Стилистика кода у биндингов Unity с префиксами m_, чрезмерным использованием internal и бесполезными комментариями наподобие «x — Left coordinate, x — Top coordinate». Пример.

  • Существование авто-сериалайзера Unity который работает хуже чем любой другой из существующих в мире, я думаю что сериалайзер на конвейерах в Factorio работал бы лучше, и как минимум, поддерживал бы сериализацию словаря.

  • Перезагрузка всех ресурсов с надписью «Hold on» после каждой правки кода. Команда Unity клянется что ничего не могут с этим сделать, но это нетерпимо.

  • В Unity до сих пор нет поддержки C# 9, я просто напомню, что последняя версия уже C# 10.

  • Асинхронные юнит тесты обещали, но конечно, не сделали. Приходиться создавать костыли.

Конец

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

Для небольших игр с простой 3д или 2д графикой

  • Unity. Не смотря на все минусы перечисленные выше, скорее всего для простых игр он вам подойдет.

  • Godot. Сам не пробовал, по ощущениям Unity v2, но опенсоурс и не будет требовать от вас деньги и ставить свой логотип в начале.

  • GameMaker: Studio. Создан для 2д игр, на нем сделали Undertale. Без подписки минимум за 10$ делать ничего не хочет.

  • Microsoft XNA. Не движок, но фреймворк для создания игр на C#, больше не поддерживается. Но на нем создают довольно много разных инди игр и по сей день, одна из самых популярных это Stardew Valley.

Для средних или больших игр с 3д графикой

  • UnrealEngine. Больше мне советовать нечего, скорее всего он вам подойдет. Писать код на C++ это сжигание тонны времени, но блюпринты могут вас спасти.

Другое

  • Amethyst. Движок на Rust, пока без своего редактора, естественно опенсоурс.

  • Bevy. Конкурент предыдущего движка, можете попробовать оба.

© Habrahabr.ru