Object pooling in Unity

Tulenber 10 April, 2020 ⸱ Intermediate ⸱ 6 min ⸱ 2019.3.8f1 ⸱

This post is about a pattern that will help to collect all your objects in one pool. UPDATED implementation can be found at "Object pooling Rev. 2"

In a previous post, "Infinite 2D background in Unity,“ we showed a trick on how to make an endless background using one picture, approximately the same approach used to everything that can be involved in the game. The larger the game, the more difficult it is to simulate all the game objects in real-time, so everything that goes beyond the screen is usually hidden and deleted to save such limited and valuable computing power. At the same time, allocating and freeing memory is one of the most expensive operations for computing. Also, in languages ​​with a garbage collector (C#), this becomes a matter of time for its service. Accordingly, with a sufficient amount of available RAM, an interesting approach is to reuse created, but not used objects, instead of deleting them; this is what the pattern called the Object Pooling does, which we will cover in this post.

Theory

The theoretical part well described in sufficient detail, and you can refer to this book.

Practice

The main tasks of our implementation will be simplicity and minimalism.

The implementation based on the previous article about the endless background, so if you want to repeat this example and have no idea what to do, you can start with it and make the changes below.

  1. This script used in prefab is responsible for all object pools(there may be several)
 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
using System;
using System.Collections.Generic;
using UnityEngine;

public class ObjectPool : MonoBehaviour
{
    // There are no support of Dictionary type for Inspector, so let's use this
    [Serializable]
    public class PrefabData
    {
        public string name;
        public GameObject prefab;
    }
    [SerializeField] private List<PrefabData> prefabDatas = null;
    
    // Prefabs for new object instances
    private Dictionary<string, GameObject> prefabs = new Dictionary<string, GameObject>();
    // Pools for free objects
    private Dictionary<string, Queue<GameObject>> pools = new Dictionary<string, Queue<GameObject>>();

    private void Awake()
    {
        // Put information about our pools to Dictionary
        foreach (var prefabData in prefabDatas)
        {
            prefabs.Add(prefabData.name, prefabData.prefab);
            pools.Add(prefabData.name, new Queue<GameObject>());
        }
        prefabDatas = null;
    }

    public GameObject GetObject(string poolName)
    {
        // If there are free object in pool return it
        if(pools[poolName].Count > 0)
        {
            return pools[poolName].Dequeue();
        }

        // If object pool empty return new Instance
        return Instantiate(prefabs[poolName]);
    }

    public void ReturnObject(string poolName, GameObject poolObject)
    {
        // Return object to pool
        pools[poolName].Enqueue(poolObject);
    }
}
  1. Example of pool object
 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
using UnityEngine;

public class Projectile : MonoBehaviour
{
    [SerializeField] private float speed = 0;
    // Link to the pool handler object
    [SerializeField] public ObjectPool objectPool = null;

    // Pool name
    public const string PoolName = "Projectiles";

    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 object leave screen we return the object to the pool
        gameObject.SetActive(false);
        objectPool.ReturnObject(PoolName, gameObject);
    }
}
  1. Example of receiving 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
using UnityEngine;

public class Fire : MonoBehaviour
{
    // References to scene objects
    [SerializeField] private Transform leftCannon = null;
    [SerializeField] private Transform rightCannon = null;
    [SerializeField] private UnevirseHandler universe = null;
    [SerializeField] private Transform space = null;
    private bool _rightCannon = false;

    // Link to the object pool
    [SerializeField] private ObjectPool objectPool = null;

    void Update()
    {
        if (Input.GetMouseButtonDown (0)) {
            // Get an object from the pool
            GameObject pr = objectPool.GetObject(Projectile.PoolName);

            // Objects from pool require proper 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;
            prpr.objectPool = objectPool;
            pr.SetActive(true);

            _rightCannon = !_rightCannon;
        }
    }
}
  • This script not related to pooling, but if you want to repeat this code then you can use it to change example from the previous post
 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
using UnityEngine;

public class UnevirseHandler : MonoBehaviour
{
    // References to scene objects
    [SerializeField] private Camera mainCamera = null;
    [SerializeField] private GameObject ship = null;
    [SerializeField] private GameObject space = null;
    
    // The radius of a possible camera view
    private float _spaceCircleRadius = 0;

    // Original background object dimensions
    private float _backgroundOriginalSizeX = 0;
    private float _backgroundOriginalSizeY = 0;
    
    private Vector2 _shipPosition = Vector2.zero;
    private Vector2 _lookVector = Vector2.zero;
    private Vector2 _stvp = Vector2.zero;

    public Vector2 LookVector => _lookVector;

    void Start()
    {
        // Original Background Sizes
        SpriteRenderer sr = space.GetComponent<SpriteRenderer>();
        var originalSize = sr.size;
        _backgroundOriginalSizeX = originalSize.x;
        _backgroundOriginalSizeY = originalSize.y;

        // Camera height equal to the orthographic size
        float orthographicSize = mainCamera.orthographicSize;
        // Camera width equals to orthographic size multiplied by aspect 
        float screenAspect = (float)Screen.width / (float)Screen.height;
        _spaceCircleRadius = Mathf.Sqrt(orthographicSize * screenAspect * orthographicSize * screenAspect + orthographicSize * orthographicSize);

        // The final background sprite size should allow you to move by one basic background texture size in any direction + overlap the radius of the camera also in all directions
        sr.size = new Vector2(_spaceCircleRadius * 2 + _backgroundOriginalSizeX * 2, _spaceCircleRadius * 2 + _backgroundOriginalSizeY * 2);
        
        _shipPosition = ship.transform.position;
    }

    void Update()
    {
        // Mouse coordinates in World space
        _stvp = mainCamera.ScreenToWorldPoint(Input.mousePosition);
        // Direction to the mouse pointer
        _lookVector = _stvp - _shipPosition;

        float rotZ = Mathf.Atan2(_lookVector.y, _lookVector.x) * Mathf.Rad2Deg;
        ship.transform.rotation = Quaternion.Euler(0f, 0f, rotZ - 90);
    }
}

Demonstration

In the result below, you can see that new objects are created only in the absence of free ones.
Result

Conclusion

Unity does not offer us any reference implementation of object pools, and since this is a prevalent pattern, you can find a wide range of variations of it. Code, given in the article, does not have such a useful thing as pre-filling the pool. Also, it requires the double-entry of the pool name, which increases the complexity of the configuration. And in general, you can make a rather voluminous decision for the sake of additional syntactic sugar and functionality; however, for a start, we will focus on a minimalistic version. See you next time! =)

UPDATED implementation can be found at "Object pooling Rev. 2"



Privacy policyCookie policyTerms of service
Tulenber 2020