Resource Management in Unity - Object Pool Technology(3)

This article shares resource management in Unity - object pool technology (3)

In the last two articles, we have learned two object pool implementations of common classes. Today we will introduce the object pool of GameObject (mainly prefabricated instantiated objects) in Unity.

The necessity of GameObjectPool

We know that in most cases, the development of the front-end of the game does not involve the creation and destruction of a large number of objects like the development of the server. The general container has dozens or hundreds of objects at most, and there are not too many objects. There is a lot of performance pressure, so the object pool of ordinary classes is not used much on the client side. Generally, a few occasions such as resource management will involve the use of a huge number of ordinary class objects, while the game front-end uses more object pools in the The use of a large number or complex game objects, such as a large number of bullets, enemies, monsters, UI, etc.

Because game objects generally involve display-related parts, the construction and destruction operations will be more complicated than ordinary class objects, occupy more resources, and cause higher costs. In order to avoid unnecessary waste of resources and improve game experience, in large In games, using object pools of game objects is a common solution.

In Unity, most resources usually do not need to be instantiated manually, such as texture, atlas, shader, material, sound, animation, etc., are generally used through prefabricated instantiation, so we call the object pool of GameObject , More refers to an object pool of prefab instantiated objects .

Optimization of ObjectPool

In the first article, we introduced an implementation of the object pool. Before we further introduce the object pool of the game object, we need to optimize the implementation a little to make it more general.

The main optimization points are:

  • Abstract the interface of the object pool:IObjectPool
  • Standardize the delegation of creating objects and the delegation of various trigger points, and add variable length parameters

Object pool interface:IObjectPool

We encapsulate generation and recycling as interfaces to be more general.

Object-generating methods can now accept variable-length parameters.

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

Various commissions

Encapsulate and expose the delegate, and can accept variable-length parameters.

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;

full code

The other parts have not changed much. For the sake of completeness, the code is still posted here:

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

The object pool of the game object has its unique characteristics compared with the ordinary object pool:

  • The stored object types are:GameObject

  • It is necessary to provide a delegate for creating objects, and hand over the work of creating objects to the outside to be compatible with more situations

  • The mount point of the object needs to be provided ( Transform), the mount point is invisible by default ( SetActive(false)), when the object in the object pool is mounted under it, it is invisible by default, and there is no need to call it again SetActive(false)to improve performance

  • Implement the object pool interface and hold a common object pool instead of inheriting the object pool

  • Do some basic operations related to game objects in each delegate, such as: set position/rotation, set parent node, destroy object, etc.

For details, please refer to the following code and comments:

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);
    }
    //---------------------------------------------
}

The code is relatively simple, so I won't repeat it here.

PrefabObjectPool

With the object pool of game objects, we can use it to implement the object pool of prefabricated objects. In most cases, we use the object pool of prefabricated objects.

Prefabrication is a very special existence. It is just a persistent game object, but it can mount various components and accommodate various other game objects, which greatly simplifies our understanding of various games. Object maintenance, basically in most cases, we only need to pay attention to the prefab and its instantiated objects.

Here we use inheritance to implement, because the prefab object itself is a kind of game object, the slight difference is that when constructing the object, it is instantiated from the prefab.

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;
    }
}

Note that GameObjectPoolin , we provide a default constructor, and encapsulate the instantiation and initialization of the held common object pool as an initialization method, so the implementation becomes very PrefabObjectPoolsimple.

PrefabObjectPoolAccepts a prefab object, and overrides the create object method to instantiate from the prefab when creating a new object.

With the above content, we can use it normally.

Example of use

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();
    }
}

The logic is very simple, click Spawnto generate an object, and use it, click to Recyclerecycle the object.

insert image description here
insert image description here

Summarize

Today we introduced the object pool of game objects, and we can also implement the object pool manager later to manage multiple object pools, which will be introduced again in the final set of resource management solutions.

Forgive the author for posting so much code today. In fact, the basic theory of the object pool has been introduced in detail in the previous two articles. This article is more about the application on the infrastructure.

The core concept of the object pool is very simple, but it is not easy to use it well. We will also use the object pool technology extensively in resource management, and gradually everyone will be familiar with it.

The next article will introduce another commonly used technology: citation counting . I believe that many students have come into contact with it. The author will try to sort out its basic theory and give its application. I hope that interested students will continue to pay attention.

Well, that's all for today's content, I hope it will be helpful to everyone.

Guess you like

Origin blog.csdn.net/woodengm/article/details/122003136