Введение

Привет, начинающие разработчики игр! 🚀

Сегодня мы углубимся в мощную технику оптимизации игры — объединение объектов. Это жемчужина оптимизации, которую должен иметь в своем арсенале каждый разработчик игр. Представьте себе сокращение накладных расходов на постоянное создание и уничтожение игровых объектов, а вместо этого их «переработку». Звучит круто, правда? Давайте разберем это, используя простой и лаконичный пример кода!

Почему объединение объектов? 🤔

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

Понимание реализации класса ObjectPoolManager.

    //Your prefab to be pooled to memory.
    [SerializeField] private GameObject _prefab;

    //Total count of the objects to be generated in the pool.
    [SerializeField] private int _objectPoolCount;

    //If checked true, all objects will stay under this gameobject in hierarchy.
    [SerializeField] private bool _parentToThisObject = true;

    //A queue to hold/store all pooled objects.
    private Queue<GameObject> _objectPoolQueue = new Queue<GameObject>();

_prefab — это игровой объект, который мы собираемся объединить. Думайте об этом как о пуле в стрелялке или повторно используемом эффекте частиц.

_objectPoolCount — определяет, сколько объектов мы хотим «предварительно создать» и иметь готовых к работе.

_objectPoolQueue — это очередь для хранения наших объектов в пуле. Мы используем очередь, потому что это быстро и эффективно для нашего варианта использования.

Вот где начинается волшебство. В методе InitializeObjectPool() мы создаем наши объекты и держим их готовыми к действию.

 //Object Pool Logic
    private void InitializeObjectPool()
    {
        //Iterating till the total count.
        for (int i = 0; i < _objectPoolCount; i++)
        {
            GameObject tempGO = Instantiate(_prefab);

            //Enqueuing the generated object to the memory.
            _objectPoolQueue.Enqueue(tempGO);

            if (_parentToThisObject)
                tempGO.transform.SetParent(this.transform);

            //Setting name for the object.
            tempGO.SetActive(false);
        }
    }

К концу этого метода у нас будет _objectPoolCount количество игровых объектов, все деактивированные и ожидающие в очереди.

Получение и возврат объектов

Хотите игровой объект? Задайте метод GetObject(). Готово? Передайте его методу ReturnObject().

Когда вам нужен объект из пула, получите его, как показано ниже.

Предположим, вы объединили игровые объекты в пул в памяти.

 GameObject bullet = ObjectPoolManager.Instance.GetObject();

Давайте посмотрим на реализацию метода GetObject().

public GameObject GetObject()
    {
        if (_objectPoolQueue.Count > 0)
        {
            //Dequeue an object from the queue and allow to be used in the game.
            GameObject obj = _objectPoolQueue.Dequeue();
            obj.SetActive(true);
            return obj;
        }
        else
        {
            //Generate a new object and return if none of the objects are available in the memory.
            //This is an edge case.
            GameObject obj = Instantiate(_prefab);
            return obj;
        }
    }

Функция GetObject() занимает центральное место в системе объединения объектов. Его основная цель — предоставить игровой объект — либо путем повторного использования ранее созданного объекта, либо путем создания нового.

При запросе объекта:

  • Сначала функция проверяет, есть ли в очереди уже доступные объекты.
  • Если в очереди есть объект, он удаляется из очереди, активируется (чтобы он был виден в игровой сцене), а затем возвращается вызывающему объекту. Этот процесс повторно использует объекты, которые были созданы ранее, но в настоящее время не используются.
  • Однако, если очередь пуста (т. е. все объекты в настоящее время используются), новый игровой объект создается из префаба и возвращается вызывающей стороне. Этот сценарий гарантирует, что система останется гибкой, обслуживая ситуации, когда спрос превышает первоначальный размер пула.

Если ваш объект завершил свое использование в игре, вы можете вернуть его обратно в очередь или в пул объектов, как показано ниже.

 ObjectPoolManager.Instance.ReturnObject(bullet);

Давайте кратко рассмотрим ReturnObject(), чтобы увидеть, что происходит, когда вы возвращаете объект обратно в пул.

   //Use this method to return an object back to the memory after the use.
    public void ReturnObject(GameObject obj)
    {
        if (obj != null)
        {
            _objectPoolQueue.Enqueue(obj);
            obj.SetActive(false);
        }
    }

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

Заключение по объединению объектов с помощью очередей и списков

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

Преимущества использования очереди:

  1. «Первым пришел — первым обслужен» (FIFO). По своей природе очередь работает по принципу FIFO. Это гарантирует, что объекты, которые были первыми деактивированы (или возвращены в пул), будут повторно активированы первыми. Это может привести к лучшей локальности памяти, потенциально улучшая когерентность кэша и производительность.
  2. Ясное намерение: когда вы используете очередь для объединения объектов, намерение ясно. Вы перерабатываете объекты в том порядке, в котором они были возвращены, что делает код семантически более понятным для разработчиков.
  3. Эффективность: такие операции, как Enqueue и Dequeue в очереди, обычно выполняются за O(1), что обеспечивает постоянную скорость извлечения и возврата объектов в пул.

Недостатки использования списка вместо очереди для объединения объектов.

  • Накладные расходы при произвольном доступе. При использовании списка, если вы пытаетесь получить первый неактивный объект, вам может потребоваться пройтись по списку, что в худшем случае может занять O(n) времени. Хотя это может быть незначительным для небольших списков, по мере роста списка накладные расходы могут стать проблемой.
  • Неоднозначность. Использование списка не обеспечивает четкого шаблона повторного использования объектов. Разработчик потенциально может захватить объект из середины списка, что приведет к неоднородным шаблонам использования и усложнит прогнозирование кода.
  • Управление памятью. Хотя списки являются динамическими и могут увеличиваться или уменьшаться, такое изменение размера может привести к дополнительным операциям с памятью, что может повлиять на производительность. Кроме того, удаление объекта из середины списка может привести к смещению элементов, что может потребовать значительных вычислительных ресурсов.

Всего несколько строк, и ObjectPoolManager значительно повысит производительность вашей игры. Эту технику должен знать любой разработчик, новичок или профессионал. Итак, начните объединять и дайте своей игре плавность, которой она заслуживает!

Помните, что в разработке игр ключевым фактором является эффективность. Освойте объединение объектов, и вы откроете одну из многих дверей в высокопроизводительные игры! 🎮🔥

Удачного кодирования! 💻🌟

Вы можете найти мой репозиторий Git здесь: https://github.com/vivekrajgopal/Object_Pooling_Sample