Оптимизация Unity-игры под слабое железо
У оптимизации Unity длинный список возможных техник, но большая часть реального выигрыша производительности приходит от короткого списка, применённого правильно.
Сначала профилируйте — не гадайте насчёт узкого места
Оптимизация без предварительного профилирования — один из самых частых способов потратить время на изменения, не сдвигающие стрелку. Встроенный Profiler Unity показывает точно, куда реально уходит время кадра — логика процессора, рендеринг, физика или сборка мусора — прежде чем вы что-либо тронете.
Исправления с наибольшим эффектом, примерно по порядку
- Сократите draw call. Батчинг (статический батчинг для неподвижных объектов, динамический батчинг или GPU instancing для движущихся) и текстурный атлас (объединение нескольких текстур в одну) сокращают количество отдельных инструкций рендеринга, которые должен выдать процессор — обычно самый большой единичный выигрыш для более слабого железа.
- Пулинг объектов для всего, что часто порождается.
Instantiate()иDestroy()относительно дороги; переиспользование пула заранее созданных объектов (пули, частицы, враги) вместо постоянного создания и уничтожения избегает как этой стоимости, так и связанного давления на сборку мусора. - Сократите давление на сборку мусора. Частые маленькие аллокации (особенно в
Update(), выполняющемся каждый кадр) вызывают паузы сборки мусора, проявляющиеся как подёргивание. Кэширование переиспользуемых объектов и избегание аллокаций в горячих путях (вроде поэкадровой конкатенации строк) значительно это сокращает. - Понизьте частоту обновления физики там, где точность не критична. Не каждому объекту нужна частота фиксированного шага по умолчанию — менее критичные физические взаимодействия иногда могут работать на пониженной частоте без заметной разницы.
- LOD (уровень детализации) и отсечение. Снижение детализации на дальних объектах и нерендеринг того, что вне экрана, напрямую сокращает поэкадровую работу рендеринга.
// Наивно: аллоцирует каждый кадр
void Update() {
string status = "HP: " + currentHp; // новая аллокация строки каждый кадр
}
// Лучше: обновляет только когда значение реально меняется
void OnHealthChanged(int newHp) {
statusText.text = $"HP: {newHp}"; // аллоцируется только когда это реально срабатывает
}Что конкретно проверять на слабом железе
| Симптом | Вероятная причина |
|---|---|
| Стабильно низкая частота кадров | Слишком много draw call, или чрезмерно сложные шейдеры |
| Периодическое подёргивание/зависания | Всплески сборки мусора от частых аллокаций |
| Медленное время загрузки | Несжатые или слишком большие текстуры/аудио |
| Падения кадров во время боя/частиц | Отсутствие пулинга объектов для порождаемых эффектов |
Компромисс, который легко переисправить
Не всему нужна самая агрессивная оптимизация — трата непропорциональных усилий на оптимизацию редко посещаемого экрана меню, пока реальный игровой цикл остаётся неоптимизированным, — частая ошибка распределения. Профилируйте, найдите, где реально уходит время, и оптимизируйте там в первую очередь.
Это напрямую связано с более широкими архитектурными решениями — см. нашу заметку о прототипировании пет-проекта насчёт того, почему преждевременная оптимизация во время раннего прототипирования обычно неправильный инстинкт, в отличие от применения этих техник, как только механика подтверждена стоящей дальнейшего построения.
Частые вопросы
Оптимизировать по ходу дела или ждать до конца?
В основном ждать — преждевременная оптимизация систем, которые могут быть вырезаны или переработаны, тратит усилия впустую. Исключение — архитектурные решения (пулинг объектов, паттерны draw call), которые дорого встраивать позже — их стоит решать рано, даже если вы ещё не полностью их реализуете.
Какая единственная оптимизация с наибольшим эффектом для большинства Unity-игр?
Сокращение draw call, обычно через батчинг (статический или динамический) и текстурный атлас — это решает самое частое узкое место (процессор тратит слишком много времени на выдачу инструкций рендеринга), прежде чем трогать что-то более экзотическое.
Стоит ли пулинг объектов дополнительной сложности?
Для всего, что часто порождается и уничтожается — пули, частицы, враги — да, почти всегда. Вызовы Instantiate/Destroy относительно дороги, и частое порождение — один из самых частых реальных виновников проблем с производительностью.