Знакома ситуация, когда проект работает с рывками и заставляет даже мощный компьютер лагать? Это поправимо.
Цель этой статьи - не просто дать сухие инструкции, а научить тебя видеть и устранять причины низкой производительности. Мы вместе пройдем путь от 30 до 60+ кадров в секунду (FPS).
Представь, что ты строишь дом. Если заложить кривой фундамент, стены пойдут трещинами, как бы хороши они ни были. Оптимизация - это и есть наш прочный фундамент.
Unity Profiler - диагност
Прежде чем что-то "чинить", нужно понять, что именно "сломано". Пытаться оптимизировать вслепую - все равно что искать черную кошку в темной комнате, особенно когда ее там может и нет.
Unity Profiler - это встроенный инструмент, который показывает, на что процессор (CPU), видеокарта (GPU) и память тратят драгоценные миллисекунды.
Как им пользоваться?
Открой окно Profiler через
Window > Analysis > Profiler
.Запусти свою игру в редакторе.
Наблюдай за графиками.
Нас в первую очередь интересуют две вкладки: CPU Usage
и GPU Usage
. Если график CPU
показывает высокие "пики", значит, проблема в логике, скриптах или физике. Если "задыхается" GPU
- виновата графика: шейдеры, текстуры, количество объектов на экране.
CPU Bottlenecks - Узкие места процессора
Процессор - это мозг твоей игры. Он выполняет код, обсчитывает физику, анимации. Если он перегружен, игра начинает "тормозить", даже если видеокарта простаивает.
Проблема №1: Вызовы в Update() и FixedUpdate()
Метод Update()
вызывается каждый кадр. Если у тебя 100 объектов, и у каждого в Update()
происходит что-то ресурсоемкое, это создает огромную нагрузку.
Плохой пример:
using UnityEngine;
public class BadPlayerController : MonoBehaviour
{
// Каждый кадр мы ищем компонент камеры.
void Update()
{
// FindObjectOfType<T>() проходит по всем объектам на сцене, чтобы найти нужный.
Camera mainCamera = FindObjectOfType<Camera>();
// Постоянный поиск объекта по имени - тоже плохая практика.
GameObject enemy = GameObject.Find("StrongEnemy");
}
}
В этом коде каждый кадр мы заставляем Unity искать по всей сцене сначала камеру, а потом врага с именем "StrongEnemy". Это как каждый раз, когда тебе нужен карандаш, ты перерываешь весь свой стол в его поисках, вместо того чтобы просто взять его из стаканчика, куда положил его заранее.
Хороший пример(кэширование):
using UnityEngine;
public class GoodPlayerController : MonoBehaviour
{
// Мы создаем приватное поле для хранения ссылки на камеру.
private Camera _mainCamera;
// И еще одно для врага.
private GameObject _enemy;
// Start() вызывается один раз при создании объекта. Идеальное место для поиска.
void Start()
{
// Мы находим камеру один раз и "запоминаем" ее в нашей переменной.
_mainCamera = Camera.main;
// Врага тоже находим один раз.
_enemy = GameObject.Find("StrongEnemy");
}
void Update()
{
// Теперь в Update() мы просто используем уже найденные объекты.
// Никаких лишних поисков.
if (_mainCamera != null)
{
// Рабочий код
}
}
}
Здесь мы находим все нужные компоненты один раз в методе Start()
и сохраняем ссылки на них. Start()
выполняется лишь единожды при запуске объекта, поэтому дорогостоящие операции поиска не влияют на производительность в процессе игры.
Проблема №2: Сборщик мусора (Garbage Collector, GC)
Когда ты создаешь объекты, выделяется память. Когда объект больше не нужен, "сборщик мусора" эту память освобождает. Звучит полезно, но есть нюанс: когда GC работает, он может на короткое время полностью остановить твою игру. Это и есть те самые неприятные "фризы" или "лаги".
Чаще всего мусор создают строки и создание новых объектов в цикле.
Плохой пример:
using UnityEngine;
using UnityEngine.UI;
public class BadUIUpdater : MonoBehaviour
{
public Text scoreText;
private int _score = 0;
void Update()
{
_score++;
scoreText.text = "Score: " + _score;
// Каждое сложение строк ("Score: " + _score) создает в памяти новый объект строки.
// За секунду при 60 FPS создается 60 ненужных объектов. Это много мусора.
}
}
В этом коде в Update
мы постоянно создаем новую строку. Операция +
для строк не изменяет старую, а создает новую, объединяя две части. Старая строка становится "мусором".
Хороший пример (использование TextMeshPro):
using UnityEngine;
using TMPro; // 1. Подключаем пространство имен для TextMeshPro
public class GoodUIUpdaterTMP : MonoBehaviour
{
// 2. Вместо типа Text используем TextMeshProUGUI
public TextMeshProUGUI scoreText;
private int _score = 0;
void Update()
{
_score++;
// 3. Используем специальный метод SetText, который не создает мусор
scoreText.SetText("Score: {0}", _score);
}
}
Почему TextMeshPro лучше для GC?
Этот вопрос заслуживает отдельного объяснения. Проблема старого компонента Text
в том, что у него есть только одно свойство для изменения - .text
, которое принимает string
. Любая попытка передать туда число заставит C# неявно вызвать метод .ToString()
, который создает в памяти новую строку. Конкатенация (сложение) строк, как в "плохом" примере, создает еще больше мусора.
TextMeshPro был создан для решения этой проблемы.
Его метод SetText()
- это не просто замена для .text
. Он имеет множество "перегрузок", то есть версий для разных типов данных. Когда ты вызываешь scoreText.SetText("Score: {0}", _score);
, происходит следующее:
TextMeshPro
видит, что ты передаешь ему строку-шаблон и число (int
).Он не создает новую строку "Score: 123" в управляемой куче, где работает сборщик мусора.
Вместо этого он использует свой внутренний, предварительно выделенный массив символов (буфер), в который и "собирает" финальную строку.
Эта операция происходит в основном в неуправляемой памяти или с переиспользованием буферов, что не генерирует мусор, за которым пришлось бы приходить сборщику (GC).
Проще говоря, TextMeshPro
- как опытный повар, у которого есть многоразовая посуда (внутренние буферы), в то время как старый Text
для каждого блюда берет новый одноразовый контейнер, который потом нужно выбрасывать. Использование TextMeshPro
- это самый простой и эффективный способ избавиться от "лагов" в UI, связанных с GC.
Альтернативный подход: StringBuilder
Справедливости ради, стоит упомянуть и классический способ борьбы с мусором от строк, который был популярен до повсеместного внедрения TextMeshPro. Это использование класса StringBuilder
из стандартной библиотеки C#.
Его суть в том, что он представляет собой изменяемую строку. Вместо того чтобы создавать новый объект строки при каждом изменении, ты работаешь с одним и тем же объектом-конструктором, что не создает мусора.
Пример с StringBuilder:
using UnityEngine;
using UnityEngine.UI; // Используем старый UI Text для примера
using System.Text; // Подключаем пространство имен для StringBuilder
public class GoodUIUpdaterStringBuilder : MonoBehaviour
{
public Text scoreText;
private int _score = 0;
// Создаем экземпляр StringBuilder один раз, заранее выделив память
private StringBuilder _scoreStringBuilder = new StringBuilder("Score: ", 12);
void Update()
{
// Увеличивает значение переменной _score на единицу в каждом кадре
_score++;
// Мы не создаем новую строку, а изменяем существующую.
_scoreStringBuilder.Length = 7; // Очищаем старое значение, оставляя "Score: "
_scoreStringBuilder.Append(_score); // Добавляем новое число в конец
// Присваиваем результат. Мусора почти нет (ToString() все же выделяет немного).
scoreText.text = _scoreStringBuilder.ToString();
}
}
Этот метод абсолютно рабочий и значительно лучше, чем просто сложение строк. Однако, как ты можешь видеть, он требует больше кода и менее читаем по сравнению с лаконичным методом SetText()
у TextMeshPro
. Сегодня в Unity TextMeshPro
является предпочтительным решением, но знание о StringBuilder
остается полезным для других, более сложных задач, где требуется многократная манипуляция со строками вне UI.
GPU Bottlenecks - Узкие места видеокарты
Если процессор - мозг, то видеокарта - это художник. Она рисует все, что ты видишь на экране. Если заставить ее рисовать слишком много или слишком сложно, FPS упадет.
Проблема №1: Draw Calls (Вызовы отрисовки)
Draw Call - это команда от CPU к GPU на отрисовку одного объекта с одним материалом. Чем больше таких команд, тем хуже. Оптимально - держать их количество как можно ниже.
Решения:
Static Batching: Если у тебя много статичных (неподвижных) объектов с одинаковым материалом (например, заборы, деревья, стены), Unity может объединить их в один большой объект перед отправкой на рендер. Это сильно сокращает Draw Calls. Просто выдели объекты и поставь галочку
Static
в инспекторе.Dynamic Batching: Для маленьких движущихся объектов с одинаковым материалом Unity тоже может их "склеивать" на лету. Работает автоматически, но с ограничениями (например, на количество вершин в модели).
Атласы текстур: Вместо 10 материалов для 10 разных объектов (каждый со своей текстурой), создай одну большую текстуру (атлас), где будут все 10 картинок, и один материал. Так 10 объектов можно будет нарисовать за один Draw Call.
Проблема №2: Физика
Физические расчеты могут быть очень требовательны к процессору. Особенно если на сцене много объектов с компонентом Rigidbody
и сложными коллайдерами (Mesh Collider
).
Что делать?
Используй простые коллайдеры (
Box
,Sphere
,Capsule
) вместоMesh Collider
, где это возможно.Mesh Collider
- самый медленный.Настрой матрицу коллизий (
Edit > Project Settings > Physics
). Если пули не должны сталкиваться с пулями, а бонусы - с бонусами, просто сними галочки в этой матрице. Меньше проверок - выше производительность.Уменьшай количество
Fixed Timestep
вProject Settings > Time
, если физика не требует высокой точности. Это уменьшит частоту вызоваFixedUpdate
.
Продвинутые техники
Когда основы освоены, можно переходить к более мощным инструментам.
Object Pooling (Пул объектов)
Часто в играх нужно постоянно создавать и уничтожать объекты. Классический пример - пули. Постоянное Instantiate()
и Destroy()
создает много мусора и нагружает CPU.
Пул объектов - это техника, при которой мы заранее создаем нужное количество объектов (например, 100 пуль), выключаем их и складываем в "коробку". Когда нужна пуля, мы не создаем новую, а берем готовую из коробки, включаем и используем. Когда пуля больше не нужна, мы не уничтожаем ее, а выключаем и возвращаем обратно в коробку.
Пример реализации пула:
using System.Collections.Generic;
using UnityEngine;
public class ObjectPool : MonoBehaviour
{
// Префаб объекта, который мы будем "пулить".
public GameObject objectToPool;
// Количество объектов, которое мы создадим заранее.
public int amountToPool;
// "Коробка" для наших объектов - список.
private List<GameObject> _pooledObjects;
void Start()
{
_pooledObjects = new List<GameObject>();
GameObject tmp;
// В самом начале создаем нужное количество объектов.
for (int i = 0; i < amountToPool; i++)
{
// Создаем объект из префаба.
tmp = Instantiate(objectToPool);
// Выключаем его, чтобы он не был виден и не работал.
tmp.SetActive(false);
// Добавляем в наш список (в "коробку").
_pooledObjects.Add(tmp);
}
}
// Метод, чтобы получить объект из пула.
public GameObject GetPooledObject()
{
// Проходим по всему списку.
for (int i = 0; i < amountToPool; i++)
{
// Ищем неактивный объект (тот, что лежит в "коробке").
if (!_pooledObjects[i].activeInHierarchy)
{
// Если нашли - возвращаем его.
return _pooledObjects[i];
}
}
// Если свободных объектов нет, возвращаем null.
return null;
}
}
Этот скрипт создает пул. Другой скрипт, например, у оружия, будет вызывать метод GetPooledObject()
, чтобы "взять" пулю, активировать ее (SetActive(true)
), задать ей позицию и скорость. А сама пуля, достигнув цели или пролетев нужное расстояние, будет деактивировать себя (SetActive(false)
), тем самым "возвращаясь" в пул.
LOD (Level of Detail)
Зачем рисовать модель с 10 000 полигонов, если она находится в 200 метрах от камеры и занимает на экране всего пару пикселей? LOD - это механизм, который позволяет использовать разные версии модели в зависимости от расстояния до камеры:
Близко: высокополигональная, красивая модель.
Средне: модель попроще.
Далеко: совсем простая модель или даже просто картинка.
Это сильно экономит ресурсы GPU. Настроить LOD можно в компоненте LOD Group
, который добавляется на объект.
Что осталось за кадром: пути для дальнейшего изучения
Эта статья - фундамент. Мы рассмотрели самые частые и действенные способы оптимизации, которые дадут наибольший прирост производительности на старте. Однако мир оптимизации в Unity огромен. Если вы освоили эти техники и хотите двигаться дальше, вот несколько направлений для углубленного изучения:
DOTS (Data-Oriented Technology Stack) и ECS (Entity Component System): Это совершенно иная парадигма программирования, позволяющая достичь производительности при работе с тысячами однотипных объектов. Это сложная, но очень мощная технология от самих Unity.
Addressable Asset System: Система для управления ассетами и памятью. Позволяет загружать и выгружать ресурсы по требованию, что критически важно для больших игр, чтобы снизить потребление оперативной памяти.
Memory Profiler: Отдельный, более мощный инструмент для отслеживания утечек памяти и анализа того, какие именно ассеты и объекты занимают память в данный момент.
Оптимизация шейдеров (Shader optimization): Мы лишь вскользь коснулись темы GPU. Глубокая оптимизация шейдеров, уменьшение числа их вариантов и использование более простых вычислений может кардинально ускорить рендеринг.
Заключение
Оптимизация - это систематическая работа.
Всегда начинай с Profiler. Не гадай, а измеряй.
Кэшируй ссылки на компоненты и объекты.
Следи за созданием "мусора", особенно в
Update()
.Объединяй объекты для снижения Draw Calls.
Используй Object Pooling для часто создаваемых/уничтожаемых объектов.
Применяй LOD для моделей, которые могут быть далеко от камеры.
Надеюсь, эта статья была для тебя полезна. Теперь ты знаешь, как сделать свои проекты чуть быстрее.
Jijiki
проблема с текстом более фундаментальна, чтобы погрузиться в неё на простейшем уровне создайте просто строку(принципиально 1 - хендлер строки) у которой есть курсор(это не буффер строк, а просто тестовая 1 строка) и отбойник(нуль терминатор), вставку делаёте по курсору, и тут можно создать функции по дозаписи в нужную область, соотв размер строки будет меняться. (зависит от языка тоесть придётся углубится как работать с памятью)
вторая фундментальная проблема-вопрос это организация выводимого текста, на каждый глиф мы имеем текстуру по коду символа - если вкратце, важно 1 разок просто для себя где-то в черновом варианте это проделать и проблема строк и отрисовки пропадёт,
появится следующий момент, почему происходит нагрузка при отрисовке текста. для ответа на этот вопрос придётся разбираться более детально почему так.
Пс может Юнити/Анриал всё делает на бекграунде, но заметил что со строками везде какието недоразумения
почему происходит выделение строки в toString потомучто например надо из числа получить строку
а ключевая строка уже выделена и та строка с числом уже не нужна когда она отправляется в ключевую строку