using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections.Generic;
using Lean.Common;
namespace Lean.Pool
{
/// This component allows you to pool GameObjects, giving you a very fast alternative to Instantiate and Destroy.
/// Pools also have settings to preload, recycle, and set the spawn capacity, giving you lots of control over your spawning.
[ExecuteInEditMode]
[HelpURL(LeanPool.HelpUrlPrefix + "LeanGameObjectPool")]
[AddComponentMenu(LeanPool.ComponentPathPrefix + "GameObject Pool")]
public class LeanGameObjectPool : MonoBehaviour, ISerializationCallbackReceiver
{
[System.Serializable]
public class Delay
{
public GameObject Clone;
public float Life;
}
public enum NotificationType
{
None,
SendMessage,
BroadcastMessage,
IPoolable,
BroadcastIPoolable
}
public enum StrategyType
{
ActivateAndDeactivate,
DeactivateViaHierarchy
}
/// All active and enabled pools in the scene.
public static LinkedList Instances = new LinkedList();
/// The prefab this pool controls.
public GameObject Prefab
{
set
{
if (value != prefab)
{
UnregisterPrefab();
prefab = value;
RegisterPrefab();
}
}
get
{
return prefab;
}
}
[SerializeField] [UnityEngine.Serialization.FormerlySerializedAs("Prefab")] private GameObject prefab;
/// If you need to peform a special action when a prefab is spawned or despawned, then this allows you to control how that action is performed.
/// NoneIf you use this then you must rely on the OnEnable and OnDisable messages.
/// SendMessageThe prefab clone is sent the OnSpawn and OnDespawn messages.
/// BroadcastMessageThe prefab clone and all its children are sent the OnSpawn and OnDespawn messages.
/// IPoolableThe prefab clone's components implementing IPoolable are called.
/// Broadcast IPoolableThe prefab clone and all its child components implementing IPoolable are called.
public NotificationType Notification = NotificationType.IPoolable;
/// This allows you to control how spawned/despawned GameObjects will be handled. The DeactivateViaHierarchy mode should be used if you need to maintain your prefab's de/activation state.
/// ActivateAndDeactivate = Despawned clones will be deactivated and placed under this GameObject.
/// DeactivateViaHierarchy = Despawned clones will be placed under a deactivated GameObject and left alone.
public StrategyType Strategy = StrategyType.ActivateAndDeactivate;
/// Should this pool preload some clones?
public int Preload;
/// Should this pool have a maximum amount of spawnable clones?
public int Capacity;
/// If the pool reaches capacity, should new spawns force older ones to despawn?
public bool Recycle;
/// Should this pool be marked as DontDestroyOnLoad?
public bool Persist;
/// Should the spawned clones have their clone index appended to their name?
public bool Stamp;
/// Should detected issues be output to the console?
public bool Warnings = true;
/// This stores all spawned clones in a list. This is used when Recycle is enabled, because knowing the spawn order must be known. This list is also used during serialization.
[SerializeField]
private List spawnedClonesList = new List();
/// This stores all spawned clones in a hash set. This is used when Recycle is disabled, because their storage order isn't important. This allows us to quickly find the Clone associated with the specified GameObject.
private HashSet spawnedClonesHashSet = new HashSet();
/// All the currently despawned prefab instances.
[SerializeField]
private List despawnedClones = new List();
/// All the delayed destruction objects.
[SerializeField]
private List delays = new List();
[SerializeField]
private Transform deactivatedChild;
/// Node within Instances.
private LinkedListNode node;
private static Dictionary prefabMap = new Dictionary();
private static List tempPoolables = new List();
/// If you're using the Strategy = DeactivateViaHierarchy mode, then all despawned clones will be placed under this.
public Transform DeactivatedChild
{
get
{
if (deactivatedChild == null)
{
var child = new GameObject("Depawned Clones");
child.SetActive(false);
deactivatedChild = child.transform;
deactivatedChild.SetParent(transform, false);
}
return deactivatedChild;
}
}
#if UNITY_EDITOR
/// This will return false if you have pre-loaded prefabs do not match the Prefab.
/// NOTE: This is only availible in the editor.
public bool DespawnedClonesMatch
{
get
{
for (var i = despawnedClones.Count - 1; i >= 0; i--)
{
var clone = despawnedClones[i];
if (clone != null && UnityEditor.PrefabUtility.GetCorrespondingObjectFromSource(clone) != prefab)
{
return false;
}
}
return true;
}
}
#endif
/// Find the pool responsible for handling the specified prefab.
public static bool TryFindPoolByPrefab(GameObject prefab, ref LeanGameObjectPool foundPool)
{
return prefabMap.TryGetValue(prefab, out foundPool);
}
/// Find the pool responsible for handling the specified prefab clone.
/// NOTE: This can be an expensive operation if you have many large pools.
public static bool TryFindPoolByClone(GameObject clone, ref LeanGameObjectPool pool)
{
foreach (var instance in Instances)
{
// Search hash set
if (instance.spawnedClonesHashSet.Contains(clone) == true)
{
pool = instance;
return true;
}
// Search list
for (var j = instance.spawnedClonesList.Count - 1; j >= 0; j--)
{
if (instance.spawnedClonesList[j] == clone)
{
pool = instance;
return true;
}
}
}
return false;
}
/// Returns the amount of spawned clones.
public int Spawned
{
get
{
return spawnedClonesList.Count + spawnedClonesHashSet.Count;
}
}
/// Returns the amount of despawned clones.
public int Despawned
{
get
{
return despawnedClones.Count;
}
}
/// Returns the total amount of spawned and despawned clones.
public int Total
{
get
{
return Spawned + Despawned;
}
}
/// This will either spawn a previously despanwed/preloaded clone, recycle one, create a new one, or return null.
/// NOTE: This method is designed to work with Unity's event system, so it has no return value.
public void Spawn()
{
var clone = default(GameObject); TrySpawn(ref clone);
}
/// This will either spawn a previously despanwed/preloaded clone, recycle one, create a new one, or return null.
/// NOTE: This method is designed to work with Unity's event system, so it has no return value.
public void Spawn(Vector3 position)
{
var clone = default(GameObject); TrySpawn(ref clone, position, transform.localRotation);
}
/// This will either spawn a previously despanwed/preloaded clone, recycle one, create a new one, or return null.
public GameObject Spawn(Transform parent, bool worldPositionStays = false)
{
var clone = default(GameObject); TrySpawn(ref clone, parent, worldPositionStays); return clone;
}
/// This will either spawn a previously despanwed/preloaded clone, recycle one, create a new one, or return null.
public GameObject Spawn(Vector3 position, Quaternion rotation, Transform parent = null)
{
var clone = default(GameObject); TrySpawn(ref clone, position, rotation, parent); return clone;
}
/// This will either spawn a previously despanwed/preloaded clone, recycle one, create a new one, or return null.
public bool TrySpawn(ref GameObject clone, Transform parent, bool worldPositionStays = false)
{
if (prefab == null) { if (Warnings == true) Debug.LogWarning("You're attempting to spawn from a pool with a null prefab", this); return false; }
if (parent != null && worldPositionStays == true)
{
return TrySpawn(ref clone, prefab.transform.position, Quaternion.identity, Vector3.one, parent, worldPositionStays);
}
return TrySpawn(ref clone, transform.localPosition, transform.localRotation, transform.localScale, parent, worldPositionStays);
}
/// This will either spawn a previously despanwed/preloaded clone, recycle one, create a new one, or return null.
public bool TrySpawn(ref GameObject clone, Vector3 position, Quaternion rotation, Transform parent = null)
{
if (prefab == null) { if (Warnings == true) Debug.LogWarning("You're attempting to spawn from a pool with a null prefab", this); return false; }
if (parent != null)
{
position = parent.InverseTransformPoint(position);
rotation = Quaternion.Inverse(parent.rotation) * rotation;
}
return TrySpawn(ref clone, position, rotation, prefab.transform.localScale, parent, false);
}
/// This will either spawn a previously despanwed/preloaded clone, recycle one, create a new one, or return null.
public bool TrySpawn(ref GameObject clone)
{
if (prefab == null) { if (Warnings == true) Debug.LogWarning("You're attempting to spawn from a pool with a null prefab", this); return false; }
var transform = prefab.transform;
return TrySpawn(ref clone, transform.localPosition, transform.localRotation, transform.localScale, null, false);
}
/// This will either spawn a previously despanwed/preloaded clone, recycle one, create a new one, or return null.
public bool TrySpawn(ref GameObject clone, Vector3 localPosition, Quaternion localRotation, Vector3 localScale, Transform parent, bool worldPositionStays)
{
if (prefab != null)
{
// Spawn a previously despanwed/preloaded clone?
for (var i = despawnedClones.Count - 1; i >= 0; i--)
{
clone = despawnedClones[i];
despawnedClones.RemoveAt(i);
if (clone != null)
{
SpawnClone(clone, localPosition, localRotation, localScale, parent, worldPositionStays);
return true;
}
if (Warnings == true) Debug.LogWarning("This pool contained a null despawned clone, did you accidentally destroy it?", this);
}
// Make a new clone?
if (Capacity <= 0 || Total < Capacity)
{
clone = CreateClone(localPosition, localRotation, localScale, parent, worldPositionStays);
// Add clone to spawned list
if (Recycle == true)
{
spawnedClonesList.Add(clone);
}
else
{
spawnedClonesHashSet.Add(clone);
}
// Activate?
if (Strategy == StrategyType.ActivateAndDeactivate)
{
clone.SetActive(true);
}
// Notifications
InvokeOnSpawn(clone);
return true;
}
// Recycle?
if (Recycle == true && TryDespawnOldest(ref clone, false) == true)
{
SpawnClone(clone, localPosition, localRotation, localScale, parent, worldPositionStays);
return true;
}
}
else
{
if (Warnings == true) Debug.LogWarning("You're attempting to spawn from a pool with a null prefab", this);
}
return false;
}
/// This will despawn the oldest prefab clone that is still spawned.
[ContextMenu("Despawn Oldest")]
public void DespawnOldest()
{
var clone = default(GameObject);
TryDespawnOldest(ref clone, true);
}
private bool TryDespawnOldest(ref GameObject clone, bool registerDespawned)
{
MergeSpawnedClonesToList();
// Loop through all spawnedClones from the front (oldest) until one is found
while (spawnedClonesList.Count > 0)
{
clone = spawnedClonesList[0];
spawnedClonesList.RemoveAt(0);
if (clone != null)
{
DespawnNow(clone, registerDespawned);
return true;
}
if (Warnings == true) Debug.LogWarning("This pool contained a null spawned clone, did you accidentally destroy it?", this);
}
return false;
}
/// This method will despawn all currently spawned prefabs managed by this pool.
[ContextMenu("Despawn All")]
public void DespawnAll()
{
// Merge
MergeSpawnedClonesToList();
// Despawn
for (var i = spawnedClonesList.Count - 1; i >= 0; i--)
{
var clone = spawnedClonesList[i];
if (clone != null)
{
DespawnNow(clone);
}
}
spawnedClonesList.Clear();
// Clear all delays
for (var i = delays.Count - 1; i >= 0; i--)
{
LeanClassPool.Despawn(delays[i]);
}
delays.Clear();
}
/// This will either instantly despawn the specified gameObject, or delay despawn it after t seconds.
public void Despawn(GameObject clone, float t = 0.0f)
{
if (clone != null)
{
// Delay the despawn?
if (t > 0.0f)
{
DespawnWithDelay(clone, t);
}
// Despawn now?
else
{
TryDespawn(clone);
// If this clone was marked for delayed despawn, remove it
for (var i = delays.Count - 1; i >= 0; i--)
{
var delay = delays[i];
if (delay.Clone == clone)
{
delays.RemoveAt(i);
}
}
}
}
else
{
if (Warnings == true) Debug.LogWarning("You're attempting to despawn a null gameObject", this);
}
}
/// This allows you to remove all references to the specified clone from this pool.
/// A detached clone will act as a normal GameObject, requiring you to manually destroy or otherwise manage it.
/// NOTE: If this clone has been despawned then it will still be parented to the pool.
public void Detach(GameObject clone)
{
if (clone != null)
{
if (spawnedClonesHashSet.Remove(clone) == true || spawnedClonesList.Remove(clone) == true || despawnedClones.Remove(clone) == true)
{
// If this clone was marked for delayed despawn, remove it
for (var i = delays.Count - 1; i >= 0; i--)
{
var delay = delays[i];
if (delay.Clone == clone)
{
delays.RemoveAt(i);
}
}
}
else
{
if (Warnings == true) Debug.LogWarning("You're attempting to detach a GameObject that wasn't spawned from this pool.", clone);
}
}
else
{
if (Warnings == true) Debug.LogWarning("You're attempting to detach a null GameObject", this);
}
}
/// This method will create an additional prefab clone and add it to the despawned list.
[ContextMenu("Preload One More")]
public void PreloadOneMore()
{
if (prefab != null)
{
// Create clone
var clone = CreateClone(Vector3.zero, Quaternion.identity, Vector3.one, null, false);
// Add clone to despawned list
despawnedClones.Add(clone);
// Deactivate it
if (Strategy == StrategyType.ActivateAndDeactivate)
{
clone.SetActive(false);
clone.transform.SetParent(transform, false);
}
else
{
clone.transform.SetParent(DeactivatedChild, false);
}
if (Warnings == true && Capacity > 0 && Total > Capacity) Debug.LogWarning("You've preloaded more than the pool capacity, please verify you're preloading the intended amount.", this);
}
else
{
if (Warnings == true) Debug.LogWarning("Attempting to preload a null prefab.", this);
}
}
/// This will preload the pool based on the Preload setting.
[ContextMenu("Preload All")]
public void PreloadAll()
{
if (Preload > 0)
{
if (prefab != null)
{
for (var i = Total; i < Preload; i++)
{
PreloadOneMore();
}
}
else if (Warnings == true)
{
if (Warnings == true) Debug.LogWarning("Attempting to preload a null prefab", this);
}
}
}
/// This will destroy all preloaded or despawned clones. This is useful if your prefab has been modified, and the old ones are no longer useful.
[ContextMenu("Clean")]
public void Clean()
{
for (var i = despawnedClones.Count - 1; i >= 0; i--)
{
DestroyImmediate(despawnedClones[i]);
}
despawnedClones.Clear();
}
protected virtual void Awake()
{
if (Application.isPlaying == true)
{
PreloadAll();
if (Persist == true)
{
DontDestroyOnLoad(this);
}
}
}
protected virtual void OnEnable()
{
node = Instances.AddLast(this);
RegisterPrefab();
}
protected virtual void OnDisable()
{
UnregisterPrefab();
Instances.Remove(node);
node = null;
}
protected virtual void Update()
{
// Decay the life of all delayed destruction calls
for (var i = delays.Count - 1; i >= 0; i--)
{
var delay = delays[i];
delay.Life -= Time.deltaTime;
// Skip to next one?
if (delay.Life > 0.0f)
{
continue;
}
// Remove and pool delay
delays.RemoveAt(i); LeanClassPool.Despawn(delay);
// Finally despawn it after delay
if (delay.Clone != null)
{
Despawn(delay.Clone);
}
else
{
if (Warnings == true) Debug.LogWarning("Attempting to update the delayed destruction of a prefab clone that no longer exists, did you accidentally delete it?", this);
}
}
}
private void RegisterPrefab()
{
if (prefab != null)
{
var existingPool = default(LeanGameObjectPool);
if (prefabMap.TryGetValue(prefab, out existingPool) == true)
{
Debug.LogWarning("You have multiple pools managing the same prefab (" + prefab.name + ").", existingPool);
}
else
{
prefabMap.Add(prefab, this);
}
}
}
private void UnregisterPrefab()
{
// Skip actually null prefabs, but allow destroyed prefabs
if (Equals(prefab, null) == true)
{
return;
}
var existingPool = default(LeanGameObjectPool);
if (prefabMap.TryGetValue(prefab, out existingPool) == true && existingPool == this)
{
prefabMap.Remove(prefab);
}
}
private void DespawnWithDelay(GameObject clone, float t)
{
// If this object is already marked for delayed despawn, update the time and return
for (var i = delays.Count - 1; i >= 0; i--)
{
var delay = delays[i];
if (delay.Clone == clone)
{
if (t < delay.Life)
{
delay.Life = t;
}
return;
}
}
// Create delay
var newDelay = LeanClassPool.Spawn() ?? new Delay();
newDelay.Clone = clone;
newDelay.Life = t;
delays.Add(newDelay);
}
private void TryDespawn(GameObject clone)
{
if (spawnedClonesHashSet.Remove(clone) == true || spawnedClonesList.Remove(clone) == true)
{
DespawnNow(clone);
}
else
{
if (Warnings == true) Debug.LogWarning("You're attempting to despawn a GameObject that wasn't spawned from this pool, make sure your Spawn and Despawn calls match.", clone);
}
}
private void DespawnNow(GameObject clone, bool register = true)
{
// Add clone to despawned list
if (register == true)
{
despawnedClones.Add(clone);
}
// Messages?
InvokeOnDespawn(clone);
// Deactivate it
if (Strategy == StrategyType.ActivateAndDeactivate)
{
clone.SetActive(false);
clone.transform.SetParent(transform, false);
}
else
{
clone.transform.SetParent(DeactivatedChild, false);
}
}
private GameObject CreateClone(Vector3 localPosition, Quaternion localRotation, Vector3 localScale, Transform parent, bool worldPositionStays)
{
var clone = DoInstantiate(prefab, localPosition, localRotation, localScale, parent, worldPositionStays);
if (Stamp == true)
{
clone.name = prefab.name + " " + Total;
}
else
{
clone.name = prefab.name;
}
return clone;
}
private GameObject DoInstantiate(GameObject prefab, Vector3 localPosition, Quaternion localRotation, Vector3 localScale, Transform parent, bool worldPositionStays)
{
#if UNITY_EDITOR
if (Application.isPlaying == false && UnityEditor.PrefabUtility.IsPartOfRegularPrefab(prefab) == true)
{
if (worldPositionStays == true)
{
return (GameObject)UnityEditor.PrefabUtility.InstantiatePrefab(prefab, parent);
}
else
{
var clone = (GameObject)UnityEditor.PrefabUtility.InstantiatePrefab(prefab, parent);
clone.transform.localPosition = localPosition;
clone.transform.localRotation = localRotation;
clone.transform.localScale = localScale;
return clone;
}
}
#endif
if (worldPositionStays == true)
{
return Instantiate(prefab, parent, true);
}
else
{
var clone = Instantiate(prefab, localPosition, localRotation, parent);
clone.transform.localPosition = localPosition;
clone.transform.localRotation = localRotation;
clone.transform.localScale = localScale;
return clone;
}
}
private void SpawnClone(GameObject clone, Vector3 localPosition, Quaternion localRotation, Vector3 localScale, Transform parent, bool worldPositionStays)
{
// Register
if (Recycle == true)
{
spawnedClonesList.Add(clone);
}
else
{
spawnedClonesHashSet.Add(clone);
}
// Update transform
var cloneTransform = clone.transform;
cloneTransform.SetParent(null, false);
cloneTransform.localPosition = localPosition;
cloneTransform.localRotation = localRotation;
cloneTransform.localScale = localScale;
cloneTransform.SetParent(parent, worldPositionStays);
// Make sure it's in the current scene
if (parent == null)
{
SceneManager.MoveGameObjectToScene(clone, SceneManager.GetActiveScene());
}
// Activate
if (Strategy == StrategyType.ActivateAndDeactivate)
{
clone.SetActive(true);
}
// Notifications
InvokeOnSpawn(clone);
}
private void InvokeOnSpawn(GameObject clone)
{
switch (Notification)
{
case NotificationType.SendMessage: clone.SendMessage("OnSpawn", SendMessageOptions.DontRequireReceiver); break;
case NotificationType.BroadcastMessage: clone.BroadcastMessage("OnSpawn", SendMessageOptions.DontRequireReceiver); break;
case NotificationType.IPoolable: clone.GetComponents(tempPoolables); for (var i = tempPoolables.Count - 1; i >= 0; i--) tempPoolables[i].OnSpawn(); break;
case NotificationType.BroadcastIPoolable: clone.GetComponentsInChildren(tempPoolables); for (var i = tempPoolables.Count - 1; i >= 0; i--) tempPoolables[i].OnSpawn(); break;
}
}
private void InvokeOnDespawn(GameObject clone)
{
switch (Notification)
{
case NotificationType.SendMessage: clone.SendMessage("OnDespawn", SendMessageOptions.DontRequireReceiver); break;
case NotificationType.BroadcastMessage: clone.BroadcastMessage("OnDespawn", SendMessageOptions.DontRequireReceiver); break;
case NotificationType.IPoolable: clone.GetComponents(tempPoolables); for (var i = tempPoolables.Count - 1; i >= 0; i--) tempPoolables[i].OnDespawn(); break;
case NotificationType.BroadcastIPoolable: clone.GetComponentsInChildren(tempPoolables); for (var i = tempPoolables.Count - 1; i >= 0; i--) tempPoolables[i].OnDespawn(); break;
}
}
private void MergeSpawnedClonesToList()
{
if (spawnedClonesHashSet.Count > 0)
{
spawnedClonesList.AddRange(spawnedClonesHashSet);
spawnedClonesHashSet.Clear();
}
}
public void OnBeforeSerialize()
{
MergeSpawnedClonesToList();
}
public void OnAfterDeserialize()
{
if (Recycle == false)
{
for (var i = spawnedClonesList.Count - 1; i >= 0; i--)
{
var clone = spawnedClonesList[i];
spawnedClonesHashSet.Add(clone);
}
spawnedClonesList.Clear();
}
}
}
}
#if UNITY_EDITOR
namespace Lean.Pool
{
using UnityEditor;
[CanEditMultipleObjects]
[CustomEditor(typeof(LeanGameObjectPool))]
public class LeanGameObjectPool_Inspector : LeanInspector
{
protected override void DrawInspector()
{
BeginError(Any(t => t.Prefab == null));
if (Draw("prefab", "The prefab this pool controls.") == true)
{
Each(t => { t.Prefab = (GameObject)serializedObject.FindProperty("prefab").objectReferenceValue; }, true);
}
EndError();
Draw("Notification", "If you need to peform a special action when a prefab is spawned or despawned, then this allows you to control how that action is performed. None = If you use this then you must rely on the OnEnable and OnDisable messages. SendMessage = The prefab clone is sent the OnSpawn and OnDespawn messages. BroadcastMessage = The prefab clone and all its children are sent the OnSpawn and OnDespawn messages. IPoolable = The prefab clone's components implementing IPoolable are called. Broadcast IPoolable = The prefab clone and all its child components implementing IPoolable are called.");
Draw("Strategy", "This allows you to control how spawned/despawned GameObjects will be handled. The DeactivateViaHierarchy mode should be used if you need to maintain your prefab's de/activation state.\n\nActivateAndDeactivate = Despawned clones will be deactivated and placed under this GameObject.\n\nDeactivateViaHierarchy = Despawned clones will be placed under a deactivated GameObject and left alone.");
Draw("Preload", "Should this pool preload some clones?");
Draw("Capacity", "Should this pool have a maximum amount of spawnable clones?");
Draw("Recycle", "If the pool reaches capacity, should new spawns force older ones to despawn?");
Draw("Persist", "Should this pool be marked as DontDestroyOnLoad?");
Draw("Stamp", "Should the spawned clones have their clone index appended to their name?");
Draw("Warnings", "Should detected issues be output to the console?");
EditorGUILayout.Separator();
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.IntField("Spawned", tgt.Spawned);
EditorGUILayout.IntField("Despawned", tgt.Despawned);
EditorGUILayout.IntField("Total", tgt.Total);
EditorGUI.EndDisabledGroup();
if (Application.isPlaying == false)
{
if (Any(t => t.DespawnedClonesMatch == false))
{
EditorGUILayout.HelpBox("Your preloaded clones no longer match the Prefab.", MessageType.Warning);
}
}
}
[MenuItem("GameObject/Lean/Pool", false, 1)]
private static void CreateLocalization()
{
var gameObject = new GameObject(typeof(LeanGameObjectPool).Name);
Undo.RegisterCreatedObjectUndo(gameObject, "Create LeanGameObjectPool");
gameObject.AddComponent();
Selection.activeGameObject = gameObject;
}
}
}
#endif