Unity. Scriptable Object и память

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

Unity. Scriptable Object и память

Как столкнулся, в нашем проекте есть заготовленные куски уровней в виде prefab, в них и меши и логика и эффекты. При генерации уровень собирается из нескольких блоков. Так же есть Scriptable Object для хранения описания игрового уровня, какие блоки, что за уровень и.т.п., на менеджере всего этого дела висел список этих описаний уровней. В определенный момент столкнулся с тем что свободной оперативной памяти 0.

Для примера.

В проекте prefab, а в нем меши.
В проекте prefab, а в нем меши.
Так же в проекте Scriptable Object, в через инспектор накинута ссылка на prefab. Лежать может в любой папке.
Так же в проекте Scriptable Object, в через инспектор накинута ссылка на prefab. Лежать может в любой папке.
А на сцене скрипт, которому через инспектор накинута ссылка на scriptable object.
А на сцене скрипт, которому через инспектор накинута ссылка на scriptable object.

Проблема первая.

В чем собственно проблема? Как только 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 билдах с подключением профайлера, как на ПК, так и на консоли.

Интересно, только я такое проморгал в своем проекте?)

7070 показов
4K4K открытий
39 комментариев

Не понял, почему первая проблема - это проблема. Подтянули в память объект с кучей референсов, юнити стандартно (!) для себя загружает объекты по цепочкам ссылок в память. Если хочется сохранить такую архитектуру, но без загрузки всего-всего, то правильно указали в ответе вам,  лучше в Addressables и смотреть. А в сторону Resources не смотреть)

Ответить

Проблема с точки зрения подхода к инструментарию движка. Даже самими Unity.

До недавних пор, в своих мануалах они писали, что ресурсы - быстрое и эффективное решение по хранению контента, чтобы не держать миллиард вещей на сцене.

Не без помощи активного и пытливого комьюнити выяснилось, что ресурсы - та еще головная боль если так посмотреть.

Теперь же сами Unity признали это и пишут, что ресурсы - это плохо, не используйте. Что с одной стороны хорошо.

Но до кучи в список плохих практик, как я в этом же посте и выяснил, залетели и ассет бандлы, ровно с той же формулировкой - не используйте. Почему? А почему раньше можно было использовать?

И вот такой подход у них к мануалам много где наблюдается.
Чего только стоят новичковые туториалы, где место действия - Update()

Ответить
Автор

 Не понял, почему первая проблема - это проблема.Яж говорю, в первом случае мне показалось что ссылка на scriptable object не потянет за собой в память префабы, на которые у него ссылки, а с ними префабы на которые ссылки у префаба, а с ними всее меши, которые лежат в префабе... И мне кажется для многих это не очевидно, особенно тех кто только начнет что то делать)

Ответить

Комментарий недоступен

Ответить

Дело не в этом. Он же там объясняет. Что есть в юнити  есть обертки вокруг объектов. Дестроем ты удаляешь объект, а обертка остается. preset ссылается на обертку(при этом из-за перегруженного оператора "==" сравнение с null будет истиной), таким образом движок делается юзер фриндли, чтобы не падало все дальнейшее выполнение. Поэтому GC видит ссылку(а ты нет:)).
Ты можешь после Destroy вывести в лог результат:
bool result = (object)preset == null;
и увидеть, что он скажет - false.
Хотя при простом сравнении с null, вернет true.

Или например, когда ты делаешь GetComponent<> для компонента, которого на самом деле нет сейчас на объекте, то создается фейковый null объект (под него выделяется память), но за то, у тебя всё не падает дальше с концами.

Ответить

Так Статик объекты GC не удаляет, откуда ему знать, что уже не надо?

Ответить

Вот это да, не знал, но подозревал. Про первый пункт, обычно храню в scriptable script только описание уровней и настройки, а сам уровень отдельным файлом, который загружается во время загрузки уровня. Наверно поэтому удавалось избежать такой проблемы.

Ответить