Оптимизация производительности игр в Unity

Практический курс по повышению FPS и стабильности проектов на Unity. Вы научитесь находить узкие места, оптимизировать рендеринг и код, а также эффективно управлять памятью.

1. Основы профилирования: работа с Unity Profiler и Frame Debugger

Основы профилирования: работа с Unity Profiler и Frame Debugger

Оптимизация игры — это не гадание на кофейной гуще и не случайное удаление объектов со сцены в надежде на повышение FPS. Это инженерный процесс, основанный на точных данных. Прежде чем менять код или сжимать текстуры, необходимо ответить на вопрос: «Где именно теряется производительность?». Инструменты для ответа на этот вопрос — Unity Profiler и Frame Debugger.

Золотое правило оптимизации

Главный принцип работы с производительностью звучит так: сначала профилирование, потом оптимизация. Попытки ускорить игру без замеров называются «преждевременной оптимизацией» и часто приводят к усложнению кода без реального прироста кадров в секунду.

Ваша цель — уложиться в бюджет времени кадра. Бюджет зависит от целевой частоты кадров (FPS).

где — время, доступное для отрисовки одного кадра в миллисекундах, а — желаемое количество кадров в секунду.

Для 60 FPS бюджет составляет 16.6 мс. Для 30 FPS — 33.3 мс. Если ваша игра тратит на кадр 20 мс, она никогда не выдаст стабильные 60 FPS. Профилировщик показывает, на что именно уходят эти миллисекунды.

Unity Profiler: Центр управления полетами

Unity Profiler — это основной инструмент для анализа производительности. Он собирает данные о работе CPU, GPU, памяти, рендеринге, физике и звуке.

Чтобы открыть его, перейдите в меню: Window > Analysis > Profiler (или нажмите Ctrl+7).

!Интерфейс Unity Profiler с основными модулями анализа

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

Критически важный момент: никогда не доверяйте данным профилирования в редакторе Unity на 100%. Редактор добавляет огромный оверхед (накладные расходы): отрисовка интерфейса редактора, обработка Gizmos, работа фоновых служб Unity. Эти процессы потребляют ресурсы CPU и памяти, искажая картину.

Для получения достоверных данных необходимо:

  • Сделать Development Build (в Build Settings поставьте галочки Development Build и Autoconnect Profiler).
  • Запустить игру на целевом устройстве (телефон, консоль, другой ПК).
  • В окне Profiler выбрать устройство в выпадающем меню Play Mode.
  • Модуль CPU Usage

    Это самый часто используемый модуль. Он показывает, чем занят центральный процессор. В нижней части окна есть несколько режимов отображения, два из которых наиболее полезны:

  • Timeline (Временная шкала): Показывает потоки и порядок выполнения задач. Здесь видно, как задачи распределяются по времени и есть ли простои, когда CPU ждет завершения работы GPU.
  • Hierarchy (Иерархия): Группирует вызовы функций и сортирует их по нагрузке. Это лучший способ найти конкретный скрипт, который тормозит игру.
  • В режиме Hierarchy обратите внимание на колонки: * Total %: Какой процент времени кадра заняла функция (включая вложенные вызовы). * Self %: Сколько времени заняла сама функция (без учета вложенных вызовов). * GC Alloc: Объем памяти, выделенный сборщиком мусора в этом кадре.

    Если вы видите высокое значение в GC Alloc (например, несколько килобайт каждый кадр) — это проблема. Частые выделения памяти приводят к работе сборщика мусора (Garbage Collector), который вызывает микро-фризы (лаги).

    Deep Profiling

    По умолчанию Unity профилирует только основные движковые вызовы и методы Start, Update, FixedUpdate. Если тормозит метод, который вы вызываете внутри Update, обычный профилировщик покажет только общее время Update.

    Кнопка Deep Profile в верхней панели заставляет Unity инструментировать каждый вызов C# метода. Это позволяет увидеть полную иерархию вызовов.

    Осторожно: Deep Profiling добавляет колоссальную нагрузку на CPU и потребляет много памяти. На слабых устройствах игра может просто зависнуть. Используйте этот режим только для точечного поиска проблем и выключайте сразу после нахождения узкого места.

    Frame Debugger: Анатомия кадра

    Если Profiler отвечает на вопрос «Сколько времени это заняло?», то Frame Debugger отвечает на вопрос «Как это было нарисовано?».

    Frame Debugger позволяет остановить игру на одном кадре и просмотреть список всех команд отрисовки (Draw Calls), отправленных на видеокарту, шаг за шагом.

    Открыть инструмент: Window > Analysis > Frame Debugger.

    Как с этим работать

  • Нажмите Enable во время игры.
  • Игра встанет на паузу.
  • Слева появится список всех Draw Calls.
  • Перемещаясь по списку, вы увидите, как сцена собирается по частям: сначала фон, потом геометрия, тени, пост-эффекты и UI.
  • !Пошаговая сборка кадра в Frame Debugger

    Зачем это нужно?

    Главная задача Frame Debugger — понять, почему объекты не батчатся (не объединяются в один вызов отрисовки).

    В Unity есть механизм Batching (батчинг). Если у вас есть 100 одинаковых деревьев, Unity постарается нарисовать их за 1 раз, а не за 100. Это значительно экономит ресурсы CPU.

    Если вы выберете Draw Call в Frame Debugger, справа в панели будет написано, почему этот объект рисуется отдельно. Например:

    > "Objects have different materials" — объекты используют разные материалы. > "Objects have different lightmaps" — объекты запечены в разные карты света.

    Эта информация бесценна для оптимизации рендеринга. Часто бывает, что вы случайно изменили параметр материала у одного объекта, и это сломало батчинг для всей группы, удвоив нагрузку на процессор.

    Типичные проблемы, выявляемые профилированием

    1. Debug.Log в релизе

    Частая ошибка новичков — оставлять логирование в методах Update. Запись текста в консоль — это очень тяжелая операция, связанная с выделением памяти и работой со строками.

    В профилировщике это будет видно как огромные пики в DebugLogHandler и большие значения GC Alloc из-за конкатенации строк.

    2. GetComponent в Update

    Поиск компонентов — не бесплатная операция.

    Профилировщик покажет высокую нагрузку в GetComponent. Решение: кэшировать компонент в методе Start.

    3. Разрыв батчинга

    Если Frame Debugger показывает 2000 Draw Calls для простой сцены, проверьте материалы. Возможно, вы используете Renderer.material вместо Renderer.sharedMaterial в скриптах. Обращение к .material создает уникальную копию материала для объекта, делая его невозможным для батчинга с другими такими же объектами.

    Итоги

    * Оптимизация начинается только после получения данных профилирования. Не оптимизируйте наугад. * Профилировать нужно на целевом устройстве (билде), а не в редакторе, чтобы исключить оверхед Unity Editor. * Unity Profiler помогает найти «тяжелые» скрипты и утечки памяти (GC Alloc). * Frame Debugger позволяет разобрать кадр по слоям и понять причины разрыва батчинга (почему объекты не рисуются вместе). * Следите за бюджетом кадра: для 60 FPS у вас есть всего 16.6 мс.

    2. Оптимизация графики: Draw Calls, батчинг и работа с освещением

    Оптимизация графики: Draw Calls, батчинг и работа с освещением

    В предыдущей статье мы научились находить узкие места с помощью Profiler и Frame Debugger. Часто выясняется, что игра тормозит не из-за сложной логики C#, а из-за того, как мы отправляем графику на отрисовку. Центральный процессор (CPU) просто не успевает подготавливать команды для видеокарты (GPU). В этой статье мы разберем, как снизить нагрузку на CPU с помощью батчинга и оптимизации освещения.

    Анатомия Draw Call и SetPass Call

    Чтобы нарисовать объект на экране, CPU должен отправить команду GPU. Это называется Draw Call (вызов отрисовки). Однако сам по себе вызов отрисовки — это лишь полбеды. Гораздо дороже обходится смена состояния рендера, известная как SetPass Call.

    Представьте, что вы художник. * Draw Call — это мазок кистью. * SetPass Call — это смена кисти, смешивание новой краски и мытье палитры.

    Если вы рисуете лес и для каждого дерева меняете кисть, вы потратите 90% времени на подготовку и только 10% на рисование. Unity работает так же: если объекты используют разные материалы (разные шейдеры, текстуры или параметры), CPU вынужден перенастраивать GPU перед каждым объектом.

    !CPU готовит команды (Draw Calls и SetPass), а GPU их исполняет. Слишком частая смена материалов перегружает CPU.

    Батчинг: объединяй и властвуй

    Батчинг (Batching) — это процесс объединения нескольких объектов в один Draw Call. Главное условие для любого вида батчинга: объекты должны использовать один и тот же материал.

    1. Static Batching (Статический батчинг)

    Это самый эффективный метод для неподвижных объектов (стены, пол, декорации). Unity заранее объединяет меши (Mesh) объектов в один большой меш.

    Как включить:

  • Выберите объект в инспекторе.
  • Поставьте галочку Static (или конкретно Batching Static).
  • Убедитесь, что в Project Settings > Player включен Static Batching.
  • Плюсы: * Значительно снижает нагрузку на CPU.

    Минусы: * Требует больше оперативной памяти, так как хранит копии объединенных мешей.

    Потребление памяти при статическом батчинге можно выразить так:

    где — итоговая память, — количество сбатченных групп, — объем вершинного буфера группы, — объем буфера треугольников (индексов). Если у вас 1000 одинаковых деревьев, Static Batching создаст гигантский меш, содержащий геометрию всех 1000 деревьев, что может переполнить память.

    2. Dynamic Batching (Динамический батчинг)

    Работает для движущихся объектов. Unity каждый кадр собирает вершины объектов в один буфер на CPU и отправляет на GPU.

    Ограничения: * Работает только с низкополигональными моделями (обычно до 300 вершин, зависит от атрибутов шейдера). * Создает нагрузку на CPU для сбора геометрии каждый кадр. * В современных версиях Unity (особенно в URP/HDRP) часто заменяется на SRP Batcher.

    3. GPU Instancing

    Идеальное решение для отрисовки огромного количества одинаковых мешей (трава, астероиды, пули, юниты в RTS). В отличие от Static Batching, здесь не создается гигантский меш. GPU получает одну модель и список координат, где её нужно нарисовать.

    Как включить: В материале объекта поставьте галочку Enable GPU Instancing.

    Если вы хотите менять цвет или свойства для каждого инстанса, нельзя просто менять material.color (это создаст копию материала и сломает батчинг). Используйте MaterialPropertyBlock:

    Текстурные атласы

    Поскольку батчинг требует одинакового материала, а материал ссылается на одну текстуру, возникает проблема: как сбатчить деревянный стол и железный стул?

    Решение — Текстурный атлас. Это одна большая текстура, содержащая изображения для множества объектов.

    !Объединение нескольких текстур в один атлас позволяет использовать один материал для разных объектов.

    Используя атлас, вы можете назначить один материал на разные модели, просто меняя UV-координаты. Это позволяет рисовать всю комнату (стены, мебель, мелкие предметы) за 1 Draw Call.

    Оптимизация освещения

    Освещение — одна из самых тяжелых операций для GPU и CPU. Каждый источник света, влияющий на объект, усложняет шейдер и добавляет проходы отрисовки.

    Forward Rendering и Pixel Lights

    В классическом Forward Rendering сложность отрисовки объекта зависит от количества источников света, которые на него светят. Количество Draw Calls () можно приблизительно оценить формулой:

    где — количество вызовов отрисовки, — количество объектов, — количество пиксельных источников света, влияющих на объект.

    Если у вас 10 объектов и 4 источника света, вы можете получить до 50 Draw Calls только на эту группу.

    Решение: Ограничьте Pixel Light Count в настройках качества (Quality Settings). Лишние источники света будут просчитываться повершинно (Vertex Lighting), что гораздо дешевле, хоть и менее красиво.

    Запекание света (Light Baking)

    Самый эффективный способ оптимизации — вообще не считать свет в реальном времени.

    Lightmapping — процесс, когда Unity заранее рассчитывает свет, тени и отскоки лучей (Global Illumination) и сохраняет их в специальные текстуры (лайтмапы). Во время игры эти текстуры просто накладываются на объекты.

    Преимущества: * Нулевая нагрузка на GPU для расчета теней в рантайме. * Фотореалистичное глобальное освещение.

    Недостатки: * Увеличивает размер билда (текстуры занимают место). * Свет статичен (нельзя двигать лампы или стены).

    Light Probes (Световые зонды)

    Лайтмапы работают только для статики. Но как быть с динамическим персонажем, который бежит по темному коридору в светлую комнату? Он должен менять освещенность.

    Для этого используются Light Probes. Это точки в пространстве, которые хранят информацию о свете в данной зоне. Динамический объект интерполирует цвет между ближайшими зондами.

    Использование зондов дешевле, чем освещение в реальном времени, так как вычисления происходят на уровне простых математических операций, а не тяжелых шейдерных расчетов теней.

    Тени

    Тени — это, по сути, отрисовка геометрии еще раз с точки зрения источника света (Shadow Map).

  • Hard Shadows дешевле, чем Soft Shadows (меньше фильтрации).
  • Shadow Distance — критический параметр. Нет смысла рисовать тени на расстоянии 500 метров. Уменьшите дистанцию в Quality Settings, чтобы выиграть производительность.
  • Используйте Shadow Cascades (каскадные тени), чтобы тени вблизи были четкими, а вдали — размытыми, но дешевыми.
  • Итоги

  • Draw Calls и SetPass Calls — главные враги CPU. Снижайте их количество, объединяя объекты.
  • Используйте Static Batching для неподвижных объектов (ценой оперативной памяти) и GPU Instancing для множества одинаковых объектов.
  • Объединяйте текстуры в Атласы, чтобы разные модели могли использовать один материал.
  • Избегайте лишних источников света в реальном времени (Real-time). Используйте запекание (Baking) и Light Probes везде, где это возможно.
  • Настраивайте дистанцию теней (Shadow Distance) под нужды геймплея, не оставляйте значения по умолчанию.