Статья о паттерне, который поможет собрать все ваши объекты в одном бассейне. ОБНОВЛЁННУЮ реализацию можно найти в статье "Пулы объектов. Ревизия №2.“
В предыдущем посте "Бесконечный 2D фон в Unity" мы показали трюк, как сделать бесконечный фон при помощи одной картинки, примерно такой же подход используется ко всему, что может быть задействовано в игре. Чем больше игра, тем сложнее моделировать все игровые объекты в реальном времени, поэтому всё, что выходит за рамки экрана обычно прячется и удаляется, дабы не занимать такие ограниченные и ценные вычислительные мощности. При этом выделение и высвобождение памяти одна из наиболее затратных операций, с точки зрения этих самых вычислительных мощностей, а в языках со сборщиком мусора(коим и является применяемый в Unity C#) это становится ещё и вопросом времени его работы. Соответственно, при достаточном объёме доступной оперативной памяти, интересным подходом становится переиспользование созданных, но не используемых объектов, вместо их удаления. Именно этим и занимается паттерн под названием пул объектов(Object Pooling), который мы рассмотрим в этой статье.
Теория
Теоретическая часть описана довольно подробно и за ней можно обратиться к этой книге.
Практика
Основными задачами нашей реализации будут простота и минималистичность.
Реализация будет основана на предыдущей статье о бесконечном фоне, так что если вы захотите повторить данный пример и не очень представляете что делать, можете начать с неё и внести приведённые ниже изменения.
За пулы объектов(их может быть несколько) будет отвечать префаб с приведённым ниже скриптом
usingSystem;usingSystem.Collections.Generic;usingUnityEngine;publicclassObjectPool:MonoBehaviour{// Хак из-за отсутствия поддержки отображения Dictionary в инспекторе
// Словарь с необходимыми объектами будет создаваться из листа с описанием префабов
[Serializable]publicclassPrefabData{publicstringname;publicGameObjectprefab;} [SerializeField]privateList<PrefabData>prefabDatas=null;// Префабы для создания новых объектов
privateDictionary<string,GameObject>prefabs=newDictionary<string,GameObject>();// Пулы для свободных объектов
privateDictionary<string,Queue<GameObject>>pools=newDictionary<string,Queue<GameObject>>();privatevoidAwake(){// Перекладываем информацию о пулах из нашей структуры в Dictionary
foreach(varprefabDatainprefabDatas){prefabs.Add(prefabData.name,prefabData.prefab);pools.Add(prefabData.name,newQueue<GameObject>());}prefabDatas=null;}publicGameObjectGetObject(stringpoolName){// При наличии свободного объекта в пуле возвращаем его
if(pools[poolName].Count>0){returnpools[poolName].Dequeue();}// Если пул пуст, то создаём новый объект
returnInstantiate(prefabs[poolName]);}publicvoidReturnObject(stringpoolName,GameObjectpoolObject){// Возвращаем объект в пул
pools[poolName].Enqueue(poolObject);}}
usingUnityEngine;publicclassProjectile:MonoBehaviour{ [SerializeField]privatefloatspeed=0;// Ссылка на объект с пулами
[SerializeField]publicObjectPoolobjectPool=null;// Имя, используемое для работы с пулом
publicconststringPoolName="Projectiles";privateVector2_moveVector=Vector2.zero;publicVector2MoveVector{set=>_moveVector=value;}voidUpdate(){transform.Translate(_moveVector.x*Time.deltaTime*speed,_moveVector.y*Time.deltaTime*speed,0,Space.World);}voidOnBecameInvisible(){// После выхода объекта за границы экрана возвращаем его в пул
gameObject.SetActive(false);objectPool.ReturnObject(PoolName,gameObject);}}
usingUnityEngine;publicclassFire:MonoBehaviour{// Информация, не относящаяся к пулам
[SerializeField]privateTransformleftCannon=null; [SerializeField]privateTransformrightCannon=null; [SerializeField]privateUnevirseHandleruniverse=null; [SerializeField]privateTransformspace=null;privatebool_rightCannon=false;// Ссылка на объект с пулами
[SerializeField]privateObjectPoolobjectPool=null;voidUpdate(){if(Input.GetMouseButtonDown(0)){// Берём объект из пула
GameObjectpr=objectPool.GetObject(Projectile.PoolName);// Объекты из пула нуждаются в правильной очистке и инициализации
pr.transform.SetParent(space);pr.transform.SetPositionAndRotation(_rightCannon?rightCannon.position:leftCannon.position,_rightCannon?rightCannon.rotation:leftCannon.rotation);Projectileprpr=pr.GetComponent<Projectile>();prpr.MoveVector=universe.LookVector.normalized;prpr.objectPool=objectPool;pr.SetActive(true);_rightCannon=!_rightCannon;}}}
Не имеющий отношения к пулам объектов, но изменённый с предыдущей статьи код для UniverseHandler, что бы вы могли повторить пример полностью
usingUnityEngine;publicclassUnevirseHandler:MonoBehaviour{// Ссылки на объекты
[SerializeField]privateCameramainCamera=null; [SerializeField]privateGameObjectship=null; [SerializeField]privateGameObjectspace=null;// Радиус возможной видимости для камеры
privatefloat_spaceCircleRadius=0;// Размер фона
privatefloat_backgroundOriginalSizeX=0;privatefloat_backgroundOriginalSizeY=0;privateVector2_shipPosition=Vector2.zero;privateVector2_lookVector=Vector2.zero;privateVector2_stvp=Vector2.zero;publicVector2LookVector=>_lookVector;voidStart(){// Получаем исходный размер фона
SpriteRenderersr=space.GetComponent<SpriteRenderer>();varoriginalSize=sr.size;_backgroundOriginalSizeX=originalSize.x;_backgroundOriginalSizeY=originalSize.y;// Высота равна ортографическому размеру камеры
floatorthographicSize=mainCamera.orthographicSize;// Ширина равна ортографическому размеру помноженному на соотношение сторон
floatscreenAspect=(float)Screen.width/(float)Screen.height;_spaceCircleRadius=Mathf.Sqrt(orthographicSize*screenAspect*orthographicSize*screenAspect+orthographicSize*orthographicSize);// Конечный размер фона должен позволять сдвинуться на один базовый размер фона в любом направлении + перекрыть радиус камеры также во всех направлениях
sr.size=newVector2(_spaceCircleRadius*2+_backgroundOriginalSizeX*2,_spaceCircleRadius*2+_backgroundOriginalSizeY*2);_shipPosition=ship.transform.position;}voidUpdate(){// Положение мышки в пространстве
_stvp=mainCamera.ScreenToWorldPoint(Input.mousePosition);// Вектор для направления звездолёта
_lookVector=_stvp-_shipPosition;floatrotZ=Mathf.Atan2(_lookVector.y,_lookVector.x)*Mathf.Rad2Deg;ship.transform.rotation=Quaternion.Euler(0f,0f,rotZ-90);}}
Демонстрация
В приведённом ниже результате можно увидеть, что новые объекты создаются только при отсутствии свободных.
Заключение
Какой-либо референсной имплементации пулов объектов Unity нам не предлагает, а так как это очень популярный паттерн, то можно найти довольно большой спектр его реализаций. Приведённая в статье реализация не обладает такой полезной вещью, как предварительное наполнение пула и требует двойного ввода имени пула, что повышает сложность конфигурации. Да и в целом можно сделать довольно объёмное решения ради дополнительного синтаксического сахара и функциональности, однако, для начала остановимся на минималистичном варианте. Пока! =)