Unity оптимизирует вашу игру с помощью профайлера
Производительность — ключевой аспект любой игры, и неудивительно, что независимо от того, насколько хороша игра, если она плохо работает на компьютере пользователя, она не будет такой приятной.
Поскольку не у всех есть высококлассный ПК или устройство (если вы ориентируетесь на мобильные устройства), важно помнить о производительности на протяжении всего процесса разработки.
Есть несколько причин, по которым игра может работать медленно:
- Рендеринг (слишком много высокополигональных сеток, сложных шейдеров или эффектов изображения)
- Аудио (в основном из-за неправильных настроек импорта звука)
- Неоптимизированный код (скрипты, содержащие функции, требующие высокой производительности, не в тех местах)
В этом уроке я покажу, как оптимизировать ваш код с помощью Unity Profiler.
Профилировщик
Исторически отладка производительности в Unity была утомительной задачей, но с тех пор была добавлена новая функция под названием Profiler.
Профайлер — это инструмент в Unity, который позволяет быстро выявлять узкие места в вашей игре путем мониторинга потребления памяти, что значительно упрощает процесс оптимизации.
Плохая производительность
Плохая производительность может случиться в любой момент: допустим, вы работаете над экземпляром врага, и когда вы помещаете его в сцену, он работает нормально, без каких-либо проблем, но по мере того, как вы создаете больше врагов, вы можете заметить частоту кадров в секунду (количество кадров в секунду). ) начинают падать.
Проверьте пример ниже:
В Сцене у меня есть Куб с прикрепленным к нему скриптом, который перемещает Куб из стороны в сторону и отображает имя объекта:
SC_ShowName.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SC_ShowName : MonoBehaviour
{
bool moveLeft = true;
float movedDistance = 0;
// Start is called before the first frame update
void Start()
{
moveLeft = Random.Range(0, 10) > 5;
}
// Update is called once per frame
void Update()
{
//Move left and right in ping-pong fashion
if (moveLeft)
{
if(movedDistance > -2)
{
movedDistance -= Time.deltaTime;
Vector3 currentPosition = transform.position;
currentPosition.x -= Time.deltaTime;
transform.position = currentPosition;
}
else
{
moveLeft = false;
}
}
else
{
if (movedDistance < 2)
{
movedDistance += Time.deltaTime;
Vector3 currentPosition = transform.position;
currentPosition.x += Time.deltaTime;
transform.position = currentPosition;
}
else
{
moveLeft = true;
}
}
}
void OnGUI()
{
//Show object name on screen
Camera mainCamera = Camera.main;
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
}
}
Глядя на статистику, мы видим, что игра работает со скоростью более 800 кадров в секунду, поэтому на производительность это практически не влияет.
Но давайте посмотрим, что произойдет, если мы продублируем Куб 100 раз:
Fps упал более чем на 700 пунктов!
ПРИМЕЧАНИЕ. Все тесты проводились с отключенной вертикальной синхронизацией.
Как правило, начинать оптимизацию рекомендуется, когда игра начинает тормозить, зависать или если частота кадров падает ниже 120.
Как использовать профайлер?
Чтобы начать использовать Profiler, вам понадобится:
- Начните игру, нажав кнопку «Играть».
- Откройте Profiler, перейдя в Window -> Analysis -> Profiler (или нажмите Ctrl + 7).
- Появится новое окно, которое выглядит примерно так:
- На первый взгляд это может показаться устрашающим (особенно со всеми этими графиками и т. д.), но это не та часть, которую мы будем рассматривать.
- Нажмите на вкладку «Временная шкала» и измените ее на «Иерархия»:
- Вы заметите 3 раздела (EditorLoop, PlayerLoop и Profiler.CollectEditorStats):
- Разверните PlayerLoop, чтобы увидеть все части, на которые расходуется вычислительная мощность (ПРИМЕЧАНИЕ. Если значения PlayerLoop не обновляются, нажмите кнопку "Clear" в верхней части окна профилировщика).
Для достижения наилучших результатов направьте своего игрового персонажа в ситуацию (или место), где игра зависает больше всего, и подождите пару секунд.
- Немного подождав, остановите игру и просмотрите список PlayerLoop.
Вам нужно посмотреть на значение GC Alloc, которое означает «Распределение сборки мусора». Это тип памяти, который был выделен компонентом, но больше не нужен и ожидает освобождения с помощью сборки мусора. В идеале код не должен генерировать мусор (или быть как можно ближе к 0).
Время ms также является важным значением: оно показывает, сколько времени потребовалось для выполнения кода в миллисекундах, поэтому в идеале вам также следует стремиться уменьшить это значение (путем кэширования значений, избегания вызова функций, требовательных к производительности при каждом обновлении и т. д.)..).
Чтобы быстрее найти проблемные детали, щелкните столбец «Распределение GC», чтобы отсортировать значения от большего к меньшему.)
- На диаграмме использования ЦП щелкните любое место, чтобы перейти к этому кадру. В частности, нам нужно посмотреть пики, где fps был самым низким:
Вот что показал профайлер:
GUI.Repaint выделяет 45,4 КБ, что довольно много, при его расширении можно получить больше информации:
- Это показывает, что большая часть выделений поступает из методов GUIUtility.BeginGUI() и OnGUI() в скрипте SC_ShowName, зная, что мы можем начать оптимизацию.
GUIUtility.BeginGUI() представляет собой пустой метод OnGUI() (да, даже пустой метод OnGUI() выделяет довольно много памяти).
Используйте Google (или другую поисковую систему), чтобы найти незнакомые вам имена.
Вот часть OnGUI(), которую необходимо оптимизировать:
void OnGUI()
{
//Show object name on screen
Camera mainCamera = Camera.main;
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
}
Оптимизация
Приступим к оптимизации.
Каждый скрипт SC_ShowName вызывает свой собственный метод OnGUI(), что не очень хорошо, учитывая, что у нас 100 экземпляров. Так что же с этим можно сделать? Ответ: иметь один скрипт с методом OnGUI(), который вызывает метод GUI для каждого куба.
- Во-первых, я заменил метод OnGUI() по умолчанию в скрипте SC_ShowName на public void GUIMethod(), который будет вызываться из другого скрипта:
public void GUIMethod()
{
//Show object name on screen
Camera mainCamera = Camera.main;
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
}
- Затем я создал новый скрипт и назвал его SC_GUIMethod:
SC_GUIMethod.cs
using UnityEngine;
public class SC_GUIMethod : MonoBehaviour
{
SC_ShowName[] instances; //All instances where GUI method will be called
void Start()
{
//Find all instances
instances = FindObjectsOfType<SC_ShowName>();
}
void OnGUI()
{
for(int i = 0; i < instances.Length; i++)
{
instances[i].GUIMethod();
}
}
}
SC_GUIMethod будет прикреплен к случайному объекту на сцене и вызовет все методы графического интерфейса.
- Мы перешли от 100 отдельных методов OnGUI() к одному, давайте нажмем кнопку воспроизведения и посмотрим результат:
- GUIUtility.BeginGUI() теперь выделяет только 368 байт вместо 36,7 КБ, что является большим сокращением!
Однако метод OnGUI() по-прежнему выделяет память, но поскольку мы знаем, что он вызывает GUIMethod() только из сценария SC_ShowName, мы сразу приступим к отладке этого метода.
Но Профайлер показывает только глобальную информацию, как нам увидеть, что именно происходит внутри метода?
Для отладки внутри метода Unity имеет удобный API под названием Profiler.BeginSample.
Profiler.BeginSample позволяет вам захватить определенный раздел скрипта, показывая, сколько времени потребовалось для его завершения и сколько памяти было выделено.
- Прежде чем использовать класс Profiler в коде, нам необходимо импортировать пространство имен UnityEngine.Profiling в начале скрипта:
using UnityEngine.Profiling;
- Образец профилировщика захватывается путем добавления Profiler.BeginSample("SOME_NAME"); в начале захвата и добавления Profiler.EndSample(); в конце захвата, например этот:
Profiler.BeginSample("SOME_CODE");
//...your code goes here
Profiler.EndSample();
Поскольку я не знаю, какая часть GUIMethod() вызывает выделение памяти, я заключил каждую строку в Profiler.BeginSample и Profiler.EndSample (но если в вашем методе много строк, вам определенно не нужно заключать каждую строку, просто разделите ее на равные куски и дальше работайте).
Вот последний метод с реализованными образцами профилировщика:
public void GUIMethod()
{
//Show object name on screen
Profiler.BeginSample("sc_show_name part 1");
Camera mainCamera = Camera.main;
Profiler.EndSample();
Profiler.BeginSample("sc_show_name part 2");
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
Profiler.EndSample();
Profiler.BeginSample("sc_show_name part 3");
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
Profiler.EndSample();
}
- Теперь нажимаю Play и смотрю, что показывает в Профайлере:
- Для удобства я искал в Профайлере "sc_show_", поскольку все образцы начинаются с этого имени.
- Интересно... Очень много памяти выделяется в sc_show_names часть 3, что соответствует этой части кода:
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
После некоторого поиска в Google я обнаружил, что для получения имени объекта выделяется довольно много памяти. Решение состоит в том, чтобы присвоить имя объекта строковой переменной в void Start(), чтобы он вызывался только один раз.
Вот оптимизированный код:
SC_ShowName.cs
using UnityEngine;
using UnityEngine.Profiling;
public class SC_ShowName : MonoBehaviour
{
bool moveLeft = true;
float movedDistance = 0;
string objectName = "";
// Start is called before the first frame update
void Start()
{
moveLeft = Random.Range(0, 10) > 5;
objectName = gameObject.name; //Store Object name to a variable
}
// Update is called once per frame
void Update()
{
//Move left and right in ping-pong fashion
if (moveLeft)
{
if(movedDistance > -2)
{
movedDistance -= Time.deltaTime;
Vector3 currentPosition = transform.position;
currentPosition.x -= Time.deltaTime;
transform.position = currentPosition;
}
else
{
moveLeft = false;
}
}
else
{
if (movedDistance < 2)
{
movedDistance += Time.deltaTime;
Vector3 currentPosition = transform.position;
currentPosition.x += Time.deltaTime;
transform.position = currentPosition;
}
else
{
moveLeft = true;
}
}
}
public void GUIMethod()
{
//Show object name on screen
Profiler.BeginSample("sc_show_name part 1");
Camera mainCamera = Camera.main;
Profiler.EndSample();
Profiler.BeginSample("sc_show_name part 2");
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
Profiler.EndSample();
Profiler.BeginSample("sc_show_name part 3");
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), objectName);
Profiler.EndSample();
}
}
- Посмотрим, что показывает профайлер:
Все выборки выделяют 0B, поэтому память больше не выделяется.