Объединение и повторное использование объектов
Пулирование и повторное использование объектов — это шаблон программирования, который возвращает экземпляры часто используемых объектов в пул, из которого они могут быть получены и использованы снова. Пулирование объектов снижает накладные расходы, вызванные многократным созданием и уничтожением новых экземпляров объектов, и ограничивает общее количество выделений и отмены выделений. Это помогает свести к минимуму накладные расходы на сбор мусора (GC) и уменьшить нагрузку на CPU.
Вы можете объединять GameObjects, но вы также можете объединять другие типы, которые вы часто используете, включая типы коллекций, такие как массивы, списки и словари. Вы можете написать свою собственную логику объединения, но для удобства Unity предоставляет набор APIs в пространстве имен UnityEngine.Pool с реализациями для объединения GameObjects и нескольких типов коллекций.
Примечание: Как и многие другие ядра Unity APIs, UnityEngine.Pool APIs не являются потоково-безопасными и могут быть безопасно вызваны только из основного потока.
Объединение и повторное использование GameObjects
Некоторые типы объектов в играх, такие как снаряды, могут появляться многократно и часто, даже если только несколько из них находятся в сцене одновременно. Вместо того, чтобы создавать новый объект снаряда из префаб каждый раз, когда он стреляется, вы можете создать пул объектов снаряда в начале игры, а затем повторно использовать их по мере необходимости.
Создание пула объектов
Unity ObjectPool<T> класс реализует шаблон переиспользуемого пула объектов и является самым простым способом организовать пул объектов в приложении. Экземпляр ObjectPool<T> управляет пулом объектов типа T. Когда вы создаете ObjectPool<T> через ObjectPool<T> конструктор, вы предоставляете параметры для определения следующих ключевых аспектов пул:
- Начальная емкость бассейна.
- Максимальная емкость, до которой может расти пул.
- Выполняются ли проверки безопасности коллекции для предотвращения повторного возвращения одного и того же экземпляра объекта в пул.
- Серия C#
Action<T>delegates, которые являются обратными вызовами, вызываемыми Unity при выполнении следующих ключевых действий на пуле:- Обратный вызов для вызова при получении объекта из пула.
- Обратный вызов, который будет вызван при возвращении объекта в пул.
- Обратный вызов для создания нового экземпляра объекта, когда он запрашивается, а пул пустой.
- Обратный вызов для вызова при уничтожении экземпляра объекта, когда он возвращается в пул, но пул работает на максимальной мощности.
Получение объектов из пула
Вы вызываете ObjectPool<T>.Get для получения объекта из пула. Это действие также вызывает метод, который вы предоставили в качестве параметра actionOnGet конструктору ObjectPool, что дает вам возможность подготовить и активировать объект для использования.
Если вы запрашиваете объект из пула, когда пул пуст, создается новый экземпляр объекта. Процесс создания вызывает метод, который вы предоставили в качестве параметра createFunc конструктору ObjectPool.
ObjectPool<T>.Get имеет перегрузку Get(out PooledObject<T> pooledObject). Когда PooledObject удаляется, он автоматически освобождает экземпляр. Это полезно для кратковременных областей.
Для получения дополнительной информации и примеров кодов см. ссылки PooledObject<T>.Get API и.
Возвращение объектов в пул
Когда вы закончите работу с объектом, вы вернете его в пул, вызвав ObjectPool<T>.Release с возвращаемым объектом в качестве аргумента. Это действие также вызывает метод, который вы предоставили в качестве параметра actionOnRelease конструктору ObjectPool, что даёт возможность деактивировать и сбросить состояние возвращаемого объекта.
Если вы вернете объект в пул, когда он заполнен, объект будет уничтожен. Процесс уничтожения вызывает метод, который вы предоставили в качестве параметра actionOnDestroy конструктору ObjectPool.
Передача true в качестве параметра collectionCheck конструктору включает проверку безопасности коллекции. Если проверки коллекции включены и вы пытаетесь возвратить объект, который уже находится в пуле, Unity вызывает исключение. Даже когда это включено, проверка коллекции доступна только в Редакторе, а не в сборках Player. Включение проверки коллекции увеличивает накладные расходы, но захватывает ошибки двойного выпуска и рекомендуется во время разработки.
Сброс использованных объектов
Объекты, которые вы берёте из пула, во время использования могут менять своё состояние. Например, GameObject противника, пока он активен в сцене, может изменить положение, здоровье, состояние анимации и состояние физики. Важно сбрасывать это состояние при возврате объекта в пул, чтобы при следующем получении он начинал с того же состояния, что и при первом использовании.
В зависимости от того, какое состояние вы сбрасываете, объект можно сбрасывать либо при получении из пула в делегате actionOnGet, либо при освобождении его обратно в пул в делегировании actionOnRelease. Другим вариантом является реализация настраиваемого метода деактивации на самом объединенном объекте и вызов этих методов из действий get или release пула.
состояние, которое вам нужно сбросить, зависит от конкретного поведения и компонентов вашего объединённого в пул объекта. Обычно при возврате в пул требуется остановить сопрограммы, отписаться от событий, сбросить физическое состояние, очистить анимации и остановить системы частиц. Также важно отключать GameObject при возврате в пул, чтобы они не получали вызовов Update, пока они неактивны в пуле.
Пример: объединение снарядов
Типичная реализация ObjectPool<T> для снарядов состоит из следующих ключевых этапов:
- Создайте пул неактивных объектов снаряда в начале игры. Предоставьте конструктору необходимые делегаты для вызова в качестве обратных вызовов при создании нового объединенного объекта, получении объекта из пула, освобождении объекта обратно в пул и уничтожении объекта из-за того, что пул заполнен.
- Когда требуется снаряд (например, когда пользователь стреляет из оружия), извлеките неактивный снаряд из пуля с помощью
ObjectPool<T>.Get. Из соответствующего обратного вызоваactionOnGetактивируйте извлеченный объект и инициализируйте любые соответствующие свойства, такие как его положение и направление. - Когда снаряд больше не нужен (например, когда он попадает в цель или выходит из пределов), возвращайте его в пул с помощью
ObjectPool<T>.Release. Обычно снаряду дается ссылка на свой собственный пул, чтобы он мог освободиться после завершения. Из соответствующего обратного вызоваactionOnReleaseдеактивируйте возвращаемый объект и сбросьте все соответствующие свойства — например, скорость и состояние анимации.
Ниже приведен код компонента скрипта для снаряда, который объединен в ObjectPool<T>. Обратите внимание: у снаряда есть открытое свойство, дающее ему ссылку на собственный пул, — по нему снаряд через некоторое время возвращает себя в пул:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;
public class RevisedProjectile : MonoBehaviour
{
// Deactivate after delay
[SerializeField] private float timeoutDelay = 3f;
private IObjectPool<RevisedProjectile> objectPool;
// Public property to give the projectile a reference to its ObjectPool
public IObjectPool<RevisedProjectile> ObjectPool { set => objectPool = value; }
public void Deactivate()
{
StartCoroutine(DeactivateRoutine(timeoutDelay));
}
IEnumerator DeactivateRoutine(float delay)
{
yield return new WaitForSeconds(delay);
// Reset the moving Rigidbody
Rigidbody rBody = GetComponent<Rigidbody>();
rBody.linearVelocity = new Vector3(0f, 0f, 0f);
rBody.angularVelocity = new Vector3(0f, 0f, 0f);
// Release the projectile back to the pool
objectPool.Release(this);
}
}
Ниже приводится код для компонента, прикрепленного к пушечной башне, который стреляет снарядом и управляет пулом объектов снарядов. Пушка вызывает Get на пуле объектов для получения снарядов из пуля при стрельбе пользователя:
using UnityEngine;
using UnityEngine.Pool;
using UnityEngine.Events;
public class RevisedGun : MonoBehaviour
{
[Tooltip("Prefab to shoot")]
[SerializeField] private RevisedProjectile projectilePrefab;
[Tooltip("Projectile force")]
[SerializeField] private float muzzleVelocity = 1500f;
[Tooltip("End point of gun where shots appear")]
[SerializeField] private Transform muzzlePosition;
[Tooltip("Time between shots / smaller = higher rate of fire")]
[SerializeField] private float cooldownWindow = 0.1f;
[SerializeField] private UnityEvent m_GunFired;
// Stack-based ObjectPool available with Unity 2021 and above
private IObjectPool<RevisedProjectile> objectPool;
// Throw an exception if we try to return an existing item, already in the pool
[SerializeField] private bool collectionCheck = true;
// Extra options to control the pool capacity and maximum size
[SerializeField] private int defaultCapacity = 20;
[SerializeField] private int maxSize = 100;
private float nextTimeToShoot;
private void Awake()
{
objectPool = new ObjectPool<RevisedProjectile>(CreateProjectile,
OnGetFromPool, OnReleaseToPool, OnDestroyPooledObject,
collectionCheck, defaultCapacity, maxSize);
}
// Invoked when creating an item to populate the object pool
private RevisedProjectile CreateProjectile()
{
RevisedProjectile projectileInstance = Instantiate(projectilePrefab);
projectileInstance.ObjectPool = objectPool;
return projectileInstance;
}
// Invoked when returning an item to the object pool
private void OnReleaseToPool(RevisedProjectile pooledObject)
{
pooledObject.gameObject.SetActive(false);
}
// Invoked when retrieving the next item from the object pool
private void OnGetFromPool(RevisedProjectile pooledObject)
{
pooledObject.gameObject.SetActive(true);
}
// Invoked when the maximum number of pooled items is exceeded (i.e. destroy the pooled object)
private void OnDestroyPooledObject(RevisedProjectile pooledObject)
{
Destroy(pooledObject.gameObject);
}
private void FixedUpdate()
{
// Invokes the Shoot method if the delay time is exceeded
if (Input.GetButton("Fire1") && Time.time > nextTimeToShoot && objectPool != null)
{
Shoot();
}
}
private void Shoot()
{
// Get a pooled object instead of instantiating
RevisedProjectile bulletObject = objectPool.Get();
if (bulletObject == null)
return;
// Align to gun barrel/muzzle position
bulletObject.transform.SetPositionAndRotation(muzzlePosition.position, muzzlePosition.rotation);
// Move projectile forward
bulletObject.GetComponent<Rigidbody>().AddForce(bulletObject.transform.forward * muzzleVelocity, ForceMode.Acceleration);
// Turn off after a few seconds
bulletObject.Deactivate();
// Set cooldown delay
nextTimeToShoot = Time.time + cooldownWindow;
m_GunFired.Invoke();
}
}
Полный рабочий пример проекта, использующий этот код, включая все необходимые префабы и рабочую башню, которая стреляет пулированными снарядами, доступен для загрузки в качестве бесплатного пакета из Asset Store. Для получения дополнительной информации, см. Используйте пулирование объектов для повышения производительности C# скриптов в Unity.
Обработка разгрузки сцены для объединенных объектов
По умолчанию пул объектов привязан к сцене, в которой он был создан. Когда эта сцена разгружается, Unity уничтожает все GameObjects в этой сцене, включая пул и любые объединенные экземпляры.
Вы можете сделать так, чтобы пул объектов сохранялся после перезагрузки сцены, разместив менеджера пулов под корнем GameObject, на котором вы вызываете object.DontDestroyOnLoad. Вы можете вызвать transform.SetParent для восстановления пулов объектов обратно под корнем пулов при освобождении, чтобы они не были уничтожены сценой.
Если при разгрузке в сцене могут быть активные объединенные экземпляры, убедитесь, что они освобождаются в OnDisable и OnDestroy или зарегистрируйте обратный вызов SceneManager.activeSceneChanged для извлечения или уничтожения элементов.
Используйте ObjectPool<T>.Clear для уничтожения всех неактивных экземпляров, например, между уровнями.
Объединение и повторное использование типов коллекций
Также можно объединять и повторно использовать объекты коллекции из пространства имен System.Collection, такие как списки и словари.
Классы коллекций обнаруживают метод Clear, который удаляет значения коллекции, но не высвобождает память, выделенную для коллекции. Это полезно, если вы хотите выделить временные коллекции помощников для сложных вычислений.
В следующем примере код выделяет nearestNeighbors List один раз в кадре для сбора набора точек данных, что является неэффективной плохой практикой:
// Bad C# script example. This Update method allocates a new `List` every frame.
void Update() {
List<float> nearestNeighbors = new List<float>();
findDistancesToNearestNeighbors(nearestNeighbors);
nearestNeighbors.Sort();
// … use the sorted list somehow …
}
Вместо этого, вы можете поднять это List за пределы метода, в содержащий его класс, и очищайте и переиспользуйте его каждый раз, чтобы вашему коду не приходилось выделять новый List каждый кадр:
// Good C# script example. This method re-uses the same List every frame.
List<float> m_NearestNeighbors = new List<float>();
void Update() {
m_NearestNeighbors.Clear();
findDistancesToNearestNeighbors(NearestNeighbors);
m_NearestNeighbors.Sort();
// … use the sorted list somehow …
}
В этом примере код сохраняет и повторно использует память экземпляров List в нескольких кадрах. Код выделяет новую память только тогда, когда List нуждается в расширении.
Пространство имен UnityEngine.Pool также предоставляет готовые реализации пулов для нескольких типов коллекций, включая List<T>, HashSet<T>и Dictionary<TKey, TValue>. Они работают по тому же принципу, что и ObjectPool<T>, позволяя получать и выпускать экземпляры объектов пулов коллекций по мере необходимости. Дополнительные сведения и примеры см. в справочной документации API для ,CollectionPool<T>, и других классов пулов типов коллекций в том же пространстве имен.
Дополнительные ресурсы
- Учебник: Использование пулирования объектов для повышения производительности C# скриптов в Unity
- Введение управляемой памяти
- Управление справочными типами
- Оптимизация массивов
- Инстантизация снарядов и взрывов.