Программист unity3d, инди-разработчик
0,0
рейтинг
14 января в 11:53

Разработка → Простой пул объектов в 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
}

Алексей Буравов @AlexdeBur
карма
3,0
рейтинг 0,0
Программист unity3d, инди-разработчик
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (10)

  • 0
    Следующий класс PoolManager управляет пулами различных объектов. Класс статический для упрощения доступа к объектам, т.е. не нужно создавать синглтоны, инстансы и прочее.

    Как решается то, что статик класс содержит линки на мертвые GameObject-ы при смене сцены? Нужно или чистить или просто применять локальный для сцены пул (который может быть как синглтоном, так и просто отдельным компонентом со ссылкой на него), который будет умирать самостоятельно.
    • 0
      Да, резонное замечание, надо дописать. Просто в моем проекте сцена, где нужен пул, всего одна и очистка не требуется. Вообще при смене сцены и переинициализации от PoolSetup в objects запишется новое значение. Но необходимости очистки при смене сцен это не отменяет, согласен.
    • 0
      Ну и если хочется универсальности, то можно прикрутить загрузку префабов из ресурсов:
          public sealed class PoolObject : MonoBehaviour {
              public PoolContainer Pool { get; private set; }
      
              public Transform CachedTransform { get; private set; }
      
              void Awake () {
                  CachedTransform = transform;
              }
      
              public void SetPool (PoolContainer pool) {
                  Pool = pool;
              }
      
              public void Recycle () {
                  if (Pool != null) {
                      Pool.Recycle (this);
                  }
              }
      
              public void SetActive (bool state) {
                  gameObject.SetActive (state);
              }
          }
      
      public sealed class PoolContainer : MonoBehaviour {
              readonly Stack<PoolObject> _store = new Stack<PoolObject> (64);
      
              GameObject _prefab;
      
              public string PrefabPath = "UnknownPrefab";
      
              bool LoadPrefab () {
                  _prefab = Resources.Load<GameObject> (PrefabPath);
                  if (_prefab == null) {
                      Debug.LogWarning ("Cant load asset " + PrefabPath);
                      return false;
                  }
                  #if UNITY_EDITOR
                  if (_prefab.GetComponent <PoolObject> () != null) {
                      Debug.LogWarning ("PoolObject cant be used on prefabs");
                      _prefab = null;
                      UnityEditor.EditorApplication.isPaused = true;
                      return false;
                  }
                  #endif
      
                  return true;
              }
      
              public PoolObject Get () {
                  if (_prefab == null) {
                      if (!LoadPrefab ()) {
                          return null;
                      }
                  }
      
                  PoolObject obj;
                  if (_store.Count > 0) {
                      obj = _store.Pop ();
                  } else {
                      var go = Instantiate<GameObject> (_prefab);
                      obj = go.AddComponent<PoolObject> ();
                      obj.SetPool (this);
                  }
                  obj.SetActive (false);
                  return obj;
              }
      
              public void Recycle (PoolObject obj) {
                  if (obj != null && obj.Pool == this) {
                      obj.SetActive (false);
                      if (!_store.Contains (obj)) {
                          _store.Push (obj);
                      }
                  } else {
                      #if UNITY_EDITOR
                      Debug.LogWarning ("Invalid obj to recycle", obj);
                      #endif
                  }
              }
          }
      

      Вот и весь пулинг. В сцене делается ГО с пул-контейнером + настройкой, а в компоненте, где требуется инстанцировать объекты, делается паблик свойство с типом PoolContainer, в которое потом перетаскивается созданный ГО в инспекторе визуально (дизайнер оценит).
      Можно делать всякие защитные меры или специальное поведение:
      public sealed class RecycleAfterTime : MonoBehaviour {
              public float Timeout = 1f;
      
              float _endTime;
      
              PoolObject _poolObject;
      
              void OnEnable () {
                  _endTime = Time.time + Timeout;
              }
      
              void LateUpdate () {
                  if (Time.time >= _endTime) {
                      OnRecycle ();
                  }
              }
      
              void OnRecycle () {
                  var poolObj = GetComponent <PoolObject> ();
                  if (poolObj != null) {
                      poolObj.Recycle ();
                  } else {
                      gameObject.SetActive (false);
                  }
              }
          }
      
  • 0
    Вам стоит добавить кеширование получения компонентов.
    • 0
      Ну по сути тут только transform два раза подряд вызывается, потому и не заморачивался. Но да, надо исправить.
      • 0
        Я видел много вызовов getComponent
        • 0
          Скорее всего, вы про эти строки:
          		objects.Add(temp.GetComponent<PoolObject> ());
          		if (temp.GetComponent<Animator> ())
          			temp.GetComponent<Animator> ().StartPlayback ();
          


          Тут можно закэшировать аниматор. А PoolObject кастуется лишь раз. AddObject для любого из объектов пула вызывается лишь при создании.
  • +1
    1. Как на счет того, чтобы сделать размер пула? В большинстве случаев объектов внутри этого размера должно хватать для сцены. Если иногда возникает ситуация, что нужно создать чуть больше объектов, то пул создает их и уничтожает при последующем освобождении, вновь оставляя в пуле только обычное количество объектов.
    2. Может быть имеет смысл инициализировать все пулы с заданным количеством объектов перед загрузкой уровня, чтобы не возникало скачков FPS при создании объектов во время игрового цикла? Т.е. пул не досоздает объекты по мере необходимости, а перед стартом уровня создает их с запасом и потом выдает только готовые.
    • +1
      1. Так и делается (переменная count в структуре PoolPart класса PoolManager), но я не вводил уничтожение объектов, т.к. если вам единожды понадобилось 300 пуль вместо 30, то есть вероятность, что понадобится снова, но они уже созданы.
      2. Так все и происходит, досоздание объектов происходит только тогда, когда все объекты в пуле уже выданы «на руки».
      • 0
        А что если это будут не пули, а тяжеловесные объекты? Предположим, у нас генерится случайный мир и вероятность такая, что обычно каждый вид объекта попадает на сцену в количестве 10 штук. Но иногда случайно все же может быть сгенерировано 20 одинаковых штук. В этом случае пул досоздаст 10 объектов. Если их не уничтожить, то так и будем их таскать всю игру, даже если больше никогда не сгенерится 20 штук. Опять же, такая ситуация может случится и с другими видами объектов. В итоге получим, что вместо 10 штук на каждый вид, будет храниться 20 — т.е. памяти потребуется вдвое больше. А еще один плохой момент, это что вначале игры памяти будет хватать, а в процессе она будет захватываться. Поэтому я бы может быть даже ввел счетчик и подстраивал размер пула под текущие нужды

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.