Unity PlayerLoop содержит функции для взаимодействия с ядром игрового движка. Эта структура включает в себя ряд систем, которые выполняют инициализацию и покадровое обновление. Все ваши скрипты будут опираться на этот PlayerLoop для создания игрового процесса. При профилировании вы увидите пользовательский код вашего проекта в контуре PlayerLoop, а компоненты редактора - в контуре EditorLoop.
Важно понимать порядок выполнения FrameLoop в Unity. Каждый скрипт Unity запускает несколько функций событий в заранее определенном порядке. Узнайте разницу между функциями Awake, Start, Update и другими, которые создают жизненный цикл сценария для повышения производительности.
В качестве примера можно привести использование FixedUpdate вместо Update при работе с Rigidbody или использование Awake вместо Start для инициализации переменных или состояния игры до ее начала. Используйте их, чтобы минимизировать код, выполняемый на каждом кадре. Awake вызывается только один раз за время жизни экземпляра сценария и всегда перед функцией Start. Это означает, что вам следует использовать Start для работы с объектами, которые, как вы знаете, могут обращаться к другим объектам, или запрашивать их после инициализации.
Порядок выполнения функций событий см. на схеме жизненного цикла сценария.
Если ваш проект предъявляет высокие требования к производительности (например, игра с открытым миром), подумайте о создании собственного менеджера обновлений с помощью Update, LateUpdate или FixedUpdate.
Чаще всего Update или LateUpdate используются для запуска логики только при выполнении некоторого условия. Это может привести к появлению множества покадровых обратных вызовов, которые фактически не выполняют никакого кода, кроме проверки этого условия.
Всякий раз, когда Unity вызывает метод сообщения, например Update или LateUpdate, он делает interop-вызов - то есть вызов со стороны C/C++ на сторону управляемого C#. Для небольшого количества объектов это не является проблемой. Когда у вас тысячи объектов, эти накладные расходы становятся значительными.
Подписывайте активные объекты на этот менеджер обновлений, когда им нужны обратные вызовы, и отписывайте, когда они не нужны. Этот паттерн позволяет сократить количество вызовов interop для ваших объектов Monobehaviour.
Примеры реализации приведены в разделе " Методы оптимизации для конкретных игровых движков ".
Подумайте, должен ли код выполняться каждый кадр. Вы можете убрать ненужную логику из Update, LateUpdate и FixedUpdate. Эти функции событий Unity - удобное место для размещения кода, который должен обновляться каждый кадр, но вы можете извлечь любую логику, которая не должна обновляться с такой частотой.
Выполняйте логику только тогда, когда ситуация меняется. Не забывайте использовать такие приемы, как паттерн наблюдателя в виде событий для запуска определенной сигнатуры функции.
Если вам нужно использовать Update, вы можете запускать код каждые n кадров. Это один из способов применения Time Slicing, распространенной техники распределения большой нагрузки на несколько кадров.
В этом примере мы запускаем функцию ExampleExpensiveFunction раз в три кадра.
Хитрость заключается в том, чтобы чередовать эту работу с другой, которая выполняется на других кадрах. В этом примере вы могли бы "запланировать" другие дорогостоящие функции, когда Time.frameCount % interval == 1 или Time.frameCount % interval == 2.
В качестве альтернативы можно использовать пользовательский класс Update Manager для обновления подписанных объектов каждые n кадров.
В версиях Unity, предшествующих 2020.2, GameObject.Find, GameObject.GetComponent и Camera.main могут быть дорогими, поэтому лучше избегать их вызова в методах Update.
Кроме того, старайтесь не размещать дорогостоящие методы в OnEnable и OnDisable, если они часто вызываются. Частое обращение к этим методам может привести к скачкам процессора.
По возможности запускайте дорогостоящие функции, такие как MonoBehaviour.Awake и MonoBehaviour.Startна этапе инициализации. Кэшируйте необходимые ссылки и используйте их в дальнейшем. Ознакомьтесь с предыдущим разделом о Unity PlayerLoop для более подробного описания порядка выполнения сценария.
Вот пример, демонстрирующий неэффективное использование повторного вызова GetComponent:
void Update()
{
Renderer myRenderer = GetComponent<Renderer>();
ExampleFunction(myRenderer);
}
Вместо этого вызывайте GetComponent только один раз, поскольку результат работы функции кэшируется. Кэшированный результат может быть повторно использован в Update без дополнительных вызовов GetComponent.
Подробнее о порядке выполнения функций мероприятия.
Записи в журнале (особенно в Update, LateUpdate или FixedUpdate) могут снижать производительность, поэтому отключите записи в журнале перед сборкой. Чтобы сделать это быстро, создайте условный атрибут вместе с директивой препроцессирования.
Например, вы можете создать пользовательский класс, как показано ниже.
Создайте сообщение в журнале с помощью своего пользовательского класса. Если вы отключите препроцессор ENABLE_LOG в разделе " Настройки игрока" > "Скриптинг" "Определить символы", все ваши логстаты исчезнут одним махом.
Работа со строками и текстом - частый источник проблем с производительностью в проектах Unity. Вот почему удаление операторов журнала и их дорогостоящего форматирования строк потенциально может стать большим выигрышем в производительности.
Аналогично, пустые MonoBehaviours требуют ресурсов, поэтому следует удалить пустые методы Update или LateUpdate. Если вы используете эти методы для тестирования, используйте директивы препроцессора:
#if UNITY_EDITOR
void Update()
{
}
#endif
Здесь вы можете использовать обновление в редакторе для тестирования без лишних накладных расходов на сборку.
В этой статье блога о 10 000 вызовов Update объясняется, как Unity выполняет Monobehaviour.Update.
Используйте опции Stack Trace в Настройках проигрывателя , чтобы контролировать, какой тип сообщений журнала отображается. Если ваше приложение регистрирует ошибки или предупреждения в сборке релиза (например, для создания отчетов о сбоях в природе), отключите трассировку стека, чтобы повысить производительность.
Узнайте больше о протоколировании трассировки стека.
Unity не использует строковые имена для внутреннего обращения к свойствам аниматоров, материалов или шейдеров. Для скорости все имена свойств хэшируются в идентификаторы свойств, и эти идентификаторы используются для обращения к свойствам.
При использовании метода Set или Get в аниматоре, материале или шейдере используйте метод с целочисленным значением вместо методов со строковым значением. Методы со строковым значением выполняют хеширование строк, а затем передают хешированный идентификатор методам с целочисленным значением.
Используйте Animator.StringToHash для имен свойств аниматоров и Shader.PropertyToID для имен свойств материалов и шейдеров.
С этим связан выбор структуры данных, который влияет на производительность, поскольку итерации выполняются тысячи раз за кадр. Следуйте руководству MSDN по структурам данных в C# в качестве общего руководства по выбору правильной структуры.
Инстанцирование и уничтожение могут вызывать скачки сборки мусора (GC). Это, как правило, медленный процесс, поэтому вместо того, чтобы регулярно создавать и уничтожать игровые объекты (например, стреляя пулями из пистолета), используйте пулы предварительно выделенных объектов, которые можно повторно использовать и перерабатывать.
Создайте многоразовые экземпляры в тот момент игры, например во время экрана меню или экрана загрузки, когда скачки процессора менее заметны. Отслеживайте этот "пул" объектов с помощью коллекции. Во время игры просто включайте следующий доступный экземпляр, когда это необходимо, и отключайте объекты вместо того, чтобы уничтожать их, а затем возвращайте их в пул. Это уменьшает количество управляемых распределений в проекте и может предотвратить проблемы с GC.
Аналогично, избегайте добавления компонентов во время выполнения; вызов AddComponent влечет за собой определенные расходы. Unity должна проверять наличие дубликатов или других необходимых компонентов при добавлении компонентов во время выполнения. Инстанцирование Prefab с уже установленными компонентами является более производительным, поэтому используйте его в сочетании с пулом объектов.
Кроме того, при перемещении трансформаций используйте Transform.SetPositionAndRotation чтобы обновить сразу и позицию, и вращение. Это позволяет избежать необходимости дважды изменять Transform.
Если вам нужно инстанцировать GameObject во время выполнения, сделать его родителем и изменить его положение для оптимизации, смотрите ниже.
Подробнее о Object.Instantiate см. в разделе Scripting API.
О том, как создать простую систему Object Pooling в Unity, читайте здесь.
Храните неизменяемые значения или настройки в ScriptableObject, а не в MonoBehaviour. ScriptableObject - это актив, который живет внутри проекта. Его нужно настроить только один раз, и он не может быть напрямую прикреплен к игровому объекту.
Создайте поля в ScriptableObject для хранения значений или настроек, а затем ссылайтесь на ScriptableObject в своих MonoBehaviours. Использование полей из ScriptableObject позволяет избежать ненужного дублирования данных каждый раз, когда вы инстанцируете объект с этим MonoBehaviour.
Посмотрите это учебное пособие "Введение в ScriptableObjects " и найдите соответствующую документацию здесь.
Одно из самых полных наших руководств, в котором собрано более 80 действенных советов по оптимизации игр для ПК и консолей. Эти подробные советы, созданные нашими экспертами в области Success и Accelerate Solutions, помогут вам получить максимальную отдачу от Unity и повысить производительность вашей игры.