Hero background image
Использование пула объектов для повышения производительности скриптов C# в Unity

Реализовав в своем проекте Unity общие паттерны проектирования игрового программирования, вы сможете эффективно создавать и поддерживать чистую, организованную и читаемую кодовую базу. Паттерны проектирования не только сокращают время на рефакторинг и тестирование, но и ускоряют процессы внедрения и разработки, способствуя созданию прочного фундамента для развития вашей игры, команды разработчиков и бизнеса.

Воспринимайте паттерны проектирования не как готовые решения, которые можно скопировать и вставить в код, а как дополнительные инструменты, которые при правильном использовании помогут вам создавать более крупные и масштабируемые приложения.

На этой странице рассказывается об объединении объектов и о том, как оно может помочь повысить производительность вашей игры. В нем приведен пример того, как реализовать встроенную в Unity систему объединения объектов в своих проектах.

Содержание этой статьи основано на бесплатной электронной книге, Повысьте уровень своего кода с помощью паттернов программирования игрв которой рассказывается о хорошо известных паттернах проектирования и приводятся практические примеры их использования в вашем проекте Unity.

Другие статьи из серии "Шаблоны программирования игр Unity" доступны на хабе " Лучшие практики Unity ", или перейдите по следующим ссылкам:

4-2 Иерархия
Понимание пула объектов в Unity

Объединение объектов в пул - это шаблон проектирования, который позволяет оптимизировать производительность за счет снижения вычислительной мощности процессора, необходимой для выполнения повторяющихся вызовов создания и уничтожения. Вместо этого, благодаря объединению объектов, существующие объекты GameObject можно использовать снова и снова.

Ключевая функция пула объектов заключается в том, чтобы создавать объекты заранее и хранить их в пуле, а не создавать и уничтожать их по требованию. Когда объект нужен, его берут из пула и используют, а когда он больше не нужен, его возвращают в пул, а не уничтожают.

На изображении выше показан распространенный вариант использования объединения объектов - стрельба снарядами из орудийной башни. Давайте разберем этот пример пошагово.

Вместо того чтобы создавать, а затем уничтожать, паттерн объектного пула использует набор инициализированных объектов, которые хранятся в деактивированном пуле и ждут своего часа. Затем шаблон создает все объекты, необходимые в определенный момент перед началом игры. Пул должен быть активирован в подходящий момент, когда игрок не заметит заикания, например во время экрана загрузки.

Как только игровые объекты из пула будут использованы, они деактивируются и будут готовы к использованию, когда они снова понадобятся игре. Когда объект нужен, вашему приложению не нужно сначала его инстанцировать. Вместо этого он может запросить его из пула, активировать и деактивировать, а затем вернуть его в пул вместо того, чтобы уничтожить.

Этот паттерн позволяет снизить затраты на управление памятью для запуска сборки мусора, о чем будет рассказано в следующем разделе.

Электронная книга Unity Profiling
Распределение памяти

Прежде чем перейти к примерам использования пула объектов, давайте вкратце рассмотрим основную проблему, которую он помогает решить.

Техника объединения в пул полезна не только для сокращения циклов процессора, затрачиваемых на операции инстанцирования и уничтожения. Он также оптимизирует управление памятью, уменьшая накладные расходы на создание и уничтожение объектов, которые требуют выделения и деаллокации памяти, а также вызова конструкторов и деструкторов.

Управляемая память в Unity

Среда сценариев C# в Unity предлагает управляемую систему памяти. Он помогает управлять освобождением памяти, поэтому вам не нужно вручную запрашивать ее в коде. Система управления памятью также помогает контролировать доступ к памяти, обеспечивая освобождение памяти, которую вы больше не используете, и предотвращая доступ к памяти, которая не подходит для вашего кода.

Unity использует сборщик мусора, чтобы освободить память от объектов, которые ваше приложение и Unity больше не используют. Однако это также влияет на производительность во время выполнения, поскольку выделение управляемой памяти отнимает у процессора много времени, а сборка мусора (GC) может помешать процессору выполнять другую работу, пока он не выполнит свою задачу.

Каждый раз, когда вы создаете новый объект или уничтожаете существующий в Unity, память выделяется и удаляется. Именно здесь на помощь приходит объединение объектов: Это уменьшает заикание, которое может возникнуть из-за скачков сборки мусора. Скачки GC часто сопровождают создание или уничтожение большого количества объектов, связанных с выделением памяти. Помимо преждевременной сборки мусора, этот процесс также может вызвать фрагментацию памяти, что затрудняет поиск свободных смежных областей памяти.

Перерабатывая одни и те же объекты путем их отключения и активации, вы можете создать эффект, например, выпустить сотни пуль за пределы экрана, хотя на самом деле вы просто отключили и переработали их.

Узнайте больше об управлении памятью в нашем руководстве по расширенному профилированию.

Образец проекта пула объектов
Using UnityEngine.Pool

Хотя вы можете создать свою собственную систему для реализации пула объектов, в Unity есть встроенный класс ObjectPool, который можно использовать для эффективной реализации этого паттерна в вашем проекте (доступен в Unity 2021 LTS и выше).

Давайте рассмотрим, как использовать встроенную систему объединения объектов с помощью UnityEngine.Pool API на примере этого проекта, доступного на Github. Попав на страницу Github, перейдите в раздел Assets>7 Object Pool >Scripts > ExampleUsage2021 для получения файлов.

Примечание: Вы можете посмотреть этот учебник из Unity Learn, чтобы увидеть пример объединения объектов из более ранней версии Unity.

Этот пример представляет собой турель, быстро стреляющую снарядами (по умолчанию установлено 10 снарядов в секунду) при нажатии кнопки мыши. Каждый снаряд путешествует по экрану и должен быть уничтожен, когда покинет его. Без объединения объектов в пул это может создать значительную нагрузку на процессор и управление памятью, как объяснялось в предыдущем разделе.

Благодаря использованию объединения объектов создается впечатление, что за кадром стреляют сотни пуль, в то время как на самом деле они просто отключены и используются снова и снова.

Код в примере скрипта помогает убедиться, что размер пула достаточно велик, чтобы показать одновременно активные объекты, тем самым маскируя тот факт, что одни и те же объекты постоянно используются повторно.

Если вы пользовались системой частиц Unity, то не понаслышке знаете, что такое пул объектов. Компонент Particle System содержит настройку максимального количества частиц. Это позволяет повторно использовать имеющиеся частицы, не позволяя эффекту превысить максимальное количество. Пул объектов работает аналогично, но с любым GameObject по вашему выбору.

Распаковка RevisedGun.cs

Давайте посмотрим на код в RevisedGun.cs который находится в демонстрационной версии на Github по адресу Assets>7 Object Pool >Scripts > ExampleUsage2021.

Первое, что бросается в глаза, - это включение пространства имен пула:

using UnityEngine.Pool;

Используя API UnityEngine.Pool, вы получаете основанный на стеке ObjectPool класс для отслеживания объектов по шаблону пула объектов. В зависимости от ваших потребностей вы также можете использовать класс CollectionPool (List, HashSet, Dictionary и т. д.).

Затем вы применяете специальные настройки для характеристик стрельбы, включая префаб для порождения (с именем projectilePrefab типа RevisedProjectile).

Интерфейс ObjectPool ссылается на RevisedProjectile.cs (о котором будет рассказано в следующем разделе) и инициализируется в функции Awake.

private void Awake()

{
objectPool = new ObjectPool<RevisedProjectile>(CreateProjectile,

OnGetFromPool, OnReleaseToPool,

OnDestroyPooledObject,collectionCheck, defaultCapacity, maxSize);
}

Если вы изучите конструктор ObjectPool<T0>, то увидите, что он включает в себя полезную возможность задать некоторую логику, когда:

Сначала создайте объединенный элемент для заполнения пула

Взятие предмета из пула

Возвращение предмета в бассейн

Уничтожение объединенного объекта (например, при превышении максимального лимита)

Обратите внимание, что встроенный класс ObjectPool также включает опции для размера пула по умолчанию и максимального размера, последний - это максимальное количество элементов, хранящихся в пуле. Он срабатывает при вызове Release, и если пул переполнен, то вместо этого он уничтожается.

Давайте посмотрим, как в примере кода выполняется несколько действий, которые определяют, как Unity должна эффективно обрабатывать объединение объектов в соответствии с вашим конкретным случаем использования.

Сначала передается createFunc, который используется для создания нового экземпляра, когда пул пуст. В данном случае это CreateProjectile(), который инстанцирует новый профиль Prefab.

private RevisedProjectile CreateProjectile()

{
RevisedProjectile projectileInstance = Instantiate(projectilePrefab);
projectileInstance.ObjectPool = objectPool;

return projectileInstance;
}

OnGetFromPool вызывается, когда вы запрашиваете экземпляр GameObject, поэтому вы включаете GameObject, который вы получаете из пула по умолчанию.

private void OnGetFromPool(RevisedProjectile pooledObject)

{
pooledObject.gameObject.SetActive(true);
}

Функция OnReleaseToPool используется, когда GameObject больше не нужен и возвращается обратно в пул - в данном примере это просто деактивация объекта.

private void OnReleaseToPool(RevisedProjectile pooledObject)

{
pooledObject.gameObject.SetActive(false);
}

OnDestroyPooledObject вызывается, когда вы превышаете максимально допустимое количество объединенных элементов. Если пул уже заполнен, объект будет уничтожен.

private void OnDestroyPooledObject(RevisedProjectile pooledObject)

{
Destroy(pooledObject.gameObject);
}

CollectionChecks используется для инициализации IObjectPool и вызовет исключение, если вы попытаетесь освободить GameObject, который уже был возвращен менеджеру пула, но эта проверка выполняется только в редакторе. Отключив его, вы можете сэкономить несколько циклов процессора, однако при этом есть риск получить обратно объект, который уже был активирован.

Как следует из названия, defaultCapacity - это размер по умолчанию стека/списка, который будет содержать ваши элементы, и, следовательно, сколько памяти вы хотите выделить заранее. MaxPoolSize - это максимальный размер стека, и созданный пул GameObjects никогда не должен превышать этот размер. Это значит, что если вы вернете предмет в пул, который переполнен, то вместо этого предмет будет уничтожен.

Тогда в FixedUpdate() вы получите объединенный объект вместо того, чтобы инстанцировать новый снаряд каждый раз, когда вы запускаете логику для стрельбы пулей.

RevisedProjectile bulletObject = objectPool.Get();

Все очень просто.

Unpacking RevisedProjectile.cs

Теперь давайте посмотрим на скрипт RevisedProjectile.cs.

Помимо установки ссылки на ObjectPool, что делает освобождение объекта обратно в пул более удобным, есть несколько деталей, представляющих интерес.

TimeoutDelay используется для отслеживания того, когда снаряд был "использован" и может быть снова возвращен в игровой пул - по умолчанию это происходит через три секунды.

Функция Deactivate() активирует корутину DeactivateRoutine(float delay), которая не только выпускает снаряд обратно в бассейн с помощью objectPool.Release(this), но и сбрасывает параметры скорости движущегося Rigidbody.

Этот процесс решает проблему "грязных предметов": предметов, которые использовались в прошлом и нуждаются в сбросе из-за их нежелательного состояния.

Как видно из этого примера, API UnityEngine.Pool делает настройку пулов объектов эффективной, поскольку вам не нужно перестраивать шаблон с нуля, если у вас нет для этого особого случая.

Вы не ограничены только GameObjects. Пулинг - это техника оптимизации производительности для повторного использования любого типа сущностей C#: GameObject, instanced Prefab, словарь C# и так далее. Unity предлагает несколько альтернативных классов пулов для других сущностей, например DictionaryPool<T0,T1>, который поддерживает словари, и HashSetPool<T0> для хэш-наборов. Подробнее о них можно узнать из документации.

LinkedPool использует связанный список для хранения коллекции экземпляров объектов для повторного использования, что может привести к лучшему управлению памятью (в зависимости от вашего случая), поскольку вы используете память только для тех элементов, которые действительно хранятся в пуле.

Сравните это с ObjectPool, который просто использует стек C# и массив C# под ним и, как таковой, содержит большой кусок смежной памяти. Недостатком является то, что вы тратите больше памяти на элемент и больше циклов процессора на управление этой структурой данных в LinkedPool, чем в ObjectPool, где вы можете использовать defaultSize и maxSize для настройки своих потребностей.

Обложка блога
Другие способы реализации объединения объектов

Использование пулов объектов зависит от конкретного приложения, но обычно эта схема возникает, когда оружие должно стрелять несколькими снарядами, как показано в предыдущем примере.

Хорошее правило - профилировать код каждый раз, когда вы инстанцируете большое количество объектов, поскольку вы рискуете вызвать всплеск сборки мусора. Если вы обнаруживаете значительные скачки, которые подвергают игровой процесс риску заикания, подумайте об использовании пула объектов. Помните, что объединение объектов в пулы может усложнить кодовую базу из-за необходимости управлять несколькими жизненными циклами пулов. Кроме того, вы можете зарезервировать память, которая не нужна вашему геймплею, создав слишком много преждевременных пулов.

Как уже упоминалось ранее, существует несколько других способов реализовать пул объектов, помимо примера, включенного в эту статью. Один из способов - создать собственную реализацию, которую можно настроить под свои нужды. Но вам придется помнить о сложностях, связанных с безопасностью типов и потоков, а также с определением пользовательского распределения/выделения объектов.

К счастью, в Unity Asset Store есть несколько отличных альтернатив, которые помогут вам сэкономить время.

Более продвинутые ресурсы по программированию в Unity

Электронная книга, Повысьте уровень своего кода с помощью паттернов игрового программированиясодержит более подробный пример простой системы пользовательского пула объектов. Unity Learn также предлагает введение в объединение объектов, которое вы можете найти здесь, и полное руководство по использованию новой встроенной системы объединения объектов в 2021 LTS.

Все продвинутые технические электронные книги и статьи доступны на сайте Лучшие практики Unity хабе. Электронные книги также доступны на сайте. передовые методы на странице документации.

Понравился ли вам этот контент?