Простой пул объектов в Unity3D

в 8:53, , рубрики: C#, destroy., game development, instantiate, pool, unity3d, оптимизация, разработка, физика

В процессе разработки я столкнулся с необходимостью создания пула объектов. Прочитав эту и другие статьи, решил написать для своих нужд пул попроще с доступом к объекту по строке (названию префаба).

Итак, начнем. Пул состоит из четырех скриптов. Состояние вкл/выкл на объекте в пуле определяется его свойством 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 ();

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

Управление в редакторе:

Простой пул объектов в Unity3D - 1

Спавн пуль и кораблей:

Простой пул объектов в Unity3D - 2

Послесловие

Разумеется, у данной реализации множество недостатков. Перечислю основные:

1) Доступ по строке можно заменить доступом по, например, целочисленному ключу-идентификатору, что ускорило бы работу;
2) Нет обработки ошибок и исключений (методы просто вернут null), практически нет проверок;
3) Необходимость наличия на сцене по сути синглтона PoolSetup, хотя на него никто и не ссылается.

Полный код

PoolObject

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
} 

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
		
	#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

} 

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;
	}

	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;
	}

}

PoolSetup

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js