Search Unity

GetInstanceID - What is it really for?

Discussion in 'Scripting' started by meat5000, Jan 27, 2015.

  1. meat5000

    meat5000

    Joined:
    Mar 5, 2013
    Posts:
    118
    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. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    8,388
    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?
     
    DonLoquacious likes this.
  3. meat5000

    meat5000

    Joined:
    Mar 5, 2013
    Posts:
    118
    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 :D 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.
     
  4. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    8,388
    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. ;)

    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. meat5000

    meat5000

    Joined:
    Mar 5, 2013
    Posts:
    118
    I like your analogy :)

    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.
     
  6. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,466
    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.
     
    JoeStrout likes this.
  7. meat5000

    meat5000

    Joined:
    Mar 5, 2013
    Posts:
    118
    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 :p I would get there though.
     
  8. petersvp

    petersvp

    Joined:
    Dec 20, 2013
    Posts:
    42
    Guys..... This one is extremely useful for Edit Mode scripts to detect being Instantiated from prefab OR duplicated with Ctrl+D!
     
  9. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,821
    I mean... interesting necro.

    I will say, I definitely use GetInstanceID for hashing.
     
  10. DoenitZ

    DoenitZ

    Joined:
    Oct 23, 2012
    Posts:
    16
    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!

    Code (CSharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5.  
    6. public class ObjectPooler : MonoBehaviour {
    7.  
    8.     [System.Serializable]
    9.     public class ObjectPoolItem {
    10.         public GameObject objectToPool;
    11.         public int amountToPool;
    12.         public bool shouldExpand = true;
    13.         public Transform parent;
    14.     }
    15.  
    16.     //This internal class was created just to visualize in the editor if needed
    17.     //the pools created. I find it is best to actually hide the variable
    18.     //It can be easily replaced by a List<List<GameObject>> object
    19.     [System.Serializable]
    20.     public class ListOfObjects {
    21.         public List<GameObject> GOList;
    22.         public ListOfObjects() {
    23.             GOList = new List<GameObject>();
    24.         }
    25.     }
    26.  
    27.     public static ObjectPooler SharedInstance;
    28.  
    29.     //List of items to be pooled
    30.     public List<ObjectPoolItem> itemsToPool;
    31.  
    32.     //The actual objects in the different pools
    33.     [HideInInspector]
    34.     public List<ListOfObjects> pooledObjects;
    35.     //An internal array used as a key list to access the different pools and avoid duplicate pool lists
    36.     List<int> objectID = new List<int>();
    37.  
    38.     /// <summary>
    39.     /// The SharedInstance will allow access to the pool without using external references
    40.     /// Just use it as "ObjectPooler.SharedInstance.GetPooledObject(PREFAB)
    41.     /// The prefab should be the object to pool
    42.     /// </summary>
    43.     void Awake() {
    44.         SharedInstance = this;
    45.     }
    46.  
    47.     /// <summary>
    48.     /// If we put some items on the editor, then the Start method will create pools for those
    49.     /// </summary>
    50.     void Start() {
    51.         if (pooledObjects == null) {
    52.             pooledObjects = new List<ListOfObjects>();
    53.         }
    54.         foreach (ObjectPoolItem item in itemsToPool) {
    55.             if (AddIDToList(item.objectToPool)) {
    56.  
    57.                 ListOfObjects listOfObjects = new ListOfObjects();
    58.                 for (int i = 0; i < item.amountToPool; i++) {
    59.                     GameObject obj = (GameObject)Instantiate(item.objectToPool);
    60.  
    61.                     if (item.objectToPool.transform.parent != null) {
    62.                         obj.transform.SetParent(item.objectToPool.transform.parent);
    63.                     } else {
    64.                         obj.transform.SetParent(this.gameObject.transform);
    65.                     }
    66.                     item.parent = obj.transform.parent;
    67.  
    68.                     obj.SetActive(false);
    69.                     listOfObjects.GOList.Add(obj);
    70.  
    71.                 }
    72.                 pooledObjects.Add(listOfObjects);
    73.             }
    74.         }
    75.     }
    76.     /// <summary>
    77.     /// Helper method to avoid duplicated pools. If the Instance ID was already added to a pool, nothing is done
    78.     /// </summary>
    79.     /// <param name="GO"></param>
    80.     /// <returns></returns>
    81.     private bool AddIDToList(GameObject GO) {
    82.  
    83.         foreach (int currentID in objectID) {
    84.             if (GO.GetInstanceID() == currentID) {
    85.                 return false;
    86.             }
    87.         }
    88.         objectID.Add(GO.GetInstanceID());
    89.         return true;
    90.     }
    91.     /// <summary>
    92.     /// Used to instantiate an object pool by code when needed
    93.     /// </summary>
    94.     /// <param name="objectToPool"> The prefab, or GameObject to pool. A pool of objects using the instance Id as a key will be created</param>
    95.     /// <param name="amount"> how many starting objects do you need created</param>
    96.     /// <param name="shouldResize"> Is this a resizable pool? </param>
    97.     /// <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>
    98.     public void CreateObjectPool(GameObject objectToPool, int amount, bool shouldResize, GameObject objectParent = null) {
    99.  
    100.         if (AddIDToList(objectToPool)) {
    101.             ObjectPoolItem item = new ObjectPoolItem() {
    102.                 shouldExpand = shouldResize,
    103.                 amountToPool = amount,
    104.                 objectToPool = objectToPool
    105.             };
    106.  
    107.             if (pooledObjects == null) {
    108.                 pooledObjects = new List<ListOfObjects>();
    109.             }
    110.  
    111.             if (itemsToPool == null) {
    112.                 itemsToPool = new List<ObjectPoolItem>();
    113.             }
    114.  
    115.             itemsToPool.Add(item);
    116.             ListOfObjects list = new ListOfObjects();
    117.  
    118.             for (int i = 0; i < item.amountToPool; i++) {
    119.                 GameObject obj = (GameObject)Instantiate(item.objectToPool);
    120.                 if (objectParent != null) {
    121.                     obj.transform.SetParent(objectParent.gameObject.transform);
    122.                 } else {
    123.                     obj.transform.SetParent(this.gameObject.transform);
    124.                 }
    125.  
    126.                 item.parent = obj.transform.parent;
    127.                 obj.SetActive(false);
    128.                 list.GOList.Add(obj);
    129.             }
    130.             pooledObjects.Add(list);
    131.         }
    132.     }
    133.     /// <summary>
    134.     /// This will return the object in the pool, using a pool created with a GetInstanceID key.
    135.     /// Maybe a dictionary is better? Still, prefered to used a double list scheme
    136.     /// </summary>
    137.     /// <param name="prefab"></param>
    138.     /// <returns></returns>
    139.     public GameObject GetPooledObject(GameObject prefab) {
    140.         int currentID = prefab.GetInstanceID();
    141.         int poolIndex = objectID.IndexOf(currentID);
    142.  
    143.         if (poolIndex != -1) {
    144.             for (int i = 0; i < pooledObjects[poolIndex].GOList.Count; i++) {
    145.                 if (!pooledObjects[poolIndex].GOList[i].activeInHierarchy) {
    146.                     return pooledObjects[poolIndex].GOList[i];
    147.                 }
    148.             }
    149.             if (itemsToPool[poolIndex].shouldExpand) {
    150.                 GameObject obj = (GameObject)Instantiate(itemsToPool[poolIndex].objectToPool);
    151.  
    152.                 obj.transform.SetParent(itemsToPool[poolIndex].parent);
    153.  
    154.                 obj.SetActive(false);
    155.                 pooledObjects[poolIndex].GOList.Add(obj);
    156.                 return obj;
    157.             }
    158.         }
    159.         return null;
    160.     }
    161.  
    162.     /// <summary>
    163.     /// Parameters for a timed game object disabler.
    164.     /// </summary>
    165.     class Parameters {
    166.         public GameObject go;
    167.         public float time;
    168.     }
    169.  
    170.     /// <summary>
    171.     /// Use this instead of a "destroyAfterTime".
    172.     /// </summary>
    173.     /// <param name="go"></param>
    174.     /// <param name="time"></param>
    175.     public void ReturnToPoolTimed(GameObject go, float time) {
    176.         Parameters parameters = new Parameters() {
    177.             go = go,
    178.             time = time
    179.         };
    180.         StartCoroutine("disableGameObject", parameters);
    181.     }
    182.  
    183.     IEnumerator DisableGameObject(Parameters parameters) {
    184.         yield return new WaitForSeconds(parameters.time);
    185.         if (parameters.go != null) {
    186.             parameters.go.SetActive(false);
    187.         }
    188.     }
    189. }
    190.  
     
    Last edited: Jan 5, 2018
    meat5000 likes this.
  11. DoenitZ

    DoenitZ

    Joined:
    Oct 23, 2012
    Posts:
    16
    meat5000 likes this.
  12. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,821
    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:
    Code (csharp):
    1.  
    2.     /// <summary>
    3.     ///   <para>Returns the instance id of the object.</para>
    4.     /// </summary>
    5.     [SecuritySafeCritical]
    6.     public int GetInstanceID()
    7.     {
    8.       this.EnsureRunningOnMainThread();
    9.       return this.m_InstanceID;
    10.     }
    11.  
    12.     public override int GetHashCode()
    13.     {
    14.       return this.m_InstanceID;
    15.     }
    16.  
    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:
    https://github.com/lordofduct/space...llections/ObjectInstanceIDEqualityComparer.cs

    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:
    Code (csharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5.  
    6. public class ObjectPooler : MonoBehaviour
    7. {
    8.  
    9.     #region Singleton Interface
    10.  
    11.     private static ObjectPooler _instance;
    12.     public static ObjectPooler SharedInstance
    13.     {
    14.         get { return _instance; }
    15.     }
    16.  
    17.     /// <summary>
    18.     /// The SharedInstance will allow access to the pool without using external references
    19.     /// Just use it as "ObjectPooler.SharedInstance.GetPooledObject(PREFAB)
    20.     /// The prefab should be the object to pool
    21.     /// </summary>
    22.     void Awake()
    23.     {
    24.         if(_instance != null && !object.ReferenceEquals(_instance, this))
    25.         {
    26.             Debug.LogWarning("ObjectPooler Instance already exists");
    27.             Object.Destroy(this);
    28.             return;
    29.         }
    30.         _instance = this;
    31.     }
    32.  
    33.     #endregion
    34.  
    35.     #region Fields
    36.  
    37.     /// <summary>
    38.     /// This is an editor only collection of entries.
    39.     /// 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.
    40.     /// Instead lets use the 'ListOfObjects' class to track this information once the game starts.
    41.     /// </summary>
    42.     [SerializeField]
    43.     private ObjectPoolItem[] _itemsToPool;
    44.  
    45.     //The actual objects in the different pools
    46.     [System.NonSerialized]
    47.     private Dictionary<GameObject, ListOfObjects> _pools = new Dictionary<GameObject, ListOfObjects>();
    48.  
    49.     #endregion
    50.  
    51.     #region CONSTRUCTOR
    52.  
    53.     void Start()
    54.     {
    55.         for (int i = 0; i < _itemsToPool.Length; i++)
    56.         {
    57.             var item = _itemsToPool[i];
    58.             if (!_pools.ContainsKey(item.objectToPool))
    59.             {
    60.                 ListOfObjects listOfObjects = new ListOfObjects()
    61.                 {
    62.                     Info = item
    63.                 };
    64.                 for (int j = 0; j < item.amountToPool; j++)
    65.                 {
    66.                     GameObject obj = (GameObject)Instantiate(item.objectToPool);
    67.  
    68.                     if (item.objectToPool.transform.parent != null)
    69.                     {
    70.                         obj.transform.SetParent(item.objectToPool.transform.parent);
    71.                     }
    72.                     else
    73.                     {
    74.                         obj.transform.SetParent(this.gameObject.transform);
    75.                     }
    76.                     item.parent = obj.transform.parent;
    77.  
    78.                     obj.SetActive(false);
    79.                     listOfObjects.GOList.Add(obj);
    80.  
    81.                 }
    82.                 _pools.Add(item.objectToPool, listOfObjects);
    83.             }
    84.         }
    85.     }
    86.  
    87.     #endregion
    88.  
    89.     #region Methods
    90.  
    91.     /// <summary>
    92.     /// Used to instantiate an object pool by code when needed
    93.     /// </summary>
    94.     /// <param name="objectToPool"> The prefab, or GameObject to pool. A pool of objects using the instance Id as a key will be created</param>
    95.     /// <param name="amount"> how many starting objects do you need created</param>
    96.     /// <param name="shouldResize"> Is this a resizable pool? </param>
    97.     /// <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>
    98.     public void CreateObjectPool(GameObject objectToPool, int amount, bool shouldResize, GameObject objectParent = null)
    99.     {
    100.         if (!_pools.ContainsKey(objectToPool))
    101.         {
    102.             ListOfObjects list = new ListOfObjects()
    103.             {
    104.                 Info = new ObjectPoolItem()
    105.                 {
    106.                     shouldExpand = shouldResize,
    107.                     amountToPool = amount,
    108.                     objectToPool = objectToPool
    109.                 }
    110.             };
    111.  
    112.             for (int i = 0; i < list.Info.amountToPool; i++)
    113.             {
    114.                 GameObject obj = (GameObject)Instantiate(list.Info.objectToPool);
    115.                 if (objectParent != null)
    116.                 {
    117.                     obj.transform.SetParent(objectParent.gameObject.transform);
    118.                 }
    119.                 else
    120.                 {
    121.                     obj.transform.SetParent(this.gameObject.transform);
    122.                 }
    123.  
    124.                 list.Info.parent = obj.transform.parent;
    125.                 obj.SetActive(false);
    126.                 list.GOList.Add(obj);
    127.             }
    128.             _pools.Add(objectToPool, list);
    129.         }
    130.     }
    131.  
    132.     /// <summary>
    133.     /// This will return the object in the pool, using a pool created with a GetInstanceID key.
    134.     /// Maybe a dictionary is better? Still, prefered to used a double list scheme
    135.     /// </summary>
    136.     /// <param name="prefab"></param>
    137.     /// <returns></returns>
    138.     public GameObject GetPooledObject(GameObject prefab)
    139.     {
    140.         ListOfObjects list;
    141.         if (!_pools.TryGetValue(prefab, out list)) return null;
    142.  
    143.         for (int i = 0; i < list.GOList.Count; i++)
    144.         {
    145.             if (!list.GOList[i].activeInHierarchy)
    146.             {
    147.                 return list.GOList[i];
    148.             }
    149.         }
    150.         if(list.Info != null && list.Info.shouldExpand)
    151.         {
    152.             GameObject obj = (GameObject)Instantiate(list.Info.objectToPool);
    153.  
    154.             obj.transform.SetParent(list.Info.parent);
    155.  
    156.             obj.SetActive(false);
    157.             list.GOList.Add(obj);
    158.             return obj;
    159.         }
    160.         return null;
    161.     }
    162.     /// <summary>
    163.     /// Use this instead of a "destroyAfterTime".
    164.     /// </summary>
    165.     /// <param name="go"></param>
    166.     /// <param name="time"></param>
    167.     public void ReturnToPoolTimed(GameObject go, float time)
    168.     {
    169.         if (go == null) return;
    170.  
    171.         if (time > 0f)
    172.             this.StartCoroutine(this.DisableGameObject(go, time));
    173.         else
    174.             go.SetActive(false);
    175.     }
    176.  
    177.     IEnumerator DisableGameObject(GameObject go, float time)
    178.     {
    179.         yield return new WaitForSeconds(time);
    180.         if (go != null)
    181.         {
    182.             go.SetActive(false);
    183.         }
    184.     }
    185.     #endregion
    186.  
    187.     #region Special Types
    188.  
    189.     [System.Serializable]
    190.     public class ObjectPoolItem
    191.     {
    192.         public GameObject objectToPool;
    193.         public int amountToPool;
    194.         public bool shouldExpand = true;
    195.         public Transform parent;
    196.     }
    197.  
    198.     /// <summary>
    199.     /// Internal structure of pools... this should not have been serializable since it never gets serialized!
    200.     /// </summary>
    201.     private class ListOfObjects
    202.     {
    203.         public List<GameObject> GOList = new List<GameObject>();
    204.         public ObjectPoolItem Info;
    205.     }
    206.  
    207.     #endregion
    208.  
    209. }
    210.  
    Although... there's a LOT more changes I would do to that class if it were up to me...
     
    Last edited: Jan 5, 2018
    meat5000, JoeStrout and DonLoquacious like this.
  13. DoenitZ

    DoenitZ

    Joined:
    Oct 23, 2012
    Posts:
    16
    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 :)

    Thank you in any case!
     
  14. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,821
    Well.... how about this.

    This last week I've been porting/refactoring my framework of code over to Unity 2017.3.
    https://github.com/lordofduct/spacepuppy-unity-framework-3.0

    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:
    https://github.com/lordofduct/spacepuppy-unity-framework-3.0/blob/master/SPSpawn/Spawn/SpawnPool.cs

    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.
     
  15. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,821
    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:
    https://github.com/lordofduct/spacepuppy-unity-framework-3.0/tree/master/SPSpawn/Spawn

    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):
    https://github.com/lordofduct/spacepuppy-unity-framework-3.0/blob/master/SPSpawn/Spawn/SpawnPool.cs
    (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':
    Code (csharp):
    1.  
    2.         #region Static Multiton Interface
    3.  
    4.         public const string DEFAULT_SPAWNPOOL_NAME = "Spacepuppy.PrimarySpawnPool";
    5.  
    6.         private static SpawnPool _defaultPool;
    7.         private static HashSet<SpawnPool> _pools = new HashSet<SpawnPool>();
    8.  
    9.         public static SpawnPool DefaultPool
    10.         {
    11.             get
    12.             {
    13.                 if (_defaultPool == null) CreatePrimaryPool();
    14.                 return _defaultPool;
    15.             }
    16.         }
    17.      
    18.         public static int PoolCount { get { return _pools.Count; } }
    19.  
    20.         public static IEnumerable<SpawnPool> AllSpawnPools
    21.         {
    22.             get { return _pools; }
    23.         }
    24.  
    25.         public static void CreatePrimaryPool()
    26.         {
    27.             if (PrimaryPoolExists) return;
    28.  
    29.             var go = new GameObject(DEFAULT_SPAWNPOOL_NAME);
    30.             _defaultPool = go.AddComponent<SpawnPool>();
    31.         }
    32.  
    33.         public static bool PrimaryPoolExists
    34.         {
    35.             get
    36.             {
    37.                 if (_defaultPool != null) return true;
    38.  
    39.                 _defaultPool = null;
    40.                 var point = (from p in GameObject.FindObjectsOfType<SpawnPool>() where p.name == DEFAULT_SPAWNPOOL_NAME select p).FirstOrDefault();
    41.                 if (!object.ReferenceEquals(point, null))
    42.                 {
    43.                     _defaultPool = point;
    44.                     return true;
    45.                 }
    46.                 else
    47.                 {
    48.                     return false;
    49.                 }
    50.             }
    51.         }
    52.  
    53.         #endregion
    54.  
    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:
    Code (csharp):
    1.  
    2.         #region Fields
    3.  
    4.         [SerializeField()]
    5.         [ReorderableArray(DrawElementAtBottom = true, ChildPropertyToDrawAsElementLabel = "ItemName", ChildPropertyToDrawAsElementEntry = "_prefab")]
    6.         private List<PrefabCache> _registeredPrefabs = new List<PrefabCache>();
    7.  
    8.         [System.NonSerialized()]
    9.         private Dictionary<int, PrefabCache> _prefabToCache = new Dictionary<int, PrefabCache>();
    10.  
    11.         #endregion
    12.  
    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:
    Code (csharp):
    1.  
    2.         #region CONSTRUCTOR
    3.  
    4.         protected override void Awake()
    5.         {
    6.             base.Awake();
    7.  
    8.             _pools.Add(this);
    9.             if(this.name == DEFAULT_SPAWNPOOL_NAME && _defaultPool == null)
    10.             {
    11.                 _defaultPool = this;
    12.             }
    13.         }
    14.  
    15.         protected override void Start()
    16.         {
    17.             base.Start();
    18.  
    19.             var e = _registeredPrefabs.GetEnumerator();
    20.             while(e.MoveNext())
    21.             {
    22.                 if (e.Current.Prefab == null) continue;
    23.  
    24.                 e.Current.Load();
    25.                 _prefabToCache[e.Current.PrefabID] = e.Current;
    26.             }
    27.         }
    28.  
    29.         protected override void OnDestroy()
    30.         {
    31.             base.OnDestroy();
    32.  
    33.             if (object.ReferenceEquals(this, _defaultPool))
    34.             {
    35.                 _defaultPool = null;
    36.             }
    37.             _pools.Remove(this);
    38.  
    39.             var e = _registeredPrefabs.GetEnumerator();
    40.             while(e.MoveNext())
    41.             {
    42.                 e.Current.Clear();
    43.             }
    44.         }
    45.  
    46.         #endregion
    47.  
    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:
    Code (csharp):
    1.  
    2.         #region Properties
    3.  
    4.         private string _cachedName;
    5.         public new string name
    6.         {
    7.             get
    8.             {
    9.                 if (_cachedName == null) _cachedName = this.gameObject.name;
    10.                 return _cachedName;
    11.             }
    12.             set
    13.             {
    14.                 this.gameObject.name = value;
    15.                 _cachedName = value;
    16.             }
    17.         }
    18.  
    19.         #endregion
    20.  
    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:
    Code (csharp):
    1.  
    2.         public IPrefabCache Register(GameObject prefab, string sname, int cacheSize = 0, int resizeBuffer = 1, int limitAmount = 1)
    3.         {
    4.             if (object.ReferenceEquals(prefab, null)) throw new System.ArgumentNullException("prefab");
    5.             if (_prefabToCache.ContainsKey(prefab.GetInstanceID())) throw new System.ArgumentException("Already manages prefab.", "prefab");
    6.  
    7.             var cache = new PrefabCache(prefab, sname)
    8.             {
    9.                 CacheSize = cacheSize,
    10.                 ResizeBuffer = resizeBuffer,
    11.                 LimitAmount = limitAmount
    12.             };
    13.  
    14.             _registeredPrefabs.Add(cache);
    15.             _prefabToCache[cache.PrefabID] = cache;
    16.             cache.Load();
    17.             return cache;
    18.         }
    19.  
    20.         public bool UnRegister(GameObject prefab)
    21.         {
    22.             var cache = this.FindPrefabCache(prefab);
    23.             if (cache == null) return false;
    24.  
    25.             return this.UnRegister(cache);
    26.         }
    27.  
    28.         public bool UnRegister(int prefabId)
    29.         {
    30.             PrefabCache cache;
    31.             if (!_prefabToCache.TryGetValue(prefabId, out cache)) return false;
    32.  
    33.             return this.UnRegister(cache);
    34.         }
    35.  
    36.         public bool UnRegister(IPrefabCache cache)
    37.         {
    38.             var obj = cache as PrefabCache;
    39.             if (obj == null) return false;
    40.             if (obj.Owner != this) return false;
    41.  
    42.             obj.Clear();
    43.             _registeredPrefabs.Remove(obj);
    44.             _prefabToCache.Remove(obj.PrefabID);
    45.             return true;
    46.         }
    47.  
    48.         public bool Contains(int prefabId)
    49.         {
    50.             return _prefabToCache.ContainsKey(prefabId);
    51.         }
    52.  
    53.         public bool Contains(string sname)
    54.         {
    55.             var e = _registeredPrefabs.GetEnumerator();
    56.             while (e.MoveNext())
    57.             {
    58.                 if (e.Current.Name == sname)
    59.                 {
    60.                     return true;
    61.                 }
    62.             }
    63.             return false;
    64.         }
    65.  
    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:
    Code (csharp):
    1.             if (object.ReferenceEquals(prefab, null)) throw new System.ArgumentNullException("prefab");
    2.             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:
    Code (csharp):
    1.             var cache = new PrefabCache(prefab, sname)
    2.             {
    3.                 CacheSize = cacheSize,
    4.                 ResizeBuffer = resizeBuffer,
    5.                 LimitAmount = limitAmount
    6.             };
    And we add it to our collections and load it:
    Code (csharp):
    1.             _registeredPrefabs.Add(cache);
    2.             _prefabToCache[cache.PrefabID] = cache;
    3.             cache.Load();
    4.             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:
    Code (csharp):
    1.  
    2.             internal void Load()
    3.             {
    4.                 this.Clear();
    5.                 if (_prefab == null) return;
    6.  
    7.                 for(int i = 0; i < this.CacheSize; i++)
    8.                 {
    9.                     _instances.Add(this.CreateCachedInstance());
    10.                 }
    11.             }
    12.  
    13.             private SpawnedObjectController CreateCachedInstance()
    14.             {
    15.                 var obj = Object.Instantiate(this.Prefab, Vector3.zero, Quaternion.identity);
    16.                 obj.name = _itemName + "(CacheInstance)";
    17.                 var cntrl = obj.AddOrGetComponent<SpawnedObjectController>();
    18.                 cntrl.Init(_owner, this.Prefab.GetInstanceID(), _itemName);
    19.  
    20.                 obj.transform.parent = _owner.transform;
    21.                 obj.transform.localPosition = Vector3.zero;
    22.                 obj.transform.rotation = Quaternion.identity;
    23.              
    24.                 obj.SetActive(false);
    25.  
    26.                 return cntrl;
    27.             }
    28.  
    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:
    Code (csharp):
    1.  
    2.             #region Fields
    3.  
    4.             [SerializeField]
    5.             private string _itemName;
    6.             [SerializeField]
    7.             private GameObject _prefab;
    8.          
    9.             [Tooltip("The starting CacheSize.")]
    10.             public int CacheSize = 0;
    11.             [Tooltip("How much should the cache resize by if an empty/used cache is spawned from.")]
    12.             public int ResizeBuffer = 1;
    13.             [Tooltip("The maximum number of instances allowed to be cached, 0 or less means infinite.")]
    14.             public int LimitAmount = 0;
    15.  
    16.             [System.NonSerialized()]
    17.             private SpawnPool _owner;
    18.             [System.NonSerialized()]
    19.             private HashSet<SpawnedObjectController> _instances = new HashSet<SpawnedObjectController>(com.spacepuppy.Collections.ObjectReferenceEqualityComparer<SpawnedObjectController>.Default);
    20.             [System.NonSerialized()]
    21.             private HashSet<SpawnedObjectController> _activeInstances = new HashSet<SpawnedObjectController>(com.spacepuppy.Collections.ObjectReferenceEqualityComparer<SpawnedObjectController>.Default);
    22.  
    23.             #endregion
    24.  
    _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:
    Code (csharp):
    1.  
    2.             internal SpawnedObjectController Spawn(Vector3 pos, Quaternion rot, Transform par)
    3.             {
    4.                 if(_instances.Count == 0)
    5.                 {
    6.                     int cnt = this.Count;
    7.                     int newSize = cnt + this.ResizeBuffer;
    8.                     if (this.LimitAmount > 0) newSize = Mathf.Min(newSize, this.LimitAmount);
    9.  
    10.                     if(newSize > cnt)
    11.                     {
    12.                         for(int i = cnt; i < newSize; i++)
    13.                         {
    14.                             _instances.Add(this.CreateCachedInstance());
    15.                         }
    16.                     }
    17.                 }
    18.  
    19.                 if(_instances.Count > 0)
    20.                 {
    21.                     var cntrl = _instances.Pop();
    22.  
    23.                     _activeInstances.Add(cntrl);
    24.  
    25.                     cntrl.transform.parent = par;
    26.                     cntrl.transform.position = pos;
    27.                     cntrl.transform.rotation = rot;
    28.                     cntrl.SetSpawned();
    29.  
    30.                     return cntrl;
    31.                 }
    32.                 else
    33.                 {
    34.                     var obj = Object.Instantiate(this.Prefab, pos, rot, par);
    35.                     if (obj != null)
    36.                     {
    37.                         var controller = obj.AddOrGetComponent<SpawnedObjectController>();
    38.                         controller.Init(_owner, this.Prefab.GetInstanceID());
    39.                         controller.SetSpawned();
    40.                         _owner.SignalSpawned(controller);
    41.                         return controller;
    42.                     }
    43.                     else
    44.                     {
    45.                         return null;
    46.                     }
    47.                 }
    48.             }
    49.  
    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:
    Code (csharp):
    1.  
    2.             internal bool Despawn(SpawnedObjectController cntrl)
    3.             {
    4.                 if (!_activeInstances.Remove(cntrl)) return false;
    5.  
    6.                 cntrl.SetDespawned();
    7.                 cntrl.transform.parent = _owner.transform;
    8.                 cntrl.transform.localPosition = Vector3.zero;
    9.                 cntrl.transform.rotation = Quaternion.identity;
    10.  
    11.                 _instances.Add(cntrl);
    12.                 return true;
    13.             }
    14.  
    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:
    Code (csharp):
    1.  
    2.             internal bool Purge(SpawnedObjectController cntrl)
    3.             {
    4.                 if (_activeInstances.Remove(cntrl))
    5.                     return true;
    6.                 if (_instances.Remove(cntrl))
    7.                     return true;
    8.  
    9.                 return false;
    10.             }
    11.  
    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:
    Code (csharp):
    1.  
    2.         /// <summary>
    3.         /// Match an object to its prefab if this pool manages the GameObject.
    4.         /// </summary>
    5.         /// <param name="obj"></param>
    6.         /// <returns></returns>
    7.         private PrefabCache FindPrefabCache(GameObject obj)
    8.         {
    9.             //TODO - figure out the best way to match a gameobject to the cache pool.
    10.             //as it stands this depends on the prefab being a shared instance across all scripts...
    11.             //I am unsure if that's how unity works, and what the limitations are on that.
    12.             //consider creating a system of relating equal prefabs
    13.  
    14.             //test if the object is the prefab in question
    15.             int id = obj.GetInstanceID();
    16.             if (_prefabToCache.ContainsKey(id)) return _prefabToCache[id];
    17.  
    18.             var controller = obj.FindComponent<SpawnedObjectController>();
    19.             if (controller == null && controller.Pool != this) return null;
    20.  
    21.             id = controller.PrefabID;
    22.             if (_prefabToCache.ContainsKey(id)) return _prefabToCache[id];
    23.          
    24.             return null;
    25.         }
    26.  
    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:
    Code (csharp):
    1.  
    2.         private void SignalSpawned(SpawnedObjectController cntrl)
    3.         {
    4.             this.gameObject.Broadcast<IOnSpawnHandler, SpawnedObjectController>(cntrl, (o, c) => o.OnSpawn(c));
    5.             cntrl.gameObject.Broadcast<IOnSpawnHandler, SpawnedObjectController>(cntrl, (o, c) => o.OnSpawn(c));
    6.         }
    7.  
    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:
    https://github.com/lordofduct/space...r/SpacepuppyUnityFramework/Utils/Messaging.cs

    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:
    https://github.com/lordofduct/space...ster/SPSpawn/Spawn/SpawnedObjectController.cs

    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:
    Code (csharp):
    1.  
    2.         public event System.EventHandler OnSpawned;
    3.         public event System.EventHandler OnDespawned;
    4.         public event System.EventHandler OnKilled;
    5.  
    Fields:
    Code (csharp):
    1.  
    2.         #region Fields
    3.  
    4.         [System.NonSerialized()]
    5.         private SpawnPool _pool;
    6.         [System.NonSerialized]
    7.         private int _prefabId;
    8.         [System.NonSerialized()]
    9.         private string _sCacheName;
    10.      
    11.         [System.NonSerialized()]
    12.         private bool _isSpawned;
    13.  
    14.         #endregion
    15.  
    _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:
    Code (csharp):
    1.  
    2.         #region CONSTRUCTOR
    3.  
    4.         internal void Init(SpawnPool pool, int prefabId)
    5.         {
    6.             _pool = pool;
    7.             _prefabId = prefabId;
    8.             _sCacheName = null;
    9.         }
    10.  
    11.         /// <summary>
    12.         /// 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.
    13.         /// </summary>
    14.         /// <param name="pool"></param>
    15.         /// <param name="prefab">prefab this was spawned from</param>
    16.         /// <param name="sCacheName"></param>
    17.         internal void Init(SpawnPool pool, int prefabId, string sCacheName)
    18.         {
    19.             _pool = pool;
    20.             _prefabId = prefabId;
    21.             _sCacheName = sCacheName;
    22.         }
    23.  
    24.         internal void DeInit()
    25.         {
    26.             _pool = null;
    27.             _prefabId = 0;
    28.             _sCacheName = null;
    29.         }
    30.  
    31.         protected override void OnDestroy()
    32.         {
    33.             base.OnDestroy();
    34.  
    35.             if(!GameLoop.ApplicationClosing && _pool != null)
    36.             {
    37.                 _pool.Purge(this);
    38.             }
    39.             if (this.OnKilled != null) this.OnKilled(this, System.EventArgs.Empty);
    40.         }
    41.  
    42.         #endregion
    43.  
    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:
    Code (csharp):
    1.  
    2.         #region Methods
    3.  
    4.         /// <summary>
    5.         /// This method ONLY called by SpawnPool
    6.         /// </summary>
    7.         internal void SetSpawned()
    8.         {
    9.             _isSpawned = true;
    10.             this.gameObject.SetActive(true);
    11.             if (this.OnSpawned != null) this.OnSpawned(this, System.EventArgs.Empty);
    12.         }
    13.  
    14.         /// <summary>
    15.         /// This method ONLY called by SpawnPool
    16.         /// </summary>
    17.         internal void SetDespawned()
    18.         {
    19.             _isSpawned = false;
    20.             this.gameObject.SetActive(false);
    21.             if (this.OnDespawned != null) this.OnDespawned(this, System.EventArgs.Empty);
    22.         }
    23.  
    24.         public void Purge()
    25.         {
    26.             if (_pool != null) _pool.Purge(this);
    27.         }
    28.  
    29.         public GameObject CloneObject(bool fromPrefab = false)
    30.         {
    31.             if (fromPrefab && _pool != null && _pool.Contains(_prefabId))
    32.                 return _pool.SpawnByPrefabId(_prefabId, this.transform.position, this.transform.rotation);
    33.             else
    34.                 return _pool.Spawn(this.gameObject, this.transform.position, this.transform.rotation);
    35.         }
    36.  
    37.         #endregion
    38.  
    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:
    Code (csharp):
    1.  
    2.         #region IKillableEntity Interface
    3.  
    4.         public bool IsDead
    5.         {
    6.             get { return !_isSpawned; }
    7.         }
    8.  
    9.         public void Kill()
    10.         {
    11.             if (!_pool.Despawn(this))
    12.             {
    13.                 ObjUtil.SmartDestroy(this.gameObject);
    14.             }
    15.             else
    16.             {
    17.                 //TODO - need a cleaner way of doing this
    18.                 using (var lst = TempCollection.GetList<Rigidbody>())
    19.                 {
    20.                     this.transform.GetComponentsInChildren<Rigidbody>(lst);
    21.                     var e = lst.GetEnumerator();
    22.                     while(e.MoveNext())
    23.                     {
    24.                         e.Current.velocity = Vector3.zero;
    25.                         e.Current.angularVelocity = Vector3.zero;
    26.                     }
    27.                 }
    28.                 using (var lst = TempCollection.GetList<Rigidbody2D>())
    29.                 {
    30.                     this.transform.GetComponentsInChildren<Rigidbody2D>(lst);
    31.                     var e = lst.GetEnumerator();
    32.                     while (e.MoveNext())
    33.                     {
    34.                         e.Current.velocity = Vector2.zero;
    35.                         e.Current.angularVelocity = 0f;
    36.                     }
    37.                 }
    38.             }
    39.             if (this.OnKilled != null) this.OnKilled(this, System.EventArgs.Empty);
    40.         }
    41.  
    42.             #endregion
    43.  
    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:

    Code (csharp):
    1.  
    2. var cntrl = objToDestroy.GetComponent<SpawnedObjectController>();
    3. if(cntrl.Pool != null && cntrl.Pool.Despawn(cntrl))
    4. {
    5.     //do nothing, it was returned to the pool
    6. }
    7. else
    8. {
    9.     //not a cached instance
    10.     Object.Destroy(objToDestroy);
    11. }
    12.  
    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:
    https://github.com/lordofduct/space...r/SpacepuppyUnityFramework/IKillableEntity.cs

    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:

    Code (csharp):
    1.  
    2. var killable = objToDestroy.GetComponent<IKillableEntity>();
    3. if(killable != null)
    4.     killable.Kill();
    5. else
    6.     Object.Destroy(objToDestroy);
    7.  
    And then with some extension methods I can streamline this even further:
    https://github.com/lordofduct/space...puppyUnityFramework/Utils/GameObjUtil.cs#L255

    And now we can just say:
    Code (csharp):
    1. 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:
    https://github.com/lordofduct/space...0/blob/master/SPSpawn/Spawn/Events/i_Spawn.cs


    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:


    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:
    https://github.com/lordofduct/space...0/blob/master/SPSpawn/Spawn/Events/i_Spawn.cs

    For starters we have our fields:
    Code (csharp):
    1.  
    2.         #region Fields
    3.  
    4.         [SerializeField()]
    5.         [Tooltip("If left empty the default SpawnPool will be used instead.")]
    6.         private SpawnPool _spawnPool;
    7.      
    8.         [SerializeField]
    9.         private Transform _spawnedObjectParent;
    10.  
    11.         [SerializeField()]
    12.         [WeightedValueCollection("Weight", "Prefab")]
    13.         [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.")]
    14.         private List<PrefabEntry> _prefabs;
    15.  
    16.         [SerializeField()]
    17.         private SPEvent _onSpawnedObject = new SPEvent(TRG_ONSPAWNED);
    18.  
    19.         #endregion
    20.  
    _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:
    Code (csharp):
    1.  
    2.         #region Methods
    3.  
    4.         public GameObject Spawn()
    5.         {
    6.             if (!this.CanTrigger) return null;
    7.  
    8.             if (_prefabs == null || _prefabs.Count == 0) return null;
    9.  
    10.             if(_prefabs.Count == 1)
    11.             {
    12.                 return this.Spawn(_prefabs[0].Prefab);
    13.             }
    14.             else
    15.             {
    16.                 return this.Spawn(_prefabs.PickRandom((o) => o.Weight).Prefab);
    17.             }
    18.         }
    19.  
    20.         private GameObject Spawn(GameObject prefab)
    21.         {
    22.             if (prefab == null) return null;
    23.  
    24.             var pool = _spawnPool != null ? _spawnPool : SpawnPool.DefaultPool;
    25.             var go = pool.Spawn(prefab, this.transform.position, this.transform.rotation, _spawnedObjectParent);
    26.          
    27.             if (_onSpawnedObject != null && _onSpawnedObject.Count > 0)
    28.                 _onSpawnedObject.ActivateTrigger(this, go);
    29.  
    30.             return go;
    31.         }
    32.  
    33.         public GameObject Spawn(int index)
    34.         {
    35.             if (!this.enabled) return null;
    36.  
    37.             if (_prefabs == null || index < 0 || index >= _prefabs.Count) return null;
    38.             return this.Spawn(_prefabs[index].Prefab);
    39.         }
    40.  
    41.         public GameObject Spawn(string name)
    42.         {
    43.             if (!this.enabled) return null;
    44.  
    45.             if (_prefabs == null) return null;
    46.             for (int i = 0; i < _prefabs.Count; i++)
    47.             {
    48.                 if (_prefabs[i].Prefab != null && _prefabs[i].Prefab.name == name) return this.Spawn(_prefabs[i].Prefab);
    49.             }
    50.             return null;
    51.         }
    52.  
    53.         #endregion
    54.  
    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
    Code (csharp):
    1.  
    2.         #region ITriggerable Interface
    3.  
    4.         public override bool CanTrigger
    5.         {
    6.             get { return base.CanTrigger && _prefabs != null && _prefabs.Count > 0; }
    7.         }
    8.  
    9.         public override bool Trigger(object sender, object arg)
    10.         {
    11.             if (!this.CanTrigger) return false;
    12.  
    13.             if (arg is string)
    14.             {
    15.                 return this.Spawn(arg as string) != null;
    16.             }
    17.             else if (ConvertUtil.ValueIsNumericType(arg))
    18.             {
    19.                 return this.Spawn(ConvertUtil.ToInt(arg)) != null;
    20.             }
    21.             else
    22.             {
    23.                 return this.Spawn() != null;
    24.             }
    25.         }
    26.  
    27.         #endregion
    28.  
    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.
     
    Last edited: Jan 6, 2018
    Richardm1985 likes this.
  16. meat5000

    meat5000

    Joined:
    Mar 5, 2013
    Posts:
    118
    Yes! You guys are hitting the nail on the head here.
     
  17. XRA

    XRA

    Joined:
    Aug 26, 2010
    Posts:
    191
    just make sure NOT to mix GetHashCode() and GetInstanceID(), they are no longer the same in standalone builds.
     
    JoeStrout likes this.
  18. joasus12345

    joasus12345

    Joined:
    Dec 26, 2018
    Posts:
    5
    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
     
  19. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    8,388
    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.
     
    spilat12 and lordofduct like this.
  20. spilat12

    spilat12

    Joined:
    Feb 10, 2015
    Posts:
    10
    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.