Unity. Scriptable Object и память
Захотелось поделиться неожиданной для меня находкой, связанной с тем как выделяется память и как легко столкнуться с крупной утечкой, которую без перезапуска не почистить.
Как столкнулся, в нашем проекте есть заготовленные куски уровней в виде prefab, в них и меши и логика и эффекты. При генерации уровень собирается из нескольких блоков. Так же есть Scriptable Object для хранения описания игрового уровня, какие блоки, что за уровень и.т.п., на менеджере всего этого дела висел список этих описаний уровней. В определенный момент столкнулся с тем что свободной оперативной памяти 0.
Для примера.
Проблема первая.
В чем собственно проблема? Как только Unity видит переменную со ссылкой на scriptable object из проекта, он тянет его в память, а потом и все на что он ссылается, в том числе префабы блоков. А префабы блоков тянут конечно весь тот контент, что содержат. Для меня лично было не очевидно что ссылки на контейнеры данных, scriptable object, потянут за собой по цепочке пол проекта в память :) А у меня бюджет памяти не очень большой тем временем.
Решение. Можно загружать описания уровня по имени из Resources, разбить их вообще на два файла, в одном текстовая информация, для выбора уровней, а в другом связи с ассетами. Второй грузить только когда они нужны на сцене. Можно еще и с относительно новыми Adressables повозиться.
Проблема вторая.
Есть ещё одна менее очевидная проблема, если загрузить Scriptable Object в память при помощи:
preset = Resources.Load("ScriptablePreset") as ScriptableOne;
И потом удалить со сцены объект, который это делал (или вообще все объекты и даже сцену другую загрузить), то загруженный Scriptable Object будет висеть мертвым грузом в памяти и вместе с ним все на что ссылается он и далее по цепочке. Можно звать Garbage Collector, Resources.UnloadUnusedAssets, грузить другие сцены, все будет бестолку и сколько угодно времени, в памяти будет висеть то, на что никто уже не ссылается. Получалось что уровень я создал, потом удалил, но в памяти весь контент висит дальше, получается большой утечка памяти и через сколько то запусков на устройстве с сильно ограниченной памятью, типа Switch/Mobile игра крашнется.
Решение. Но как же быть? Выход то простой, нужно на OnDestruction явно обнулить переменную, где мы хранили указатель на Scriptable Object, GC сочтет это за руководство к действию.
private void OnDestroy(){ preset = null;}
Вот так, старые добрые деструкторы пиши. На всякий случай даже уточнил на форуме Unity не баг ли это, нет, "так и задумано". В profiler / profile analyzer | memory profiler такие ситуации отследить не очень то просто, у ассетов будет просто не указано кто его удерживает в памяти, т.к. обьект удален, и становится непонятно почему его сборщик мусора не ловит.
p.s. Все тестирование использования памяти было выполнено в development билдах с подключением профайлера, как на ПК, так и на консоли.
Интересно, только я такое проморгал в своем проекте?)
Не понял, почему первая проблема - это проблема. Подтянули в память объект с кучей референсов, юнити стандартно (!) для себя загружает объекты по цепочкам ссылок в память. Если хочется сохранить такую архитектуру, но без загрузки всего-всего, то правильно указали в ответе вам, лучше в Addressables и смотреть. А в сторону Resources не смотреть)
Проблема с точки зрения подхода к инструментарию движка. Даже самими Unity.
До недавних пор, в своих мануалах они писали, что ресурсы - быстрое и эффективное решение по хранению контента, чтобы не держать миллиард вещей на сцене.
Не без помощи активного и пытливого комьюнити выяснилось, что ресурсы - та еще головная боль если так посмотреть.
Теперь же сами Unity признали это и пишут, что ресурсы - это плохо, не используйте. Что с одной стороны хорошо.
Но до кучи в список плохих практик, как я в этом же посте и выяснил, залетели и ассет бандлы, ровно с той же формулировкой - не используйте. Почему? А почему раньше можно было использовать?
И вот такой подход у них к мануалам много где наблюдается.
Чего только стоят новичковые туториалы, где место действия - Update()
По туториалам и Update(), кстати, не соглашусь скорее. Новичку полезно давать информацию по частям с увеличением сложности, и если поначалу в update() проще показать работу какой-то логики, то и прекрасно. Иначе он завязнет, например, в условных делегатах и ивентах, не дойдя до самой темы урока.
Я думаю, просто потому что появились Addressables, которые предоплагают большую абстракцию, нормальный поассетный доступ, разрешение зависимостей, возможность отказаться от строковых литералов, удаленную загрузку ассетов и, конечно же, херовые мануалы, особенно поначалу )
А Resources.. это же костылище изначально, папки с волшебным именем, логика работы которых выбиваеnся из логики самой юнити, и которые как бы предлагают юзеру создать в них хранилище забытого хлама, лежащее прямо в памяти.
До Addressables уже давно существовали ассет бандлы. Unity уже несколько лет не советует Resources использовать.
Addressables - как замена ресурсов, это костыль, что будет много хуже во многих случаях. Ну и пользоваться им на много сложнее, чем ресурсами или бандлами. По сути, это просто обёртка над бандлами с системой наполнения бандлов, где последние собираются Избыточно. И вот эта избыточность не даёт полноценно работать с памятью, хотя в целом профит есть.
Хлам создаёт каждый из этих инструментов, но в последние годы юнитеки не решают проблемы, а плодят)
Если нужен полный контроль памяти, то стоит каждый ресурс создавать и удалять в рантайме напрямую, если это возможно.
PS: По вашим словам достаточно папку Resources переместить на уровень выше, сделать равнозначным с Assets. Ну и Editor тоже выбивается.
Яж говорю, в первом случае мне показалось что ссылка на scriptable object не потянет за собой в память префабы, на которые у него ссылки, а с ними префабы на которые ссылки у префаба, а с ними всее меши, которые лежат в префабе... И мне кажется для многих это не очевидно, особенно тех кто только начнет что то делать)
Вот вам и автоматическое управление памятью с GC...
Дело не в этом. Он же там объясняет. Что есть в юнити есть обертки вокруг объектов. Дестроем ты удаляешь объект, а обертка остается. preset ссылается на обертку(при этом из-за перегруженного оператора "==" сравнение с null будет истиной), таким образом движок делается юзер фриндли, чтобы не падало все дальнейшее выполнение. Поэтому GC видит ссылку(а ты нет:)).
Ты можешь после Destroy вывести в лог результат:
bool result = (object)preset == null;
и увидеть, что он скажет - false.
Хотя при простом сравнении с null, вернет true.
Или например, когда ты делаешь GetComponent<> для компонента, которого на самом деле нет сейчас на объекте, то создается фейковый null объект (под него выделяется память), но за то, у тебя всё не падает дальше с концами.
Так Статик объекты GC не удаляет, откуда ему знать, что уже не надо?
так preset не объявлен как static, это обычная переменная
Вот это да, не знал, но подозревал. Про первый пункт, обычно храню в scriptable script только описание уровней и настройки, а сам уровень отдельным файлом, который загружается во время загрузки уровня. Наверно поэтому удавалось избежать такой проблемы.
Да, как то так, но все равно стоит пристально следить за тем кто на что ссылается, особенно крупное, и удалять самому, чтобы не утекало)
Не держите ничего в Resources вообще, пацаны, вы матерям еще нужны.
Ну разве только мизерную текстовую информацию.
Все без исключения тянется в память и хоть ты тресни.
И через прямые ссылки не надо контент тянуть, он также навечно в памяти застрянет.
Лучше через бандлы работать, либо через Adressables (но я про них пока мало знаю).
К сожалению, на этапе небольшого проекта мы забили на такие вещи хуц, а теперь мучаемся, пытаясь рефакторить что-то паралелльно с вводом новых фич.
Да, сейчас по докам если смотреть, то вообще получается вот что
Все так.
AssetBundleСтоп что, а с ними-то теперь что не так?
Или они так ненавязчиво продвигают addresables?
Да
Ну впрочем, это до тех пор, пока они не придумают новый супер эффективный способ, а потом скажут, что старый был говно. ( ͡° ͜ʖ ͡°)
Ну впрочем, это до тех пор, пока они не придумают новый супер эффективный способ, а потом скажут, что старый был говно. ( ͡° ͜ʖ ͡°)
С чего это из Resources всё тянется в память? Всё тянется в билд, но не в оперативку.
https://forum.unity.com/threads/unity-resources-ram-usage.952371/
Так ты либо Resources используешь, либо ассет бандлы. Зачем всё сразу то?
Ну вот так
Пост в ленте теряется. Думаю, неплохо бы было какую-нибудь из пикч в превью поставить.
Если только такую =)
Комментарий недоступен
У меня на них почти весь контент игр строится, еще добавить сюда Odin inspector и вообще круто.
Про обнуление референса не знал, но то, что юнити тянет вообще всё по референсам узнал, когда начинал работать с Addressables. В итоге теперь всё жирное храню в них или в ассетбандлах
это же в эдиторе только? У меня проект сейчас больше 16гб озу выжирает
Нет, все тестировалось в билдах.
А решение то простое, документацию почитать:)
С первым пунктом да, а вот второй какой то не очевидный
Хм, а таой вопрос задам - если я делаю Instantiate SO, то есть ли вариант его потом переименовать?
Scriptable Object же не для этого, он ассетом лежит в проекте ещё до запуска 🤔 Если речь про то, чтобы его создавать через editor-скрипты, не в сцене, то да, можно переименовать..
Вот смотри, у меня есть поле СО в которое при старте instatiate СО, второй рисунок. Вот как его можно переименовать, чтобы убрать из названия clone?
Clone же только на том что создано во рантайме, зачем его вообще переименовывать? Надеюсь ты его потом на сцене по имени не ищешь) Вообще можно через .name же имя поменять, в сцене.
Нет конечно, что за глупости, я же написал что у меня есть поле под СО, зачем мне его по имени искать? Просто интересно, есть ли такая возможность, т.к. сам не нашёл.
У СОшника же есть свойство name, его не пробовал редачить?