[Unity] unity中对象池的使用

        在现代游戏引擎设计中中,使用的大多都是托管内存,基于此我们不必手动管理内存,造成内存泄露等问题。对于unity中的游戏对象,每隔一段时间,Mono的垃圾回收机制将会检测内存,将没有再被引用的内存释放回收。当我们使用一个新的游戏物品(实例)时,首先需要实例化它(Instantiate,实际上也是基于new的机制实现的),在我们使用GameObject完成后便摧毁它(Destory)

        但是,申请实例化时,意味着我们需要在托管堆上分配一块新的内存给Object,在回收时,我们需要等待回收机制收回该块内存,这意味着当我们使用一些短期GameObject(比如在射击游戏中的子弹,往往单个存在时间只有1s左右,但是在同一时刻场景中往往存在着大量的发射和销毁事件)。与此同时,由于unity引擎自身引擎设计的问题,大多数UnityEngine API只能在主线程上运行,特别是在GameObject.Instantiate上,频繁调用Instantiate和Destory去实例化和销毁,在主线程上会造成卡顿。

        因此,对象池思想出现了。本篇文章只讨论针对unity GameObject的对象池思想。对象池思想的核心是当我们暂时不再需要某个单个物品时,不再使用Destory,而只是隐藏它,即使用GameObject.SetActive(false),并将其放入一个重用字典(或者数组)中,我们称这个存储所有激活或非激活的物品的称为池子。之后需要时,我们会先从重用数组中试着找是否当前有可用的实例并显示,如果没有再去实例化它。

        我们现在来实现一个对象池的脚本。对象池脚本不一定需要搭载在游戏物品上,因此这里的单例没有使用MonoBehaviour,不过也可以用作为Manager管理类搭载在游戏物品上管理。然后我们用一个字典,是一个string(游戏物品名称)-游戏物品队列的键值 来存储所有当前被隐藏的物品。

public class ObjectPool
{
    private static ObjectPool instance;
    private Dictionary<string, Queue<GameObject>> objectPool = new Dictionary<string, Queue<GameObject>>();
    private GameObject pool;
    public static ObjectPool Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new ObjectPool();
            }
            return instance;
        }
    }
}

        首先我们来实现创建物品逻辑,思路如下:先根据名称在对象池字典中寻找相应队列中是否有该物品的隐藏实例,如果有则从队列中取出该物品并将其setActive(true),如果没有则实例化它,设置对应的父物品方便在窗口中查看,最后返回该物品。

    public GameObject GetObject(GameObject prefab)
    {
        GameObject _object;

        if (pool == null) //当场景没有对象池时(第一次进入游戏或者切换了场景),新建一个对象池游戏物品并清空字典
        {
            pool = new GameObject("ObjectPool");
            objectPool = new Dictionary<string, Queue<GameObject>>();
        }
        //如果池子里没有该物品
        if (!objectPool.ContainsKey(prefab.name) || objectPool[prefab.name].Count == 0) 
        {
            //实例化它,加入队列
            _object = GameObject.Instantiate(prefab);
            PushObject(_object);

            GameObject childPool = GameObject.Find(prefab.name + "Pool");
            if (!childPool)
            {
                childPool = new GameObject(prefab.name + "Pool");
                childPool.transform.SetParent(pool.transform);
            }
            //设置到相对于的子物品下,方便管理
            _object.transform.SetParent(childPool.transform);
        }
        //从队列中提取对象,返回
        _object = objectPool[prefab.name].Dequeue();
        _object.SetActive(true);
        return _object;
    }

       在回收物品时,我们只需要将其SetActive(false),并放入该物品的存储队列即可

    public void PushObject(GameObject prefab)
    {
        //通过Instantiate实例化的物品都带有(Clone)后缀,我们将其去除再存储
        string _name = prefab.name.Replace("(Clone)", string.Empty);
        if (!objectPool.ContainsKey(_name))
            objectPool.Add(_name, new Queue<GameObject>());
        objectPool[_name].Enqueue(prefab);
        prefab.SetActive(false);
    }

        这样,一个简单的对象池就实现出来了。我们使用 ObjectPool.Instance.GetObject代替GameObject.Instantiate,ObjectPool.Instance.push代替GameObject.Destroy即可。

        现在我们来实践一下这个对象池的用法,这里写了一个简单的功能,鼠标左键在鼠标位置生成物品1,右键生成物品2,然后每个生成的物品都会在1s后被回收:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ObjectPoolTest : MonoBehaviour
{
    public GameObject mouseLeft; //左键生成
    public GameObject mouseRight; //右键生成
    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Mouse0))
        {
            var obj = ObjectPool.Instance.GetObject(mouseLeft);
            obj.transform.position = Camera.main.ScreenToWorldPoint(Input.mousePosition) + new Vector3(0,0,10);
            StartCoroutine(DelayAction(() => ObjectPool.Instance.PushObject(obj), 1f));
        }
        if (Input.GetKeyDown(KeyCode.Mouse1))
        {
            var obj = ObjectPool.Instance.GetObject(mouseRight);
            obj.transform.position = Camera.main.ScreenToWorldPoint(Input.mousePosition) + new Vector3(0, 0, 10); ;
            StartCoroutine(DelayAction(() => ObjectPool.Instance.PushObject(obj), 1f));
        }
    }
    IEnumerator DelayAction(Action callback,float timer)
    {
        yield return new WaitForSeconds(timer);
        callback?.Invoke();
    }
}

         (这里物品1用了蓝色正方形,物品2用了红色圆形,给脚本赋值对应的物品)

        实际表现效果和Hierarchy窗口显示:

       

        这里只是介绍了一个简单对象池,Unity资源商店中也有封装好,有更多功能的对象池插件,感兴趣的也可以直接去使用更成熟的池子(毕竟已经有车了为什么还要去造轮子么

        总之,对象池通过提前申请好内存区域和保存使程序不必在运行时再去频繁的做内存交换造成性能损失,但它是以内存占用为代价的,如何处理池子中已经不用的资源也是一个问题。在游戏的预加载阶段,也可以提前载入数据,在切换场景等空闲时段再集中对池子中内容回收优化。

更新:

在Unity 2021版本后,Unity提供了内置的对象池API,现在可以不用自己实现一套对象池框架了。

参考官方脚本手册:Pool.ObjectPool<T0> - Unity 脚本 API

使用内置对象池API需要引入命名空间 

using UnityEngine.Pool;

  • 13
    点赞
  • 69
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Unity,可以通过编写脚本来手动实现对象池,也可以使用Unity提供的对象池类来创建对象池。 以下是手动实现对象池的示例代码: ```csharp using UnityEngine; using System.Collections.Generic; public class ObjectPool : MonoBehaviour { public GameObject prefab; // 要缓存的对象的预制件 public int poolSize; // 缓存池的大小 private List<GameObject> objectPool = new List<GameObject>(); void Start () { // 创建缓存池的对象 for (int i = 0; i < poolSize; i++) { GameObject obj = Instantiate(prefab); obj.SetActive(false); objectPool.Add(obj); } } // 从对象池获取空闲对象 public GameObject GetObject() { foreach (GameObject obj in objectPool) { if (!obj.activeInHierarchy) { obj.SetActive(true); return obj; } } // 如果没有空闲对象,则创建一个新对象 GameObject newObj = Instantiate(prefab); objectPool.Add(newObj); return newObj; } // 将对象放回对象池 public void ReturnObject(GameObject obj) { obj.SetActive(false); } } ``` 以上代码演示了如何手动实现一个简单的对象池。首先在Start方法创建一定数量的对象,然后在GetObject方法遍历对象池,找到一个未激活的对象返回。如果没有空闲对象,则创建一个新对象并添加到对象池。最后,在ReturnObject方法将不再需要的对象放回对象池,将其激活状态设为false。 使用以上代码时,需要将希望缓存的对象的预制件赋值给prefab变量,并指定缓存池的大小。 Unity也提供了可重用对象池的类ObjectPool,可以在需要使用时直接调用。以下是Unity官方文档的示例代码: ```csharp using UnityEngine; using UnityEngine.Pool; public class ObjectPoolExample : MonoBehaviour { public GameObject prefab; public int poolSize; private ObjectPool<GameObject> objectPool; void Start() { objectPool = new ObjectPool<GameObject>(() => Instantiate(prefab), poolSize, item => item.SetActive(true), item => item.SetActive(false)); } void Update() { // 从池获取对象 GameObject obj = objectPool.Get(); // 将对象放回池 objectPool.Release(obj); } } ``` 以上代码演示了如何使用Unity提供的ObjectPool类创建可重用对象池。首先,在Start方法创建一个ObjectPool对象,并指定预制件创建方法、池大小、对象取出和放回时的回调函数等。然后,在Update方法可以通过Get方法取出对象,通过Release方法将对象放回对象池。 无论是手动实现对象池还是使用Unity提供的ObjectPool类,都可以有效地减少对象创建和销毁的次数,提高性能并减少内存占用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值