Resource management in Unity - object pool technology (1)

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

In the next few days, the author will write several articles about resource management in Unity according to his own understanding.

It will probably involve:

  • Object pool : object pool divided into ordinary classes and GameObject (mainly prefabricated)

  • reference counting technique

  • The basic concept, classification, and basic use of resources in Unity : including Resources, AssetDatabase

  • Ab package in Unity : contains the introduction, generation, loading and unloading of Ab package

  • Using Profiler in Unity for memory analysis

  • A complete set of resource management solutions

Today I will share the first part of the object pool technology: the object pool of common classes.

What is an object pool and why is it needed

Before officially starting, we need to understand why an object pool is needed.

The instantiation of objects may be a relatively heavy work, such as the instantiation of prefabricated objects, which need to do a lot of things behind the scenes, such as loading the resources it depends on, instantiating these resources, assigning values ​​​​to resource objects, assigning values ​​​​to prefabricated objects, and finally getting Prefabricated instantiated objects. For the same prefab, if you need to use and destroy its instantiated object multiple times, it will cause resource waste and lag. We can save its instantiated object when it is not in use instead of directly destroying it. The cached object is directly given to the cached object without instantiation when it is used next time, which can make full use of resources and speed up the process.

Simply put, the object pool is a technology that saves multiple objects that do not need to be used, and gives them directly when they need to be used next time without instantiating them.

The most common example of object pooling technology in our game development is the use of bullets.

Imagine, if we don't use the object pool, we will create and destroy objects frequently, which has a great impact on performance.

The object pool is a typical space-for-time technology. For a smooth player experience, we usually preload and pre-create a certain number of objects before entering the scene and store them in the object pool, and then use them directly during the game That's it.

Simple object pool implementation

First of all, we first implement a simple object pool that meets the basic needs: objects can be generated, recycled, and destroyed.

public class SimpleObjectPool<T> where T : class, new() {
    protected readonly Stack<T> m_ObjPool;

    protected int m_MaxCount;
    protected int m_OnceReleaseCount;

    public SimpleObjectPool(int initCapacity = 5, int maxCount = 500, int onceReleaseCount = 10) {
        m_ObjPool = new Stack<T>(initCapacity);

        m_MaxCount = maxCount;
        m_OnceReleaseCount = onceReleaseCount;
    }

    public T Spawn() {
        var obj = m_ObjPool.Count <= 0 ? CreateObj() : m_ObjPool.Pop();
        return obj;
    }

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

        m_ObjPool.Push(obj);
    }

    private T CreateObj() {
        var obj = new T();
        return obj;
    }

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

        Debug.Log("[ReleaseOverflowObj] 当前池中数量: " + m_ObjPool.Count);
    }
}

We use generics to create object pools, which are compatible with most classes.

The first line public class SimpleObjectPool<T> where T : class, new(), which where T : class, new()means that there are two constraints on the contained structure: the structure Tmust be a class ( class), and have a no-argument constructor ( new()).

We use the stack as a container for objects, because we will add and delete frequently, and the complexity of the stack for these two operations is O(1)level.

Another common implementation is to use an array. Each position of the array represents a slot. The object to be recycled is inserted into the slot, and the index is returned. When it needs to be used, it relies on the index to obtain it. The object pool is used in this way Xlua.

SpawnThe and Recycleinterface represent generating and recycling objects respectively, and the logic is relatively simple.

Our implementation also has a maximum capacity limit before destroying the configured number of objects.

we need more

The above implementation is sufficient in some simple scenarios, but in daily development we often need to know when objects are generated and destroyed, such as doing some cleanup operations when they are actually destroyed.

Next we add some new functions: Notify the caller when the object is generated, recycled, and destroyed, and the object can be created according to the method provided by the caller.

public class ObjectPool<T> : IDisposable where T : class, new() {
    public delegate T NewObj();

    protected readonly Stack<T> m_ObjPool;
    protected int m_MaxCount;
    protected int m_OnceReleaseCount;

    protected UnityAction<T> m_BeforeSpawn, m_AfterRecycle, m_AfterRelease;
    protected NewObj m_NewObjDelegate;

    public ObjectPool(int initCapacity = 5, int maxCount = 500, int onceReleaseCount = 10,
                      UnityAction<T> beforeSpawn = null, UnityAction<T> afterRecycle = null, UnityAction<T> afterRelease = null, 
                      NewObj newObj = null) {

        m_ObjPool = new Stack<T>(initCapacity);

        m_MaxCount = maxCount;
        m_OnceReleaseCount = onceReleaseCount;

        m_BeforeSpawn = beforeSpawn;
        m_AfterRecycle = afterRecycle;
        m_AfterRelease = afterRelease;
        m_NewObjDelegate = newObj;
    }

    public void Dispose() {
        if (m_AfterRelease != null) {
            var array = m_ObjPool.ToArray();
            foreach(var obj in array) {
                m_AfterRelease(obj);
            }
        }

        m_ObjPool.Clear();

        m_BeforeSpawn = m_AfterRecycle = m_AfterRelease = null;
        m_NewObjDelegate = null;
    }

    private T CreateObj() {
        var obj = m_NewObjDelegate != null ? m_NewObjDelegate() : new T();
        return obj;
    }

    /// <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();
            m_AfterRelease?.Invoke(obj);
        }

        Debug.Log("[ReleaseOverflowObj] 当前池中数量: " + m_ObjPool.Count);
    }

    public T Spawn() {
        var obj = m_ObjPool.Count <= 0 ? CreateObj() : m_ObjPool.Pop();
        m_BeforeSpawn?.Invoke(obj);

        return obj;
    }

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

        m_ObjPool.Push(obj);
        m_AfterRecycle?.Invoke(obj);
    }
}

As you can see, we have added several delegates and called them at the corresponding positions:

  • UnityAction<T> m_BeforeSpawn: notify before object is generated
  • UnityAction<T> m_AfterRecycle: Notify before recycling the object
  • UnityAction<T> m_AfterRelease: notify before destroying the object
  • NewObj m_NewObjDelegate: generate object

Finally, implement the interface IDisposable, clean up these delegates when the object pool is destroyed, and develop a good habit of cleaning up delegates, so as to avoid Xluanormal garbage collection when used in such objects.

BTW: m_AfterRecycle?.Invoke(obj);the equivalent ofif (m_AfterRecycle != null) m_AfterRecycle(obj);

Example of use

It is very simple to use, that is, apply when you need it, and recycle it when you don't need it.

public class Animal {
    public int id;
    public string name;

    public void Reset() {
        id = 0;
        name = string.Empty;
    }
}

public class ObjectPoolTest {
    public static void Test() {
        var objectPool = new ObjectPool<Animal>(1, 10, 5,
            animal => {
                Debug.Log(string.Format("before spawn code: {0} ", animal.GetHashCode()));
            }, 
            animal => {
                Debug.Log(string.Format("after recycle code: {0}, name: {1}", animal.GetHashCode(), animal.name));

                animal.Reset();
            },
            animal => {
                Debug.Log(string.Format("after release code: {0}", animal.GetHashCode()));
            });

        var index = 1;
        var lst = new List<Animal>();
        for(var i = 0; i < 20; i++) {
            var animal = objectPool.Spawn();
            animal.id = i;
            animal.name = "name: " + index++;

            lst.Add(animal);
        }

        for(var i = 18; i > 0; i--) {
            var animal = lst[i];
            objectPool.Recycle(animal);

            lst.Remove(animal);
        }

        for(var i = 0; i < 2; i++) {
            var animal = objectPool.Spawn();
            animal.id = i;
            animal.name = "name: " + index++;

            lst.Add(animal);
        }

        foreach(var animal in lst) {
            objectPool.Recycle(animal);
        }

        objectPool.Dispose();
    }
}

We create some objects, then use them, then recycle them, then make some more objects, and finally recycle all objects.

The whole process will reach the maximum number of objects, and then keep recycling. The output will not be posted here, and interested students can try it by themselves.

at last

Today I shared the object pool of common classes, and in the next article we will use the object pool technology to manage GameObjectobjects in Unity.

Well, that's it for today, I hope it can be helpful to everyone.

Guess you like

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