Unity удалить объект из памяти

Обновлено: 05.07.2024

Я новичок в единстве, и на днях мне было интересно, что было бы полезно, если бы я мог создавать некоторые объекты в моей игровой сцене и, по мере их создания, сделать так, чтобы они автоматически удалялись через некоторое время ,

Я думал о том, чтобы автоматически добавлять к ним сценарий Destroy, чтобы они могли быть удалены через некоторое время, используя:

Я не уверен, что это то, что мне нужно, и если я не знаю, как бы я это сделал.

Как я могу сделать это так, чтобы мои объекты автоматически удалялись через некоторое время, в том смысле, что у меня их много?

2 ответа

Довольно легко добавить любой компонент в игровой объект при создании экземпляра, включая классы, используя GameObject.AddComponent<T>() . Если ваш сценарий хорош с самого начала, вы можете сделать это inline . Если вам нужно что-то сделать со своим скриптом после его добавления, вы можете создать ссылки при создании экземпляра.

Для большей ясности , обратите внимание, что я говорю классы вместо скриптов . Когда вы создаете скрипт, внутри этого скрипта с тем же именем создается class . Когда вы перетаскиваете скрипт на объект в инспекторе, это добавляется класс. Это не совпадение . Хотя компилятор не будет жаловаться, если вы приступите к изменению имени этого класса, Unity будет жаловаться на то, что он больше не сможет найти исходный класс и начнет бросать ошибки. Вы не можете понять, что вы можете создавать дополнительные классы после первого, в том же файле сценария. Хотя вы не можете просто «перетащить» эти дополнительные классы на объект через инспектор Unity, вы можете добавить их к игровому объекту, используя AddComponent<> .

Добавление класса inline

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

Добавление и ссылка на класс

Иногда просто создание класса не будет сокращать его. То же самое может произойти для вашего экземпляра игрового объекта. Если мы создадим их inline, как показывает вышеприведенный пример, мы не создаем никаких ссылок для дальнейших манипуляций. Возможно, нам захочется создать ссылку на игровой объект, который мы создадим для последующего использования, или даже добавить его в список всех игровых объектов, которые мы создали из того же класса. Аналогичным образом, мы можем захотеть получить локальную ссылку на добавленный класс ObjectDestroyer , чтобы выполнить дополнительную настройку.

Вы можете создавать ссылку только на объект игры или создавать ссылку только на класс, который вы добавляете к игровому объекту. Если ссылаться только на объект игры, вы можете создать его встроенный, как в приведенном выше примере.

Я ссылался на свой GameObject, но не на мой класс! Oh Noeeeeeee!

Конечно, вы можете обнаружить, что вам нужно получить доступ к вашему ObjectDestroyer , прикрепленному к игровому объекту, но не создать (или иначе потерял) ссылку на него. Аналогично приведенным выше примерам мы можем использовать GetComponent<>() , чтобы захватить ссылку после начального создания.

Последнее, что касается типов, поскольку вы запускаете в него

Поскольку вы используете тип T , используя приведенные выше примеры, я счел разумным дать быстрое объяснение, чтобы избежать потенциальной путаницы. Когда вы видите указанный T (например, GetComponent<T>() или List<T>() ), это просто означает, что этот метод или класс generic . Это означает, что вы можете заменить T на множество других типов, и это сработает.

Это сложнее, но для новичка все, что вам нужно знать, это то, что GetComponent<T>() можно записать как GetComponent<Collider>() , если вы хотите получить прикрепленный коллайдер, или как GetComponent<Rigidbody>() , если вы хотите получить прикрепленное Rigidbody или даже GetComponent<DooHickey>() , если вы хотите получить прикрепленный класс с именем DooHickey .

Существуют ограничения на то, как вы можете использовать генерики, но в общем смысле приведенные выше примеры не должны приводить вас в заблуждение. Просто знайте, что если вы попробуете GetComponent<T>() и играобъект не содержит такой компонент, он вернет null .

Просто прикрепите этот скрипт к вашему gameObject /prefab и установите для переменной lifeTime, как долго вы хотите, чтобы этот объект жил:

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

p.s. Вопрос касается разработки 2Д игр.

Вариантов может быть много. Надо понимать, что внутренние ресурсы игровые, такие как текстуры, звуки итд - это не managed code.
Первый кандидат - это статическая ссылка на ресурсы из скрипта - тогда Unity не будет их удалять.
Еще можно нагенерить ресурсов из кода (меш там создавать, текстурку) каждый кадр новый или и ссылку на них забыть удалить. Натечь может много.
Но по фотографии это не лечится - нужно профайлером гонять.

Про это я первым делом подумал, что надо пробежаться по коду и все проверить. Меня смущает почему АнгриБердс не смогли сделать нормально игру, может есть какой то жуткий камень в этом.

DenBraun
> если какие то хитрости при разработке на юнити, чтоб не возникло ужасных последствий утечки памяти?
Есть, уходить с юнити на нормальный C++ движок.

Dmitry10
> Есть, уходить с юнити на нормальный C++ движок.

На С++ нету утечек что ли ? :)

innuendo
сделаешь сам утечки, тогда и будут :)

innuendo
> На С++ нету утечек что ли ?
Если их специально не делать, то нету.

war_zes
> сделаешь сам утечки, тогда и будут :)
+100500

Dmitry10
> > На С++ нету утечек что ли ?
> Если их специально не делать, то нету.

innuendo
Так об этом и речь, поэтому и рекомендую забить на криворукий юнити и перейти на нормальный C++ движок.

Dmitry10
> и перейти на нормальный C++ движок.

На котором также возможны мемлики от кривых рук ? :)

innuendo
перейти на нормальный C++ движок, где мемликов нет.

Dmitry10
> рекомендую забить на криворукий юнити и перейти на нормальный C++ движок.

Есть мнение, что в мемликах ТС виноват вовсе не Юнити.

Dmitry10
> перейти на нормальный C++ движок, где мемликов нет.

Если руки кривые - мемлики бывают даже там, где их нет :)

Ogra
> Есть мнение, что в мемликах ТС виноват вовсе не Юнити.
Да, я тоже слышал, что это на него зелёные человечки порчу наводят.

> Если руки кривые - мемлики бывают даже там, где их нет :)
В точку, поэтому лучше не связываться с криворукими движками, типа Юниту, потому что даже если твой код будет идеально безупречен, сам Юнити будет тормозить и лагать.

Dmitry10
> даже если твой код будет идеально безупречен, сам Юнити будет тормозить и
> лагать.

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

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

Так же в проекте Scriptable Object, в через инспектор накинута ссылка на prefab. Лежать может в любой папке. А на сцене скрипт, которому через инспектор накинута ссылка на 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()

Вот так, старые добрые деструкторы пиши. На всякий случай даже уточнил на форуме Unity не баг ли это, нет, "так и задумано". В profiler / profile analyzer | memory profiler такие ситуации отследить не очень то просто, у ассетов будет просто не указано кто его удерживает в памяти, т.к. обьект удален, и становится непонятно почему его сборщик мусора не ловит.

В процессе разработки своей игры мы столкнулись с проблемами, время от времени вызывавшими торможения игрового процесса. Потратив какое-то время в Unity Profiler, мы обнаружили два типа проблем:

Цель этой статьи — не написать туториал по использованию профилировщика; я хотел просто рассказать о том, что нас в основном интересовало в процессе профилирования.

Unity Profiler — всегда самый лучший способ поиска причин задержек в скриптах. Настоятельно рекомендую профилировать игру непосредственно в устройстве, а не в редакторе. Поскольку наша игра создавалась для iOS, мне нужно было подключить устройство и использовать показанные на изображении Build Settings, после чего профилировщик подключался автоматически.


Build Settings (параметры сборки) для профилирования

Если вы попробуете загуглить «Random lag in Unity» или другой похожий запрос, то обнаружите, что большинство людей рекомендует сосредоточиться на сборке мусора, поэтому именно так я и поступил. Мусор генерируется каждый раз, когда вы прекращаете использовать какой-то объект (экземпляр класса), после чего время от времени запускается сборщик мусора Unity для уборки беспорядка и освобождения памяти, что требует безумного количества времени и приводит к падению частоты кадров.

Как найти в профилировщике скрипты, вызывающие появление мусора?

Просто выберите CPU Usage -> Choose Hierarchy view -> Sort by GC Alloc


Параметры Profiler для сборки мусора

Ваша задача — добиться одних нулей в столбце GC alloc для геймплейной сцены.

Ещё один хороший способ — отсортировать записи по «Time ms» (времени исполнения) и оптимизировать скрипты, чтобы они занимали как можно меньше времени. Этот шаг имел для нас огромное влияние, потому что один из наших компонентов содержал большой цикл for, для выполнения которого требовалась целая вечность (да, мы пока не нашли способа избавиться от цикла), поэтому оптимизация времени выполнения всех скриптов была для нас абсолютно необходима, ведь нам требовалось сэкономить время выполнения на этом затратном цикле for, сохраняя при этом стабильную частоту в 60 fps.

На основании данных профилирования я разделил оптимизацию на две части:

  • Избавление от мусора
  • Снижение времени выполнения

В этой части я расскажу, что мы делали, чтобы избавиться от мусора. Это самые фундаментальные знания, которые должен понимать любой разработчик; они стали важной частью нашего ежедневного анализа в каждом pull/merge request.

Первое правило: никаких новых объектов в методах Update

В идеале методы Update, FixedUpdate и LateUpdate не должны содержать ключевых слов «new». Всегда нужно использовать то, что у вас уже есть.

Иногда создание нового объекта сокрыто в некоторых внутренних методах Unity, поэтому оно не так очевидно. Мы расскажем об этом позже.

Второе правило: создавать один раз и использовать многократно!

По сути, это означает, что выделять память для всего, что можно, следует в методах Start и Awake. Это правило очень похоже на первое. На самом деле это просто ещё один способ устранения ключевых слов «new» из методов Update.

  • создаёт новые экземпляры
  • ищет какие-нибудь игровые объекты

Вот примеры внесённых нами изменений:

Выделение памяти под списки в методе Start, их очистка (Clear) и повторное использование при необходимости.


Хранение ссылок и повторное использование их следующим образом:


То же относится к методу FindGameObjectsWithTag или любому другому методу, возвращающему новый массив.

Третье правило: остерегайтесь строк и избегайте их конкатенации

Когда дело доходит до создания мусора, то строки ужасны. Даже простейшие операции со строками могут создавать много мусора. Почему? Строки — это просто массивы, и эти массивы неизменяемы (immutable). Это означает, что каждый раз, когда вы конкатенируете две строки, создаётся новый массив, а старый превращается в мусор. К счастью, можно использовать StringBuilder, чтобы избежать или минимизировать такое создание мусора.

Вот пример того, как можно улучшить ситуацию:


С показанным выше примером всё в порядке, но в нём есть ещё много возможностей для улучшения кода. Как видите, почти всю строку можно считать статической. Мы разделяем строку на две части для двух объектов UI.Text. Сначала одна содержит только статический текст «Player » + name + " has score ", который можно назначить в методе Start, а вторая содержит значение счёта, которое обновляется в каждом кадре. Всегда делайте статические строки действительно статическими и генерируйте их в методе Start или Awake. После этого усовершенствования почти всё в порядке, но по-прежнему генерируется немного мусора при вызове Int.ToString(), Float.ToString() и т.п.

Мы решили эту проблему генерацией и предварительным выделением памяти под все возможные строки. Это может показаться глупой тратой памяти, но такое решение идеально соответствует нашим потребностям и полностью решает проблему. Итак, в конечном итоге мы получили статический массив, доступ к которому можно напрямую получать доступ при помощи индексов, чтобы брать нужную строку, обозначающую число:

Четвёртое правило: кешировать значения, возвращаемые методами доступа

Это может быть очень сложно, потому что даже простой метод доступа (accessor), наподобие показанного ниже, генерирует мусор:


Старайтесь избегать использования методов доступа в методе Update. Вызывайте метод доступа только один раз в методе Start и кешируйте возвращаемое значение.

В общем случае я рекомендую НЕ вызывать никаких методов доступа к строкам или методов доступа к массивам в методе Update. В большинстве случаев достаточно один раз получить ссылку в методе Start.

Вот ещё два распространённых примера ещё одного неоптимизированного кода методов доступа:

Пятое правило: используйте функции, не выполняющие выделение памяти

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


Для этого конкретного случая можно найти не выделяющую память функцию под названием


Многие другие функции тоже имеют подобные альтернативы, поэтому всегда проверяйте в документации наличие NonAlloc-функций.

Шестое правило: не используйте LINQ

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

Седьмое правило: создавать один раз и использовать многократно, часть 2

В нашем случае используется следующий сценарий пулинга объектов. У нас есть сгенерированный уровень, заполненный препятствиями, существующими в течение определённого периода времени, пока игрок не пройдёт эту часть уровня. Экземпляры таких препятствий создаются из префабов в случае соблюдения определённых условий. Код находится в методе Update. Этот код совершенно неэффективен с точки зрения памяти и времени выполнения. Мы решили проблему, сгенерировав пул из 40 препятствий: при необходимости мы получаем препятствия из пула и возвращаем объект обратно в пул, когда он больше не нужен.

Восьмое правило: внимательнее с упаковкой-преобразованием (Boxing)!

Boxing генерирует мусор! Но что такое boxing? Чаще всего boxing возникает, когда вы передаёте тип значения (int, float, bool и т.д.) в функцию, которая ожидает параметр типа Object.


Также у нас есть сеттер, задающий значения в этом словаре:


Boxing здесь довольно очевиден. Можно вызвать функцию следующим образом:


Тогда значение «12» подвергается boxing-у и это генерирует мусор.

Мы решили проблему, создав отдельные контейнеры данных для каждого примитивного типа, а предыдущий контейнер Object используется только для типов-ссылок.


Также у нас есть отдельные сеттеры для каждого типа данных:


И все эти сеттеры реализованы таким образом, что вызывают одинаковую обобщённую функцию:


Проблема boxing-а решена!

Девятое правило: циклы всегда под подозрением

Это правило очень похоже на первое и второе. Просто старайтесь убрать весь необязательный код из циклов из соображений производительности и выделения памяти.

В общем случае мы стремимся избавиться от циклов в методах Update, но если без них не обойтись, то мы по крайней мере избегаем любого выделения памяти в таких циклах. Итак, следуйте правилам 1–8 и примените их к циклам в целом, а не только в методах Update.

Десятое правило: никакого мусора во внешних библиотеках

В случае, если выяснится, что часть мусора генерируется кодом, скачанным из Asset store, то у этой проблемы есть множество вариантов решения. Но прежде чем выполнять реверс-инжиниринг и отладку, просто снова зайдите в Asset store и обновите библитеку. В нашем случае все используемые ассеты по-прежнему поддерживались авторами, продолжающими выпускать улучшающие производительность обновления, поэтому это решило все наши проблемы. Зависимости должны быть актуальными! Я лучше избавлюсь от библиотеки, чем сохраню неподдерживаемую.

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

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

Честно говоря, некоторые из этих правил приводят к гораздо хуже читаемому коду, а иногда даже нарушают рекомендации, например, упомянутое в одном из представленных ниже правил встраивание кода.

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

Первое правило: правильный порядок выполнения

Переместите код из методов FixedUpdate, Update, LateUpdate в методы Start и Awake. Знаю, это звучит безумно, но поверьте, если вы углубитесь в свой код, то найдёте сотни строк кода, которые можно переместить в методы, исполняемые только один раз.

В нашем случае такой код обычно связан с

  • Вызовами GetComponent<>
  • Вычислениями, которые на самом деле возвращают в каждом кадре одинаковый результат
  • Многократным созданием экземпляров одних и тех же объектов, обычно списков
  • Поиском GameObjects
  • Получением ссылок на Transform и использованием других методов доступа

Второе правило: выполняйте код только тогда, когда это необходимо

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


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


Этот код гораздо лучше, особенно если действия намного сложнее, чем простое изменение UI.

Как бы то ни было, этого всё равно было для нас недостаточно, и мы хотели реализовать совершенно обобщённое решение, поэтому создали библиотеку, реализующую Flux в Unity. Это привело к очень простому решению, при котором всё состояние игры хранится в объекте «Store», а все элементы UI и другие компоненты уведомляются при изменении состояния и реагируют на это изменение без кода в методе Update.

Третье правило: циклы всегда под подозрением

Это точно такое же правило, которое я упоминал в первой части статьи. Если в коде есть какой-то цикл, итеративно обходящий большое количество элементов, то для улучшения производительности цикла используйте оба правила из обеих частей статьи.

Четвёртое правило: For лучше, чем Foreach

Цикл Foreach очень легко написать, но «очень сложно» выполнять. Внутри цикла Foreach используются Enumerator для итеративной обработки набора данных и возврата значения. Это сложнее, чем итерация индексов в простом цикле For.

Поэтому в нашем проекте мы по возможности заменили циклы Foreach на For:


В нашем случае с большим циклом for это изменение очень значимо. Простой цикл for ускорил код в два раза.

Пятое правило: массивы лучше, чем списки

В своём коде мы выяснили, что большинство списков (List) имеет постоянную длину или мы можем вычислить максимальное количество элементов. Поэтому мы реализовали их заново на основе массивов и в некоторых случаях это привело к двукратному ускорению итераций над данными.

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

Шестое правило: операции с Float лучше, чем операции с векторами

Это различие едва заметно, если вы не выполняете тысячи подобных операций, как и было в нашем случае, поэтому для нас увеличение производительности оказалось значимым.

Мы вносили подобные изменения:

Седьмое правило: искать объекты правильно

Всегда думайте, действительно ли нужно использовать метод GameObject.Find(). Этот метод тяжёл и занимает безумное количество времени. Никогда не следует использовать такой метод в методах Update. Мы выяснили, что большинство наших вызовов Find можно заменить прямыми ссылками в редакторе, что, разумеется, гораздо лучше.


В случае, если так сделать невозможно, то хотя бы рассмотрите возможность использования меток (Tag) и поиска объекта по его метке при помощи GameObject.FindWithTag.

Итак, в общем случае: Прямая ссылка > GameObject.FindWithTag() > GameObject.Find()

Восьмое правило: работайте только с относящимися к делу объектами

В нашем случае это было важно для распознавания коллизий при помощи RayCast-ов (CircleCast и т.п.). Вместо распознавания коллизий и принятия решения о том, какие из них важны, в коде, мы переместили игровые объекты на соответствующие слои, чтобы можно было вычислять коллизии только для нужных объектов.

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

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

Десятое правило: опасайтесь хитростей с камерой!

Так легко использовать Camera.main, но производительность этого действия очень плоха. Причина заключается в том, что за кулисами каждого вызова Camera.main движок Unity на самом деле выполняет для получения результата FindGameObjectsWithTag(), поэтому мы уже понимаем, что часто его вызывать не нужно, и лучше всего решить эту проблему, кешировав ссылку в методе Start или Awake.

Одиннадцатое правило: LocalPosition лучше, чем Position

Везде, где это возможно, используйте для геттеров и сеттеров Transform.LocalPosition вместо Transform.Position. Внутри каждого вызова Transform.Position выполняется гораздо больше операций, например, вычисление глобальной позиции в случае вызова геттера или вычисление локальной позиции из глобальной в случае вызова сеттера. В нашем проекте выяснилось, что можно использовать LocalPositions в 99% случаев использования Transform.Position, и в коде при этом не нужно делать никаких других изменений.

Двенадцатое правило: не использовать LINQ

Об этом уже говорили в первой части. Просто не используйте его, вот и всё.

Тринадцатое правило: не бойтесь (иногда) нарушать правила

Иногда даже вызов простой функции может быть слишком затратным. В этом случае всегда следует рассмотреть возможность встраивания кода (Code Inlining). Что это значит? По сути, мы просто берём код из функции и копируем его непосредственно в место, где хотим использовать функцию, чтобы избежать вызовов дополнительных методов.

В нашем случае нашлось несколько функций, которые мы решили встроить для улучшения производительности, особенно в большом цикле for.

Читайте также: