Пулы объектов. Версия №2.

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

Доработки по результатам боевых испытаний первой версии пулов объектов.

Не так давно мы рассматривали паттерн пулы объектов, создав их минималистичную и довольно прямолинейную реализацию. Она решала поставленную перед ней задачу, но всё-таки была не очень удобна в использовании, так как вводила дополнительные сущности в виде имён, которые приходилось передавать между объектами. В этой статье мы приведём обновлённую версию пулов, которая лишена этих недостатков.

Singleton

Для начала хотелось бы сделать из пула объектов синглетон, это поможет избежать лишней передачи ссылки в использующие пулы объекты.

Эта реализация ничем не отличается от приведённой в соответствующей статье, кроме названия, с целью защиты от конфликтов.

 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

Данная реализация основана на использовании метода GetInstanceID() который позволяет получить уникальный идентификатор объекта.

Пулы привязываются к идентификатору префаба. В случае отсутствия пула для префаба создаётся новый, поэтому можно не задавать список пулов заранее, они будут созданы по мере необходимости. Новые объекты привязываются к пулу также по идентификатору.
Open Test Runner

В случае указания префаба для пула заранее, можно настроить его предварительное наполнение.
Open Test Runner

При отсутствии синглетона, предоставляющего работу с пулами, новые объекты будут создаваться и удаляться через Instantiate() и Destroy()

 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
98
using System;
using System.Collections.Generic;
using UnityEngine;

public class KhtPool : KhtSingleton<KhtPool>
{
    // Хак из-за отсутствия поддержки отображения Dictionary в инспекторе
    // Словарь с необходимыми объектами будет создаваться из листа с описанием префабов
    [Serializable]
    public class PrefabData
    {
        public GameObject prefab;
        public int initPoolSize = 0;
    }
    [SerializeField] private List<PrefabData> prefabDatas = null;
    
    // Привязка пула к идентификатору префаба
    private readonly Dictionary<int, Queue<GameObject>> _pools = new Dictionary<int, Queue<GameObject>>();
    // Привязка объекта к пулу по его идентификатору
    private readonly Dictionary<int, int> _objectToPoolDict = new Dictionary<int, int>();

    private new void Awake()
    {
        // Настройка синглетона
        base.Awake();

        // При необходимости предварительно наполняем пулы объектов
        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;
    }

    // Получение объекта из пула
    public static GameObject GetObject(GameObject prefab)
    {
        // В случае отсутствия синглетона просто создаём новый объект
        if (!Instance)
        {
            return Instantiate(prefab);
        }

        // Уникальный идентификатор префаба, по которому осуществляется привязка к пулу
        int prefabId = prefab.GetInstanceID();
        // Если пула для префаба не существует, то создаём новый
        if (!Instance._pools.ContainsKey(prefabId))
        {
            Instance._pools.Add(prefabId, new Queue<GameObject>());
        }

        // При наличии объекта в пуле возвращаем его
        if (Instance._pools[prefabId].Count > 0)
        {
            return Instance._pools[prefabId].Dequeue();
        }

        // В случае нехватки объектов создаём новый
        GameObject retObject = Instantiate(prefab);
        // Добавляем привязку объекта к пулу по его идентификатору
        Instance._objectToPoolDict.Add(retObject.GetInstanceID(), prefabId);
        
        return retObject;
    }

    // Возврат объекта в пул
    public static void ReturnObject(GameObject poolObject)
    {
        // В случае отсутствия синглетона просто уничтожаем объект
        if (!Instance)
        {
            Destroy(poolObject);
            return;
        }

        // Идентификатор объекта для определения пула
        int objectId = poolObject.GetInstanceID();

        // В случае отсутствия привязки объекта к пулу просто его уничтожаем
        if (!Instance._objectToPoolDict.TryGetValue(objectId, out int poolId))
        {
            Destroy(poolObject);
            return;
        }

        // Возвращаем объект в пул
        Instance._pools[poolId].Enqueue(poolObject);
        poolObject.transform.SetParent(Instance.transform);
        poolObject.SetActive(false);
    }
}

Использование

Использование объекта из пула

 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
{
    // Информация, не относящаяся к пулам
    [SerializeField] private Transform leftCannon = null;
    [SerializeField] private Transform rightCannon = null;
    [SerializeField] private UnevirseHandler universe = null;
    
    [SerializeField] private Transform space = null;
    
    // Префаб для получения объекта из пула
    [SerializeField] private GameObject projectilePrefab = null;

    private bool _rightCannon = false;

    void Update()
    {
        if (Input.GetMouseButtonDown (0)) {
            // Получени объекта из пула
            GameObject pr = KhtPool.GetObject(projectilePrefab);

            // Объекты из пула нуждаются в правильной очистке и инициализации
            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;
        }
    }
}

Пример объекта, который будет храниться в пуле

 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() {
        // После выхода объекта за границы экрана возвращаем его в пул
        KhtPool.ReturnObject(gameObject);
    }
}

Результат

Новые объекты создаются при необходимости:
Open Test Runner

Заключение

Обновлённая версия сильно упрощает нашу предыдущую реализацию, хотя и расширяет возможности для добавления дополнительного функционала, такого как запрет на создание новых пулов, или запрет их расширения. Но эти фичи используются сильно реже, чем предварительное наполнение, так что оставим их на будущее. Пока! =)



Privacy policyCookie policyTerms of service
Tulenber 2020