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使设置对象池更快,因为你不必从头重建模式。这是另一个不需要重新发明的轮子。

相关推荐
魔士于安16 小时前
unity 低多边形 无人小村 木质建筑 晾衣架 盆子手推车,桌子椅子,罐子,水井
游戏·unity·游戏引擎·贴图·模型
RReality16 小时前
【Unity Shader URP】简易卡通着色(Simple Toon)实战教程
ui·unity·游戏引擎·图形渲染·材质
魔士于安17 小时前
unity 骷髅人 连招 武器 刀光 扭曲空气
游戏·unity·游戏引擎·贴图·模型
瑞瑞小安19 小时前
Unity功能篇:文本框随文字内容动态调整
ui·unity
南無忘码至尊20 小时前
Unity学习90天-第7天-学习委托与事件(简化版)
学习·unity·游戏引擎
君莫愁。20 小时前
【Unity】解决UGUI的Button无法点击/点击无反应的排查方案
unity·c#·游戏引擎·解决方案·ugui·按钮·button
南無忘码至尊1 天前
Unity学习90天 - 第 6天 - 学习协程 Coroutine并实现每隔 2 秒生成一波敌人
学习·unity·c#·游戏引擎
张老师带你学1 天前
unity 老版本资源迁移,第一人称,完整城市,有出身点房内视图,有gun shop视图,urp
科技·游戏·unity·模型·游戏美术
mxwin2 天前
Unity URP 下 UI 特效开发指南 深入探索顶点色、Mask 交互与扭曲特效的实战技巧
ui·unity·游戏引擎·shader
CandyU22 天前
Unity入门
unity·游戏引擎