Оптимизация производительности
Проект запускается на топовых девайсах разработчика без вопросов. На mid-range Android 2020 года — 20 fps и перегрев через 5 минут. На iPhone 11 — стабильные 60, но на iPhone XR — просадки в тяжёлых сценах. Это стандартная ситуация, когда оптимизация не закладывалась в архитектуру с самого начала, а делается «потом» — что всегда дороже и болезненнее.
Инструменты профилирования
Прежде чем оптимизировать — измерить. Оптимизация без профилирования — это угадывание.
| Инструмент | Назначение |
|---|---|
| Unity Profiler | CPU/GPU время по системам, GC allocations, audio |
| Frame Debugger | Инспекция каждого draw call в кадре |
| Memory Profiler | Снимок памяти, граф зависимостей ассетов |
| RenderDoc | Глубокий анализ GPU-состояния, актуален для PC/Console |
| Android GPU Inspector | Профилирование GPU на реальном Android-устройстве |
| Xcode Instruments | GPU + memory на iOS (Metal Performance HUD) |
| Snapdragon Profiler | Qualcomm GPU — детальная статистика шейдеров |
Правило первое: профилируйте на целевом железе, а не в редакторе. Editor добавляет существенный оверхед — цифры из Play Mode не репрезентативны для сборки.
Правило второе: смотрите на GPU-время и CPU-время раздельно. Bottleneck может быть на CPU (слишком много draw calls, тяжёлая логика), на GPU (сложные шейдеры, overdraw, fillrate), или в памяти (GC allocations, texture streaming). Лечение разное.
Оптимизация Draw Calls: батчинг в деталях
Draw call — команда CPU к GPU «нарисуй это». Каждый вызов имеет overhead на стороне CPU независимо от сложности геометрии. На мобильных 200-300 draw calls за кадр — уже граница, после которой начинаются проблемы. Цель: минимизировать количество draw calls, объединяя геометрию с одинаковым материалом.
Static Batching
Объединяет статичные (неподвижные) меши в единый большой меш при сборке или на старте сцены. Требования:
-
Staticфлаг на объекте (или хотя быBatching Static) - Одинаковый материал
Плюсы: нет CPU-overhead в рантайме, работает со всеми платформами. Минусы: увеличивает потребление памяти (объединённый меш хранится отдельно от оригинала) и время загрузки сцены. Для сцен с тысячами статичных объектов — осторожно с памятью, смотрим через Memory Profiler.
Dynamic Batching
Объединяет меши в рантайме для каждого кадра. Требования жёстче:
- Меньше 900 вертексных атрибутов на меш (Unity ограничение)
- Одинаковый материал
- Одинаковый масштаб (или не-негативный масштаб по одной оси)
На практике Dynamic Batching эффективен только для мелких объектов (партиклы, UI-элементы, мелкий дебрис). Для персонажей и окружения обычно не подходит из-за ограничения по вертексам. В URP Dynamic Batching по умолчанию отключён — его вытеснил SRP Batcher.
SRP Batcher
SRP Batcher — не классический батчинг геометрии, а оптимизация CPU-overhead при подготовке draw calls. Вместо того чтобы каждый кадр заново загружать uniform-данные шейдера (матрицы, свойства материала), SRP Batcher кэширует их в GPU-памяти и обновляет только изменившиеся.
Результат: draw calls остаются по количеству прежними, но каждый занимает меньше времени CPU. В сценах с большим количеством уникальных материалов SRP Batcher даёт ощутимый прирост — иногда 2-3x по CPU-времени рендера.
Требование: шейдер должен быть совместим с SRP Batcher — объявлять все per-object свойства в UnityPerDraw CBUFFER. Стандартные URP Lit/Unlit шейдеры совместимы. Кастомные шейдеры — проверяем в Inspector материала: покажет SRP Batcher compatible: Yes/No.
GPU Instancing
Для множества копий одного и того же меша с одним материалом (деревья, трава, NPC одного типа, снаряды). GPU Instancing отправляет один draw call с массивом per-instance данных (матрицы трансформации, цвет) — GPU рисует все копии за один раз.
Включается на материале: Enable GPU Instancing checkbox. В шейдере — поддержка через UNITY_INSTANCING_BUFFER (стандартные URP шейдеры поддерживают). Ограничение: все инстансы в одном batch должны иметь одинаковый материал и меш.
Graphics.DrawMeshInstanced / Graphics.DrawMeshInstancedIndirect — для процедурного рендеринга инстансов без GameObject overhead (трава, частицы, процедурный контент). Indirect-версия позволяет формировать список инстансов на GPU через Compute Shader.
Сравнение подходов:
| Метод | Лучший сценарий | Ограничение |
|---|---|---|
| Static Batching | Статичное окружение | Память |
| SRP Batcher | Много уникальных материалов | CPU overhead только |
| GPU Instancing | Много копий одного объекта | Одинаковый материал/меш |
| Dynamic Batching | Мелкие объекты в URP | Вертексное ограничение |
Оптимизация памяти для мобильных
Мобильные платформы — жёсткие ограничения по RAM. iOS убивает приложение при превышении памяти без предупреждения (memory pressure kill). Android — аналогично, но с onLowMemory callback. Целевые бюджеты:
- iOS: < 1 GB для современных устройств, < 512 MB для поддержки iPhone 8/X
- Android: < 800 MB для широкой совместимости, учитывать что Android сам занимает ~400-600 MB
Addressables и Asset Bundles
Загружать всё сразу при старте — неприемлемо для больших проектов. Addressables (надстройка над Asset Bundles) — система адресуемой асинхронной загрузки ассетов.
Ключевые принципы:
Явная выгрузка: Addressables.ReleaseInstance / Addressables.Release. Addressables не выгружают ассеты автоматически при уничтожении объекта. Типичная ошибка: Addressables.InstantiateAsync в цикле без Release — память растёт до краша.
Reference counting: ассет выгружается только когда все его handles освобождены. Архитектурный паттерн: сервис/менеджер держит handle загруженного ассета, освобождает при переходе между сценами или явном вызове.
Groups и Bundle Strategy: группируем ассеты по логике загрузки:
-
Pack Together— все ассеты группы в одном bundle (загрузка одним запросом) -
Pack Separately— каждый ассет в своём bundle (гранулярная загрузка) -
Pack Together by Label— по меткам (гибкий вариант)
Для уровней: все ассеты одного уровня в одном bundle. Шаренные ассеты (общие текстуры UI, шрифты) — в отдельной группе с Prevent Updates для стабильного кэша.
Texture Memory
Текстуры — основной потребитель памяти в большинстве игр. Анализ через Memory Profiler: вкладка All Of Memory → Texture2D — сразу виден список самых тяжёлых текстур.
Практические меры:
-
Mipmap: для 3D-текстур — включить, для UI — выключить (
Advanced > Generate Mip Maps: false). UI-текстуры рендерятся в фиксированном пространстве, mipmap только тратит память -
Max Size: проверить, не завышен ли
Max Sizeв Import Settings. 4096 для мобильной иконки — типичная ошибка - Крассовская проблема: текстуры, на которые ссылаются неиспользуемые Materials в памяти — Memory Profiler покажет reference chain
-
Streaming Mipmaps: для open world —
Texture Streamingв Quality Settings. Загружает mip-уровни по мере приближения камеры
GC Allocations
C# garbage collector в Unity — stop-the-world. Если за кадр аллоцировано много heap-памяти, GC-пауза вызовет видимый фриз. Цель: нулевые аллокации в hot path (Update, FixedUpdate, рендер).
Типичные источники аллокаций, которые находим в Profiler:
-
stringконкатенация в Update ("Score: " + score→StringBuilderилиstring.Format) - LINQ в hot path (
Where,Select,ToList→ ручные циклы с предаллоцированными списками) -
GetComponent<T>()каждый кадр → кэшировать вAwake/Start -
new Vector3()и другие value types в некоторых паттернах — проверять Profiler - Boxing value types при передаче в
objectпараметры
LOD и Culling
LOD Group — переключение на упрощённую геометрию при удалении объекта от камеры. Стандарт для 3D окружения: LOD0 (100%), LOD1 (30-50% треугольников), LOD2 (10-15%), Culled (объект невидим). Для мобильных порог Culled ставим агрессивнее — меньше рисуем за кадр.
Occlusion Culling — Unity не рендерит объекты за стенами и препятствиями. Требует запечённые occlusion данные (Window > Rendering > Occlusion Culling > Bake). Для открытых пространств эффект минимален, для indoor сцен — существенен.
Frustum Culling работает автоматически — объекты вне FOV камеры не рендерятся. Но draw call на проверку всё равно происходит. Для сцен с тысячами объектов — кастомный spatial partitioning (Quadtree, Octree) для ускорения culling-теста.
Оптимизация VR
VR — отдельный класс задач. Фреймрейт 72/90 Hz нельзя нарушать, иначе motion sickness. Дополнительно к стандартным методам:
- Single Pass Instanced Rendering — рендер для обоих глаз за один проход (см. VR-раздел)
- Fixed Foveated Rendering (Quest) — снижение разрешения на периферии
- Late Latching (Quest 3) — обновление позиции контроллера максимально поздно перед рендером, снижает perceived latency
- Dynamic Resolution в URP/HDRP — автоматическое снижение разрешения рендера при просадке fps
Для Quest профилируем через OVR Metrics Tool — показывает CPU/GPU time прямо в гарнитуре в рантайме, что удобнее чем профилирование через USB.





