Ранее я уже писал про организацию очереди объектов, которую использовал при спавне противников в игре и прочих объектов. Сейчас немного подумав я решил улучшить эту систему и попытаться создать наиболее универсальный вариант очереди + разумеется ускорить скрипты из предыдущей статьи (ссылка).
Весь код состоит из двух скриптов. Первый скрипт руководит пулом и получает задания на спавн объектов, скрипт устанавливается на родительский объект на сцене для спавнящихся объектов. Второй скрипт устанавливается на элементы очереди, а именно на их префабы. Чтобы уменьшить количество компонентов на объектах можно использовать наследование.
Основной скрипт (спавнер). В основе скрипта лежат три списка, по данным из которых решается какой объект будет создан, а какой удален (например, для очистки очереди):
- Первый список содержит все элементы, которые пул создавал, за исключением удаленных со сцены. Данный список представлен с помощью класса List.
- Второй - элементы, которые в данный момент пул может использовать, так как они доступны для повторного использования, например, умерший противник попадает в этот список и спавнер может его заново использовать. Не забудьте написать метод для сброса характеристик объекта, чтобы, например, умерший противник не появился снова с нулевым здоровьем (я такие ошибки почему-то иногда делаю, скорее всего из-за невнимательно, кажется это наследственное). Данный список представлен с помощью класса Queue. Queue, как и List является оберткой для коллекции, но в отличии от List извлекать объекты из Queue можно только сначала списка, а добавлять только в конец. Таким образом Queue является вариантом очереди первым пришел, первым ушел. По данным с форумов, где я искал самый лучший вариант для коллекции доступных объектов этот оказался наиболее быстрым.
- Третий - поступающие задачи. Каждая задача содержит набор действий, которые будут выполнены для выбранного из списка доступного объекта. Например, среди действий может быть обнуление характеристик, расчет позиции спавна. Также обязательно в список действий должно быть включено gameobject.setActive(true), так как система пула не включает объект после того как он был использован.
Как все работает на практике. В родительский скрипт поступает задача на обработку через одну из следующих функций:
public void AddTask(Action<PoolNodeByAlexScript> action, int index = 0)
{
pools[index].tasksSpawn.Enqueue(action);
}
public void AddTask(Action<PoolNodeByAlexScript> action, string name)
{
AddTask(action, pools.FindIndex(x => x.prefab.name == name));
}
В родительском пуле есть несколько очередей для различных типов объектов. Например тут может очередь спавна противников первого уровня и очередь ракет, которые они выпускают и так далее. После поступления задачи при следующем Update будет опрос каждой очереди на наличие заданий и если задание есть оно будет выполнено. Исключение из правил может быть, в своем скрипте я сделал параметр, который отвечает за это исключение, если этот параметр положительный, то в случае переполнения очереди существующих объектов в пуле (да тут есть максимальный размер пула, это сделано для того, чтобы из-за какого-нибудь косяка в игре не появилось 100000 и более объектов из-за которых все повиснет + для контроля каких-либо событий) задание не будет удалено из очереди, если оно не будет выполнено во время текущего опроса, оно уйдет в конец очереди заданий.
При использовании объекта вызывается метод.
/// <summary>
/// Выполняет действия по использованию объекта пула, после выполнения данной функции статус объекта = занят.
/// </summary>
/// <param name="task">Задача</param>
public void SetProperties(System.Action<PoolNodeByAlexScript> task)
{
task(this);
}
Тут сразу важное замечание. В скриптах я только первое время предусматривал возможность вызова другого компонента, который находится на объекте, но потом я просто стал использовать наследование PoolNodeByAlexScript, который находится на клоне, чтобы избегать применения GetComponent, что дает возможность в некоторых случаях выполнять операции спавна не в основном потоке приложения.
Ниже полный код родительского скрипта и скрипта клона:
PoolByAlexScript.cs
using LibByAlex.Collection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
public class PoolByAlexScript : MonoBehaviour
{
/// <summary>
/// Данные о пуле (содержат префаб, размер пула, общую коллекцию объектов и коллекцию свободных объектов)
/// </summary>
[Serializable]
public class PoolData
{
/// <summary>
/// Максимальный размер пула
/// </summary>
[Tooltip("Максимальный размер пула")]
public int poolMaxSize = 1;
/// <summary>
/// Префаб
/// </summary>
[Tooltip("Префаб")]
public PoolNodeByAlexScript prefab = null;
/// <summary>
/// Основная коллекция пула
/// </summary>
[Tooltip("Основная коллекция пула")]
public List<PoolNodeByAlexScript> collectionPool = new List<PoolNodeByAlexScript>();
/// <summary>
/// Коллекция свободных объектов пула
/// </summary>
[Tooltip("Коллкция свободных объектов пула")]
public Queue<PoolNodeByAlexScript> collectionPoolFree = new Queue<PoolNodeByAlexScript>();
/// <summary>
/// Задачи на спавн
/// </summary>
[Tooltip("Задачи на спавн")]
public Queue<Action<PoolNodeByAlexScript>> tasksSpawn = new Queue<Action<PoolNodeByAlexScript>>();
public void ClearTime(float timerItemDestroy, float timeNow)
{
int n = collectionPoolFree.Count;
for (int i=0;i<n;i++)
{
var item = collectionPoolFree.Dequeue();
if (timeNow - item.lastFreeTime > timerItemDestroy)
{
Destroy(item);
}
else
{
collectionPoolFree.Enqueue(item);
}
}
}
public void ClearTime(float timerItemDestroy)
{
ClearTime(timerItemDestroy, Time.time);
}
public void SpawnDo(PoolByAlexScript poolByAlexScript)
{
if (tasksSpawn.Any())
{
if (collectionPoolFree.Any())
{
PoolNodeByAlexScript item = collectionPoolFree.Dequeue();
SpawnTask(item);
}
else
{
SpawnTaskNew(poolByAlexScript);
}
}
}
void SpawnTask(PoolNodeByAlexScript item)
{
var task = tasksSpawn.Dequeue();
item.SetProperties(task);
}
void SpawnTaskNew(PoolByAlexScript poolByAlexScript)
{
if (collectionPool.Count < poolMaxSize)
{
var task = tasksSpawn.Dequeue();
var item = Instantiate(prefab, poolByAlexScript.transform);
item.Init(poolByAlexScript, this);
item.SetProperties(task);
collectionPool.Add(item);
}
else if (poolByAlexScript.isWaitFreeCellPool == false)
{
tasksSpawn.Dequeue();
}
}
}
/// <summary>
/// Переключать свободный статус объекта очереди, если объект вкючается или выключается (подходит для простых игр)
/// </summary>
[Tooltip("Переключать свободный статус объекта очереди, если объект вкючается или выключается (подходит для простых игр)")]
public bool enableDisableIsFree = false;
/// <summary>
/// Таймер отчистки очереди
/// </summary>
[Tooltip("Таймер отчистки очереди")]
public float timerCleanCollection = 30f;
/// <summary>
/// Таймер уничтожения объекта
/// </summary>
[Tooltip("Таймер уничтожения объекта")]
public float timerItemDestroy = 30f;
/// <summary>
/// При спавне если нет свободных ячеек в пуле ждать свободную ячейку или убрать из очереди ожидания спавна объект
/// </summary>
[Tooltip("При спавне если нет свободных ячеек в пуле ждать свободную ячейку или убрать из очереди ожидания спавна объект")]
public bool isWaitFreeCellPool = true;
/// <summary>
/// Последнее время отчистки коллекции
/// </summary>
[Tooltip("Последнее время отчистки коллекции")]
private float lastTimeCleanCollection;
/// <summary>
/// Массив действий выполняемых во время Update
/// </summary>
[Tooltip("Массив действий выполняемых во время Update")]
private Action[] MassActionUpdate;
/// <summary>
/// Занята коллекция или нет
/// </summary>
[Tooltip("Занята коллекция или нет")]
public bool collectionIsBusy = false;
/// <summary>
/// Массив пулов.
/// </summary>
[Tooltip("Массив пулов")]
public PoolData[] pools;
void Start()
{
lastTimeCleanCollection = Time.time;
if (timerItemDestroy > 0 && timerCleanCollection > 0)
{
MassActionUpdate = new Action[] { UpdateCleanCollection, UpdateTasksSpawn };
}
else
{
MassActionUpdate = new Action[] { UpdateTasksSpawn };
}
}
void Update()
{
foreach (var act in MassActionUpdate)
act();
}
/// <summary>
/// Операция по отчистке списка
/// </summary>
void UpdateCleanCollection()
{
if (Time.time - lastTimeCleanCollection > timerCleanCollection && collectionIsBusy == false)
{
collectionIsBusy = true;
for (int i = 0; i < pools.Length; i++)
{
pools[i].ClearTime(timerItemDestroy);
}
collectionIsBusy = false;
lastTimeCleanCollection = Time.time;
}
}
/// <summary>
/// Выполнение операций по спавну из всех очередей
/// </summary>
void UpdateTasksSpawn()
{
if (collectionIsBusy == false)
{
collectionIsBusy = true;
int n = pools.Length;
for (int i=0;i<pools.Length;i++)
{
pools[i].SpawnDo(this);
}
collectionIsBusy = false;
}
}
public void AddTask(Action<PoolNodeByAlexScript> action, int index = 0)
{
pools[index].tasksSpawn.Enqueue(action);
}
public void AddTask(Action<PoolNodeByAlexScript> action, string name)
{
AddTask(action, pools.FindIndex(x => x.prefab.name == name));
}
}
PoolNodeByAlexScript.cs
using UnityEngine;
using System.Collections;
public class PoolNodeByAlexScript : MonoBehaviour
{
/// <summary>
/// Указатель на пул
/// </summary>
[Tooltip("Указатель на пул")]
public PoolByAlexScript poolByAlexScript;
[HideInInspector]
public PoolByAlexScript.PoolData poolData;
/// <summary>
/// Последений раз когда объект стал свободным
/// </summary>
[Tooltip("Последений раз когда объект стал свободным")]
public float lastFreeTime;
void Awake()
{
if (name.EndsWith("(Clone)"))
name = name.Substring(0, name.Length - 7);
}
/// <summary>
/// Первичная инициализация объекта
/// </summary>
/// <param name="poolByAlexScript">Указатель на родительский пулл</param>
/// <param name="poolData">Указатель на класс данных очереди к которой относится объект</param>
public void Init(PoolByAlexScript poolByAlexScript, PoolByAlexScript.PoolData poolData)
{
this.poolByAlexScript = poolByAlexScript;
this.poolData = poolData;
}
/// <summary>
/// Выполняет действия по использованию объекта пула, после выполнения данной функции статус объекта = занят.
/// </summary>
/// <param name="task">Задача</param>
public void SetProperties(System.Action<PoolNodeByAlexScript> task)
{
task(this);
}
public void SetFree(bool value)
{
lastFreeTime = Time.time;
if (value == true)
{
poolData.collectionPoolFree.Enqueue(this);
}
}
public virtual void OnEnable()
{
if (poolByAlexScript.enableDisableIsFree)
SetFree(false);
}
public virtual void OnDisable()
{
if (poolByAlexScript.enableDisableIsFree)
SetFree(true);
}
public virtual void OnDestroy()
{
poolData.collectionPool.Remove(this);
}
}
|