В процессе разработки я столкнулся с необходимостью создания пула объектов. Прочитав эту и другие статьи, решил написать для своих нужд пул попроще с доступом к объекту по строке (названию префаба).
Итак, начнем. Пул состоит из четырех скриптов. Состояние вкл/выкл на объекте в пуле определяется его свойством Unity activeInHierarchy, чтобы не городить дополнительных переменных.
1. Pool Object
Компонент Pool Object должен находиться на любом объекте, используемом в пуле. Его основное предназначение — вернуть объект обратно в пул.
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
[AddComponentMenu("Pool/PoolObject")]
public class PoolObject : MonoBehaviour {
#region Interface
public void ReturnToPool () {
gameObject.SetActive (false);
}
#endregion
}
У класса один-единственный метод. На самом деле можно было бы обойтись и без него, но таким образом мы разделяем объекты, предназначенные для пула и нет (уничтожаемые обычным способом).
2. Object Pooling
Идем дальше. Класс Object Pooling — собственно сам пул, который выдает свободные объекты по требованию и создает новые при нехватке.
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
[AddComponentMenu("Pool/ObjectPooling")]
public class ObjectPooling {
#region Data
List<PoolObject> objects;
Transform objectsParent;
#endregion
Здесь objects — все объекты, содержащиеся в пуле, objectsParent используется только как их родитель в иерархии на сцене (чтобы не было простыни объектов).
Добавление происходит с помощью метода AddObject, принимающего образец, который нужно добавить и родителя в иерархии на сцене.
void AddObject(PoolObject sample, Transform objects_parent) {
GameObject temp = GameObject.Instantiate(sample.gameObject);
temp.name = sample.name;
temp.transform.SetParent (objects_parent);
objects.Add(temp.GetComponent<PoolObject> ());
if (temp.GetComponent<Animator> ())
temp.GetComponent<Animator> ().StartPlayback ();
temp.SetActive(false);
}
Создается Gameobject temp, ему присваивается имя образца, после чего он добавляется в наш List. Затем объект выключается до тех пор, пока его не «потребуют» снаружи.
Отдельно о строках:
if (temp.GetComponent<Animator> ())
temp.GetComponent<Animator> ().StartPlayback ();
Введены они были, т.к. при создании объекта без них аниматор не стартовал (update не успевало вызваться). В итоге, когда при старте сцены создавалось, например 100 пуль и сразу выключалось, при запросе 50 пуль, аниматор стартовал у всех одновременно и начинались проседания фпс (на многих объектах постоянно проигрывается анимация). Если в проекте не предполагается использование большого числа объектов с аниматорами, данный код не нужен.
Рассмотрим инициализацию:
public void Initialize (int count, PoolObject sample, Transform objects_parent) {
objects = new List<PoolObject> (); //инициализируем List
objectsParent = objects_parent; //инициализируем локальную переменную для последующего использования
for (int i=0; i<count; i++) {
AddObject(sample, objects_parent); //создаем объекты до указанного количества
}
}
Второй метод данного класса — GetObject(), возвращающий Gameobject:
public PoolObject GetObject () {
for (int i=0; i<objects.Count; i++) {
if (objects[i].gameObject.activeInHierarchy==false) {
return objects[i];
}
}
AddObject(objects[0], objectsParent);
return objects[objects.Count-1];
}
Логика проста — проходимся по листу, если какой-то из объектов в пуле выключен (т.е. свободен) — возвращаем его, иначе добавляем новый.
3. PoolManager
Следующий класс PoolManager управляет пулами различных объектов. Класс статический для упрощения доступа к объектам, т.е. не нужно создавать синглтоны, инстансы и прочее.
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public static class PoolManager{
private static PoolPart[] pools;
private static GameObject objectsParent;
[System.Serializable]
public struct PoolPart {
public string name; //имя префаба
public PoolObject prefab; //сам префаб, как образец
public int count; //количество объектов при инициализации пула
public ObjectPooling ferula; //сам пул
}
Вся информация хранится в структуре PoolPart.
Инициализация производится массивом этих структур: (ferula, возможно не совсем удачное название, но позволяет не запутаться в куче pool-ов):
public static void Initialize(PoolPart[] newPools) {
pools = newPools; //заполняем информацию
objectsParent = new GameObject ();
objectsParent.name = "Pool"; //создаем на сцене объект Pool, чтобы не заслонять иерархию
for (int i=0; i<pools.Length; i++) {
if(pools[i].prefab!=null) {
pools[i].ferula = new ObjectPooling(); //создаем свой пул для каждого префаба
pools[i].ferula.Initialize(pools[i].count, pools[i].prefab, objectsParent.transform);
//инициализируем пул заданным количество объектов
}
}
}
Второй метод данного статического класса — GetObject, аналог стандартного Instantiate, но по имени объекта. Он проверяет все существующие пулы, и если находит правильный — дергает его метод GetObject() у класса ObjectPooling:
public static GameObject GetObject (string name, Vector3 position, Quaternion rotation) {
GameObject result = null;
if (pools != null) {
for (int i = 0; i < pools.Length; i++) {
if (string.Compare (pools [i].name, name) == 0) { //если имя совпало с именем префаба пула
result = pools[i].ferula.GetObject ().gameObject; //дергаем объект из пула
result.transform.position = position;
result.transform.rotation = rotation;
result.SetActive (true); //выставляем координаты и активируем
return result;
}
}
}
return result; //если такого объекта нет в пулах, вернет null
}
4. PoolSetup
Однако необходимо редактировать объекты, предназначенные для использования в пуле, и их количество, в инспекторе Unity. Для этого придется написать класс-обертку, наследника MonoBehaviour, вешающегося на объекты:
using UnityEngine;
using System.Collections;
[AddComponentMenu("Pool/PoolSetup")]
public class PoolSetup : MonoBehaviour {//обертка для управления статическим классом PoolManager
#region Unity scene settings
[SerializeField] private PoolManager.PoolPart[] pools; //структуры, где пользователь задает префаб для использования в пуле и инициализируемое количество
#endregion
#region Methods
void OnValidate() {
for (int i = 0; i < pools.Length; i++) {
pools[i].name = pools[i].prefab.name; //присваиваем имена заранее, до инициализации
}
}
void Awake() {
Initialize ();
}
void Initialize () {
PoolManager.Initialize(pools); //инициализируем менеджер пулов
}
#endregion
}
Данный класс должен быть один на сцене, в противном случае один из них затрет пулы другого.
Использование
Теперь мы можем вызывать объекты из пула так:
Gameobject bullet = PoolManager.GetObject (bulletPrefab.name, shotPoint.position, myTransform.rotation);
Возвращаем:
GetComponent<PoolObject>.ReturnToPool ();
В результате пул работает и им достаточно просто пользоваться. Пара скринов:
Управление в редакторе:
Спавн пуль и кораблей:
Послесловие
Разумеется, у данной реализации множество недостатков. Перечислю основные:
1) Доступ по строке можно заменить доступом по, например, целочисленному ключу-идентификатору, что ускорило бы работу;
2) Нет обработки ошибок и исключений (методы просто вернут null), практически нет проверок;
3) Необходимость наличия на сцене по сути синглтона PoolSetup, хотя на него никто и не ссылается.
Полный код
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
[AddComponentMenu("Pool/PoolObject")]
public class PoolObject : MonoBehaviour {
#region Interface
public void ReturnToPool () {
gameObject.SetActive (false);
}
#endregion
}
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
[AddComponentMenu("Pool/ObjectPooling")]
public class ObjectPooling {
#region Data
List<PoolObject> objects;
Transform objectsParent;
#endregion
#region Interface
public void Initialize (int count, PoolObject sample, Transform objects_parent) {
objects = new List<PoolObject> ();
objectsParent = objects_parent;
for (int i=0; i<count; i++) {
AddObject(sample, objects_parent);
}
}
public PoolObject GetObject () {
for (int i=0; i<objects.Count; i++) {
if (objects[i].gameObject.activeInHierarchy==false) {
return objects[i];
}
}
AddObject(objects[0], objectsParent);
return objects[objects.Count-1];
}
#endregion
#region Methods
void AddObject(PoolObject sample, Transform objects_parent) {
GameObject temp;
temp = GameObject.Instantiate(sample.gameObject);
temp.name = sample.name;
temp.transform.SetParent (objects_parent);
objects.Add(temp.GetComponent<PoolObject> ());
if (temp.GetComponent<Animator> ())
temp.GetComponent<Animator> ().StartPlayback ();
temp.SetActive(false);
}
#endregion
}
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public static class PoolManager{
private static PoolPart[] pools;
private static GameObject objectsParent;
[System.Serializable]
public struct PoolPart {
public string name;
public PoolObject prefab;
public int count;
public ObjectPooling ferula;
}
public static void Initialize(PoolPart[] newPools) {
pools = newPools;
objectsParent = new GameObject ();
objectsParent.name = "Pool";
for (int i=0; i<pools.Length; i++) {
if(pools[i].prefab!=null) {
pools[i].ferula = new ObjectPooling();
pools[i].ferula.Initialize(pools[i].count, pools[i].prefab, objectsParent.transform);
}
}
}
public static GameObject GetObject (string name, Vector3 position, Quaternion rotation) {
GameObject result = null;
if (pools != null) {
for (int i = 0; i < pools.Length; i++) {
if (string.Compare (pools [i].name, name) == 0) {
result = pools[i].ferula.GetObject ().gameObject;
result.transform.position = position;
result.transform.rotation = rotation;
result.SetActive (true);
return result;
}
}
}
return result;
}
}
using UnityEngine;
using System.Collections;
[AddComponentMenu("Pool/PoolSetup")]
public class PoolSetup : MonoBehaviour {//обертка для управления статическим классом PoolManager
#region Unity scene settings
[SerializeField] private PoolManager.PoolPart[] pools;
#endregion
#region Methods
void OnValidate() {
for (int i = 0; i < pools.Length; i++) {
pools[i].name = pools[i].prefab.name;
}
}
void Awake() {
Initialize ();
}
void Initialize () {
PoolManager.Initialize(pools);
}
#endregion
}
Автор: AlexdeBur