From 48b6cc7438849d82adfd7da5911dd879ffbd8b17 Mon Sep 17 00:00:00 2001 From: magicjar <9734293+magicjar@users.noreply.github.com> Date: Mon, 1 Nov 2021 17:45:35 +0700 Subject: [PATCH] Add SimplePool by quill18 and extended by Draugor --- Runtime/Pooling.meta | 8 + Runtime/Pooling/SimplePool.cs | 230 +++++++++++++++++++++++++++++ Runtime/Pooling/SimplePool.cs.meta | 11 ++ 3 files changed, 249 insertions(+) create mode 100644 Runtime/Pooling.meta create mode 100644 Runtime/Pooling/SimplePool.cs create mode 100644 Runtime/Pooling/SimplePool.cs.meta diff --git a/Runtime/Pooling.meta b/Runtime/Pooling.meta new file mode 100644 index 0000000..98604f2 --- /dev/null +++ b/Runtime/Pooling.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3046d41e6347fb446a60e393328228f7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Pooling/SimplePool.cs b/Runtime/Pooling/SimplePool.cs new file mode 100644 index 0000000..90972fb --- /dev/null +++ b/Runtime/Pooling/SimplePool.cs @@ -0,0 +1,230 @@ +/// +/// Simple pooling for Unity. +/// Author: Martin "quill18" Glaude (quill18@quill18.com) +/// Extended: Simon "Draugor" Wagner (https://www.twitter.com/Draugor_/) +/// Latest Version: https://gist.github.com/Draugor/00f2a47e5f649945fe4466dea7697024 +/// License: CC0 (http://creativecommons.org/publicdomain/zero/1.0/) +/// UPDATES: +/// 2020-07-09: - Fixed a Bug with already inactive members getting Despawned again. thx AndySum (see: https://gist.github.com/Draugor/00f2a47e5f649945fe4466dea7697024#gistcomment-2642441) +/// 2020-06-30: - made the "parent" parameter avaible in the public API to spawn GameObjects as children +/// 2018-01-04: - Added Extension Method for Despawn on GameObjects +/// - Changed the Member Lookup so it doesn't require a PoolMemberComponent anymore. +/// - for that i added a HashSet which contains all PoolMemberIDs (HashSet has O(1) contains operator) +/// - Changed PoolDictionary from (Prefab, Pool) to (int, Pool) using Prefab.GetInstanceID +/// 2015-04-16: Changed Pool to use a Stack generic. +/// +/// Usage: +/// +/// There's no need to do any special setup of any kind. +/// +/// Instead of calling Instantiate(), use this: +/// SimplePool.Spawn(somePrefab, somePosition, someRotation); +/// +/// Instead of destroying an object, use this: +/// SimplePool.Despawn(myGameObject); +/// or this: +/// myGameObject.Despawn(); +/// +/// If desired, you can preload the pool with a number of instances: +/// SimplePool.Preload(somePrefab, 20); +/// +/// Remember that Awake and Start will only ever be called on the first instantiation +/// and that member variables won't be reset automatically. You should reset your +/// object yourself after calling Spawn(). (i.e. You'll have to do things like set +/// the object's HPs to max, reset animation states, etc...) +/// + +using UnityEngine; +using System.Collections.Generic; + +namespace Agraris.Tools.Core +{ + public static class SimplePool + { + // You can avoid resizing of the Stack's internal data by + // setting this to a number equal to or greater to what you + // expect most of your pool sizes to be. + // Note, you can also use Preload() to set the initial size + // of a pool -- this can be handy if only some of your pools + // are going to be exceptionally large (for example, your bullets.) + public const int DEFAULT_POOL_SIZE = 3; + + /// + /// The Pool class represents the pool for a particular prefab. + /// + public class Pool + { + // We append an id to the name of anything we instantiate. + // This is purely cosmetic. + private int _nextId = 1; + // The structure containing our inactive objects. + // Why a Stack and not a List? Because we'll never need to + // pluck an object from the start or middle of the array. + // We'll always just grab the last one, which eliminates + // any need to shuffle the objects around in memory. + private readonly Stack _inactive; + //A Hashset which contains all GetInstanceIDs from the instantiated GameObjects + //so we know which GameObject is a member of this pool. + public readonly HashSet MemberIDs; + // The prefab that we are pooling + private readonly GameObject _prefab; + + // Constructor + public Pool(GameObject prefab, int initialQty) + { + _prefab = prefab; + // If Stack uses a linked list internally, then this + // whole initialQty thing is a placebo that we could + // strip out for more minimal code. But it can't *hurt*. + _inactive = new Stack(initialQty); + MemberIDs = new HashSet(); + } + + // Spawn an object from our pool + public GameObject Spawn(Vector3 pos, Quaternion rot, Transform parent = null) + { + GameObject obj; + if (_inactive.Count == 0) + { + // We don't have an object in our pool, so we + // instantiate a whole new object. + obj = GameObject.Instantiate(_prefab, pos, rot); + obj.name = _prefab.name + " (" + (_nextId++) + ")"; + + // Add the unique GameObject ID to our MemberHashset so we know this GO belongs to us. + MemberIDs.Add(obj.GetInstanceID()); + } + else + { + // Grab the last object in the inactive array + obj = _inactive.Pop(); + + if (obj == null) + { + // The inactive object we expected to find no longer exists. + // The most likely causes are: + // - Someone calling Destroy() on our object + // - A scene change (which will destroy all our objects). + // NOTE: This could be prevented with a DontDestroyOnLoad + // if you really don't want this. + // No worries -- we'll just try the next one in our sequence. + + return Spawn(pos, rot, parent); + } + } + obj.transform.SetParent(parent, false); + obj.transform.position = pos; + obj.transform.rotation = rot; + obj.SetActive(true); + return obj; + } + + // Return an object to the inactive pool. + public void Despawn(GameObject obj) + { + + if (obj.activeInHierarchy) + { + obj.SetActive(false); + + // Since Stack doesn't have a Capacity member, we can't control + // the growth factor if it does have to expand an internal array. + // On the other hand, it might simply be using a linked list + // internally. But then, why does it allow us to specify a size + // in the constructor? Maybe it's a placebo? Stack is weird. + _inactive.Push(obj); + } + } + } + + // All of our pools + public static Dictionary _pools; + + /// + /// Initialize our dictionary. + /// + private static void Init(GameObject prefab = null, int qty = DEFAULT_POOL_SIZE) + { + if (_pools == null) + _pools = new Dictionary(); + + if (prefab != null) + { + //changed from (prefab, Pool) to (int, Pool) which should be faster if we have + //many different prefabs. + var prefabID = prefab.GetInstanceID(); + if (!_pools.ContainsKey(prefabID)) + _pools[prefabID] = new Pool(prefab, qty); + } + } + + /// + /// If you want to preload a few copies of an object at the start + /// of a scene, you can use this. Really not needed unless you're + /// going to go from zero instances to 100+ very quickly. + /// Could technically be optimized more, but in practice the + /// Spawn/Despawn sequence is going to be pretty darn quick and + /// this avoids code duplication. + /// + static public void Preload(GameObject prefab, int qty = 1) + { + Init(prefab, qty); + // Make an array to grab the objects we're about to pre-spawn. + var obs = new GameObject[qty]; + for (int i = 0; i < qty; i++) + obs[i] = Spawn(prefab, Vector3.zero, Quaternion.identity); + + // Now despawn them all. + for (int i = 0; i < qty; i++) + Despawn(obs[i]); + } + + /// + /// Spawns a copy of the specified prefab (instantiating one if required). + /// NOTE: Remember that Awake() or Start() will only run on the very first + /// spawn and that member variables won't get reset. OnEnable will run + /// after spawning -- but remember that toggling IsActive will also + /// call that function. + /// + static public GameObject Spawn(GameObject prefab, Vector3 pos, Quaternion rot, Transform parent = null) + { + Init(prefab); + + return _pools[prefab.GetInstanceID()].Spawn(pos, rot, parent); + } + + /// + /// Despawn the specified gameobject back into its pool. + /// + static public void Despawn(GameObject obj) + { + Pool p = null; + foreach (var pool in _pools.Values) + { + if (pool.MemberIDs.Contains(obj.GetInstanceID())) + { + p = pool; + break; + } + } + + if (p == null) + { + Debug.LogWarning("Object '" + obj.name + "' wasn't spawned from a pool. Destroying it instead."); + GameObject.Destroy(obj); + } + else + { + p.Despawn(obj); + } + } + } + + public static class SimplePoolGameObjectExtensions + { + public static void Despawn(this GameObject go) + { + SimplePool.Despawn(go); + } + } +} \ No newline at end of file diff --git a/Runtime/Pooling/SimplePool.cs.meta b/Runtime/Pooling/SimplePool.cs.meta new file mode 100644 index 0000000..977d578 --- /dev/null +++ b/Runtime/Pooling/SimplePool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d2f5a542d785dda4291db5b1ecb7c756 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: