GetInstanceID - What is it really for?

I came across GetInstanceID a while back and find it to be quite useless.

Does anyone know any uses for this? You know, that are practical?

Anyway, I created a Feedback Idea here

The idea is basically to make the GetInstanceID useful by giving the user the ability to reference GameObjects with it*.* It seems a bit much using GameObject Find etc all the time.

It would be nice for the object to throw us it’s ID so we can call on it whenever its needed. I know others have expressed a desire for this also.

I would just like some thoughts on this, really. And if someone could tell me why its not practical, it would help quell my desire.

Regards.

2 Likes

It’s intended for debugging. For example, you believe that two object references (let’s call them A and B) refer to the same object, but you’re not sure, because you have a hundred objects of the same type that are easy to get confused. So you Debug.Log the GetInstanceID of each one, and if they’re the same, then A and B refer to the exact same object. If they’re different, then they do not.

If you don’t have a need for that, then don’t use it. There’s no harm in APIs that you never use. I guarantee you others will find it useful from time to time.

I don’t understand how the proposed functionality would help anything. What would you call GetInstanceID on in the first place? If you don’t already have a reference to the object, then you have to use GameObject Find etc. If you do already have a reference, then just use that!

I don’t see anything “arduous” about storing references; it’s just A = B, which is certainly less arduous than A = B.GetInstanceID() (which would have to be later followed by some sort of FindObjectByID(A) to turn this back into a reference).

And you say “losing refs from destroyed GOs” — that doesn’t actually happen, but suppose you’re in a case where the game object is destroyed. What would your FindObjectByID function do then? Presumably it would return nil, in which case you’ve just reinvented an awkward sort of weak reference. But if that’s what you want, just use the built-in WeakReference class.

(Actually, I’m not sure WeakReference is available yet in Unity, as it’s a .NET 4.5 feature; but I do know the folks at Unity are already hard at work updating us to the latest .NET as fast as they can, at which point we’ll get this and a whole lot of other things.)

Can you give some specific examples of where “Let the user reference the object by its unique ID” would be useful, and easier than simply storing the object reference?

4 Likes

When I first encountered GetInstanceID I assumed it to be similar to retrieving the object’s hash value but it turned out to be severely limited. I encountered it through questions on Unity Answers where users were asking if there was a way to reference GOs by a Unique ID. Its one of these things that people see and instantly assume it does something in particular only to find that it in fact does not.

We all know the problems with GO.Find. It’s certainly not ideal. In fact it’s far removed from ideal. There are many cases of users wanting a simple handle or identifier rather than the current way things are handled.

Check out alucardj’s comment at the bottom of this thread. It makes sense to me.

I admit, I did rush the feedback page a bit. Probably sounds like a load of rubbish :smile: I just wanted to get it in there.

I suppose the point of it is that to me, with my Intermediate programming skills, a lot of processes would become simplified if I could interact with an object without Finding it first, by just using it’s ID. Seems logical. (Electronics Engineer, not Software Engineer).

I couldn’t design the implementation as an example. That’s a bit beyond me.

But in a nutshell, Objects HAVE a unique ID; why can’t we do more with it?

Thanks for the WeakReference link, its most interesting.

1 Like

I think it is similar to retrieving the object’s hash value. For all I can tell, it is exactly that.

And yes, you should generally avoid GO.Find. I’m with you there. What I don’t see is how being able to look up an object by its instance ID has anything to do with that.

Alucardj’s comment has to do with networking, but note that he immediately hit upon a problem with using instance ID’s for that in a multiplayer environment, which is that the “same” object on different clients will not have the same ID. And of course it must be that way; there is no way Unity can reach across the network and synchronize its IDs, if it could even know that you consider this object to be the same as that one over there, which it can’t know anyway.

So in a situation like that, you have to come up with your own identification system, using IDs handed out by some authoritative server (which could be one of the clients in a peer-to-peer game). Of all the tricky issues you have to deal with in a network game, this is one of the easiest. :wink:

Please give an example. Because I can’t think of any, and I’ve been at this a long time.

I’m not asking you to design the implementation of it; I’m asking you to give an example of a specific problem that you imagine would be more easily solved by finding an object by its ID, than by just storing the object reference.

Like what, exactly? It serves its purpose perfectly well: it gives you a unique identifier that you can log to see whether two objects are the same. That’s what it’s for. If you believe there is potentially some other good use for it, please be specific about that use.

To me, it sounds like “How come this screwdriver is only useful for driving screws? Why can’t I hammer nails with it, or maybe make toast? Reaching for the toaster is such a pain, and I never use this screwdriver…” But the fact is that, whether you need to drive screws or not, that’s what the screwdriver is for, and there already great ways to hammer nails, make toast, etc.

5 Likes

I like your analogy :slight_smile:

Ok Joe, I’m going to take what you’ve said on board. I’ll sit on this for a while and see if I can find some specific cases of usefulness. I shall return when I can effectively put in to words what my brain is grinding over.

Until then, thanks for the discussion.

How would you interact with something if you don’t go get it first? Whether it be through Find() or something akin to FindById() you still have to go get the thing you want before you can do anything with it.

There’s also nothing stopping you from rolling a thin wrapper around a Dictionary that uses the instance ID as a key. Just because Unity doesn’t offer it as part of the API doesn’t mean you can’t do it.

We do exactly that with NetworkViewIDs because NetworkView.Find() throws an exception if it can’t find the ID.

1 Like

This is a good point. It’s not something I would ordinarily think of though. I could implement something like this but it’d take me a while to work out what I’m doing and it’d really not be very elegant at all :stuck_out_tongue: I would get there though.

Guys… This one is extremely useful for Edit Mode scripts to detect being Instantiated from prefab OR duplicated with Ctrl+D!

I mean… interesting necro.

I will say, I definitely use GetInstanceID for hashing.

Well, I found a different use for the GetInstanceID method. I needed a dynamic object pooler that can be created from a prefab, in a simple way, without the need of using tags. So in my specific case, I modified the objectPooler from a RayWenderlich post () and added the necessary mods, to create a list of pools based on the Id of a prefab.

Here is the code, in case you want it. I know it is not the best solution ever, but it served me well.

Cheers!

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ObjectPooler : MonoBehaviour {

    [System.Serializable]
    public class ObjectPoolItem {
        public GameObject objectToPool;
        public int amountToPool;
        public bool shouldExpand = true;
        public Transform parent;
    }

    //This internal class was created just to visualize in the editor if needed
    //the pools created. I find it is best to actually hide the variable
    //It can be easily replaced by a List<List<GameObject>> object
    [System.Serializable]
    public class ListOfObjects {
        public List<GameObject> GOList;
        public ListOfObjects() {
            GOList = new List<GameObject>();
        }
    }

    public static ObjectPooler SharedInstance;

    //List of items to be pooled
    public List<ObjectPoolItem> itemsToPool;

    //The actual objects in the different pools
    [HideInInspector]
    public List<ListOfObjects> pooledObjects;
    //An internal array used as a key list to access the different pools and avoid duplicate pool lists
    List<int> objectID = new List<int>();

    /// <summary>
    /// The SharedInstance will allow access to the pool without using external references
    /// Just use it as "ObjectPooler.SharedInstance.GetPooledObject(PREFAB)
    /// The prefab should be the object to pool
    /// </summary>
    void Awake() {
        SharedInstance = this;
    }

    /// <summary>
    /// If we put some items on the editor, then the Start method will create pools for those
    /// </summary>
    void Start() {
        if (pooledObjects == null) {
            pooledObjects = new List<ListOfObjects>();
        }
        foreach (ObjectPoolItem item in itemsToPool) {
            if (AddIDToList(item.objectToPool)) {

                ListOfObjects listOfObjects = new ListOfObjects();
                for (int i = 0; i < item.amountToPool; i++) {
                    GameObject obj = (GameObject)Instantiate(item.objectToPool);

                    if (item.objectToPool.transform.parent != null) {
                        obj.transform.SetParent(item.objectToPool.transform.parent);
                    } else {
                        obj.transform.SetParent(this.gameObject.transform);
                    }
                    item.parent = obj.transform.parent;

                    obj.SetActive(false);
                    listOfObjects.GOList.Add(obj);

                }
                pooledObjects.Add(listOfObjects);
            }
        }
    }
    /// <summary>
    /// Helper method to avoid duplicated pools. If the Instance ID was already added to a pool, nothing is done
    /// </summary>
    /// <param name="GO"></param>
    /// <returns></returns>
    private bool AddIDToList(GameObject GO) {

        foreach (int currentID in objectID) {
            if (GO.GetInstanceID() == currentID) {
                return false;
            }
        }
        objectID.Add(GO.GetInstanceID());
        return true;
    }
    /// <summary>
    /// Used to instantiate an object pool by code when needed
    /// </summary>
    /// <param name="objectToPool"> The prefab, or GameObject to pool. A pool of objects using the instance Id as a key will be created</param>
    /// <param name="amount"> how many starting objects do you need created</param>
    /// <param name="shouldResize"> Is this a resizable pool? </param>
    /// <param name="objectParent"> Do you need to set the parent to a specific object? if not, the pool game object will be the default parent</param>
    public void CreateObjectPool(GameObject objectToPool, int amount, bool shouldResize, GameObject objectParent = null) {

        if (AddIDToList(objectToPool)) {
            ObjectPoolItem item = new ObjectPoolItem() {
                shouldExpand = shouldResize,
                amountToPool = amount,
                objectToPool = objectToPool
            };

            if (pooledObjects == null) {
                pooledObjects = new List<ListOfObjects>();
            }

            if (itemsToPool == null) {
                itemsToPool = new List<ObjectPoolItem>();
            }

            itemsToPool.Add(item);
            ListOfObjects list = new ListOfObjects();

            for (int i = 0; i < item.amountToPool; i++) {
                GameObject obj = (GameObject)Instantiate(item.objectToPool);
                if (objectParent != null) {
                    obj.transform.SetParent(objectParent.gameObject.transform);
                } else {
                    obj.transform.SetParent(this.gameObject.transform);
                }

                item.parent = obj.transform.parent;
                obj.SetActive(false);
                list.GOList.Add(obj);
            }
            pooledObjects.Add(list);
        }
    }
    /// <summary>
    /// This will return the object in the pool, using a pool created with a GetInstanceID key.
    /// Maybe a dictionary is better? Still, prefered to used a double list scheme
    /// </summary>
    /// <param name="prefab"></param>
    /// <returns></returns>
    public GameObject GetPooledObject(GameObject prefab) {
        int currentID = prefab.GetInstanceID();
        int poolIndex = objectID.IndexOf(currentID);

        if (poolIndex != -1) {
            for (int i = 0; i < pooledObjects[poolIndex].GOList.Count; i++) {
                if (!pooledObjects[poolIndex].GOList[i].activeInHierarchy) {
                    return pooledObjects[poolIndex].GOList[i];
                }
            }
            if (itemsToPool[poolIndex].shouldExpand) {
                GameObject obj = (GameObject)Instantiate(itemsToPool[poolIndex].objectToPool);

                obj.transform.SetParent(itemsToPool[poolIndex].parent);

                obj.SetActive(false);
                pooledObjects[poolIndex].GOList.Add(obj);
                return obj;
            }
        }
        return null;
    }

    /// <summary>
    /// Parameters for a timed game object disabler.
    /// </summary>
    class Parameters {
        public GameObject go;
        public float time;
    }

    /// <summary>
    /// Use this instead of a "destroyAfterTime".
    /// </summary>
    /// <param name="go"></param>
    /// <param name="time"></param>
    public void ReturnToPoolTimed(GameObject go, float time) {
        Parameters parameters = new Parameters() {
            go = go,
            time = time
        };
        StartCoroutine("disableGameObject", parameters);
    }

    IEnumerator DisableGameObject(Parameters parameters) {
        yield return new WaitForSeconds(parameters.time);
        if (parameters.go != null) {
            parameters.go.SetActive(false);
        }
    }
}
1 Like

The RayWenderlich post, which for some reason I forgot to put above, is https://www.raywenderlich.com/136091/object-pooling-unity

1 Like

I would advise using HashSet’s or even Dictionary’s which have hashing built in.

This will give you O(1) access of entries based on hash codes instead.

Note that all UnityEngine.Object’s use the instance id as their hash code for GetHashCode (GameObjects, your scripts, etc, all inherit from UnityEngine.Object).

From decompiled UnityEngine.Object:

    /// <summary>
    ///   <para>Returns the instance id of the object.</para>
    /// </summary>
    [SecuritySafeCritical]
    public int GetInstanceID()
    {
      this.EnsureRunningOnMainThread();
      return this.m_InstanceID;
    }

    public override int GetHashCode()
    {
      return this.m_InstanceID;
    }

This means that if you use it in a HashSet or as the key of a Dictionary, it determines stuff off the hash.

Basically things like ‘Contains’, instead of looping over the entire contents of the collection and checking equality. It instead calculates an index based off the hashcode. So all it has to do is take that hashcode, calculate the index (a quick arithmetic process) and then look in that slot for the object (there’s a little more to it… but it’s still not looping over the entire collection).

I don’t recall, but back in the day I don’t know if they did the GetHashCode correctly. I know I created an IEqualityComparer for this distinct purpose, which I use with sets sometimes as well… but then again, I may have just mistaken it. Either way, even if Unity didn’t return the instance id from GetHashCode, you could force it to with an IEqualityComparer:

So if you swapped out that ‘pooledObjects’ list and ‘objectID’ list and just had a Dictionary you could simplify that down a lot.

Alos… holy god that thing is all over the place.

Here… I did a simple strip down of useless stuff and organized it a bit:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ObjectPooler : MonoBehaviour
{

    #region Singleton Interface

    private static ObjectPooler _instance;
    public static ObjectPooler SharedInstance
    {
        get { return _instance; }
    }

    /// <summary>
    /// The SharedInstance will allow access to the pool without using external references
    /// Just use it as "ObjectPooler.SharedInstance.GetPooledObject(PREFAB)
    /// The prefab should be the object to pool
    /// </summary>
    void Awake()
    {
        if(_instance != null && !object.ReferenceEquals(_instance, this))
        {
            Debug.LogWarning("ObjectPooler Instance already exists");
            Object.Destroy(this);
            return;
        }
        _instance = this;
    }

    #endregion

    #region Fields
 
    /// <summary>
    /// This is an editor only collection of entries.
    /// No reason to keep managing this collection once the game starts and having to look back and forth between 2 lists that could accidentally get out of sync.
    /// Instead lets use the 'ListOfObjects' class to track this information once the game starts.
    /// </summary>
    [SerializeField]
    private ObjectPoolItem[] _itemsToPool;

    //The actual objects in the different pools
    [System.NonSerialized]
    private Dictionary<GameObject, ListOfObjects> _pools = new Dictionary<GameObject, ListOfObjects>();

    #endregion

    #region CONSTRUCTOR
 
    void Start()
    {
        for (int i = 0; i < _itemsToPool.Length; i++)
        {
            var item = _itemsToPool[i];
            if (!_pools.ContainsKey(item.objectToPool))
            {
                ListOfObjects listOfObjects = new ListOfObjects()
                {
                    Info = item
                };
                for (int j = 0; j < item.amountToPool; j++)
                {
                    GameObject obj = (GameObject)Instantiate(item.objectToPool);

                    if (item.objectToPool.transform.parent != null)
                    {
                        obj.transform.SetParent(item.objectToPool.transform.parent);
                    }
                    else
                    {
                        obj.transform.SetParent(this.gameObject.transform);
                    }
                    item.parent = obj.transform.parent;

                    obj.SetActive(false);
                    listOfObjects.GOList.Add(obj);

                }
                _pools.Add(item.objectToPool, listOfObjects);
            }
        }
    }

    #endregion

    #region Methods
 
    /// <summary>
    /// Used to instantiate an object pool by code when needed
    /// </summary>
    /// <param name="objectToPool"> The prefab, or GameObject to pool. A pool of objects using the instance Id as a key will be created</param>
    /// <param name="amount"> how many starting objects do you need created</param>
    /// <param name="shouldResize"> Is this a resizable pool? </param>
    /// <param name="objectParent"> Do you need to set the parent to a specific object? if not, the pool game object will be the default parent</param>
    public void CreateObjectPool(GameObject objectToPool, int amount, bool shouldResize, GameObject objectParent = null)
    {
        if (!_pools.ContainsKey(objectToPool))
        {
            ListOfObjects list = new ListOfObjects()
            {
                Info = new ObjectPoolItem()
                {
                    shouldExpand = shouldResize,
                    amountToPool = amount,
                    objectToPool = objectToPool
                }
            };

            for (int i = 0; i < list.Info.amountToPool; i++)
            {
                GameObject obj = (GameObject)Instantiate(list.Info.objectToPool);
                if (objectParent != null)
                {
                    obj.transform.SetParent(objectParent.gameObject.transform);
                }
                else
                {
                    obj.transform.SetParent(this.gameObject.transform);
                }

                list.Info.parent = obj.transform.parent;
                obj.SetActive(false);
                list.GOList.Add(obj);
            }
            _pools.Add(objectToPool, list);
        }
    }

    /// <summary>
    /// This will return the object in the pool, using a pool created with a GetInstanceID key.
    /// Maybe a dictionary is better? Still, prefered to used a double list scheme
    /// </summary>
    /// <param name="prefab"></param>
    /// <returns></returns>
    public GameObject GetPooledObject(GameObject prefab)
    {
        ListOfObjects list;
        if (!_pools.TryGetValue(prefab, out list)) return null;

        for (int i = 0; i < list.GOList.Count; i++)
        {
            if (!list.GOList[i].activeInHierarchy)
            {
                return list.GOList[i];
            }
        }
        if(list.Info != null && list.Info.shouldExpand)
        {
            GameObject obj = (GameObject)Instantiate(list.Info.objectToPool);

            obj.transform.SetParent(list.Info.parent);

            obj.SetActive(false);
            list.GOList.Add(obj);
            return obj;
        }
        return null;
    }
    /// <summary>
    /// Use this instead of a "destroyAfterTime".
    /// </summary>
    /// <param name="go"></param>
    /// <param name="time"></param>
    public void ReturnToPoolTimed(GameObject go, float time)
    {
        if (go == null) return;

        if (time > 0f)
            this.StartCoroutine(this.DisableGameObject(go, time));
        else
            go.SetActive(false);
    }

    IEnumerator DisableGameObject(GameObject go, float time)
    {
        yield return new WaitForSeconds(time);
        if (go != null)
        {
            go.SetActive(false);
        }
    }
    #endregion

    #region Special Types

    [System.Serializable]
    public class ObjectPoolItem
    {
        public GameObject objectToPool;
        public int amountToPool;
        public bool shouldExpand = true;
        public Transform parent;
    }
 
    /// <summary>
    /// Internal structure of pools... this should not have been serializable since it never gets serialized!
    /// </summary>
    private class ListOfObjects
    {
        public List<GameObject> GOList = new List<GameObject>();
        public ObjectPoolItem Info;
    }
 
    #endregion

}

Although… there’s a LOT more changes I would do to that class if it were up to me…

5 Likes

Nice!
I was looking into the original class, and yes, I’ve put a comment stating that a dictionary would be the way to go. One of those refactors that never get actually done…

Also, the only reason to serialize the ListOfObjects was just debugging, to see that the pool was actually being filled. And forgot to remove that…

Not trying to hijack the post, but now I’m curious, what else should you change? (not a detailed list, but just some quick suggestions)

I might have reached that moment of the always posponed refactor :slight_smile:

Thank you in any case!

Well… how about this.

This last week I’ve been porting/refactoring my framework of code over to Unity 2017.3.

This library/framework has been built over the past few years while I worked on games. Basically housing general purpose code that we’d carry with us from project to project. Things like AI, pathfinding/waypoints, spawn pools, tween engine, visual programming tools, serialization, movement motors, input handling, animation handling, etc. As well as various tools that Unity now offers, but didn’t when I created them. Like my RadicalCoroutine and SPEvent, which I created before Unity added Coroutine object identity and custom yield instructions, as well as UnityEvent. I still use mine today though since I well… prefer mine.

Currently I’m moving it to Unity 2017.3, and in the process I’m sort of stripping it of unnecessary stuff, refactoring some code that got stuck like it was, and other tuning of it.

I put it up on github as sort of a reference material. In threads I’d share snippets from it in my explanations and what not. It wasn’t really meant for distribution, but really just examples. Primarily because I could be damned if I was going to put in a large amount of support/documentation to get it off the ground for people (hence it being MIT license on github… at your own risk type stuff).

Anyways, in it I have my SpawnPool:

Tonight I’m feeling a bit brash/bored… so instead of documenting changes I’d do to the code you shared. How about instead I do a break down of my design of my SpawnPool, why I made the choices I did in it, and all that sort.

I’ll post that some time later tonight. I’m in the midst of just waking up, getting food in my belly, and doing some yoga. So not sure how long that will take.

OK, so for starters I’m going to stick this in ‘spoiler’ blocks so as not to generate a wall’o’text that will annoy the usual readers. So anyone interested, expand the spoiler (and I apologize for the spelling mistakes galore… this is long, I ain’t proof reading it).

Lets start with the actual code in question:
spacepuppy-unity-framework-3.0/SPSpawn/Spawn at master · lordofduct/spacepuppy-unity-framework-3.0 · GitHub

You will notice that I actually have a few classes:
Classes
SpawnPool - the actual spawn pool implementation.

SpawnedObjectController - A script that gets placed on instances of spawned objects to allow easy tracking of the object, return to the SpawnPool, etc.

IPrefabCache - an interface for the cache’s in the SpawnPool. Allowing for a controlled access to the cache. There are plans to add more features based on this, they just don’t exist yet… need to bake in controls that can create holes if you change the cache size after loading the pool.

ISpawnPoint - interface for spawn points that use the SpawnPool to spawn objects.

Events.i_Spawn - a spawn point

SpawnPointTracker - a script that monitors spawnpoints and tracks the spawn of objects. Lets say you have a enemy nest that spawns enemies every 5 seconds to a max of 3. And every time one dies, it’ll start spawning again up to 3 again. This can be used to facilitate since it tracks and counts every enemy spawned from a spawn point.

IOnSpawnHandler - message interface for handling an OnSpawn message (see: Unity Messaging System)

IOnDespawnHandler - message interface for handling OnDespawn message

So not only do we have our SpawnPool, but some reusable and integrated tools for interacting with the SpawnPool.

Now lets get into some detail about my choices in this design.

The SpawnPool

Lets start with the actual code (note bugs may exist, I only refactored this yesterday, I’m still in the process of working on cleaning this up):
spacepuppy-unity-framework-3.0/SPSpawn/Spawn/SpawnPool.cs at master · lordofduct/spacepuppy-unity-framework-3.0 · GitHub
(due to its length I’m not going to be copying it into this thread in whole, but instead will be copying over segments for example)

For Starters there’s the ‘Static Multiton Interface’:

        #region Static Multiton Interface

        public const string DEFAULT_SPAWNPOOL_NAME = "Spacepuppy.PrimarySpawnPool";

        private static SpawnPool _defaultPool;
        private static HashSet<SpawnPool> _pools = new HashSet<SpawnPool>();

        public static SpawnPool DefaultPool
        {
            get
            {
                if (_defaultPool == null) CreatePrimaryPool();
                return _defaultPool;
            }
        }
     
        public static int PoolCount { get { return _pools.Count; } }

        public static IEnumerable<SpawnPool> AllSpawnPools
        {
            get { return _pools; }
        }

        public static void CreatePrimaryPool()
        {
            if (PrimaryPoolExists) return;

            var go = new GameObject(DEFAULT_SPAWNPOOL_NAME);
            _defaultPool = go.AddComponent<SpawnPool>();
        }

        public static bool PrimaryPoolExists
        {
            get
            {
                if (_defaultPool != null) return true;

                _defaultPool = null;
                var point = (from p in GameObject.FindObjectsOfType<SpawnPool>() where p.name == DEFAULT_SPAWNPOOL_NAME select p).FirstOrDefault();
                if (!object.ReferenceEquals(point, null))
                {
                    _defaultPool = point;
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }

        #endregion

This primarily serves just as a global access point into the SpawnPools. Instead of a Singleton though, it’s a Multiton… that is you can have as many SpawnPools as you want, and here is an access point to all of them.

It consists of a HashSet of all of them, a ‘_defaultPool’ which acts as a primary pool if you don’t select a specific one. A way to enumerate the pools (AllSpawnPools), and some initializers.

Pretty straight forward.

Next up comes our Fields:

        #region Fields

        [SerializeField()]
        [ReorderableArray(DrawElementAtBottom = true, ChildPropertyToDrawAsElementLabel = "ItemName", ChildPropertyToDrawAsElementEntry = "_prefab")]
        private List<PrefabCache> _registeredPrefabs = new List<PrefabCache>();

        [System.NonSerialized()]
        private Dictionary<int, PrefabCache> _prefabToCache = new Dictionary<int, PrefabCache>();

        #endregion

Again, pretty straight forward. There’s just 2 fields.

_registeredPrefabs - a serialized list of our prefabs and their settings (we’ll get to PrefabCache later, it’s essentially like your ‘ObjectPoolItem’ class)

_prefabToCache - a dictionary of the prefab pools for easy look up (just like the dictionary in your code)

Our dictionary is keyed on an ‘int’, this int is the InstanceId of the prefab. You’ll see later on the reason I keyed on an int instead of the GameObject directly, but put simply, it allows for easier look-up when you don’t have the prefab, and it saves us from having to unecessarily storing references to the prefab all over the place. Especially in the SpawnedObjectController, having the prefab in their is just unecessary and forces us to do extra look up.

Our Constructor/Destructor:

        #region CONSTRUCTOR

        protected override void Awake()
        {
            base.Awake();

            _pools.Add(this);
            if(this.name == DEFAULT_SPAWNPOOL_NAME && _defaultPool == null)
            {
                _defaultPool = this;
            }
        }

        protected override void Start()
        {
            base.Start();

            var e = _registeredPrefabs.GetEnumerator();
            while(e.MoveNext())
            {
                if (e.Current.Prefab == null) continue;

                e.Current.Load();
                _prefabToCache[e.Current.PrefabID] = e.Current;
            }
        }

        protected override void OnDestroy()
        {
            base.OnDestroy();

            if (object.ReferenceEquals(this, _defaultPool))
            {
                _defaultPool = null;
            }
            _pools.Remove(this);

            var e = _registeredPrefabs.GetEnumerator();
            while(e.MoveNext())
            {
                e.Current.Clear();
            }
        }

        #endregion

Awake - lets make sure we store the SpawnPool into the hashset for global access.

Start - load up all pre-configured prefab caches.

OnDestroy - make sure we destroy/unload all cached instances.

Properties:

        #region Properties

        private string _cachedName;
        public new string name
        {
            get
            {
                if (_cachedName == null) _cachedName = this.gameObject.name;
                return _cachedName;
            }
            set
            {
                this.gameObject.name = value;
                _cachedName = value;
            }
        }

        #endregion

We actually only have one unique property to the SpawnPool. There are more in the sense of the ICollection interface, but aside from that we just have this one ‘name’ property.

Really this is just an optimization of the ‘name’ property. Unfortunately Unity doesn’t cache the ‘name’ property so any time you try to check the name a new string is genrated and then flagged for GC. This keeps that from happening.

I did this because PrimaryPoolExists loops over and checks the names of SpawnPools. Also I used to have a query that allowed you to grab a SpawnPool by name, which also did the same. This just reduced the overhead of that. Honestly though, since I removed that query, I could just redact this… but alas, that’s the whole thing about refactoring isn’t it? Lol.

Registering Methods:

        public IPrefabCache Register(GameObject prefab, string sname, int cacheSize = 0, int resizeBuffer = 1, int limitAmount = 1)
        {
            if (object.ReferenceEquals(prefab, null)) throw new System.ArgumentNullException("prefab");
            if (_prefabToCache.ContainsKey(prefab.GetInstanceID())) throw new System.ArgumentException("Already manages prefab.", "prefab");

            var cache = new PrefabCache(prefab, sname)
            {
                CacheSize = cacheSize,
                ResizeBuffer = resizeBuffer,
                LimitAmount = limitAmount
            };

            _registeredPrefabs.Add(cache);
            _prefabToCache[cache.PrefabID] = cache;
            cache.Load();
            return cache;
        }

        public bool UnRegister(GameObject prefab)
        {
            var cache = this.FindPrefabCache(prefab);
            if (cache == null) return false;

            return this.UnRegister(cache);
        }

        public bool UnRegister(int prefabId)
        {
            PrefabCache cache;
            if (!_prefabToCache.TryGetValue(prefabId, out cache)) return false;

            return this.UnRegister(cache);
        }

        public bool UnRegister(IPrefabCache cache)
        {
            var obj = cache as PrefabCache;
            if (obj == null) return false;
            if (obj.Owner != this) return false;

            obj.Clear();
            _registeredPrefabs.Remove(obj);
            _prefabToCache.Remove(obj.PrefabID);
            return true;
        }

        public bool Contains(int prefabId)
        {
            return _prefabToCache.ContainsKey(prefabId);
        }

        public bool Contains(string sname)
        {
            var e = _registeredPrefabs.GetEnumerator();
            while (e.MoveNext())
            {
                if (e.Current.Name == sname)
                {
                    return true;
                }
            }
            return false;
        }

OK, now we’re getting into the bulk of things. Here’s where we allow runtime registering new PrefabCache’s. Most of these methods are pretty straight forward, so lets really just dig into the ‘Register’ method.

We start out with some regular error/exception checks:

            if (object.ReferenceEquals(prefab, null)) throw new System.ArgumentNullException("prefab");
            if (_prefabToCache.ContainsKey(prefab.GetInstanceID())) throw new System.ArgumentException("Already manages prefab.", "prefab");

We don’t want to allow adding null prefabs, as well as disallowing prefabs that are already managed.

Next we create the PrefabCache instance:

            var cache = new PrefabCache(prefab, sname)
            {
                CacheSize = cacheSize,
                ResizeBuffer = resizeBuffer,
                LimitAmount = limitAmount
            };

And we add it to our collections and load it:

            _registeredPrefabs.Add(cache);
            _prefabToCache[cache.PrefabID] = cache;
            cache.Load();
            return cache;

We return the cache object as well for that future expansion I was planning.

In the load method we do somethings… so lets move down to the actual PrefabCache’s Load Method:

            internal void Load()
            {
                this.Clear();
                if (_prefab == null) return;

                for(int i = 0; i < this.CacheSize; i++)
                {
                    _instances.Add(this.CreateCachedInstance());
                }
            }

            private SpawnedObjectController CreateCachedInstance()
            {
                var obj = Object.Instantiate(this.Prefab, Vector3.zero, Quaternion.identity);
                obj.name = _itemName + "(CacheInstance)";
                var cntrl = obj.AddOrGetComponent<SpawnedObjectController>();
                cntrl.Init(_owner, this.Prefab.GetInstanceID(), _itemName);

                obj.transform.parent = _owner.transform;
                obj.transform.localPosition = Vector3.zero;
                obj.transform.rotation = Quaternion.identity;
             
                obj.SetActive(false);

                return cntrl;
            }

In here we actually handle loading our cache.

For starters we Clear the cache of any loaded instances just for cleanliness sake. This method really shouldn’t be called if it’s already loaded… I could have thrown an exception instead, but I guess I just went with Clearing. I might change this.

We then loop for the CacheSize creating insances. If CacheSize is 0, none are loaded.

When we create instances I name them for clarity sake I guess… it’s not really important.
I add the SpawnedObjectController and initialize it (we’ll get into more detail of this later).
And finally I parent the instance by the SpawnPoint. This is a failsafe so that if the SpawnPoint gets destroyed the caches will be destroyed as well.
And lastly I disable the instance since it’s currently cached.

All these instances get tossed into the PrefabCache’s ‘_instances’ HashSet.

Note… I use a HashSet here instead of a List because I don’t really need indexed entries. It’s a ‘pool’ not a ‘list’. I could have also used a Stack, but a Stack has a O(1) Pop, but O(n) Contains. Where as HashSet has an O(1) Contains, and I can create an O(1) Pop for it.

If you don’t know what “Big O” notation is. Basically O(1) means that you only need to iterate one entry to perform that task, where as O(n) means you need to iterate n entries of the list, where n is an average value from 0 to the length of the collection. Basically it’s like if you want to know if a List contains something you literally have to iterate the entire list and check every entry… best case is that the entry is near the beginning of the list. But it can potentially be at the end. HashSet on the other hand calculates the index based off the hash of the object, so all it has to do is look in that slot and compare it.

While we’re in the PrefabCache lets look at the other methods.

Fields:

            #region Fields

            [SerializeField]
            private string _itemName;
            [SerializeField]
            private GameObject _prefab;
         
            [Tooltip("The starting CacheSize.")]
            public int CacheSize = 0;
            [Tooltip("How much should the cache resize by if an empty/used cache is spawned from.")]
            public int ResizeBuffer = 1;
            [Tooltip("The maximum number of instances allowed to be cached, 0 or less means infinite.")]
            public int LimitAmount = 0;

            [System.NonSerialized()]
            private SpawnPool _owner;
            [System.NonSerialized()]
            private HashSet<SpawnedObjectController> _instances = new HashSet<SpawnedObjectController>(com.spacepuppy.Collections.ObjectReferenceEqualityComparer<SpawnedObjectController>.Default);
            [System.NonSerialized()]
            private HashSet<SpawnedObjectController> _activeInstances = new HashSet<SpawnedObjectController>(com.spacepuppy.Collections.ObjectReferenceEqualityComparer<SpawnedObjectController>.Default);

            #endregion

_itemName - a name for the cache for lookup by name

_prefab - the actual prefab we’re cloning

CacheSize - this is the initial size of the cache when loaded

ResizeBuffer - if the cache empties, how much do we resize by. Defaults to 1, but if say you’re using bullets and you reached your cache limit, you can assume if I needed 1 more, I probably going to need 10 more. Might as well load them right away.

LimitAmount - The maximum number of cached instances we’re allowed. If we try to spawn more than this amount, they’ll spawn, but they won’t be cached.

_owner - a reference to the SpawnPool this PrefabCache is a member of

_instances - the cached instances

_activeInstances - active instances that have been spawned.

Spawn Method:

            internal SpawnedObjectController Spawn(Vector3 pos, Quaternion rot, Transform par)
            {
                if(_instances.Count == 0)
                {
                    int cnt = this.Count;
                    int newSize = cnt + this.ResizeBuffer;
                    if (this.LimitAmount > 0) newSize = Mathf.Min(newSize, this.LimitAmount);

                    if(newSize > cnt)
                    {
                        for(int i = cnt; i < newSize; i++)
                        {
                            _instances.Add(this.CreateCachedInstance());
                        }
                    }
                }

                if(_instances.Count > 0)
                {
                    var cntrl = _instances.Pop();

                    _activeInstances.Add(cntrl);

                    cntrl.transform.parent = par;
                    cntrl.transform.position = pos;
                    cntrl.transform.rotation = rot;
                    cntrl.SetSpawned();

                    return cntrl;
                }
                else
                {
                    var obj = Object.Instantiate(this.Prefab, pos, rot, par);
                    if (obj != null)
                    {
                        var controller = obj.AddOrGetComponent<SpawnedObjectController>();
                        controller.Init(_owner, this.Prefab.GetInstanceID());
                        controller.SetSpawned();
                        _owner.SignalSpawned(controller);
                        return controller;
                    }
                    else
                    {
                        return null;
                    }
                }
            }

Here we handle spawning instances. Getting entries from the cache.

First we check if there are any cached instances available. If not, we try to resize the cache as long as it hasn’t reached its limit.

Next we check the instances count again. If there’s any, we pop one off the set (I have a special extension method for popping from HashSet). We add it to the ‘_activeInstances’ so it can be returned later. We then set its position, rotation, parent based on the passed in parameters (just like Instantiate has available).

If no instances exist at this point… that means we reached the CacheLimit. Note that these don’t get added to _activeInstances. It still gets a SpawnedObjectController so it can gain some of the features of that, but because it’s not in _activeInstances it can’t be returned to this PrefabCache. I could return null, but anything spawning something expects an object… the SpawnPool just exists to track objects.

Despawn Method:

            internal bool Despawn(SpawnedObjectController cntrl)
            {
                if (!_activeInstances.Remove(cntrl)) return false;

                cntrl.SetDespawned();
                cntrl.transform.parent = _owner.transform;
                cntrl.transform.localPosition = Vector3.zero;
                cntrl.transform.rotation = Quaternion.identity;

                _instances.Add(cntrl);
                return true;
            }

When returning objects to the pool we first try to remove it from _activeInstances. If it wasn’t in that collection (Remove returns false if the object wasn’t a member of the collection), then that means that instance was not tracked by this cache, we back out not.

Otherwise, we place the object back in the cached instances collection, reparent it by the SpawnPool, and zero out its properties.

Purge Method:

            internal bool Purge(SpawnedObjectController cntrl)
            {
                if (_activeInstances.Remove(cntrl))
                    return true;
                if (_instances.Remove(cntrl))
                    return true;

                return false;
            }

This method serves the purpose of just taking an object out of the cache all together. It doesn’t necessarily destroy the object. It just removes it from the pool so it’s no longer considered tracked in any regard.

SpawnedObjectController calls this if it gets destroyed while still being managed. This can happen if say you spawned a cached instance, then childed it to some other object. That other object then gets destroyed, so this instance also gets destroyed. The object is now destroyed, we can’t stop this. So Purge allows us to signal back to the SpawnPool that an object was inadvertently destroyed, and to stop tracking it.

You may also pre-emptively purge an object from the SpawnPool as well. Lets say you spawn a cached instance, but you want to modify that instance. Change it’s look/feel/whatever. Well if we returned this to the cache, and the respawned it later… all these changes would still exist. It’s not a proper clone of the prefab anymore! So you can Purge an instance from the SpawnPool so as to avoid this issue. Basically stating to the SpawnPool that you’re taking control of it and to stop worrying about it.

Other Stuff In SpawnPool
From here the rest of the methods in SpawnPool are just wrappers for these PrefabCache methods I just covered.

There’s 12 different ‘Spawn’ methods that allow spawning by index of the PRefabCache, by name of the PrefabCache, by the prefab itself (it looks up the cache), by prefab instance id, and 4 that return the object as the SpawnedObjectController.

There’s the wrapper ‘Purge’ and ‘Despawn’ method.

There’s the FindPrefabCache:

        /// <summary>
        /// Match an object to its prefab if this pool manages the GameObject.
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        private PrefabCache FindPrefabCache(GameObject obj)
        {
            //TODO - figure out the best way to match a gameobject to the cache pool.
            //as it stands this depends on the prefab being a shared instance across all scripts...
            //I am unsure if that's how unity works, and what the limitations are on that.
            //consider creating a system of relating equal prefabs

            //test if the object is the prefab in question
            int id = obj.GetInstanceID();
            if (_prefabToCache.ContainsKey(id)) return _prefabToCache[id];

            var controller = obj.FindComponent<SpawnedObjectController>();
            if (controller == null && controller.Pool != this) return null;

            id = controller.PrefabID;
            if (_prefabToCache.ContainsKey(id)) return _prefabToCache[id];
         
            return null;
        }

When you call one of the Spawn methods with a prefab. This tries to find the cache.

Thing is the ‘prefab’ passed in might not actually be the prefab. So it does a double test.

It first grabs the instanceid and checks if it matches any in the SpawnPool… if it does it returns that cache. But if it doesn’t, then the method continues on under the assumption maybe you handed it a cached instance. In which case it checks for a SpawnedObjectController, gets the prefab id from that, and spawns based on that.

If that’s not found… well then this clearly isn’t a managed GameObject, and null is returned.

And lastly there is the SignlaedSpawned Method:

        private void SignalSpawned(SpawnedObjectController cntrl)
        {
            this.gameObject.Broadcast<IOnSpawnHandler, SpawnedObjectController>(cntrl, (o, c) => o.OnSpawn(c));
            cntrl.gameObject.Broadcast<IOnSpawnHandler, SpawnedObjectController>(cntrl, (o, c) => o.OnSpawn(c));
        }

When we spawn an object we just call the IOnSpawnHandler on both the SpawnPool and the object being spawned. I technically use my own custom version of the Messaging System:
spacepuppy-unity-framework-3.0/SpacepuppyUnityFramework/Utils/Messaging.cs at master · lordofduct/spacepuppy-unity-framework-3.0 · GitHub

I have this because the Unity Messaging system ExecuteEvents has a Execute method (call the message on that GameObject), and a ‘ExecuteHierarchy’ (call the message up the hierarchy through the parents)… but not Broadcast version that sends the message downward through the children in the same manner that the older string version GameObject.BroadcastMessage does. So I created my own. Whatever… this is my relationship with Unity for the past 4 years. They always have 75% of the features, and I always have to tack on the extra bit… and then a year later they go “oh… we forgot that!” and they add it (I’m looking at you Coroutine & UnityEvent).

Anyways.

So how about that SpawnedObjectController:
spacepuppy-unity-framework-3.0/SPSpawn/Spawn/SpawnedObjectController.cs at master · lordofduct/spacepuppy-unity-framework-3.0 · GitHub

The SpawnedObjectController is not intended to be added by you at design/editor time. Rather the SpawnPool tacks this on at runtime. You can then check a GameObject for this to see if a SpawnPool created it, and if it did, use it to find out information about what SpawnPool created it.

Basically, we want to make the SpawnPool seamless.

You don’t have to go out of your way to know if the SpawnPool is caching instances. Instead you always use the SpawnPool.DefaultPool to Instantiate any and all objects, and if it HAPPENS to be considered a managed object, it’ll attempt to first use cached instances. Foregoing Object.Instantiate all together. This way you can write code that works with both without having to really think about it. SpawnPool does all the heavy lifting for you!

In here we have a few things.

Some Events:

        public event System.EventHandler OnSpawned;
        public event System.EventHandler OnDespawned;
        public event System.EventHandler OnKilled;

Fields:

        #region Fields

        [System.NonSerialized()]
        private SpawnPool _pool;
        [System.NonSerialized]
        private int _prefabId;
        [System.NonSerialized()]
        private string _sCacheName;
     
        [System.NonSerialized()]
        private bool _isSpawned;

        #endregion

_pool - the SpawnPool this object was spawned from
_prefabId - the prefab instance id of the object that this is a clone of
_sCacheName - the name of the cache it came from… if it came from one.
_isSpawned - is the object considered ‘active’ rather than ‘cached’.

Constructor/Destructor:

        #region CONSTRUCTOR

        internal void Init(SpawnPool pool, int prefabId)
        {
            _pool = pool;
            _prefabId = prefabId;
            _sCacheName = null;
        }

        /// <summary>
        /// Initialize with a reference to the pool that spawned this object. Include a cache name if this gameobject is cached, otherwise no cache name should be included.
        /// </summary>
        /// <param name="pool"></param>
        /// <param name="prefab">prefab this was spawned from</param>
        /// <param name="sCacheName"></param>
        internal void Init(SpawnPool pool, int prefabId, string sCacheName)
        {
            _pool = pool;
            _prefabId = prefabId;
            _sCacheName = sCacheName;
        }

        internal void DeInit()
        {
            _pool = null;
            _prefabId = 0;
            _sCacheName = null;
        }

        protected override void OnDestroy()
        {
            base.OnDestroy();

            if(!GameLoop.ApplicationClosing && _pool != null)
            {
                _pool.Purge(this);
            }
            if (this.OnKilled != null) this.OnKilled(this, System.EventArgs.Empty);
        }

        #endregion

Init - called by SpawnPool when first added to set these parameters.
DeInit - called by SpawnPool when being thrownout/purged, disassociating it from the prefab.
OnDestroy - if this thing got destroyed for whatever reason, make sure the SpawnPool gets purged of the instance.

Note these are ‘internal’ methods. They’re not really intended to be called by anyone except SpawnPool.

Methods:

        #region Methods

        /// <summary>
        /// This method ONLY called by SpawnPool
        /// </summary>
        internal void SetSpawned()
        {
            _isSpawned = true;
            this.gameObject.SetActive(true);
            if (this.OnSpawned != null) this.OnSpawned(this, System.EventArgs.Empty);
        }

        /// <summary>
        /// This method ONLY called by SpawnPool
        /// </summary>
        internal void SetDespawned()
        {
            _isSpawned = false;
            this.gameObject.SetActive(false);
            if (this.OnDespawned != null) this.OnDespawned(this, System.EventArgs.Empty);
        }

        public void Purge()
        {
            if (_pool != null) _pool.Purge(this);
        }

        public GameObject CloneObject(bool fromPrefab = false)
        {
            if (fromPrefab && _pool != null && _pool.Contains(_prefabId))
                return _pool.SpawnByPrefabId(_prefabId, this.transform.position, this.transform.rotation);
            else
                return _pool.Spawn(this.gameObject, this.transform.position, this.transform.rotation);
        }

        #endregion

SetSpawned - called by SpawnPool when its spawned and considered active.

SetDespawned - called by SpawnPool when it’s returned to the cache.

Purge - a public access point to purge a specific GameObject from its pool. Really it’s just a forward to the SpawnPool.Purge method.

CloneObject - make a copy of this object.

And finally the IKillable Interface:

        #region IKillableEntity Interface

        public bool IsDead
        {
            get { return !_isSpawned; }
        }

        public void Kill()
        {
            if (!_pool.Despawn(this))
            {
                ObjUtil.SmartDestroy(this.gameObject);
            }
            else
            {
                //TODO - need a cleaner way of doing this
                using (var lst = TempCollection.GetList<Rigidbody>())
                {
                    this.transform.GetComponentsInChildren<Rigidbody>(lst);
                    var e = lst.GetEnumerator();
                    while(e.MoveNext())
                    {
                        e.Current.velocity = Vector3.zero;
                        e.Current.angularVelocity = Vector3.zero;
                    }
                }
                using (var lst = TempCollection.GetList<Rigidbody2D>())
                {
                    this.transform.GetComponentsInChildren<Rigidbody2D>(lst);
                    var e = lst.GetEnumerator();
                    while (e.MoveNext())
                    {
                        e.Current.velocity = Vector2.zero;
                        e.Current.angularVelocity = 0f;
                    }
                }
            }
            if (this.OnKilled != null) this.OnKilled(this, System.EventArgs.Empty);
        }

            #endregion

OK… so now here’s the problem with that seamless usage thing.

We need to know if any object is cached to return it to the cache. This makes the whole ‘Object.Destroy’ moment a giant pain in the butt. You have to do this whole work around where you’re checking if the object contains a SpawnedObjectController, if it’s considered tracked, and so on and so forth. Something like:

var cntrl = objToDestroy.GetComponent<SpawnedObjectController>();
if(cntrl.Pool != null && cntrl.Pool.Despawn(cntrl))
{
    //do nothing, it was returned to the pool
}
else
{
    //not a cached instance
    Object.Destroy(objToDestroy);
}

This is annoying to do every time you want to destroy. This other work that gets involved really if you want it to be cleaner. It’s all just annoying.

So instead what I do is I have the ‘IKillableEntity’ interface:
spacepuppy-unity-framework-3.0/SpacepuppyUnityFramework/IKillableEntity.cs at master · lordofduct/spacepuppy-unity-framework-3.0 · GitHub

Now with this, any script that I want to be killable in some special way, I can attach a script that implements this interface. And now to kill it I can do something like this:

var killable = objToDestroy.GetComponent<IKillableEntity>();
if(killable != null)
    killable.Kill();
else
    Object.Destroy(objToDestroy);

And then with some extension methods I can streamline this even further:
spacepuppy-unity-framework-3.0/SpacepuppyUnityFramework/Utils/GameObjUtil.cs at master · lordofduct/spacepuppy-unity-framework-3.0 · GitHub

And now we can just say:

objToDestroy.Kill();

Boom… now if I want to destroy an object I just use that Kill extension method. And the framework will figure out if the object should just be Destroyed, or if it should be returned to a SpawnPool. Easy peasy.

Just like we no longer use Object.Instantiate, but use SpawnPool.DefaultPool.Spawn to create objects. We now use obj.Kill instead of Object.Destroy. Making the whole system seamless.

Now lastly, lets look at a Spawn Point:
spacepuppy-unity-framework-3.0/SPSpawn/Spawn/Events/i_Spawn.cs at master · lordofduct/spacepuppy-unity-framework-3.0 · GitHub

So here is a spawn point.

For starters you may notice it’s called ‘i_Spawn’. And this naming convention might leave you scratching your head. I’ll explain.

See a few years ago my best friend/partner in crime who I make video games with wanted a way to easily prototype game design ideas. Thing is… he’s an artist, and a very skilled one at that. He worked at Vicarious Visions for years working on games like Marvel Ultimate Alliance 2, Skylanders, Transformers (DS), Guitar Hero, and much more. There’s a reason I like working with him!

Problem is… he’s an artist first. A designer second (and a good one at that). And no where near a programmer!

In the same respect I’m not an artist!

So basically whenever he wanted to protype ideas, he had to work hand in hand with me so I can write any and all code he needed. But really… he kept needing the same basic components just organized differently. And he could understand that level of things.

So… I came up with this system where he could use GameObjects as logical nodes, and then wire those nodes together through events to do things.

So say he had a GameObject with a collider, Rigidbody, and ‘t_OnEnterTrigger’ script on it. He could react to colliders being entered. t_OnEnterTrigger than just had a pointer to another GameObject, and it would call a predefined ‘Trigger’ method on those objects. So maybe we had an ‘i_PauseGame’ script that when triggered… it set the timescale to 0.

So now through a visual interface he could slap together lego style basic logical blocks.

Now… you might be saying “Isn’t this UnityEvent?”

Yes… yes it is. Well… sort of.

  1. UnityEvent didn’t exist when I designed this.
  2. UnityEvent requires referencing a specific component and calling a specific function on that component.

My artist doesn’t know what a function is… he doesn’t want to have to know the difference between the multiple methods that exist on any given script. And he doesn’t like clicking a lot.

Why can’t he just say “that GameObject over there, make it do the thing I attached the script on it to do!”

Note… this ‘ease of use’ actually came from his time working at VV. For Marvel Ultimate Alliance 2 he was on the “demolition crew”. They were the guys who made breakable objects in the game. You know… barrels that blow up, chairs that break, walls that crumble… that sort. Well the engineers there gave them very simple components that they just dropped on objects in the scene, and just pointed at other objects. This simple system just worked.

Here’s an example… this is an easter egg he actually hid in MUA2, he hid a bar of soap in the wall. You should have seen how excited he was when these guys found his easter egg:

https://www.youtube.com/watch?v=ste9CLq3Qk8

Anyways… I’m not saying I ripped off VV’s engineers… considering that I’d never even seen their engine, let alone the code under the hood. My point is that I wasn’t designing a tool for myself, I was designing a tool based on guidelines from my artist who had very particular demands!

So as a result we ended up with what we called the ‘T & I System’. In which ‘t’s trigger’ and ‘i’s respond’. The ‘i’ is a weird name… but the logic is… well… when you read class. what does an ‘i_Spawn’ do? Well… “I Spawn”, it spawns. “I Destroy”, “I Trigger”, “I Pause”, “I Move”, etc, etc etc.

Upon using this thing, it turned out to be stupid useful! (just like UnityEvent turned out to be hella awesome when it came out)

We literally use it to wire up probaly 90% of our games, it’s that freaking powerful.

But as a result I figured out I was going to need some more fine tuned access as well. The ability to call specific functions on a specific component (just like UnityEvent), send messages, etc. So we resulted in these options:

Then UnityEvent was released… I looked into it and was like “huh, it’s our T & I system… or atleast half of it”. But we stuck with ours due to the extended features, and because my artist understands it more.

I just this last week changed the name to SPEvent (from the original Trigger/Triggerable name which was confusing).

So now I create general purpose scripts for all my stuff for my artist to then use in game. And I follow this naming system so that he knows what’s what… he only ever searches for scripts that start with t_ or i_, and ignores all others.

Thusly, i_Spawn is called i_Spawn.

Lets break this bad boy down:
spacepuppy-unity-framework-3.0/SPSpawn/Spawn/Events/i_Spawn.cs at master · lordofduct/spacepuppy-unity-framework-3.0 · GitHub

For starters we have our fields:

        #region Fields

        [SerializeField()]
        [Tooltip("If left empty the default SpawnPool will be used instead.")]
        private SpawnPool _spawnPool;
     
        [SerializeField]
        private Transform _spawnedObjectParent;

        [SerializeField()]
        [WeightedValueCollection("Weight", "Prefab")]
        [Tooltip("Objects available for spawning. When spawn is called with no arguments a prefab is selected at random, unless a ISpawnSelector is available on the SpawnPoint.")]
        private List<PrefabEntry> _prefabs;

        [SerializeField()]
        private SPEvent _onSpawnedObject = new SPEvent(TRG_ONSPAWNED);

        #endregion

_spawnPool - the SpawnPool to use. Note, if it’s left blank we’ll use the DefaultPool. Like I said before, if the object isn’t considered cached, it just instantiates untracked versions. Creating a seamless system.

_spawnedObjectParent - a Transform to use as the parent, if any. This is useful for tracking objects. You can spawn all objects to be a child of the spawn point so you can know where they came from.

_prefabs - the available prefabs to spawn. You might be wondering why a List and what is that ‘WeightedValueCollection’ attribute. My artist, he likes randomness a LOT. So anything that does stuff like this… i_PlayAnimation, i_PlaySoundEffect, etc. I always have a collection on it. If there’s 1 entry than it just spawns that 1. If there’s more than one, it picks one at random. The ‘weight’ is just so you can set different probabilities to each entry. ‘WeightedValueCollection’ is a PropertyDrawer that facilitates the inspector’s visuals for this.

onSpawnedObject - there’s an SPEvent, it allows daisy chaining. A t might trigger this, and this can allow you to react to the event of a spawn.

From there there are the properties, public getters for those fields.

Next our Methods:

        #region Methods

        public GameObject Spawn()
        {
            if (!this.CanTrigger) return null;

            if (_prefabs == null || _prefabs.Count == 0) return null;

            if(_prefabs.Count == 1)
            {
                return this.Spawn(_prefabs[0].Prefab);
            }
            else
            {
                return this.Spawn(_prefabs.PickRandom((o) => o.Weight).Prefab);
            }
        }

        private GameObject Spawn(GameObject prefab)
        {
            if (prefab == null) return null;

            var pool = _spawnPool != null ? _spawnPool : SpawnPool.DefaultPool;
            var go = pool.Spawn(prefab, this.transform.position, this.transform.rotation, _spawnedObjectParent);
         
            if (_onSpawnedObject != null && _onSpawnedObject.Count > 0)
                _onSpawnedObject.ActivateTrigger(this, go);

            return go;
        }

        public GameObject Spawn(int index)
        {
            if (!this.enabled) return null;

            if (_prefabs == null || index < 0 || index >= _prefabs.Count) return null;
            return this.Spawn(_prefabs[index].Prefab);
        }

        public GameObject Spawn(string name)
        {
            if (!this.enabled) return null;

            if (_prefabs == null) return null;
            for (int i = 0; i < _prefabs.Count; i++)
            {
                if (_prefabs[i].Prefab != null && _prefabs[i].Prefab.name == name) return this.Spawn(_prefabs[i].Prefab);
            }
            return null;
        }

        #endregion

Just a simple Spawn interface (note it implement ISpawnPoint with the Spawn() method). It does the whole picking a random entry from the list and calling the SpawnPool.

ITriggerable Interface

        #region ITriggerable Interface

        public override bool CanTrigger
        {
            get { return base.CanTrigger && _prefabs != null && _prefabs.Count > 0; }
        }

        public override bool Trigger(object sender, object arg)
        {
            if (!this.CanTrigger) return false;

            if (arg is string)
            {
                return this.Spawn(arg as string) != null;
            }
            else if (ConvertUtil.ValueIsNumericType(arg))
            {
                return this.Spawn(ConvertUtil.ToInt(arg)) != null;
            }
            else
            {
                return this.Spawn() != null;
            }
        }

        #endregion

This is the implementation of the “T & I System”. This is what allows an SPEvent to easily trigger it.

Note that SPEvent can send an arg along. Some scripts might do this. For example the i_Spawn.OnSpawnedObject event includes the object that was spawned. So that way the receiving script can do something with it.

There’s then the IObservableTarget Interface… this gets into some stuff we don’t need to get into. It’s just an extension of the SPEvent so you can monitor triggers. You can have a t_ that watches other t’s and i’s for if they do stuff… and then react to that.

ISpawnPoint Interface, implements ISpawnPoint.

And lastly Special Types, it’s just a struct for the Weight/Prefab pair that is stored in the _prefabs list.

So yeah…

I hope that was informative for you.

If not… whatever. I type really fast, so it wasn’t much hassle to ramble on like that.

2 Likes

Yes! You guys are hitting the nail on the head here.

just make sure NOT to mix GetHashCode() and GetInstanceID(), they are no longer the same in standalone builds.

1 Like

Suppose you have a prefab of a gameObject. And you have 10 instances of the prefab in the scene. And prefab has a script which controls the prefab. Same with all the instances. Suppose we select one instance in the scene (Play Mode) and we want to use WASD to move it, or something like that. We can use GetInstanceID() as a unique identifier to only move the selected item. This is just a made up example. Other good uses are definitely there

Holy necro, Batman!

And no, that’s not a good use of GetInstanceID(). Whatever script handles “selection” would simply keep a reference to the selected object.

4 Likes

It’s alive! lightning flash OK no…

Every object instance ID is not static, instance IDs are changed each time you reload, open a scene, etc.

1 Like