Если коротко:

Object pooling полезен, когда одинаковые объекты часто создаются и уничтожаются во время VR-сессии: снаряды, частицы, маркеры попаданий или повторяемые элементы интерфейса. Пул заранее создаёт или сохраняет экземпляры, выдаёт их через Get и принимает обратно через Release, уменьшая работу Instantiate/Destroy и связанные выделения памяти. Каждый объект необходимо полностью сбрасывать перед повторным использованием, а размер и эффект пула — подтверждать Profiler на целевом устройстве.

Object pooling VR уменьшает повторные Instantiate и Destroy, когда одинаковый object появляется много раз за сессию. Для гарнитуры ценны не абстрактные «быстрые объекты», а ровный кадр: накопление managed allocations способно вызвать работу GC и заметный spike. Однако pool имеет смысл только после измерения конкретного сценария.

Когда пул действительно полезен

Подход подходит для снарядов, коротких particle effects, звуковых emitters, маркеров попадания, всплывающих подписей и других objects с частым одинаковым жизненным циклом. Если экземпляр создаётся при загрузке сцены и живёт до её закрытия, pooling обычно добавляет код без выигрыша.

Сначала запишите Development Build на устройстве. В CPU Usage откройте Hierarchy или Timeline, проверьте GC.Alloc, Instantiate, Destroy и сборки мусора. Порядок измерения разобран в статье про профилирование VR в Unity. Оптимизируйте только повторяемый участок, связанный с пропуском кадра или устойчивыми allocations.

Как работает UnityEngine.Pool

ObjectPool<T> — stack-based реализация IObjectPool<T>. Конструктор принимает callbacks:

  • createFunc создаёт new экземпляр при пустом пуле;
  • actionOnGet и actionOnRelease активируют и сбрасывают объект;
  • actionOnDestroy освобождает лишний элемент;
  • collectionCheck обнаруживает повторный Release;
  • defaultCapacity задаёт начальную ёмкость, maxSize — предел хранения.

Get берёт свободный object или создаёт его, Release возвращает в pool. Сверх maxSize экземпляр передаётся actionOnDestroy. API не thread-safe.

Минимальный жизненный цикл

Для GameObject удобно разделить фабрику и сброс:

pool = new ObjectPool<Projectile>(
    createFunc: CreateProjectile,
    actionOnGet: p => p.Activate(),
    actionOnRelease: p => p.ResetAndDisable(),
    actionOnDestroy: p => Destroy(p.gameObject),
    collectionCheck: true,
    defaultCapacity: 16,
    maxSize: 64);

Числа показывают форму API, а не рекомендуемый размер. Capacity выбирают по пику активных экземпляров. Пустой pool всё равно создаёт объект, слишком большой — удерживает память. При обычном завершении снаряд вызывает Release, а не Destroy.

Что сбрасывать при Release и Get

SetActive(false) не стирает состояние. Перед повторной выдачей проверьте:

  • Transform: position, rotation, scale и parent;
  • Rigidbody: velocity, angular velocity, сон и collision flags;
  • Animator и ParticleSystem: проигрывание, время, triggers;
  • TrailRenderer, LineRenderer и накопленные точки;
  • health, таймеры, счётчики попаданий и флаг «уже возвращён»;
  • подписки на events, callbacks, coroutines и async-операции;
  • ссылки на цель, owner, material properties и UI text;
  • состояние Collider, Renderer, AudioSource и вложенных GameObject.

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

Прогрев без первого скачка

defaultCapacity резервирует ёмкость внутренней коллекции, но не обязательно создаёт все игровые экземпляры. Если первый Get не должен вызывать Instantiate в бою, выполните warm-up во время загрузки: получите ожидаемое число элементов, инициализируйте их и верните.

Прогрев расходует CPU, GPU и память. Распределите его по экрану загрузки или нескольким кадрам и измерьте на гарнитуре. Для shaders и particle systems отдельно проверяйте первый render.

Когда pooling ухудшает проект

Большой pool удерживает native и managed memory, meshes, textures и ссылки, даже когда objects неактивны. Для тяжёлых уникальных prefab это может ускорить исчерпание памяти сильнее, чем редкое Instantiate. Пул также усложняет ownership: забытая coroutine, event subscription или старая цель превращаются в трудно воспроизводимую ошибку следующего использования.

Не объединяйте разные prefab в нетипизированный склад и не создавайте pool для каждой мелочи. Если object используется редко или создаётся вне активного кадра, обычный lifecycle может быть проще.

Как проверить результат

Сравните одинаковый build до и после изменения: число GC.Alloc, время CPU, пики Instantiate/Destroy, managed heap и память процесса. Запишите старт, устойчивую нагрузку и момент превышения capacity. Отдельно проверьте, что после нескольких сотен циклов objects выглядят и ведут себя как при первом запуске.

Пул принят, если он уменьшает измеренный spike без неприемлемого роста памяти и ошибок состояния. Если кадр не меняется, а код и resident memory растут, оптимизация не окупилась. Архитектуру взаимодействующих компонентов можно сверить с Unity XR Interaction Toolkit, но pool должен оставаться независимым владельцем своего жизненного цикла.

Поделиться:TelegramВКонтакте

Источники

  1. Unity 6 — ObjectPool<T> API
  2. Unity 6 — reusable memory patterns
  3. Unity 6 — tracking garbage allocations