Простой пул объектов в 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 (); 

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

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

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

Послесловие

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

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 } 

ссылка на оригинал статьи http://habrahabr.ru/post/275091/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *