9.Unity面向对象-对象池

对象池是一种优化技术,用于缓解创建和销毁大量GameObjects时的CPU压力。

对象池模式使用一组初始化好的对象,这些对象保持准备就绪并在停用的"池"中等待。当你需要对象时,你的应用程序不会实例化它。相反,你从池中请求GameObject并启用它。完成使用后,你停用对象并将其返回到池中,而不是销毁它。

对象池可以减少可能由垃圾回收尖峰引起的卡顿。GC尖峰通常伴随创建或销毁大量对象,因为需要分配内存。你可以在适当的时机预实例化你的对象池,例如在加载屏幕上,当用户不会注意到卡顿时。

对象池可以帮助你射击子弹而不会产生游戏卡顿。

示例:简单池系统

考虑一个具有两个定义的MonoBehaviour的简单池系统:

  • 一个ObjectPool,它保存要从中提取的GameObject集合
  • 一个PooledObject组件,添加到Prefab中。这有助于每个克隆的项目保持对池的引用

在ObjectPool中,你设置描述池大小的字段、你想要存储的PooledObject Prefab,以及将形成池本身的集合(本例中是一个栈)。

复制代码
public class ObjectPool : MonoBehaviour
{
    [SerializeField] private uint initPoolSize;
    [SerializeField] private PooledObject objectToPool;
    // store the pooled objects in a collection
    private Stack<PooledObject> stack;
    
    private void Start()
    {
        SetupPool();
    }
    
    // creates the pool (invoke when the lag is not noticeable)
    private void SetupPool()
    {
        stack = new Stack<PooledObject>();
        PooledObject instance = null;
        for (int i = 0; i < initPoolSize; i++)
        {
            instance = Instantiate(objectToPool);
            instance.Pool = this;
            instance.gameObject.SetActive(false);
            stack.Push(instance);
        }
    }
}

SetupPool方法填充对象池。创建一组新的PooledObject,然后实例化objectToPool的副本以用initPoolSize个元素填充它。在Start中调用SetupPool以确保它在游戏过程中运行一次。

你还需要方法来检索池中的项目(GetPooledObject)和将一个返回到池中(ReturnToPool):

复制代码
// returns the first active GameObject from the pool
public PooledObject GetPooledObject()
{
    // if the pool is not large enough, instantiate new PooledObjects
    if (stack.Count == 0)
    {
        PooledObject newInstance = Instantiate(objectToPool);
        newInstance.Pool = this;
        return newInstance;
    }
    // otherwise, just grab the next one from the list
    PooledObject nextInstance = stack.Pop();
    nextInstance.gameObject.SetActive(true);
    return nextInstance;
}

public void ReturnToPool(PooledObject pooledObject)
{
    stack.Push(pooledObject);
    pooledObject.gameObject.SetActive(false);
}

GetPooledObject仅在池为空时才创建新的PooledObject。否则,它只是返回下一个可用的元素。如果池大小足够,大多数时候你只应该获得对现有GameObject的引用。

调用GetPooledObject的客户端然后需要将池化的对象移动/旋转到位。

每个池化元素将有一个小的PooledObject组件,只是为了引用ObjectPool:

复制代码
public class PooledObject : MonoBehaviour
{
    private ObjectPool pool;
    public ObjectPool Pool { get => pool; set => pool = value; }
    
    public void Release()
    {
        pool.ReturnToPool(this);
    }
}

调用Release会禁用GameObject并将其返回到池队列。

随附的项目展示了一个基本用法示例。在这里,一个ExampleGun脚本附加到一个GameObject。它存储对对象池的引用。当用户射击时,武器脚本调用其GetPooledObject方法,而不是调用Object.Instantiate。

在射弹本身是一个ExampleProjectile脚本和一个PooledObject脚本。ExampleProjectile有一个Deactivate方法,在几秒后禁用每个发射的子弹GameObject,将其返回到可用池中。

激活的池化对象

停用的池化对象

禁用和重用池化对象

这样,你可以看起来在屏幕外发射数百个子弹,而实际上你只是禁用了它们并回收它们。只要确保你的池大小足够显示同时活动的对象。

如果你需要超过池大小,池可以实例化额外的对象。然而,大多数时候它从现有的非活动对象中提取。

如果你使用过Unity的ParticleSystem,那么你就直接体验过对象池。ParticleSystem组件包含一个关于最大粒子数的设置。这只是回收可用的粒子,防止效果超过最大数量。对象池的工作方式类似,但适用于你选择的任何GameObject。

改进

上面的示例是一个简单的。当你为实际项目部署对象池时,考虑以下升级:

  • 使其成为静态或单例:如果你需要从各种来源生成池化对象,考虑使对象池静态。这使它在应用程序中任何地方都可以访问,但无法使用Inspector。或者,将对象池模式与单例模式结合起来,使其全局可访问以便使用。
  • 使用字典管理多个池:如果你有多个想要池化的不同Prefab,将它们存储在单独的池中,并存储键值对,以便你知道查询哪个池(Prefab的InstanceID可以作为唯一键)。
  • 创造性地隐藏未使用的GameObjects:有效利用对象池的一部分是隐藏未使用的对象并将它们返回到池中。利用每一个停用池化对象的机会(例如,屏幕外、被爆炸遮挡等)
  • 检查错误:避免释放一个已经在池中的对象。否则可能导致运行时错误。
  • 添加最大大小/上限:大量池化对象会消耗内存。你可能需要删除超过某个限制的对象,以便池不会使用太多资源。

你使用对象池的方式会因应用程序而异。这种模式常见出现在枪或武器需要发射多个射弹的情况下,就像子弹地狱射击游戏一样。

每次实例化大量对象时,你都可能导致垃圾回收尖峰引起的轻微暂停。对象池可以缓解这个问题,保持你的游戏玩法流畅。

如果你使用的Unity版本是2021及以上,它包含一个内置的对象池系统,因此没有必要像前面的示例那样创建你自己的PooledObject或ObjectPool类。

UnityEngine.Pool

对象池模式非常普遍,以至于Unity 2021现在支持自己的UnityEngine.Pool API。这为你提供了一个基于堆栈的ObjectPool来跟踪你的对象。根据你的需要,你也可以使用CollectionPool(List、HashSet、Dictionary等)。

在示例项目中(参见场景),你不再需要自定义池组件。相反,用顶部的using UnityEngine.Pool;行更新枪脚本。这允许你使用内置的ObjectPool创建射弹池:

复制代码
using UnityEngine.Pool;

public class RevisedGun : MonoBehaviour
{
    ...
    // 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 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 we exceed the maximum number of pooled items (i.e. destroy the pooled object)
    private void OnDestroyPooledObject(RevisedProjectile pooledObject)
    {
        Destroy(pooledObject.gameObject);
    }
    
    private void FixedUpdate()
    {
        ...
    }
}

该脚本的大部分内容适用于原始的ExampleGun脚本。然而,ObjectPool构造函数现在包含在以下情况下设置一些逻辑的有用能力:

  • 首次创建池化项目以填充池时
  • 从池中取出项目时
  • 将项目返回到池时
  • 销毁池化对象时(例如,如果你达到最大限制)

你必须定义一些相应的小方法来传入构造函数。

请注意内置ObjectPool还包括默认池大小和最大池大小的选项。超过最大池大小的项目会触发自我销毁的操作,保持内存使用在控制范围内。

射弹脚本进行了一些小修改,以保持对ObjectPool的引用。这使得将对象释放回池中更方便。

复制代码
public class RevisedProjectile : MonoBehaviour
{
    ...
    private IObjectPool<RevisedProjectile> objectPool;
    // public property to give the projectile a reference to its ObjectPool
    public IObjectPool<RevisedProjectile> ObjectPool { set => objectPool = value; }
    ...
}

UnityEngine.Pool API使设置对象池更快,因为你不必从头重建模式。这是另一个不需要重新发明的轮子。

相关推荐
KaGme8 小时前
生成3DGS场景在unity中的呈现
3d·unity·游戏引擎
zyh______18 小时前
关于unity的序列化
unity·游戏引擎
星夜泊客20 小时前
C# : 引用类型都存在堆上吗
unity·c#
点量云实时渲染-小芹20 小时前
Unity模型数字孪生虚拟仿真webgl推流卡实时云渲染推流
unity·webgl·数字孪生·实时云渲染·虚拟仿真·云推流
mxwin1 天前
Unity Shader 齐次坐标与透视除法理解 SV_POSITION 的 w 分量
unity·游戏引擎·shader
NPUQS1 天前
【Unity 3D学习】Unity 与 Python 互通入门:点击按钮调用 Python(超简单示例)
学习·3d·unity
小贺儿开发2 天前
【Arduino与Unity交互探究】03 超声波测距模块
unity·arduino·串口通信·传感器·videoplayer·硬件交互
WarrenMondeville2 天前
1.Unity面向对象-单一职责原则
unity·设计模式·c#
WarrenMondeville2 天前
2.Unity面向对象- 开闭原则
unity·游戏引擎·开闭原则