Object pooling Rev. 2

Tulenber 12 June, 2020 ⸱ Intermediate ⸱ 6 min ⸱ 2019.3.15f1 ⸱

Let’s made improvements to object pooling by the results of field tests.

Not so long ago, we looked at the pattern "pool of objects" and create it minimalistic and relatively straightforward implementation. We solved the assigned task, but the result was not very convenient to use. Transfer of additional name parameters makes setup not so straight. In this article, we will present an updated version of the pools without these shortcomings.

Singleton

The first thing which can help avoid the unnecessary pointer transfer is the use of pool object as a singleton. This implementation is no different from the one given in the corresponding article, except for the name, with the aim of protection against conflicts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using UnityEngine;

public class KhtSingleton<T> : MonoBehaviour where T : KhtSingleton<T>
{
    private static T _instance;

    public static T Instance
    {
        get => _instance;
    }

    public static bool IsInstantiated
    {
        get => _instance != null;
    }

    protected virtual void Awake()
    {
        if (_instance != null)
        {
            Debug.LogError("["+ typeof(T) +"] '" + transform.name + "' trying to instantiate a second instance of singleton class previously created in '" + _instance.transform.name + "'");
        }
        else
        {
            _instance = (T) this;
        }
    }

    protected void OnDestroy()
    {
        if (_instance == this)
        {
            _instance = null;
        }
    }
}

Object pool

This implementation of objects pools based on the GetInstanceID() method, which allows you to get a unique identifier for an object in runtime.

Pools discovery uses the prefab identifier. If there is no pool for the given prefab, a new one will be created so you can skip the initial assigning. New objects are also tied to the pool by their identifier.
Open Test Runner

If you specify the prefab for the pool in the editor, you can configure its prefilling.
Open Test Runner

In the absence of a singleton that provides work with pools, new objects will be created and deleted through Instantiate() and Destroy() methods.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
using System;
using System.Collections.Generic;
using UnityEngine;

public class KhtPool : KhtSingleton<KhtPool>
{
    // There are no support of Dictionary type for Inspector, so let's use this
    [Serializable]
    public class PrefabData
    {
        public GameObject prefab;
        public int initPoolSize = 0;
    }
    [SerializeField] private List<PrefabData> prefabDatas = null;
    
    // Bind the pool to the prefab id
    private readonly Dictionary<int, Queue<GameObject>> _pools = new Dictionary<int, Queue<GameObject>>();
    // Bind an object to a pool by its identifier
    private readonly Dictionary<int, int> _objectToPoolDict = new Dictionary<int, int>();

    private new void Awake()
    {
        // Set up singleton
        base.Awake();

        // If necessary, prefill the pools of objects
        foreach (var prefabData in prefabDatas)
        {
            _pools.Add(prefabData.prefab.GetInstanceID(), new Queue<GameObject>());
            for (int i = 0; i < prefabData.initPoolSize; i++)
            {
                GameObject retObject = Instantiate(prefabData.prefab, Instance.transform, true);
                Instance._objectToPoolDict.Add(retObject.GetInstanceID(), prefabData.prefab.GetInstanceID());
                Instance._pools[prefabData.prefab.GetInstanceID()].Enqueue(retObject);
                retObject.SetActive(false);
            }
        }
        prefabDatas = null;
    }

    // Get the object from the pool
    public static GameObject GetObject(GameObject prefab)
    {
        // If there is no singleton, create a new object
        if (!Instance)
        {
            return Instantiate(prefab);
        }

        // Unique identifier of the prefab used to bind to the pool
        int prefabId = prefab.GetInstanceID();
        // If the pool for the prefab does not exist, then create a new
        if (!Instance._pools.ContainsKey(prefabId))
        {
            Instance._pools.Add(prefabId, new Queue<GameObject>());
        }

        // If there is an object in the pool, return it
        if (Instance._pools[prefabId].Count > 0)
        {
            return Instance._pools[prefabId].Dequeue();
        }

        // In case of shortage of objects, create a new
        GameObject retObject = Instantiate(prefab);
        // Add the binding of the object to the pool by its identifier
        Instance._objectToPoolDict.Add(retObject.GetInstanceID(), prefabId);
        
        return retObject;
    }

    // Return the object to the pool
    public static void ReturnObject(GameObject poolObject)
    {
        // In the absence of a singleton, we destroy the object
        if (!Instance)
        {
            Destroy(poolObject);
            return;
        }

        // Object identifier for pool definition
        int objectId = poolObject.GetInstanceID();

        // If there is no binding of the object to the pool, we destroy it
        if (!Instance._objectToPoolDict.TryGetValue(objectId, out int poolId))
        {
            Destroy(poolObject);
            return;
        }

        // Return the object to the pool
        Instance._pools[poolId].Enqueue(poolObject);
        poolObject.transform.SetParent(Instance.transform);
        poolObject.SetActive(false);
    }
}

Usage

Using an object from the pool

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using UnityEngine;

public class Fire : MonoBehaviour
{
    // Non-Pooling Information
    [SerializeField] private Transform leftCannon = null;
    [SerializeField] private Transform rightCannon = null;
    [SerializeField] private UnevirseHandler universe = null;
    
    [SerializeField] private Transform space = null;
    
    // Prefab to get an object from the pool
    [SerializeField] private GameObject projectilePrefab = null;

    private bool _rightCannon = false;

    void Update()
    {
        if (Input.GetMouseButtonDown (0)) {
            // Get the object from the pool
            GameObject pr = KhtPool.GetObject(projectilePrefab);

            // Objects from the pool need proper cleanup and initialization
            pr.transform.SetParent(space);
            pr.transform.SetPositionAndRotation(_rightCannon ? rightCannon.position : leftCannon.position, _rightCannon ? rightCannon.rotation : leftCannon.rotation);
            Projectile prpr = pr.GetComponent<Projectile>();
            prpr.MoveVector = universe.LookVector.normalized;
            pr.SetActive(true);

            _rightCannon = !_rightCannon;
        }
    }
}

An example of an object stored in a pool

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using UnityEngine;

public class Projectile : MonoBehaviour
{
    [SerializeField] private float speed = 0;

    private Vector2 _moveVector = Vector2.zero;

    public Vector2 MoveVector
    {
        set => _moveVector = value;
    }

    void Update()
    {
        transform.Translate(_moveVector.x * Time.deltaTime * speed, _moveVector.y * Time.deltaTime * speed, 0, Space.World);
    }

    void OnBecameInvisible() {
        // After the object leaves the screen, return it to the pool
        KhtPool.ReturnObject(gameObject);
    }
}

Result

New objects created on-demand:
Open Test Runner

Conclusion

The updated version greatly simplifies our previous implementation. However, it expands the possibilities for adding additional functionality, such as a prohibition of new pool creation or a limitation on their expanding. But these features are used much less frequently than prefilling, so let’s leave them for the future. See you next time! =)



Privacy policyCookie policyTerms of service
Tulenber 2020