Пулы объектов в Unity

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

Статья о паттерне, который поможет собрать все ваши объекты в одном бассейне.
ОБНОВЛЁННУЮ реализацию можно найти в статье "Пулы объектов. Ревизия №2.“

В предыдущем посте "Бесконечный 2D фон в Unity" мы показали трюк, как сделать бесконечный фон при помощи одной картинки, примерно такой же подход используется ко всему, что может быть задействовано в игре. Чем больше игра, тем сложнее моделировать все игровые объекты в реальном времени, поэтому всё, что выходит за рамки экрана обычно прячется и удаляется, дабы не занимать такие ограниченные и ценные вычислительные мощности. При этом выделение и высвобождение памяти одна из наиболее затратных операций, с точки зрения этих самых вычислительных мощностей, а в языках со сборщиком мусора(коим и является применяемый в Unity C#) это становится ещё и вопросом времени его работы. Соответственно, при достаточном объёме доступной оперативной памяти, интересным подходом становится переиспользование созданных, но не используемых объектов, вместо их удаления. Именно этим и занимается паттерн под названием пул объектов(Object Pooling), который мы рассмотрим в этой статье.

Теория

Теоретическая часть описана довольно подробно и за ней можно обратиться к этой книге.

Практика

Основными задачами нашей реализации будут простота и минималистичность.

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

  1. За пулы объектов(их может быть несколько) будет отвечать префаб с приведённым ниже скриптом
 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
using System;
using System.Collections.Generic;
using UnityEngine;

public class ObjectPool : MonoBehaviour
{
    // Хак из-за отсутствия поддержки отображения Dictionary в инспекторе
    // Словарь с необходимыми объектами будет создаваться из листа с описанием префабов
    [Serializable]
    public class PrefabData
    {
        public string name;
        public GameObject prefab;
    }
    [SerializeField] private List<PrefabData> prefabDatas = null;
    
    // Префабы для создания новых объектов
    private Dictionary<string, GameObject> prefabs = new Dictionary<string, GameObject>();
    // Пулы для свободных объектов
    private Dictionary<string, Queue<GameObject>> pools = new Dictionary<string, Queue<GameObject>>();

    private void Awake()
    {
        // Перекладываем информацию о пулах из нашей структуры в 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(pools[poolName].Count > 0)
        {
            return pools[poolName].Dequeue();
        }

        // Если пул пуст, то создаём новый объект
        return Instantiate(prefabs[poolName]);
    }

    public void ReturnObject(string poolName, GameObject poolObject)
    {
        // Возвращаем объект в пул
        pools[poolName].Enqueue(poolObject);
    }
}
  1. Пример объекта, который будет храниться в пуле
 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
using UnityEngine;

public class Projectile : MonoBehaviour
{
    [SerializeField] private float speed = 0;
    // Ссылка на объект с пулами
    [SerializeField] public ObjectPool objectPool = null;

    // Имя, используемое для работы с пулом
    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() {
        // После выхода объекта за границы экрана возвращаем его в пул
        gameObject.SetActive(false);
        objectPool.ReturnObject(PoolName, gameObject);
    }
}
  1. Пример создания объекта
 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
{
    // Информация, не относящаяся к пулам
    [SerializeField] private Transform leftCannon = null;
    [SerializeField] private Transform rightCannon = null;
    [SerializeField] private UnevirseHandler universe = null;
    [SerializeField] private Transform space = null;
    private bool _rightCannon = false;

    // Ссылка на объект с пулами
    [SerializeField] private ObjectPool objectPool = null;

    void Update()
    {
        if (Input.GetMouseButtonDown (0)) {
            // Берём объект из пула
            GameObject pr = objectPool.GetObject(Projectile.PoolName);

            // Объекты из пула нуждаются в правильной очистке и инициализации
            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;
        }
    }
}
  • Не имеющий отношения к пулам объектов, но изменённый с предыдущей статьи код для UniverseHandler, что бы вы могли повторить пример полностью
 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
{
    // Ссылки на объекты
    [SerializeField] private Camera mainCamera = null;
    [SerializeField] private GameObject ship = null;
    [SerializeField] private GameObject space = null;
    
    // Радиус возможной видимости для камеры
    private float _spaceCircleRadius = 0;

    // Размер фона
    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()
    {
        // Получаем исходный размер фона
        SpriteRenderer sr = space.GetComponent<SpriteRenderer>();
        var originalSize = sr.size;
        _backgroundOriginalSizeX = originalSize.x;
        _backgroundOriginalSizeY = originalSize.y;

        // Высота равна ортографическому размеру камеры
        float orthographicSize = mainCamera.orthographicSize;
        // Ширина равна ортографическому размеру помноженному на соотношение сторон
        float screenAspect = (float)Screen.width / (float)Screen.height;
        _spaceCircleRadius = Mathf.Sqrt(orthographicSize * screenAspect * orthographicSize * screenAspect + orthographicSize * orthographicSize);

        // Конечный размер фона должен позволять сдвинуться на один базовый размер фона в любом направлении + перекрыть радиус камеры также во всех направлениях
        sr.size = new Vector2(_spaceCircleRadius * 2 + _backgroundOriginalSizeX * 2, _spaceCircleRadius * 2 + _backgroundOriginalSizeY * 2);
        
        _shipPosition = ship.transform.position;
    }

    void Update()
    {
        // Положение мышки в пространстве
        _stvp = mainCamera.ScreenToWorldPoint(Input.mousePosition);
        // Вектор для направления звездолёта
        _lookVector = _stvp - _shipPosition;

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

Демонстрация

В приведённом ниже результате можно увидеть, что новые объекты создаются только при отсутствии свободных.
Result

Заключение

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

ОБНОВЛЁННУЮ реализацию можно найти в статье "Пулы объектов. Ревизия №2.“



Privacy policyCookie policyTerms of service
Tulenber 2020