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