Simple understanding and use of object pool mode (implemented in unity)

One, the concept of object pool

The object pool mode is not a unique design mode for game development. Its design ideas are the same as those of database connection pools and thread pools in other developments.

The core idea is not to delete it directly after use, but to put it back in the pool and take it out when needed. The emergence of the object pool mode mainly optimizes two points:

1. Prevent objects from being frequently created and deleted, resulting in memory jitter and frequent GC (garbage collection)

2. Object initialization cost is high

 

But because the objects of traditional software development are usually light to medium-weight, the overhead of allocating/releasing objects is negligible, so there are relatively few naive object pool applications in traditional software development. Generally, specific objects are optimized for ②, such as database connection pool and thread pool, which solves the reuse and the number of connections at the same time. But in the process of game development, because many game objects are created and deleted frequently, and game objects contain a lot of objects, even the simple object pool technology has more application scenarios.

Two, object pool operations

The following is a textual introduction to the basic operations of the object pool

Borrowing: In layman's terms, it is to get an object from the pool. If it is the first time to get an object, the pool must be initialized. If there is no more object in the pool, create one

Return: In layman's terms, the item was originally used up to be deleted, but after the object pool is applied, the object is returned to the pool, provided that the number in the pool is not greater than the preset maximum number (to prevent too much memory from exploding ), if the number in the pool is greater than the preset maximum number, delete it directly

Warm-up: It means to pre-load a certain number of objects. I personally think that this is one of the more essential parts of the object pool. If you don't do preheating, then the first time you create an object, it will directly involve initialization. An easy to understand reason is that players would rather wait 1 second longer on the loading interface than stall for 0.1 second in the game. Especially in competitive games, players will want to smash the computer (laughs). So I think if you don't do the warm-up object pool optimization, only half is done.

Shrinking: It’s almost like warming up the other way around. When returning, it said that if it exceeds the set threshold, it will not be returned to the pool but will be deleted directly, but in fact, deletion may also bring time costs, so we You can delete it first, and then delete and reduce the memory pool every time you pass the loading interface in the middle of the game. If you are afraid that the memory will burst before loading the interface, you can set an additional threshold that must be deleted, and its effect is the same as the one written above when returning. (I didn't do this function in my DEMO)

Reset: Each newly created object should be the same as the newly created one when it is "new", and cannot obviously have the state of the last used, so every time the object comes out of the pool, it should be treated with aftereffects. Place reset. In unity, the content of the manual initialization of the object is written in the OnEnable() of the object, including the force to clear the rigid body, etc. The difference between OnEnable() and Start() is that Start() is only the first when the object is first enabled. Frame run, OnEnable will run every time the object is re-enabled.

Three, specific experiments

The following is a small DEMO experiment that I did myself. Because it is just a small demo experiment, it is not very robust. I also expressed it in the comments, so if it is about completeness and soundness, don't complain too much. But if there is a principled problem in my code that leads to major optimization, please point out and learn.

Below is the script of ObjectPool. The functions are designed to make them look more like Unity's native Instantiate and Destroy.

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

public class ObjectPool : MonoBehaviour
{
    //自身单例
    public static ObjectPool me;

    //池的存储
    //TODO 此处value使用自定义封装类型而不是单纯的queue更健全
    private Dictionary<string, Queue<GameObject>> pool;

    //每个池中最大数量
    //TODO 应该每个池设置每个池单独的数量
    private int maxCount = int.MaxValue;
    public int MaxCount
    {
        get { return maxCount; }
        set
        {
            maxCount = Mathf.Clamp(value, 0, int.MaxValue);
        }
    }

    //初始化
    void Awake()
    {
        me = this;
        pool = new Dictionary<string, Queue<GameObject>>();

    }

    /// <summary>
    /// 从池中获取物体
    /// </summary>
    /// <param name="go">需要取得的物体</param>
    /// <param name="position"></param>
    /// <param name="rotation"></param>
    /// <returns></returns>
    public GameObject GetObject(GameObject go,Vector3 position,Quaternion rotation)
    {
        //如果未初始化过 初始化池
        if(!pool.ContainsKey(go.name))
        {
            pool.Add(go.name, new Queue<GameObject>());
        }
        //如果池空了就创建新物体
        if(pool[go.name].Count == 0)
        {
            GameObject newObject = Instantiate(go, position, rotation);
            newObject.name = go.name;/*
            确认名字一样,防止系统加一个(clone),或序号累加之类的
            实际上为了更健全可以给每一个物体加一个key,防止对象的name一样但实际上不同
             */

            return newObject;
        }
        //从池中获取物体
        GameObject nextObject=pool[go.name].Dequeue();
        nextObject.SetActive(true);//要先启动再设置属性,否则可能会被OnEnable重置
        nextObject.transform.position = position;
        nextObject.transform.rotation = rotation;
        return nextObject;
    }

    /// <summary>
    /// 把物体放回池里
    /// </summary>
    /// <param name="go">需要放回队列的物品</param>
    /// <param name="t">延迟执行的时间</param>
    /// TODO 应该做个检查put的gameobject的池有没有创建过池
    public void PutObject(GameObject go,float t)
    {
        if (pool[go.name].Count >= MaxCount)
            Destroy(go,t);
        else
            StartCoroutine(ExecutePut(go,t));
    }

    private IEnumerator ExecutePut(GameObject go, float t)
    {
        yield return new WaitForSeconds(t);
        go.SetActive(false);
        pool[go.name].Enqueue(go);
    }

    /// <summary>
    /// 物体预热/预加载
    /// </summary>
    /// <param name="go">需要预热的物体</param>
    /// <param name="number">需要预热的数量</param>
    /// TODO 既然有预热用空间换时间 应该要做一个清理用时间换空间的功能
    public void Preload(GameObject go,int number)
    {
        if (!pool.ContainsKey(go.name))
        {
            pool.Add(go.name, new Queue<GameObject>());
        }
        for (int i = 0; i < number; i++)
        {
            GameObject newObject = Instantiate(go);
            newObject.name = go.name;//确认名字一样,防止系统加一个(clone),或序号累加之类的
            newObject.SetActive(false);
            pool[go.name].Enqueue(newObject);
        }
    }

}

Then there is the test script GameManager

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

public class GameManager : MonoBehaviour
{

    public GameObject testObject;
    // Start is called before the first frame update
    void Start()
    {
        //预热
       ObjectPool.me.Preload(testObject, 500);
    }

    //无对象池测试
    public void TestOfNotOP()
    {
        StartCoroutine(CreateOfNotOP());
    }


    private IEnumerator CreateOfNotOP()
    {
        //统计500帧所用时间
        float t = 0.0f;
        //每一帧生成一个对象,定时2秒后自动消除
        for (int i = 0; i < 500; i++)
        {
            int x = Random.Range(-30, 30);
            int y = Random.Range(-30, 30);
            int z = Random.Range(-30, 30);
            GameObject newObject=Instantiate(testObject, new Vector3(x, y, z),Quaternion.identity);
            Destroy(newObject, 2.0f);

            yield return null;
            t += Time.deltaTime;
        }
        Debug.Log("无对象池500帧使用秒数:"+t);
    }

    //使用对象池测试
    public void TestOfOP()
    {

        StartCoroutine(CreateOfOP());
    }

    private IEnumerator CreateOfOP()
    {
        //统计500帧所用时间
        float t = 0.0f;
        //每一帧生成一个对象,定时2秒后自动消除
        for (int i = 0; i < 500; i++)
        {
            int x = Random.Range(-30, 30);
            int y = Random.Range(-30, 30);
            int z = Random.Range(-30, 30);
            GameObject newObject = ObjectPool.me.GetObject(testObject, new Vector3(x, y, z), Quaternion.identity);
            ObjectPool.me.PutObject(newObject, 2.0f);
            yield return null;
            t += Time.deltaTime;
        }
        Debug.Log("使用对象池500帧使用秒数:"+t);
    }
}

I made two buttons on a Canvas, and then made an empty GameObject used as GameManager, bound ObjectPool (object pool script) and GameManager (test script), let the OnClick events of the two buttons listen to TestOfNotOP respectively () and TestOfOP()

Finally, use a particle effect downloaded from the resource store to test

(See my B station for detailed test video)

It can be seen that playing 500 frames (one particle effect is generated per frame) is about 9.8-9.9 seconds if the object pool is not used, and playing 500 frames after applying the object pool is about 9.4-9.5 seconds. There are still optimized results. If it is an old version of unity, it may be optimized more. This is a long time ago, Yusong also complained about http://www.xuanyusong.com/archives/2925 Now unity particle effects initialization It should be optimized. In general, all optimization techniques, optimization costs and results must be measured.

Guess you like

Origin blog.csdn.net/sun124608666/article/details/112602188