Оптимізація продуктивності
Проект запускається на топових девайсах розробника без запитань. На mid-range Android 2020 року — 20 fps та перегрів через 5 хвилин. На iPhone 11 — стабільні 60, але на iPhone XR — просадки в важких сценах. Це стандартна ситуація, коли оптимізація не закладалася в архітектуру з самого початку, а робиться «потім» — що завжди дорожче та болісніше.
Інструменти профілювання
Перш ніж оптимізувати — виміряти. Оптимізація без профілювання — це гадання.
| Інструмент | Призначення |
|---|---|
| Unity Profiler | CPU/GPU час по системам, GC allocations, аудіо |
| 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 вбиває додаток при перевищенні пам'яті без попередження. 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.





