Использование пользовательского диспетчера обновлений
Unity встроенный на кадр функция события обновления как Update, FixedUpdate и LateUpdate на больших объёмах может сказаться на производительности. Хотя соответствующие обратные вызовы приходят в ваши скрипты MonoBehaviour на C#, сами вызовы исходят из нативного кода Unity. Unity вынуждена вести внутренние списки, чтобы знать, у каких объектов вызывать эти функции обновления. Экземпляры скриптов MonoBehaviour добавляются в эти списки при включении и убираются из них при отключении.
В то время как удобно добавлять соответствующие обратные вызова к каждому экземпляру MonoBehaviour в вашем проекте, который требует их, это становится все более неэффективным по мере роста числа обратных вызовов. Существует небольшая, но значительная накладная стоимость вызова обратных вызовов управляемого кода из собственного кода, что приводит к следующим последствиям:
- Ухудшение времени кадра при вызове большого количества
Updatecallbacks. - Уменьшение времени экземплярирования, когда экземплярирует префабы, которые содержат большое количество MonoBehaviours, из-за нагрузки на производительность при вызове
AwakeиOnEnableобратных вызовов на каждом компоненте в префаб.
Чтобы избежать этих проблем, вместо того, чтобы полагаться на встроенные обратные вызова, можно создать глобальный пользовательский экземпляр менеджера обновлений и подписать на него скрипты MonoBehaviour или даже стандартные объекты C#. Таким образом, менеджер обновлений может распространять Update, LateUpdateи другие обратные вызова ко всем объектам, подписавшимся на них, и весь код обновления остается в управляемом слое. Это имеет дополнительное преимущество, позволяющее коду отказываться от подписки на обратные вызова, когда у него нет операций для выполнения, что уменьшает количество функций, которые должны быть вызваны в каждом кадре.
Когда использовать пользовательский диспетчер обновлений
Настраиваемый менеджер обновлений может быть полезным, когда количество экземпляров MonoBehaviour с обратными вызовами обновлений на кадр достигает сотен или тысяч.
Производительность можно значительно повысить, исключив обратные вызова, которые выполняются редко. Рассмотрим следующий пример:
void Update() {
if(!someVeryRareCondition) { return; }
// … some operation …
}
Если в вашем проекте есть много MonoBehaviours с обратными вызовами Update, подобными этому, то значительная часть времени, затрачиваемого на выполнение обратных вызовов Update, тратится на переключение между собственными и управляемыми областями кода для выполнения MonoBehaviour, которое затем немедленно завершается. Если эти классы вместо этого подписываются на глобального менеджера обновлений только тогда, когда someVeryRareCondition является истинным, и отменяют подписку после этого, то меньше времени тратится как на переключение областей кода, так и на оценку редкого состояния.
Важно: Настраиваемый менеджер обновлений не является универсальным решением. Важно профилировать ваш проект, чтобы определить его специфические проблемы производительности и целесообразность использования настраиваемого менеджера обновлений. В зависимости от специфических узких мест производительности в вашем проекте, другие способы оптимизации производительности включают преобразование проекта для использования архитектуры Entity Component System (ECS) или настройку цикла Player.
Пример настраиваемого диспетчера обновлений
Чтобы реализовать собственный менеджер обновлений, сначала создайте скрипт C#, описывающий интерфейс:
public interface IUpdatable
{
void CustomUpdate(float deltaTime);
}
Затем можно создать скрипт MonoBehaviour для менеджера обновлений singleton. Менеджер обновлений реализует встроенный обратный вызов Update и затем другие компоненты скрипта MonoBehaviour могут подписываться на этого менеджера обновлений, а не на Update напрямую:
// Singleton update manager. Attach to a GameObject in your scene.
using System.Collections.Generic;
using UnityEngine;
public class UpdateManager : MonoBehaviour
{
private static UpdateManager _instance;
public static UpdateManager Instance => _instance;
private readonly List<IUpdatable> updatables = new List<IUpdatable>();
void Awake()
{
if (_instance == null)
_instance = this;
else
Destroy(gameObject);
}
public void Register(IUpdatable updatable)
{
if (!updatables.Contains(updatable))
updatables.Add(updatable);
}
public void Unregister(IUpdatable updatable)
{
updatables.Remove(updatable);
}
void Update()
{
float deltaTime = Time.deltaTime;
// Optionally: for performance, iterate backwards if you allow removal during iteration
foreach (var updatable in updatables)
{
updatable.CustomUpdate(deltaTime);
}
}
}
Наконец, создайте компонент скрипта MonoBehaviour, который регистрирует себя в экземпляре менеджера обновлений при включении и отменяет регистрацию при отключении. В следующем примере используется пользовательский обратный вызов update для перемещения родительского GameObject:
// Script component. Attach to a GameObject in your scene to move it on each custom update.
using UnityEngine;
public class MyMovingObject : MonoBehaviour, IUpdatable
{
void OnEnable()
{
UpdateManager.Instance.Register(this);
}
void OnDisable()
{
if (UpdateManager.Instance != null)
UpdateManager.Instance.Unregister(this);
}
public void CustomUpdate(float deltaTime)
{
// Your update logic here
transform.position += Vector3.right * deltaTime;
}
}
Дополнительные ресурсы
- 📚 Документация: Порядок выполнения функции события
- 📚 Документация: Функции событий
- 📚 Документация: 10000 Обновление вызовы