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

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

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

Для получения более подробной информации обратитесь к разделу «Введение в использование памяти в Unity» .
Кроме того, установление бюджета памяти имеет решающее значение для бесперебойной работы игры, а внедрение системы анализа или отчетности по потреблению памяти гарантирует, что каждый новый релиз не превысит этот бюджет. Интеграция тестов в режиме игры с вашей системой непрерывной интеграции (CI) для проверки потребления памяти в конкретных областях игры — еще одна стратегия для получения более глубокого понимания ситуации.
Управление активами
Это наиболее значимая и требующая принятия мер часть анализа потребления памяти. Проводите профилирование как можно раньше.
Использование памяти в играх для Android может значительно варьироваться в зависимости от типа игры, количества и типов ресурсов, а также стратегий оптимизации памяти. Однако обычно к источникам потребления памяти относятся текстуры, модели, аудиофайлы, шейдеры, анимация и скрипты.
Выявление дублирующихся активов
Первый шаг — обнаружение неправильно настроенных и дублирующихся ресурсов с помощью профилировщика памяти, инструмента создания отчетов о сборке или аудитора проекта .
Текстуры
Проанализируйте поддержку устройств для вашей игры и выберите правильный формат текстур . Вы можете разделить пакеты текстур для высокопроизводительных и низкопроизводительных устройств, используя Play Asset Delivery , Addressable или более ручной процесс с помощью AssetBundle .
Следуйте наиболее известным рекомендациям, представленным в статьях «Оптимизация производительности мобильных игр» и « Оптимизация настроек импорта текстур в Unity» . Затем попробуйте следующие решения:
Сжимайте текстуры с помощью форматов ASTC для уменьшения объема используемой памяти и экспериментируйте с более высокой частотой блоков, например, 8x8.
Если требуется использовать ETC2, упакуйте текстуры в Atlas. Размещение нескольких текстур в одной текстуре обеспечивает её степень двойки (POT), может уменьшить количество вызовов отрисовки и ускорить рендеринг.
Оптимизируйте формат и размер текстур RenderTarget . Избегайте текстур с излишне высоким разрешением. Использование текстур меньшего размера на мобильных устройствах экономит память.
Используйте упаковку текстурных каналов для экономии текстурной памяти.
Сетки и модели
Для начала проверьте основные настройки (страница 27) и подтвердите следующие параметры импорта сетки:
- Объединить избыточные и более мелкие сетки.
- Уменьшите количество вершин для объектов в сцене (например, статических или удаленных объектов).
- Создайте группы уровней детализации (LOD) для объектов с высокой геометрией.
Материалы и шейдеры
- Неиспользуемые варианты шейдеров удаляются программно в процессе сборки.
- Объедините часто используемые варианты шейдеров в единые универсальные шейдеры, чтобы избежать дублирования шейдеров.
- Включите динамическую загрузку шейдеров, чтобы решить проблему большого объема памяти, занимаемого предварительно загруженными шейдерами в видеопамяти/оперативной памяти. Однако обратите внимание, если компиляция шейдеров вызывает сбои в работе программы.
- Используйте динамическую загрузку шейдеров, чтобы предотвратить загрузку всех вариантов. Для получения дополнительной информации обратитесь к статье в блоге «Улучшения времени сборки шейдеров и использования памяти» .
- Правильно используйте создание экземпляров материалов, задействуя
MaterialPropertyBlocks.
Аудио
Для начала проверьте основные настройки (страница 41) и подтвердите следующие параметры импорта сетки:
- При использовании сторонних аудиодвижков, таких как FMOD или Wwise, удаляйте неиспользуемые или избыточные ссылки
AudioClip. - Предварительная загрузка аудиоданных. Отключите предварительную загрузку для клипов, которые не требуются немедленно во время выполнения или запуска сцены. Это помогает уменьшить нагрузку на память во время инициализации сцены.
Анимации
- Настройте параметры сжатия анимации в Unity, чтобы минимизировать количество ключевых кадров и исключить избыточные данные.
- Уменьшение количества ключевых кадров: автоматическое удаление ненужных ключевых кадров.
- Кватернионное сжатие: сжимает данные о вращении для уменьшения использования памяти.
Настройки сжатия можно изменить в разделе « Параметры импорта анимации» на вкладке «Риг» или «Анимация» .
Используйте повторно анимационные клипы вместо дублирования анимационных клипов для разных объектов.
Используйте Animator Override Controllers , чтобы повторно использовать Animator Controller и заменять определенные клипы для разных персонажей.
Запекайте анимации, основанные на физических принципах: если ваши анимации основаны на физических принципах или являются процедурными, запекайте их в анимационные клипы, чтобы избежать вычислений во время выполнения.
Оптимизация скелетной риг-системы: использование меньшего количества костей в риге позволяет снизить сложность и потребление памяти.
- Избегайте чрезмерного количества костей при работе с мелкими или неподвижными предметами.
- Если определенные кости не анимированы или не нужны, удалите их из рига.
Уменьшите продолжительность анимационного ролика.
- Обрезайте анимационные клипы, оставляя только необходимые кадры. Избегайте хранения неиспользуемых или чрезмерно длинных анимаций.
- Вместо создания длинных клипов с повторяющимися движениями используйте зацикленные анимации.
Убедитесь, что подключен или активирован только один компонент анимации. Например, отключите или удалите устаревшие компоненты анимации , если вы используете Animator .
Избегайте использования аниматора, если он не нужен. Для простых визуальных эффектов используйте библиотеки для анимации или реализуйте визуальный эффект в скрипте. Система аниматора может быть ресурсоемкой, особенно на недорогих мобильных устройствах.
При обработке большого количества анимаций используйте систему заданий (Job System) , поскольку эта система была полностью переработана для повышения эффективности использования памяти.
Сцены
При загрузке новых сцен в качестве зависимостей добавляются ресурсы. Однако без надлежащего управления жизненным циклом ресурсов эти зависимости не отслеживаются счетчиками ссылок. В результате ресурсы могут оставаться в памяти даже после выгрузки неиспользуемых сцен, что приводит к фрагментации памяти.
- Используйте пул объектов Unity для повторного использования экземпляров GameObject для повторяющихся элементов игрового процесса, поскольку пул объектов использует стек для хранения коллекции экземпляров объектов для повторного использования и не является потокобезопасным. Минимизация
InstantiateиDestroyобъектов повышает как производительность ЦП, так и стабильность памяти. - Разгрузка активов:
- Разгружайте ресурсы стратегически в менее критичные моменты, такие как заставки или экраны загрузки.
- Частое использование метода
Resources.UnloadUnusedAssetsприводит к скачкам нагрузки на ЦП из-за больших внутренних операций мониторинга зависимостей. - Проверьте наличие значительных скачков загрузки ЦП в маркере профилирования GC.MarkDependencies . Удалите или уменьшите частоту его выполнения и вместо этого вручную выгружайте определенные ресурсы с помощью Resources.UnloadAsset , а не полагайтесь на всеобъемлющий
Resources.UnloadUnusedAssets().
- Вместо постоянного использования Resources.UnloadUnusedAssets, следует перестраивать сцены.
- Вызов метода
Resources.UnloadUnusedAssets()дляAddressablesможет непреднамеренно выгрузить динамически загружаемые пакеты. Тщательно управляйте жизненным циклом динамически загружаемых ресурсов.
Разнообразный
Фрагментация, вызванная переходами между сценами — При вызове метода
Resources.UnloadUnusedAssets()Unity выполняет следующие действия:- Освобождает память для ресурсов, которые больше не используются.
- Выполняет операцию, аналогичную сборке мусора, для проверки кучи управляемых и собственных объектов на наличие неиспользуемых ресурсов и их выгрузки.
- Очищает память текстур, сеток и ресурсов при условии отсутствия активных ссылок.
AssetBundleилиAddressable— внесение изменений в эту область является сложным процессом и требует коллективных усилий команды для реализации стратегий. Однако, после освоения этих стратегий, значительно улучшается использование памяти, уменьшается размер загружаемых файлов и снижаются затраты на облачные сервисы. Для получения дополнительной информации об управлении ресурсами в Unity с помощьюAddressablesсм. раздел «AssetBundle».Централизованное управление зависимостями: систематически группирует общие зависимости, такие как шейдеры, текстуры и шрифты, в отдельные пакеты или группы
Addressable. Это уменьшает дублирование и обеспечивает эффективную выгрузку ненужных ресурсов.Используйте
Addressablesдля отслеживания зависимостей — Addressables упрощают загрузку и выгрузку, автоматически удаляя зависимости, на которые больше не ссылаются. Переход кAddressablesдля управления контентом и разрешения зависимостей может быть жизнеспособным решением в зависимости от конкретных условий игры. Анализируйте цепочки зависимостей с помощью инструмента Analyze, чтобы выявить ненужные дубликаты или зависимости. В качестве альтернативы, если вы используете AssetBundles, обратитесь к Unity Data Tools.TypeTrees— еслиAddressablesиAssetBundlesвашей игры собираются и развертываются с использованием той же версии Unity, что и у игрока, и не требуют обратной совместимости с другими сборками игрока, рассмотрите возможность отключения записиTypeTree, что должно уменьшить размер пакета и объем памяти, занимаемый сериализованными файловыми объектами. Измените процесс сборки в локальном пакете Addressables , установив ContentBuildFlags в значение DisableWriteTypeTree .
Пишите код, оптимизированный для сборщика мусора.
Unity использует сборку мусора (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() . Однако, если другие ссылки на сцену или объект GameObject не были очищены, управляемая память может сохраняться как «утекший объект оболочки» (Leaked Shell Object ). Для получения дополнительной информации об управляемых объектах оболочки обратитесь к руководству по управляемым объектам оболочки .
Кроме того, утечки памяти могут быть вызваны подписками на события, лямбда-выражениями и замыканиями, конкатенацией строк и некорректным управлением объектами пула:
- Для начала ознакомьтесь с инструкцией по поиску утечек памяти, чтобы правильно сравнивать снимки памяти Unity.
- Проверьте наличие подписок на события и утечек памяти. Если объекты подписываются на события (например, через делегаты или UnityEvents), но не отписываются должным образом перед уничтожением, менеджер событий или издатель могут сохранять ссылки на эти объекты. Это препятствует сборке мусора для этих объектов, что приводит к утечкам памяти.
- Отслеживайте глобальные события или события отдельных классов, которые не отменяются при уничтожении объекта. Например, отписывайтесь от делегатов или отключайте их в деструкторах объектов.
- Убедитесь, что уничтожение объектов из пула полностью аннулирует ссылки на компоненты текстовой сетки , текстуры и родительские игровые объекты.
- Следует помнить, что при сравнении снимков Unity Memory Profiler и обнаружении разницы в потреблении памяти без явной причины , эта разница может быть вызвана графическим драйвером или самой операционной системой.
Фрагментация памяти
Фрагментация памяти происходит, когда множество небольших выделений освобождаются в случайном порядке. Выделение памяти в куче происходит последовательно, это означает, что новые блоки памяти создаются, когда в предыдущем блоке заканчивается место. Следовательно, новые объекты не заполняют пустые области старых блоков, что приводит к фрагментации. Кроме того, большие временные выделения могут вызвать постоянную фрагментацию на протяжении всей игровой сессии.
Эта проблема особенно актуальна, когда крупные краткосрочные инвестиции осуществляются рядом с долгосрочными.
Распределение ресурсов должно осуществляться группами в зависимости от срока их действия; в идеале, ресурсы с длительным сроком действия следует распределять одновременно, на ранних этапах жизненного цикла приложения.
Наблюдатели и организаторы мероприятий
- Помимо проблемы, упомянутой в разделе «Утечки памяти» , со временем утечки памяти могут способствовать фрагментации, оставляя неиспользуемую память, выделенную для объектов, которые больше не используются.
- Убедитесь, что уничтожение объектов из пула полностью аннулирует ссылки на компоненты текстовой сетки , текстуры и родительские
GameObjects. - Менеджеры событий часто создают и хранят списки или словари для управления подписками на события. Если они динамически увеличиваются и уменьшаются во время выполнения, это может способствовать фрагментации памяти из-за частого выделения и освобождения памяти.
Код
- Иногда сопрограммы выделяют память, чего легко избежать, кэшируя оператор возврата IEnumerator вместо того, чтобы каждый раз объявлять новый.
- Необходимо постоянно отслеживать состояния жизненного цикла объектов в пуле, чтобы избежать появления «фантомных» ссылок
UnityEngine.Object.
Ресурсы
- Используйте динамические системы резервного копирования для текстовых игр, чтобы избежать предварительной загрузки всех шрифтов в многоязычных случаях.
- Организуйте ресурсы (например, текстуры и частицы) по типу и предполагаемому жизненному циклу.
- Уменьшите количество ресурсов с неиспользуемыми атрибутами жизненного цикла, такими как избыточные изображения пользовательского интерфейса и статические сетки.
Распределения, основанные на продолжительности жизни
- Для обеспечения компактного распределения ресурсов следует выделять долгосрочные активы на начальном этапе жизненного цикла приложения.
- Для ресурсоемких или временных структур данных (например, физических кластеров) используйте NativeCollections или пользовательские распределители памяти.
Действия с памятью, связанные с кодом и исполняемыми файлами.
Игровые исполняемые файлы и плагины также влияют на использование памяти.
Метаданные IL2CPP
IL2CPP генерирует метаданные для каждого типа (например, классов, обобщений и делегатов) во время сборки, которые затем используются во время выполнения для рефлексии, проверки типов и других операций, специфичных для среды выполнения. Эти метаданные хранятся в памяти и могут существенно влиять на общий объем памяти, занимаемый приложением. Кэш метаданных IL2CPP вносит значительный вклад во время инициализации и загрузки. Кроме того, IL2CPP не дедуплицирует некоторые элементы метаданных (например, обобщенные типы или сериализованную информацию), что может привести к чрезмерному использованию памяти. Это усугубляется повторяющимся или избыточным использованием типов в проекте.
Метаданные IL2CPP можно сократить следующим образом:
- Следует избегать использования API рефлексии , поскольку они могут вносить существенный вклад в выделение метаданных IL2CPP.
- Отключение встроенных пакетов
- Внедрение полного совместного использования обобщенных типов в Unity 2022 должно помочь снизить накладные расходы, связанные с обобщениями. Однако для дальнейшего сокращения выделений памяти следует уменьшить использование обобщений.
Удаление кода
Помимо уменьшения размера сборки, удаление кода также снижает потребление памяти. При сборке с использованием скриптового бэкенда IL2CPP удаление управляемого байт-кода (активированное по умолчанию) удаляет неиспользуемый код из управляемых сборок. Процесс работает путем определения корневых сборок, а затем использования статического анализа кода для определения того, какой другой управляемый код используют эти корневые сборки. Любой недоступный код удаляется. Для получения дополнительной информации об удалении управляемого кода см. статью в блоге «TTales from the optimization trenches: Better managed code stripping with Unity 2020 LTS» и документацию по удалению управляемого кода .
Местные распределители
Поэкспериментируйте с собственными распределителями памяти, чтобы точно настроить их работу. Если игре не хватает памяти, используйте меньшие блоки памяти, даже если это потребует более медленных распределителей. Подробнее см. пример динамического распределителя памяти в куче .
Управление собственными плагинами и SDK.
Найдите проблемный плагин — удалите каждый плагин и сравните снимки памяти игры. Это включает в себя отключение значительной части функциональности кода с помощью символов определения скриптов и рефакторинг сильно связанных классов с интерфейсами. Проверьте свой код, используя шаблоны игрового программирования, чтобы упростить процесс отключения внешних зависимостей, не делая игру неиграбельной.
Свяжитесь с автором плагина или SDK — большинство плагинов не являются открытым исходным кодом.
Воспроизведите проблему с использованием памяти плагином — вы можете написать простой плагин (используйте этот плагин Unity в качестве примера), который будет выделять память. Проверьте снимки памяти с помощью Android Studio (поскольку Unity не отслеживает эти выделения) или вызовите класс
MemoryInfoи методRuntime.totalMemory()в том же проекте.
Плагин Unity выделяет память для Java и нативных приложений; вот как это сделать:
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
}