Решение LMK в вашей игре Unity — это систематический процесс:

Получить снимок памяти
Используйте Unity Profiler для получения снимка памяти, управляемой Unity. На рисунке 2 показаны уровни управления памятью, которые Unity использует для управления памятью в вашей игре.

Управляемая память
Управление памятью в Unity реализует контролируемый уровень памяти , который использует управляемую кучу и сборщик мусора для автоматического выделения и назначения памяти. Система управляемой памяти представляет собой среду для написания скриптов на C#, основанную на Mono или IL2CPP . Преимущество системы управляемой памяти заключается в том, что она использует сборщик мусора для автоматического освобождения выделенной памяти.
Неуправляемая память C#
Неуправляемый слой памяти C# обеспечивает доступ к нативному слою памяти, обеспечивая точный контроль над выделением памяти при использовании кода C#. Доступ к этому слою управления памятью осуществляется через пространство имён Unity.Collections и с помощью таких функций, как UnsafeUtility.Malloc и UnsafeUtility.Free .
Родная память
Внутреннее ядро Unity на C/C++ использует собственную систему памяти для управления сценами, ресурсами, графическими API, драйверами, подсистемами и буферами плагинов. Хотя прямой доступ ограничен, вы можете безопасно манипулировать данными с помощью C# API Unity и использовать эффективный нативный код. Нативная память редко требует прямого взаимодействия, но вы можете отслеживать её влияние на производительность с помощью профилировщика и настраивать параметры для оптимизации.
Память не распределяется между C# и машинным кодом, как показано на рисунке 3. Данные, требуемые C#, выделяются в управляемом пространстве памяти каждый раз, когда они необходимы.
Например, для доступа управляемого кода игры (C#) к данным в собственной памяти движка вызов GameObject.transform выполняет нативный вызов для доступа к данным в собственной области памяти, а затем возвращает значения в C# с помощью привязок . Привязки обеспечивают правильные соглашения о вызовах для каждой платформы и обеспечивают автоматическую маршалинг управляемых типов в их собственные эквиваленты.
Это происходит только в первый раз, поскольку управляемая оболочка для доступа к свойству преобразования сохраняется в машинном коде. Кэширование свойства преобразования может сократить количество взаимных вызовов между управляемым и машинным кодом, но эффективность кэширования зависит от частоты использования этого свойства. Также обратите внимание, что Unity не копирует части машинной памяти в управляемую при доступе к этим API.

Более подробную информацию см. во введении к памяти в Unity .
Кроме того, определение бюджета памяти критически важно для бесперебойной работы игры, а внедрение системы аналитики или отчётности по потреблению памяти гарантирует, что каждый новый релиз не будет превышать этот бюджет. Интеграция тестов режима игры с непрерывной интеграцией (CI) для проверки потребления памяти в определённых областях игры — ещё одна стратегия для получения более точной информации.
Управление активами
Это наиболее значимая и требующая внимания часть потребления памяти. Профилируйте её как можно раньше.
Использование памяти в играх для Android может значительно различаться в зависимости от типа игры, количества и типов ресурсов, а также стратегий оптимизации памяти. Однако, как правило, на потребление памяти влияют текстуры, сетки, аудиофайлы, шейдеры, анимация и скрипты.
Обнаружение дублированных активов
Первым шагом является обнаружение плохо настроенных активов и дублированных активов с помощью профилировщика памяти, инструмента отчета о сборке или Project Auditor .
Текстуры
Проанализируйте поддержку устройств вашей игрой и выберите правильный формат текстур . Вы можете разделить пакеты текстур для устройств высокого и низкого класса, используя Play Asset Delivery , Addressable или более ручной процесс с помощью AssetBundle .
Следуйте наиболее известным рекомендациям, доступным в разделе «Оптимизация производительности мобильных игр» и в обсуждении «Оптимизация настроек импорта текстур Unity» . Затем попробуйте следующие решения:
Сжимайте текстуры с помощью форматов ASTC для уменьшения объема используемой памяти и экспериментируйте с более высокой частотой блоков, например 8x8.
Если требуется использовать ETC2, упакуйте текстуры в Atlas. Размещение нескольких текстур в одной обеспечивает её эффективность (POT), может сократить количество вызовов отрисовки и ускорить рендеринг.
Оптимизируйте формат и размер текстур RenderTarget . Избегайте использования текстур с излишне высоким разрешением. Использование текстур меньшего размера на мобильных устройствах экономит память.
Используйте упаковку каналов текстур для экономии памяти текстур.
Сетки и модели
Начните с проверки основных настроек (стр. 27) и проверьте следующие параметры импорта сетки:
- Объедините избыточные и более мелкие сетки.
- Уменьшите количество вершин для объектов в сценах (например, статических или удаленных объектов).
- Создание групп уровней детализации (LOD) для объектов с высокой геометрией.
Материалы и шейдеры
- Удаляйте неиспользуемые варианты шейдеров программным способом в процессе сборки.
- Объедините часто используемые варианты шейдеров в uber-шейдеры, чтобы избежать дублирования шейдеров.
- Включите динамическую загрузку шейдеров, чтобы решить проблему большого объёма памяти, занимаемой предзагруженными шейдерами в видеопамяти/ОЗУ. Однако обратите внимание, если компиляция шейдеров приводит к задержкам кадров.
- Используйте динамическую загрузку шейдеров, чтобы предотвратить загрузку всех вариантов. Подробнее см. в статье блога «Улучшения в сокращении времени сборки шейдеров и использовании памяти» .
- Правильно используйте создание экземпляров материалов, используя
MaterialPropertyBlocks
.
Аудио
Начните с проверки основных настроек (стр. 41) и проверьте следующие параметры импорта сетки:
- Удалите неиспользуемые или избыточные ссылки
AudioClip
при использовании сторонних аудиодвижков, таких как FMOD или Wwise. - Предварительная загрузка аудиоданных. Отключите предварительную загрузку для клипов, которые не требуются немедленно во время выполнения или запуска сцены. Это помогает снизить нагрузку на память при инициализации сцены.
Анимации
- Настройте параметры сжатия анимации Unity, чтобы минимизировать количество ключевых кадров и исключить избыточные данные.
- Сокращение количества ключевых кадров: автоматическое удаление ненужных ключевых кадров.
- Сжатие кватерниона: сжимает данные вращения для уменьшения использования памяти.
Параметры сжатия можно настроить в настройках импорта анимации на вкладке «Риг» или «Анимация» .
Повторно используйте анимационные клипы вместо дублирования анимационных клипов для разных объектов.
Используйте Animator Override Controllers для повторного использования Animator Controller и замены определенных клипов для разных персонажей.
Создавайте анимацию, основанную на физике: если ваши анимации основаны на физике или являются процедурными, создавайте их в виде анимационных клипов, чтобы избежать вычислений во время выполнения.
Оптимизируйте скелетную сборку: используйте меньше костей в своей сборке, чтобы снизить сложность и потребление памяти.
- Избегайте чрезмерного использования костей для небольших или статичных предметов.
- Если некоторые кости не анимированы или не нужны, удалите их из рига.
Уменьшите длину анимационного клипа.
- Обрезайте анимационные клипы, оставляя только необходимые кадры. Избегайте хранения неиспользуемых или слишком длинных анимаций.
- Используйте циклическую анимацию вместо создания длинных клипов для повторяющихся движений.
Убедитесь, что подключен или активирован только один компонент анимации. Например, отключите или удалите устаревшие компоненты анимации , если вы используете Animator .
Не используйте Animator без необходимости. Для простых визуальных эффектов используйте библиотеки создания промежуточных кадров или реализуйте визуальный эффект в скрипте. Система аниматора может быть ресурсоёмкой, особенно на недорогих мобильных устройствах.
При работе с большим количеством анимаций используйте систему заданий для анимации, поскольку эта система была полностью переработана для более эффективного использования памяти.
Сцены
При загрузке новых сцен ресурсы добавляются в качестве зависимостей. Однако без надлежащего управления жизненным циклом ресурсов эти зависимости не отслеживаются счётчиками ссылок. В результате ресурсы могут оставаться в памяти даже после выгрузки неиспользуемых сцен, что приводит к фрагментации памяти.
- Используйте функцию Object Pooling в Unity для повторного использования экземпляров GameObject для повторяющихся элементов игрового процесса, поскольку пул объектов использует стек для хранения коллекции экземпляров объектов для повторного использования и не является потокобезопасным. Минимизация
Instantiate
иDestroy
повышает как производительность процессора, так и стабильность памяти. - Разгрузка активов:
- Выгружайте активы стратегически в менее критические моменты, например на заставках или экранах загрузки.
- Частое использование
Resources.UnloadUnusedAssets
приводит к всплескам загрузки ЦП из-за крупных внутренних операций мониторинга зависимостей. - Проверьте наличие значительных пиков загрузки ЦП в маркере профиля GC.MarkDependencies . Удалите или уменьшите частоту его выполнения и вручную выгружайте отдельные ресурсы, используя Resources.UnloadAsset вместо того, чтобы полагаться на всеобъемлющий
Resources.UnloadUnusedAssets()
.
- Реструктурируйте сцены вместо постоянного использования Resources.UnloadUnusedAssets.
- Вызов
Resources.UnloadUnusedAssets()
дляAddressables
может привести к непреднамеренной выгрузке динамически загруженных пакетов. Тщательно управляйте жизненным циклом динамически загруженных ресурсов.
Разнообразный
Фрагментация, вызванная переходами между сценами. При вызове метода
Resources.UnloadUnusedAssets()
Unity выполняет следующие действия:- Освобождает память для активов, которые больше не используются
- Выполняет операцию, подобную сборщику мусора, для проверки кучи управляемых и собственных объектов на наличие неиспользуемых ресурсов и выгружает их.
- Очищает текстуру, сетку и память ресурсов при условии отсутствия активных ссылок.
AssetBundle
илиAddressable
— внесение изменений в этой области — сложная задача, требующая коллективных усилий всей команды для реализации стратегий. Однако, как только эти стратегии будут освоены, они значительно улучшат использование памяти, сократят размер загружаемых данных и снизят расходы на облачные вычисления. Подробнее об управлении ресурсами в Unity с помощью Addressables см. вAddressables
.Централизованные общие зависимости: систематически группируйте общие зависимости, такие как шейдеры, текстуры и шрифты, в выделенные пакеты или
Addressable
группы. Это уменьшает дублирование и обеспечивает эффективную выгрузку ненужных ресурсов.Используйте
Addressables
для отслеживания зависимостей. Адресные объекты упрощают загрузку и выгрузку, позволяя автоматически выгружать зависимости, на которые больше нет ссылок. Переход наAddressables
для управления контентом и разрешения зависимостей может быть приемлемым решением в зависимости от конкретной игры. Анализируйте цепочки зависимостей с помощью инструмента «Анализ», чтобы выявить ненужные дубликаты или зависимости. Кроме того, если вы используете AssetBundles, обратитесь к инструментам Unity Data Tools.TypeTrees
— еслиAddressables
иAssetBundles
вашей игры собраны и развернуты с использованием той же версии Unity, что и плеер, и не требуют обратной совместимости с другими сборками плеера, рассмотрите возможность отключения записиTypeTree
. Это должно уменьшить размер пакета и объём памяти, занимаемый сериализованными файловыми объектами. Измените процесс сборки в локальном пакете Addressables , установив ContentBuildFlags на DisableWriteTypeTree .
Напишите код, удобный для сборщика мусора
Unity использует сборку мусора (GC) для управления памятью, автоматически определяя и освобождая неиспользуемую память. Хотя сборка мусора (GC) крайне важна, при неправильном использовании она может привести к проблемам с производительностью (например, скачкам частоты кадров), поскольку этот процесс может на мгновение приостановить игру, что приводит к снижению производительности и неоптимальному пользовательскому опыту.
Полезные методы снижения частоты выделения памяти в управляемой куче можно найти в руководстве Unity , а примеры — в UnityPerformanceTuningBible , стр. 271.
Уменьшить выделение ресурсов сборщику мусора:
- Избегайте LINQ, лямбда-выражений и замыканий, которые выделяют кучу памяти.
- Используйте
StringBuilder
для изменяемых строк вместо конкатенации строк. - Повторно используйте коллекции, вызывая
COLLECTIONS.Clear()
вместо их повторного создания.
Более подробную информацию можно найти в электронной книге «Полное руководство по профилированию игр Unity» .
Управление обновлениями холста пользовательского интерфейса:
- Динамические изменения элементов пользовательского интерфейса. При обновлении элементов пользовательского интерфейса, таких как свойства Text, Image или
RectTransform
(например, изменение текстового содержимого, изменение размеров элементов или анимация положений) движок может выделять память для временных объектов. - Выделение строк — элементы пользовательского интерфейса, такие как текст, часто требуют обновления строк, поскольку в большинстве языков программирования строки неизменяемы.
- Грязный холст — при изменении какого-либо элемента на холсте (например, изменении размера, включении или отключении элементов или изменении свойств макета) весь холст или его часть может быть помечен как грязный и перестроен. Это может привести к созданию временных структур данных (например, данных сетки, буферов вершин или вычислений макета), что приводит к образованию мусора.
- Полные или частые обновления — если холст имеет большое количество элементов или часто обновляется (например, каждый кадр), такие перестройки могут привести к значительному перераспределению памяти.
- Динамические изменения элементов пользовательского интерфейса. При обновлении элементов пользовательского интерфейса, таких как свойства Text, Image или
Включите инкрементальную сборку мусора , чтобы уменьшить резкие скачки нагрузки за счёт распределения очистки выделенных ресурсов на несколько кадров. Проверьте, улучшает ли эта опция производительность и потребление памяти вашей игрой.
Если ваша игра требует контролируемого подхода, установите ручной режим сборки мусора . Затем, при смене уровня или в любой другой момент без активного игрового процесса, вызовите сбор мусора.
Вызов ручной сборки мусора GC.Collect() для переходов состояний игры (например, переключение уровней).
Оптимизируйте массивы , начиная с простых приемов кодирования и, при необходимости, используя собственные массивы или другие собственные контейнеры для больших массивов.
Контролируйте управляемые объекты с помощью таких инструментов, как Unity Memory Profiler, чтобы отслеживать ссылки на неуправляемые объекты, которые сохраняются после уничтожения.
Используйте маркер профилировщика для отправки данных в инструмент отчетности об эффективности для автоматизированного подхода.
Избегайте утечек памяти и фрагментации
Утечки памяти
В коде C#, если ссылка на объект Unity существует после его уничтожения, управляемый объект-обёртка, известный как Managed Shell , остаётся в памяти. Собственная память, связанная со ссылкой, освобождается при выгрузке сцены или при уничтожении GameObject, к которому привязана память, или любого из его родительских объектов методом Destroy()
. Однако, если другие ссылки на Scene или GameObject не были очищены, управляемая память может сохраняться как Leaked Shell Object . Подробнее об объектах Managed Shell см. в руководстве Managed Shell Objects .
Кроме того, утечки памяти могут быть вызваны подписками на события, лямбда-выражениями и замыканиями, конкатенацией строк и неправильным управлением объединенными объектами:
- Для начала ознакомьтесь со статьей Поиск утечек памяти для правильного сравнения снимков памяти Unity.
- Проверьте подписки на события и утечки памяти. Если объекты подписываются на события (например, делегатами или UnityEvents), но не отписываются должным образом перед уничтожением, менеджер событий или издатель могут сохранять ссылки на эти объекты. Это препятствует сборке мусора для этих объектов, что приводит к утечкам памяти.
- Отслеживайте глобальные события или события класса Singleton, регистрация которых не отменяется при уничтожении объекта. Например, отменяйте подписку или отсоединяйте делегаты в деструкторах объектов.
- Гарантировать, что уничтожение объединенных объектов полностью аннулирует ссылки на компоненты текстовой сетки , текстуры и родительские игровые объекты.
- Имейте в виду, что при сравнении снимков Unity Memory Profiler и обнаружении разницы в потреблении памяти без явной причины эта разница может быть вызвана графическим драйвером или самой операционной системой.
Фрагментация памяти
Фрагментация памяти происходит, когда множество небольших выделений освобождается в случайном порядке. Выделение кучи происходит последовательно, то есть новые блоки памяти создаются, когда предыдущий блок исчерпывает свободное место. Следовательно, новые объекты не заполняют пустые области старых блоков, что приводит к фрагментации. Кроме того, большие временные выделения могут привести к постоянной фрагментации на протяжении всего игрового сеанса.
Эта проблема становится особенно проблематичной, когда крупные краткосрочные размещения производятся вблизи долгосрочных.
Групповое распределение на основе продолжительности их жизненного цикла. В идеале долгосрочное распределение должно осуществляться одновременно, на ранних этапах жизненного цикла приложения.
Наблюдатели и менеджеры мероприятий
- Помимо проблемы, упомянутой в разделе (Утечки памяти) 77 , со временем утечки памяти могут способствовать фрагментации, оставляя неиспользуемую память, выделенную объектам, которые больше не используются.
- Гарантировать, что уничтожение объединенных объектов полностью аннулирует ссылки на компоненты текстовой сетки , текстуры и родительские
GameObjects
. - Менеджеры событий часто создают и хранят списки или словари для управления подписками на события. Если они динамически увеличиваются и уменьшаются во время выполнения, это может привести к фрагментации памяти из-за частого выделения и освобождения памяти.
Код
- Сопрограммы иногда выделяют память, чего можно легко избежать, кэшируя оператор возврата IEnumerator вместо того, чтобы каждый раз объявлять новый.
- Постоянно отслеживайте состояния жизненного цикла объединенных объектов, чтобы избежать сохранения фиктивных ссылок
UnityEngine.Object
.
Ресурсы
- Используйте динамические резервные системы для текстовых игровых процессов, чтобы избежать предварительной загрузки всех шрифтов для многоязыковых случаев.
- Организуйте активы (например, текстуры и частицы) вместе по типу и ожидаемому жизненному циклу.
- Сжимайте активы с неиспользуемыми атрибутами жизненного цикла, такими как избыточные изображения пользовательского интерфейса и статические сетки.
Распределения на весь срок жизни
- Выделяйте долгосрочные активы в начале жизненного цикла приложения, чтобы обеспечить компактное распределение.
- Используйте NativeCollections или пользовательские распределители для ресурсоемких или временных структур данных (например, физических кластеров).
Действия с памятью, связанные с кодом и исполняемыми файлами
Исполняемый файл игры и плагины также влияют на использование памяти.
Метаданные IL2CPP
IL2CPP генерирует метаданные для каждого типа (например, классов, дженериков и делегатов) во время сборки, которые затем используются во время выполнения для рефлексии, проверки типов и других операций, специфичных для времени выполнения. Эти метаданные хранятся в памяти и могут существенно влиять на общий объём памяти, занимаемый приложением. Кэш метаданных IL2CPP существенно ускоряет инициализацию и загрузку. Кроме того, IL2CPP не дедуплицирует некоторые элементы метаданных (например, дженерики или сериализованную информацию), что может привести к чрезмерному использованию памяти. Эта проблема усугубляется повторяющимся или избыточным использованием типов в проекте.
Метаданные IL2CPP можно сократить за счет:
- Избегать использования API-интерфейсов рефлексии , поскольку они могут вносить значительный вклад в распределение метаданных IL2CPP
- Отключение встроенных пакетов
- Реализация полного общего доступа к дженерикам в Unity 2022 должна помочь снизить накладные расходы, связанные с дженериками. Однако, чтобы ещё больше сократить выделение ресурсов, следует сократить использование дженериков.
Удаление кода
Помимо уменьшения размера сборки, стрипинг кода также уменьшает потребление памяти. При сборке с использованием скриптового бэкенда IL2CPP стрипинг управляемого байт-кода (активированный по умолчанию) удаляет неиспользуемый код из управляемых сборок. Процесс заключается в определении корневых сборок и последующем использовании статического анализа кода для определения другого управляемого кода, используемого этими корневыми сборками. Любой недоступный код удаляется. Подробнее об стрипинге управляемого кода см. в блоге TTales from the optimize trances: Better manageed code stripping with Unity 2020 LTS и в документации по стрипингу управляемого кода .
Собственные распределители
Поэкспериментируйте с собственными распределителями памяти для их точной настройки. Если игре не хватает памяти, используйте блоки меньшего размера, даже если это потребует более медленных распределителей. Подробнее см. в разделе « Пример динамического распределителя кучи» .
Управление собственными плагинами и SDK
Найдите проблемный плагин — удалите каждый плагин и сравните снимки памяти игры. Это включает в себя отключение большого количества функций кода с помощью Scripting Define Symbols и рефакторинг тесно связанных классов с интерфейсами. Проверьте раздел «Выравнивание кода с помощью шаблонов игрового программирования», чтобы упростить процесс отключения внешних зависимостей без ущерба для игрового процесса.
Свяжитесь с автором плагина или SDK — Большинство плагинов не имеют открытого исходного кода.
Воспроизведите использование памяти плагином — вы можете написать простой плагин (используйте этот плагин Unity в качестве примера), который выделяет память. Проверьте снимки памяти с помощью Android Studio (поскольку Unity не отслеживает эти выделения) или вызовите класс
MemoryInfo
и методRuntime.totalMemory()
в том же проекте.
Плагин Unity выделяет память Java и нативную память. Вот как это сделать:
Ява
byte[] largeObject = new byte[1024 * 1024 * megaBytes];
list.add(largeObject);
Родной
char* buffer = new char[megabytes * 1024 * 1024];
// Random data to fill the buffer
for (int i = 1; i < megabytes * 1024 * 1024; ++i) {
buffer[i] = 'A' + (i % 26); // Fill with letters A-Z
}