Unity中的资源管理-对象池技术(3)

本文分享Unity中的资源管理-对象池技术(3)

在上两篇文章中, 我们一起学习了普通类的两种对象池实现, 今天接着介绍Unity中GameObject(主要是预制实例化对象)的对象池.

GameObjectPool的必要性

我们知道在游戏前端的开发中, 大部分情况下并不会像服务器开发一样会涉及到特别大量的对象的创建和销毁, 一般的容器最多也就几十上百个对象, 并不会有太大的性能压力, 所以普通类的对象池在客户端使用的并不多, 一般也就资源管理等少数场合会涉及到巨量普通类对象的使用, 而游戏前端更多利用对象池的场合在大量或者复杂游戏对象的使用上, 比如大量子弹, 敌人, 怪物, UI等.

因为游戏对象一般会涉及到显示相关的部分, 构建和销毁操作会比普通类对象更复杂, 占用的资源更多, 造成的代价更高. 为了避免不必要的资源浪费和提高游戏体验, 在大型游戏中, 使用游戏对象的对象池是比较普遍的方案.

在Unity中, 大部分资源通常情况下是不需要手动实例化的, 比如texture, atlas, shader, material, sound, animation等, 一般都是通过预制实例化来使用, 所以我们所谓GameObject的对象池, 更多指的是预制实例化对象的对象池.

对ObjectPool的优化

在第一篇文章中, 我们介绍了对象池的一种实现, 在进一步介绍游戏对象的对象池之前, 我们需要对该实现做一点优化以便更加通用.

主要的优化点在于:

  • 抽象出对象池的接口: IObjectPool
  • 规范化创建对象的委托和各种触发点的委托, 并添加可变长参数

对象池接口: IObjectPool

我们将产生和回收封装成为接口, 以便更加通用.

现在产生对象的方法可以接受可变长参数.

public interface IObjectPool<T> : IDisposable where T : class, new() {
    T Spawn(params object[] param);
    void Recycle(T obj);
}

各种委托

将委托封装并公开, 并可以接受可变长参数.

public class ObjectPoolDelegate<T>
{
    public delegate T NewObjFunc(params object[] param);
    public delegate void ObjectPoolAction(T obj, params object[] param);
}

protected ObjectPoolDelegate<T>.ObjectPoolAction m_BeforeSpawnAction, m_AfterRecycleAction, m_AfterReleaseAction;
protected ObjectPoolDelegate<T>.NewObjFunc newObjFunc;

完整代码

其它的部分变化不大, 为了完整性, 这里还是贴出代码:

public interface IObjectPool<T> : IDisposable where T : class, new() {
    T Spawn(params object[] param);
    void Recycle(T obj);
}

public class ObjectPoolDelegate<T>
{
    public delegate T NewObjFunc(params object[] param);
    
    public delegate void ObjectPoolAction(T obj, params object[] param);
}

public class ObjectPool<T> : IObjectPool<T>, IDisposable where T : class, new() {
    protected Stack<T> m_ObjPool;
    protected Dictionary<int, T> m_ObjDict;
    
    protected int m_MaxCount;
    protected int m_OnceReleaseCount;
    
    protected ObjectPoolDelegate<T>.ObjectPoolAction m_BeforeSpawnAction, m_AfterRecycleAction, m_AfterReleaseAction;
    protected ObjectPoolDelegate<T>.NewObjFunc m_NewObjFunc;
    
    public ObjectPool(int initCapacity = 5, int maxCount = 500, int onceReleaseCount = 10,
        ObjectPoolDelegate<T>.ObjectPoolAction beforeSpawnAction = null, 
        ObjectPoolDelegate<T>.ObjectPoolAction afterRecycleAction = null, 
        ObjectPoolDelegate<T>.ObjectPoolAction afterReleaseAction = null, 
        ObjectPoolDelegate<T>.NewObjFunc newObjFunc = null) {
        
        m_ObjPool = new Stack<T>(initCapacity);
        m_ObjDict = new Dictionary<int, T>(initCapacity);
        
        m_MaxCount = maxCount;
        m_OnceReleaseCount = onceReleaseCount;
        
        m_BeforeSpawnAction = beforeSpawnAction;
        m_AfterRecycleAction = afterRecycleAction;
        m_AfterReleaseAction = afterReleaseAction;
        m_NewObjFunc = newObjFunc;
    }

    public void Dispose() {
        if (m_AfterReleaseAction != null) {
            var array = m_ObjDict.Select(s=>s.Value);
            foreach(var obj in array) {
                m_AfterReleaseAction(obj);
            }
        }
        
        m_ObjPool.Clear();
        m_ObjDict.Clear();

        m_BeforeSpawnAction = m_AfterRecycleAction = m_AfterReleaseAction = null;
        m_NewObjFunc = null;
    }
    
    /// <summary>
    /// 移除多余对象
    /// </summary>
    private void ReleaseOverflowObj() {
        Debug.Log("[ReleaseOverflowObj] 已达最大回收数量: " + m_ObjPool.Count);
        
        var removeCount = Math.Min(m_OnceReleaseCount, m_ObjPool.Count);
        while(--removeCount >= 0) {
            var obj = m_ObjPool.Pop();
            Release(obj);
        }
        
        Debug.Log("[ReleaseOverflowObj] 当前池中数量: " + m_ObjPool.Count);
    }

    public T CreateObj(params object[] param) {
        var obj = m_NewObjFunc != null ? m_NewObjFunc(param) : new T();
        m_ObjDict.Add(obj.GetHashCode(), obj);
        return obj;
    }
    
    public bool Release(T obj) {
        m_ObjDict.Remove(obj.GetHashCode());
        m_AfterReleaseAction?.Invoke(obj);
        return true;
    }

    public int Count() {
        return m_ObjPool.Count;
    }

    public T Spawn(params object[] param) {
        var obj = m_ObjPool.Count <= 0 ? CreateObj(param) : m_ObjPool.Pop();
        m_BeforeSpawnAction?.Invoke(obj, param);

        return obj;
    }

    public void Recycle(T obj) {
        if (m_ObjPool.Count >= m_MaxCount) {
            ReleaseOverflowObj();
        }

        if (m_ObjPool.Contains(obj)) return;
        
        m_ObjPool.Push(obj);
        m_AfterRecycleAction?.Invoke(obj);
    }
    
    public void Dispose() {
        if (m_AfterReleaseAction != null) {
            var array = m_ObjDict.Select(s=>s.Value);
            foreach(var obj in array) {
                m_AfterReleaseAction(obj);
            }
        }

        m_ObjPool.Clear();
        m_ObjDict.Clear();

        m_BeforeSpawnAction = m_AfterRecycleAction = m_AfterReleaseAction = null;
        m_NewObjFunc = null;
    }
}

GameObjectPool

游戏对象的对象池相比普通的对象池有它独有的特点:

  • 存放的对象类型为: GameObject

  • 需要提供创建对象的委托, 将创建对象的工作交给外部以兼容更多的情况

  • 需要提供对象的挂载点(Transform), 挂载点默认是不可见的(SetActive(false)), 将处于对象池内部的对象挂载在其之下时默认就不可见, 不需要再次调用SetActive(false)以提高性能

  • 实现对象池接口, 并持有一个普通的对象池, 而不是继承对象池

  • 在各个委托时做一些和游戏对象有关的基本操作, 如: 设置位置/旋转, 设置父节点, 销毁对象等

详细信息可以参考下面的代码和注释:

using NewObjFunc = ObjectPoolDelegate<GameObject>.NewObjFunc;
using ObjectPoolAction = ObjectPoolDelegate<GameObject>.ObjectPoolAction;

// 实现对象池接口, 使用聚合的方式实现, 降低依赖性
public class GameObjectPool : IObjectPool<GameObject> {
    // 持有一个普通的对象池
    private ObjectPool<GameObject> m_ObjectPool;
    
    // 挂载点
    private Transform m_PoolParent;

    // 创建对象的委托
    private NewObjFunc m_NewObjFunc;
    
    // 产生之前, 回收之后, 释放之后的委托
    private ObjectPoolAction m_BeforeSpawnAction, m_AfterRecycleAction, m_AfterReleaseAction;

    //---------------------------------------------
    // 公开委托属性供外部调用
    public ObjectPoolAction beforeSpawnAction {
        set => m_BeforeSpawnAction = value;
    }

    public ObjectPoolAction afterRecycleAction {
        set => m_AfterRecycleAction = value;
    }

    public ObjectPoolAction afterReleaseAction {
        set => m_AfterReleaseAction = value;
    }
    //---------------------------------------------

    // 只提供子类使用, 延迟对象池的实例化
    protected GameObjectPool() {

    }

    // 封装对象池实例化和初始化
    protected void InitPool(NewObjFunc newObjFunc, Transform poolParent, int initCapacity = 5, int maxCount = 500, int onceReleaseCount = 10) {
        Assert.IsNotNull(newObjFunc, "实例化对象函数不能为空!");
        Assert.IsNotNull(poolParent, "挂载节点不能为空!");

        m_PoolParent = poolParent;
        m_NewObjFunc = newObjFunc;

        // 不可见挂载点
        m_PoolParent.gameObject.SetActive(false);
        
        // 持有普通对象池, 并劫持各种委托
        m_ObjectPool = new ObjectPool<GameObject>(initCapacity, maxCount, onceReleaseCount, BeforeSpawn, AfterRecycle, AfterRelease, NewObj);
    }

    public GameObjectPool(NewObjFunc newObjFunc, Transform poolParent, int initCapacity = 5, int maxCount = 500, int onceReleaseCount = 10) {
        InitPool(newObjFunc, poolParent, initCapacity, maxCount, onceReleaseCount);
    }

    //---------------------------------------------
    // 对象池接口, 直接使用持有的对象池接口
    public GameObject Spawn(params object[] param) {
        var obj = m_ObjectPool.Spawn(param);
        return obj;
    }

    public void Recycle(GameObject obj) {
        m_ObjectPool.Recycle(obj);
    }

    // 清理委托和卸载对象池
    public virtual void Dispose() {
        m_ObjectPool?.Dispose();

        m_NewObjFunc = null;
        m_AfterReleaseAction = null;
    }
    //---------------------------------------------

    public GameObject CreateObj(params object[] param) {
        var obj = m_ObjectPool.CreateObj(param);
        return obj;
    }

    public bool Release(GameObject obj) {
        return m_ObjectPool.Release(obj);
    }

    // --------------------------------------------------------------------------------
    // 解析可变长参数, 依次为: 父节点, 位置, 旋转 
    protected void ParseParams(out Transform parent, out Vector3 pos, out Quaternion quaternion, params object[] param) {
        parent = m_PoolParent;
        pos = Vector3.zero;
        quaternion = Quaternion.identity;

        if (param != null) {
            if (param.Length > 0)
                parent = (Transform)param[0];

            if (param.Length > 1)
                pos = (Vector3)param[1];

            if (param.Length > 2)
                quaternion = (Quaternion)param[2];
        }
    }

    //---------------------------------------------
    // 对象池接口, 直接使用持有的对象池接口
    private GameObject NewObj(params object[] param) {
        ParseParams(out var parent, out var pos, out var quaternion, param);

        var obj = m_NewObjFunc(parent, pos, quaternion);
        return obj;
    }

    // 产生之前初始化
    protected virtual void BeforeSpawn(GameObject obj, params object[] param) {
        ParseParams(out var parent, out var pos, out var quaternion, param);

        var transform = obj.transform;
        transform.parent = parent;
        transform.position = pos;
        transform.rotation = quaternion;

        m_BeforeSpawnAction?.Invoke(obj, param);
    }

    // 回收后挂载不可见节点
    protected virtual void AfterRecycle(GameObject obj, params object[] param) {
        obj.transform.parent = m_PoolParent;

        m_AfterRecycleAction?.Invoke(obj, param);
    }

    // 销毁对象
    protected virtual void AfterRelease(GameObject obj, params object[] param) {
        Object.Destroy(obj);

        m_AfterReleaseAction?.Invoke(obj, param);
    }
    //---------------------------------------------
}

代码比较简单, 这里不再赘述.

PrefabObjectPool

有了游戏对象的对象池之后, 我们就可以利用它实现预制对象的对象池, 大部分情况下, 我们使用的是预制对象的对象池.

预制是一个十分特殊的存在, 它本身就只是一个可以持久化的游戏对象而已, 但是它可以挂载各种组件, 可以容纳各种其它游戏对象, 这可以极大的简化了我们对各种游戏对象的维护, 基本上大部分场合下, 我们只需要关注预制和其实例化出来的对象就可以了.

这里我们使用继承的方式来实现, 因为预制对象本身就是一种游戏对象, 稍微有所区别的只是构造对象的时候是从预制实例化而已.

public class PrefabObjectPool : GameObjectPool {
    private GameObject m_Prefab;

    public PrefabObjectPool(GameObject prefab, Transform poolParent, int initCapacity = 5, int maxCount = 500, int onceReleaseCount = 10) {
        Assert.IsNotNull(prefab, "预制不能为空!");
        Assert.IsNotNull(poolParent, "挂载节点不能为空!");

        m_Prefab = prefab;
        InitPool(NewObj, poolParent, initCapacity, maxCount, onceReleaseCount);
    }

    // --------------------------------------------------------------------------------
    private GameObject NewObj(params object[] param) {
        ParseParams(out var parent, out var pos, out var quaternion, param);

        var obj = Object.Instantiate(m_Prefab, pos, quaternion, parent);
        return obj;
    }
}

注意在GameObjectPool中, 我们提供了一个默认构造函数, 并将持有的普通的对象池的实例化和初始化封装为初始化方法, 所以在实现PrefabObjectPool就变的很简单了.

PrefabObjectPool接受一个预制对象, 并重写创建对象方法, 在新建对象时从预制实例化.

有了以上的内容, 我们就可以正常使用了.

使用示例

public class ObjectPoolTest : MonoBehaviour {
    public Button spawnButton;
    public Button recycleButton;

    private GameObject m_PrefabPool;
    private GameObjectPool m_Pool;
    private Dictionary<int, GameObject> m_Objects = new Dictionary<int, GameObject>();

    private void Awake() {
        // ObjectPoolTest2.Test();
        var parentTrans = new GameObject("parentTrans");
        var prefabPool = new GameObject("PrefabPool");
        m_PrefabPool = prefabPool;

        var asset = AssetDatabase.LoadAssetAtPath<GameObject>("obj.prefab");

        var objectPool = new PrefabObjectPool(asset, prefabPool.transform, 10, 10, 2);
        // var objectPool = new GameObjectPool(s => {
        //     var obj = Instantiate(asset);
        //     return obj;
        // }, prefabPool.transform, 10, 10, 2);

        m_Pool = objectPool;
        objectPool.afterReleaseAction = OnObjectRelease;

        if (spawnButton != null) {
            var count = 0;
            spawnButton.onClick.AddListener(() => {
                var obj = m_Pool.Spawn(parentTrans.transform);
                m_Objects.Add(obj.GetHashCode(), obj);

                obj.name = "Obj: " + count++;
            });
        }

        if (recycleButton != null) {
            recycleButton.onClick.AddListener(() => {
                var obj = m_Objects.FirstOrDefault();
                if (obj.Value != null) {
                    m_Pool.Recycle(obj.Value);
                    m_Objects.Remove(obj.Key);
                }
            });
        }
    }

    private void OnObjectRelease(GameObject obj, params object[] param) {
        m_Objects.Remove(obj.GetHashCode());
    }

    private void OnDestroy() {
        if (m_PrefabPool != null) {
            Destroy(m_PrefabPool);
            m_PrefabPool = null;
        }

        m_Pool.Dispose();
    }
}

逻辑很简单, 点击Spawn生成对象, 并使用, 点击Recycle回收对象.

在这里插入图片描述
在这里插入图片描述

总结

今天介绍了游戏对象的对象池, 后续还可以实现对象池的管理器, 用来管理多个对象池, 在最后整套的资源管理方案部分会再次介绍.

原谅作者今天贴了这么多代码, 其实对象池的基本理论在前面两篇文章已经介绍的很详细了, 本篇文章更多的是在基建之上的应用而已.

对象池的核心概念很简单, 要用好却不容易, 我们在资源管理中也会大量用到对象池技术, 慢慢大家就会熟悉了.

下一篇文章会介绍另一个常用的技术: 引用计数. 相信很多同学都接触过, 作者会尝试将其基本理论梳理清楚, 并给出其应用, 希望感兴趣的同学继续关注.

好了, 今天的内容就这么多, 希望对大家有所帮助.

猜你喜欢

转载自blog.csdn.net/woodengm/article/details/122003136