lenec ru

← все посты

Профилирование Unity-игры на мобилке: Profiler workflow и реальные узкие места

11K

Чтобы оптимизировать Unity-игру на мобилке, не нужно угадывать. Нужен Profiler и понимание, что в нём смотреть. Я помогал доводить пять-шесть мобильных проектов до приемлемого фреймрейта, и почти всегда первая итерация — это не «найти один волшебный гвоздь», а «срезать пять мест, каждое из которых ест по 2 мс».

Расскажу workflow, которым я пользуюсь сейчас в Unity 6 (6000.x), и типичные классы проблем — что искать в каком окне Profiler'а.

Подключение Profiler'а к устройству

Сначала важное: профилировать в редакторе бесполезно. Числа не совпадают с реальными на устройстве в десятки раз. Editor — для качественного дебага, не для оптимизации.

Что делаешь:

  1. В Build Settings включи Development Build и Autoconnect Profiler.
  2. Для Android — отметь Deep Profiling Support (включать саму Deep Profiling — только когда нужно, замедляет всё). Для iOS — то же.
  3. Собрать билд, залить, запустить.
  4. В Unity открыть Window → Analysis → Profiler. Вверху — комбобокс выбора устройства. Если устройство не появляется на Android, проверь, что adb видит его (adb devices).

Для iOS отдельный артефакт: при первой попытке профилировать билд отвалится с keychain-ошибкой. Открываешь Xcode, собираешь оттуда, доверяешь сертификату. Дальше Profiler подцепится.

На Quest и Pico схема та же, только через USB или ADB-over-Wi-Fi.

Что смотреть в первую очередь

Открыл Profiler — первым делом смотри CPU-секцию. Это главный фронт битвы на мобилках.

Включи режим Hierarchy (вверху Profiler) и сортировку по Time ms. Останови запись на момент, когда чувствуешь лаг. В корне обычно один из:

  • PlayerLoop — основной игровой цикл.
  • EditorLoop — если профилишь редактор (плохо, мы это не делаем).
  • Render Thread — рендер, отдельный поток.

Раскрываешь PlayerLoop, смотришь, кто из Update, FixedUpdate, LateUpdate съедает больше 33% фрейма. Дальше — кто внутри них. Профайлер покажет твои MonoBehaviour'ы по имени метода.

Frame Debugger — отдельный must

Если просадка на render thread, иди в Window → Analysis → Frame Debugger. Он показывает, что рендерилось, в каком порядке, сколько draw call'ов, какие шейдеры. На мобилке норма — 100–200 draw call'ов. 500+ почти всегда означает, что batching не работает.

Главные источники лагов на мобиле

Draw calls и batching

Самый частый виновник на бюджетных Android'ах. Каждый draw call — это работа CPU + GPU. На GPU мобилок (Mali, Adreno) 200+ вызовов уже могут уронить fps до 30.

Что смотреть:

  • SRP Batcher в URP — должен быть включён в URP Asset. Большинство современных шейдеров его поддерживают.
  • Static batching — для статичной геометрии. Помечай Static в инспекторе для не-двигающихся объектов.
  • GPU Instancing — для одинаковых мешей с разными трансформами (трава, камни). В материале галочка Enable GPU Instancing.
  • Атласирование текстур — несколько спрайтов в одной текстуре. Sprite Atlas в Unity это умеет, для UI работает отлично.

Frame Debugger показывает причину разрыва batch'а: разные материалы, разные текстуры, разные shadow casting modes. Часто batch разваливается из-за одного материала, отличающегося от остальных.

GC.Alloc и сборщик мусора

Если в профилере видишь регулярные пики на GC.Collect — у тебя проблема с аллокациями. Каждый сборщик мусора в Unity на мобиле — это пауза 5–30 мс. Один пик в 30 мс на 60-fps игре — заметная микро-заикалка.

В Hierarchy сортируй по GC Alloc, не по Time. Видишь, какой метод аллоцирует и сколько байт. Типичные виновники:

  • foreach по List<T> в старых версиях — было аллоцирование энумератора. В .NET 8 уже исправлено, но старые ассеты на это завязаны.
  • Конкатенация строк "score: " + score — каждый раз новая строка. Используй StringBuilder или интерполяцию с FormattableString.
  • GetComponent в Update — он не аллоцирует сам, но GetComponentsInChildren — да. Кэшируй ссылки в Awake.
  • Лямбды с захватом переменных. list.Where(x => x.id == myId) аллоцирует closure. Оборачивай в локальный метод или статический делегат.
  • LINQ вообще. На мобиле — нет. Замени на ручные циклы.
// плохо
var enemies = FindObjectsOfType<Enemy>()
    .Where(e => e.IsAlive)
    .OrderBy(e => Vector3.Distance(transform.position, e.transform.position))
    .ToList();

// лучше
_enemiesBuffer.Clear();
for (var i = 0; i < _allEnemies.Count; i++)
{
    if (_allEnemies[i].IsAlive)
        _enemiesBuffer.Add(_allEnemies[i]);
}
_enemiesBuffer.Sort(_distanceComparer);

Object pooling

Спавнить и уничтожать объекты на ходу — главный путь к GC-боли. Пули, частицы, эффекты — всё через пул.

В Unity 6 есть встроенный ObjectPool<T>, рабочий и без сторонних библиотек:

private ObjectPool<Bullet> _bulletPool;

private void Awake()
{
    _bulletPool = new ObjectPool<Bullet>(
        createFunc: () => Instantiate(_bulletPrefab),
        actionOnGet: b => b.gameObject.SetActive(true),
        actionOnRelease: b => b.gameObject.SetActive(false),
        actionOnDestroy: b => Destroy(b.gameObject),
        defaultCapacity: 32,
        maxSize: 256);
}

public Bullet Spawn()
{
    return _bulletPool.Get();
}

public void Despawn(Bullet bullet)
{
    _bulletPool.Release(bullet);
}

Animator: дороже, чем кажется

На мобилке Animator один на персонажа стоит 0.1–0.3 мс. Десять врагов — 1–3 мс. Если у тебя сотня — это треть фрейма.

Что делать:

  • Включай Optimize Game Objects в Rig-настройках. Удаляет hierarchy bones, экономит draw call'ы и обновление трансформов.
  • Animator Culling Mode — Cull Update Transforms. Анимация не обновится, если объект не виден.
  • Для второстепенных объектов рассмотри замену Animator на собственную лёгкую state-machine с прямыми вызовами. Animation Rigging хорош, но дороже Animator'а.
  • Если есть ECS-секция — для массовых юнитов перенеси анимацию на GPU skinning через Burst.

UI: внезапный пожиратель кадров

UGUI на мобилке часто оказывается виновником. Каждое изменение текста или активация панели вызывает Canvas Rebuild, который проходит layout, графики, masks. На сложных экранах это съедает 5–10 мс.

Что делать:

  • Разбей UI на несколько Canvas'ов. Один на статику (заголовок, фон), один на динамику (счётчики). Rebuild затрагивает только тот canvas, в котором что-то изменилось.
  • Раз в кадр обновляй счётчики, не каждое изменение. Накапливай в буфер, выводишь раз в 0.1 секунды.
  • Не используй Layout Group в производительных местах. Они rebuild'ят на каждое изменение. Жёсткие позиции через RectTransform быстрее.
  • Шрифты в TextMeshPro — на мобиле быстрее обычного Text. Если ещё не мигрировал — мигрируй.
  • UI Toolkit (UI Elements) в Unity 6 для in-game UI пока не везде уверенно работает — но на меню и инвентари смотрит уже неплохо.

Физика и колайдеры

Физика на мобилке — отдельная история. Что знать:

  • Mesh Collider'ы — самые дорогие. Замени на Box/Capsule/Sphere, где возможно.
  • Continuous Collision Detection — только там, где нужно. По умолчанию ставь Discrete.
  • Триггеры тоже считаются. Кучка перекрывающихся Area2D в одной точке — это N² проверок.
  • Layer Collision Matrix — отключи пары слоёв, которые не должны взаимодействовать. По умолчанию Unity проверяет всё со всем.

GPU-сторона: тени, прозрачность, overdraw

На многих мобилках CPU справляется, а GPU тонет. Frame Debugger покажет, что съедает GPU-время:

  • Тени real-time на мобилке — почти всегда too expensive. Bake light'ы для статики, тени персонажей через blob shadow projector или плоский decal.
  • Прозрачность и overdraw — каждый прозрачный спрайт рисуется поверх предыдущего, и на мобилке это болит. В URP есть Overdraw debug view, посмотри, где у тебя пятна красного — там слишком много прозрачных слоёв.
  • Шейдеры. Сложные fragment-shader'ы (parallax, screen-space refraction) на бюджетных GPU убивают fps. Имей лёгкую версию для low-end устройств.
  • Текстуры высокого разрешения. 2048×2048 на мобилке имеет смысл только для главных персонажей. Пропсы можно держать в 512×512 без визуальных потерь.
  • Compression. ASTC для Android, ASTC для iOS (с 2018 уже стандарт). Не PVRTC и не ETC2 — они либо устарели, либо хуже.

Memory Profiler: что смотреть

Окно Window → Analysis → Memory Profiler — отдельная утилита. Делает snapshot текущей памяти. Полезно делать два snapshot'а: «при старте сцены» и «после 10 минут игры», смотреть diff.

Что искать:

  • Текстуры, которые есть в памяти, но в текущей сцене не используются. Скорее всего, кто-то держит ссылку (статический менеджер с кэшем).
  • Дубликаты ScriptableObject'ов. Если один SO загружен дважды — проблема в системе ассетов.
  • Большие массивы строк, особенно с long names — типичный симптом «забыл выключить логирование в release».

Профилирование на нескольких устройствах

На моих проектах тестовый набор устройств для performance — обязателен:

  • Топовый iPhone (последний или предпоследний) — для общего sanity check'а.
  • iPhone SE 2020 или iPhone 8 — нижняя граница iOS, на которой ещё держать 60 fps.
  • Топовый Android (Pixel/Samsung) — отслеживание Mali и Adreno на топе.
  • Бюджетный Android 2–3 года назад (Mediatek Helio P-серия) — нижняя граница для F2P-проекта.

Если у вас есть бюджет — Test Lab или подобные сервисы дают доступ к десяткам устройств. Если нет — три-четыре своих покрывают 80% случаев.

Что я делаю в каждом проекте

Ритуал, который окупается:

  1. Перед каждой большой milestone — снимок Profile'а на трёх референсных устройствах. Сохраняю в репо.
  2. Веду log: «на iPhone 8 на сцене Forest fps был 58, после рефакторинга AI стал 62». Без лога потом не понять, что куда уехало.
  3. Лимиты в проекте: SRP Batcher всегда on, Mesh Collider запрещён без согласования, GameObject со 50+ детьми — ревью.
  4. Раз в месяц — Frame Debugger по горячим сценам. Часто всплывает «откуда здесь 200 draw call'ов от UI».

Чек-лист для первой оптимизационной итерации

Если у тебя сейчас игра в 25 fps, и ты не знаешь, с чего начать:

  1. Включи Profiler на устройстве, открой Hierarchy, отсортируй по Time ms.
  2. Посмотри топ-3 метода. Если это твои Update'ы — вынеси тяжёлое в FixedUpdate, корутины раз в 0.5 с, или в Job.
  3. Сортируй по GC Alloc. Если есть аллокации в Update — почини в первую очередь.
  4. Открой Frame Debugger. Если draw call'ов больше 200 — разбирайся с batching.
  5. Включи Overdraw view. Если экран красный — режь прозрачность.
  6. Посмотри Memory Profiler через 10 минут игры. Если память растёт — есть утечка.

За один день можно поднять fps в полтора-два раза, не трогая геймплей. Главное — мерить, а не угадывать.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.