当前位置: 首页 > news >正文

unity的对象池与重用

原文链接:Unity - Manual: Pooling and reusing objects

对象池与重用

对象池和重用是一种编程模式,它将频繁使用的对象实例返回到池中,以便可以从中检索并再次使用。对象池减少了重复实例化和销毁新对象实例所带来的开销,并限制了总体分配和释放的数量。这有助于最小化垃圾回收(GC)开销并降低 CPU 负载。

您可以对 GameObject 进行池化,也可以对频繁使用的其他类型进行池化,包括集合类型,如数组、列表和字典。您可以编写自己的池化逻辑,但为了方便,Unity 在 UnityEngine.Pool 命名空间中提供了一组 API,实现了对 GameObject 和几种集合类型的池化。

注意:与许多其他核心 Unity API 一样,UnityEngine.Pool API 不是线程安全的,只能从主线程安全调用。

对 GameObject 进行池化和重用

游戏中某些类型的对象(如投射物)可能会反复频繁出现,尽管场景中同时存在的只有少数几个。您可以不必每次发射时都从预制体实例化一个新的投射物对象,而是在游戏开始时创建一组投射物对象池,然后根据需要重用它们。

创建对象池

Unity 的 ObjectPool 类提供了可重用对象池模式的实现,这是在应用程序中实现对象池的最简单方式。ObjectPool 的实例管理类型为 T 的对象池。当您通过 ObjectPool 构造函数创建 ObjectPool 时,您需要提供参数来定义池的以下关键方面:

  • 池的初始容量

  • 池可以增长到的最大容量

  • 是否执行集合安全检查以防止将同一对象实例双重释放回池中

  • 一系列 C# Action 委托,即 Unity 在对池执行以下关键操作时调用的回调:

    • 从池中检索对象时调用的回调

    • 将对象返回池中时调用的回调

    • 当请求对象且池为空时创建新对象实例的回调

    • 当对象返回池但池已达到最大容量时销毁对象实例的回调

从池中检索对象

您调用 ObjectPool.Get 从池中检索对象。此操作还会调用您作为 actionOnGet 参数提供给 ObjectPool 构造函数的方法,这为您提供了准备和激活对象以供使用的机会。

如果在池为空时请求对象,则会创建新的对象实例。创建过程会调用您作为 createFunc 参数提供给 ObjectPool 构造函数的方法。

ObjectPool.Get 有一个重载 Get(out PooledObject pooledObject)。当 PooledObject 被释放时,它会自动释放实例。这对于短暂的生命周期很有用。

将对象返回池中

使用完对象后,您可以通过调用 ObjectPool.Release 并将要返回的对象作为参数来将其返回池中。此操作还会调用您作为 actionOnRelease 参数提供给 ObjectPool 构造函数的方法,这为您提供了停用和重置返回对象状态的机会。

如果在池已满时将对象返回池中,则该对象会被销毁。销毁过程会调用您作为 actionOnDestroy 参数提供给 ObjectPool 构造函数的方法。

将 true 作为 collectionCheck 参数传递给构造函数可启用集合安全检查。如果启用了集合检查,并且您尝试返回已在池中的对象,Unity 将抛出异常。即使启用了,集合检查也仅在编辑器中可用,而在播放器构建中不可用。启用集合检查会增加开销,但可以捕获双重释放错误,建议在开发期间启用。

重置已使用的对象

从池中检索的对象在使用过程中可能会改变其状态。例如,池化的敌人 GameObject 在场景中激活时可能会改变其位置、生命值、动画状态和物理状态。在将对象返回池中时重置此状态非常重要,以便在稍后再次检索时,它以与首次使用时相同的状态开始。

根据您要重置的状态,您可以选择在 actionOnGet 委托中从池中获取对象时重置对象,或在 actionOnRelease 委托中将其释放回池中时重置对象。另一个选项是在池化对象本身上实现自定义停用方法,并从池的获取或释放操作中调用这些方法。

需要重置的状态取决于池化对象的特定行为和组件。释放时常见的重置任务包括停止协程、取消事件订阅、重置物理状态、清除动画和停止粒子系统。在释放时停用 GameObject 也很重要,以防止它们在池中不活动时接收 Update 调用。

示例:池化投射物

ObjectPool 用于投射物的典型实现具有以下关键阶段:

  1. 在游戏开始时创建一组非活动的投射物对象池。为构造函数提供所需的委托,作为回调在创建新的池化对象、从池中检索对象、将对象释放回池中和销毁对象(因为池已满)时调用。

  2. 当需要投射物时(例如,当用户发射武器时),使用 ObjectPool.Get 从池中检索非活动的投射物。从相应的 actionOnGet 回调中,激活检索到的对象,并初始化任何相关属性,如其位置和方向。

  3. 当不再需要投射物时(例如,当它击中目标或超出边界时),使用 ObjectPool.Release 将其返回池中。通常会给投射物一个对其自身池的引用,以便它可以在完成后自行释放。从相应的 actionOnRelease 回调中,停用返回的对象,并重置任何相关属性,如其速度和动画状态。

以下是 ObjectPool 中池化的投射物的脚本组件代码。请注意,投射物有一个公共属性,用于赋予其对其自身池的引用,它用于在一段时间延迟后将自己释放回池中:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;public class RevisedProjectile : MonoBehaviour
{// 延迟后停用[SerializeField] private float timeoutDelay = 3f;private IObjectPool<RevisedProjectile> objectPool;// 公共属性,赋予投射物对其 ObjectPool 的引用public IObjectPool<RevisedProjectile> ObjectPool { set => objectPool = value; }public void Deactivate(){StartCoroutine(DeactivateRoutine(timeoutDelay));}IEnumerator DeactivateRoutine(float delay){yield return new WaitForSeconds(delay);// 重置移动的 RigidbodyRigidbody rBody = GetComponent<Rigidbody>();rBody.linearVelocity = new Vector3(0f, 0f, 0f);rBody.angularVelocity = new Vector3(0f, 0f, 0f);// 将投射物释放回池中objectPool.Release(this);}
}

以下是附加到发射投射物的炮塔上的组件代码。炮塔在用户射击时调用对象池的 Get 来检索池化的投射物:

using UnityEngine;
using UnityEngine.Pool;
using UnityEngine.Events;public class RevisedGun : MonoBehaviour
{[Tooltip("要发射的预制体")][SerializeField] private RevisedProjectile projectilePrefab;[Tooltip("投射物力度")][SerializeField] private float muzzleVelocity = 1500f;[Tooltip("枪管/枪口位置,射击出现的位置")][SerializeField] private Transform muzzlePosition;[Tooltip("射击间隔/越小 = 射速越高")][SerializeField] private float cooldownWindow = 0.1f;[SerializeField] private UnityEvent m_GunFired;// 基于堆栈的 ObjectPool,Unity 2021 及以上版本可用private IObjectPool<RevisedProjectile> objectPool;// 如果我们尝试返回已在池中的现有项目,则抛出异常[SerializeField] private bool collectionCheck = true;// 控制池容量和最大大小的额外选项[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);}// 创建项目以填充对象池时调用private RevisedProjectile CreateProjectile(){RevisedProjectile projectileInstance = Instantiate(projectilePrefab);projectileInstance.ObjectPool = objectPool;return projectileInstance;}// 将项目返回对象池时调用private void OnReleaseToPool(RevisedProjectile pooledObject){pooledObject.gameObject.SetActive(false);}// 从对象池检索下一个项目时调用private void OnGetFromPool(RevisedProjectile pooledObject){pooledObject.gameObject.SetActive(true);}// 超过池化项目最大数量时调用(即销毁池化对象)private void OnDestroyPooledObject(RevisedProjectile pooledObject){Destroy(pooledObject.gameObject);}private void FixedUpdate(){// 如果延迟时间超过,则调用 Shoot 方法if (Input.GetButton("Fire1") && Time.time > nextTimeToShoot && objectPool != null){Shoot();}}private void Shoot(){// 获取池化对象而不是实例化RevisedProjectile bulletObject = objectPool.Get();if (bulletObject == null)return;// 对齐到枪管/枪口位置bulletObject.transform.SetPositionAndRotation(muzzlePosition.position, muzzlePosition.rotation);// 向前移动投射物bulletObject.GetComponent<Rigidbody>().AddForce(bulletObject.transform.forward * muzzleVelocity, ForceMode.Acceleration);// 几秒后关闭bulletObject.Deactivate();// 设置冷却延迟nextTimeToShoot = Time.time + cooldownWindow;m_GunFired.Invoke();}
}

使用此代码的完整工作示例项目,包括所有必要的预制体和发射池化投射物的工作炮塔,可作为免费包从资源商店下载。

处理池化对象的场景卸载

默认情况下,对象池与其创建的场景绑定。当该场景卸载时,Unity 会销毁该场景中的所有 GameObject,包括池和任何池化实例。

您可以通过将池管理器放置在调用 object.DontDestroyOnLoad 的根 GameObject 下,使对象池在场景重新加载后持久存在。您可以在释放时调用 transform.SetParent 将池化项目重新设置为池根的子级,以便它们不会随场景一起销毁。

如果在卸载时场景中可能有活动的池化实例,请确保它们在 OnDisable 和 OnDestroy 中自行释放,或注册 SceneManager.activeSceneChanged 回调以召回或销毁项目。

避免在池化实例不活动时对其进行长期引用。使用 ObjectPool.Clear 销毁所有非活动实例,例如在关卡之间。

对集合类型进行池化和重用

您还可以对 System.Collection 命名空间中的集合对象进行池化和重用,例如 List 和 Dictionary。

集合类公开了 Clear 方法,该方法会移除集合的值,但不会释放分配给集合的内存。如果您想为复杂计算分配临时辅助集合,这很有用。

以下示例代码每帧分配一次 nearestNeighbors 列表以收集一组数据点,这是一种低效的不良实践:

// 糟糕的 C# 脚本示例。此 Update 方法每帧分配一个新的 `List`。
void Update() {List<float> nearestNeighbors = new List<float>();findDistancesToNearestNeighbors(nearestNeighbors);nearestNeighbors.Sort();// … 以某种方式使用排序后的列表 …
}

相反,您可以将此 List 从方法中提升到包含类中,并在每次使用时清除和重用它,这样您的代码就不需要每帧分配新的 List:

// 良好的 C# 脚本示例。此方法每帧重用同一个 List。
List<float> m_NearestNeighbors = new List<float>();void Update() {m_NearestNeighbors.Clear();findDistancesToNearestNeighbors(NearestNeighbors);m_NearestNeighbors.Sort();// … 以某种方式使用排序后的列表 …
}

此示例代码在多个帧中保留和重用 List 实例的内存。代码仅在 List 需要扩展时才分配新内存。

UnityEngine.Pool 命名空间还为几种集合类型提供了现成的池实现,包括 List、HashSet 和 Dictionary<TKey, TValue>。它们的操作原理与 ObjectPool 相同,允许您根据需要获取和释放池化的集合对象实例。

http://www.jsqmd.com/news/810719/

相关文章:

  • 从SolarWinds事件看供应链攻击与网络防御责任重构
  • ComfyUI-WanVideoWrapper:一站式AI视频生成解决方案
  • 如何快速搭建专业macOS开发环境:dotfiles一键安装教程
  • 国产多模态大模型“唐杰”全解析:从ChatGLM到CogVLM的进击之路
  • OmenSuperHub:彻底掌控惠普OMEN游戏本性能的开源神器
  • NoFences:免费开源桌面分区神器,让Windows桌面焕然一新
  • 我用了半年只留下这1个!2026年英语录音转文字选它真不踩坑
  • 2025届必备的六大AI科研方案推荐
  • MAA助手终极使用指南:从新手到高手的快速进阶教程
  • Gemini Pro实时流式响应优化指南(流式输出失效?这4个参数必须重设)
  • Cursor Pro破解工具深度解析:如何绕过限制实现AI编程助手永久免费使用
  • 一文看懂:什么是大语言模型
  • Degrees of Lewdity中文本地化完全指南:解决游戏语言障碍的3个实用技巧
  • 2026年4月服务好的汽车音响改装官方门店口碑推荐,坦克音响改装/豪车音响改装,汽车音响改装门店哪个好 - 品牌推荐师
  • YouTube视频自动化发布工具:从配置到集成的完整实践指南
  • 从“天乙贵人”到“驿马星”:聊聊古代命理中的那些“设计模式”与“系统架构”
  • 别再让GaAs HBT功放‘发烧’了:手把手教你搞定增益塌陷与热稳定性设计
  • 颠覆性网络拓扑可视化:基于Vue+SVG的一站式轻量级解决方案
  • 闲置包包别蒙尘!北京正规包包回收渠道盘点,变现不亏还省心 - 奢侈品回收测评
  • 深度解析碧蓝航线Live2D提取技术:从Unity资源到可编辑模型的完整转换指南
  • 消息队列选型对比
  • 2026年5月宁波财税公司哪家好 行业数智化双标杆 靠谱口碑全覆盖各类型主体 - 品牌优企推荐
  • ABAQUS岩土仿真避坑指南:手把手教你配置修正DPC帽盖模型参数
  • AI智能体集成DNS Robot:19个网络诊断工具实现自动化运维
  • IF>10将降维散点图画成烟花模样
  • 26年深圳南山外国语初三二模 旋转模型
  • 如何快速配置游戏模组加载器:面向新手的完整教程
  • 国产多模态大模型“书生”全解析:从邱锡鹏团队到产业未来
  • 别只盯着STM32和RTOS了!用ESP32-C3快速上手物联网项目(附完整项目源码)
  • 纳指ETF2—实操