Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

I'm looking for feedback on on my loopless object pool.

Discussion in 'Scripting' started by HyperBully, Apr 12, 2020.

  1. HyperBully

    HyperBully

    Joined:
    Sep 9, 2017
    Posts:
    25
    I am new to C# and unity and while working on my first game I found myself hacking my object pool every time I wanted to pool something new so I decided to make something universal that I could build once and use forever. I had to learn about generics and nullable objects so it was a good learning experience but I have no idea if I did anything right. The thing does what it says on the tin but before I start building around it, I was just hoping that somebody with more experience than me sitting at home bored might feel like taking a look at it and telling me if there's anything I can improve. I decided I didn't want to run a bunch of loops so objects are pooled in a queue. I'm not sure if that's a bad idea. All the object pool tutorials use arrays and loops to check an object's availability. My pool will actually lose all reference to an object and the object will eventually put itself back into the queue. I thought it would be leaner this way.I also added some methods to let an object be pulled from the pool as a child of some other object. But I'm a total noob so if there are consequences I'm not aware of, please share.
    Code (CSharp):
    1. public class ObjectPool : MonoBehaviour
    2. {
    3.  
    4.     [SerializeField] protected List<PoolObject> Prefabs;
    5.     public static Transform Transform { get => Instance.transform; }
    6.     protected Dictionary<Type, Queue<PoolableObject>> Pools = new Dictionary<Type, Queue<PoolableObject>>();
    7.     protected static ObjectPool Instance;
    8.  
    9.     private void Awake()
    10.     {
    11.         Instance = this; // create a staic reference
    12.         StartCoroutine("Initiate"); //build the pools
    13.     }
    14.     /// <summary>
    15.     /// Retrieves an object form the pool after doing some basic validation
    16.     /// </summary>
    17.     protected static TPoolable RetrieveFromPool<TPoolable>() where TPoolable : PoolableObject
    18.     {
    19.         var type = typeof(TPoolable);
    20.         if (!Instance.Pools.ContainsKey(type)) throw new ArgumentException("You tried a retrieve a " + type + " but none have been pooled.");
    21.         else if (Instance.Pools[type].Count == 0) throw new IndexOutOfRangeException("There are no " + type + " objects left in the pool");
    22.  
    23.         var gameObject = Instance.Pools[type].Dequeue() as TPoolable;
    24.         return gameObject;
    25.     }
    26.     /// <summary>
    27.     /// Sends a pooled object to a specific location and rotation
    28.     /// </summary>
    29.     /// <typeparam name="TPoolable">The type of object you want to retrieve from the pool</typeparam>
    30.     /// <param name="location">The new location in world space</param>
    31.     /// <param name="rotation">the new rotation in world space(default: no rotation)</param>
    32.     /// <returns>A poolable object</returns>
    33.     public static TPoolable Send<TPoolable>(Vector3 location, Quaternion? rotation = null) where TPoolable : PoolableObject
    34.     {
    35.         var gameObject = RetrieveFromPool<TPoolable>();
    36.         gameObject.transform.SetPositionAndRotation(location, rotation ?? Transform.rotation);
    37.         gameObject.Activate();
    38.         return gameObject;
    39.     }
    40.     /// <summary>
    41.     /// Gets an object from the pool an sets its parent to the the provided transform
    42.     /// </summary>
    43.     /// <typeparam name="TPoolable">The type of object you want to retrieve from the pool</typeparam>
    44.     /// <param name="parent">The new parent of the object</param>
    45.     /// <param name="location">The new location for the object(default: parent.position)</param>
    46.     /// <param name="rotation">The new rotation for the object(default: no rotation)</param>
    47.     /// <param name="relativePosition">If true, location will be set relative to the parent. (set in world space by default)</param>
    48.     /// <param name="relativeRotation">If true, rotation will be set relative to the parent. (set in world space by default)</param>
    49.     /// <returns>The requested object as a child of the given transform</returns>
    50.     public static TPoolable Get<TPoolable>(Transform parent,  Vector3? location = null, Quaternion? rotation = null, bool relativePosition = false, bool relativeRotation = false ) where TPoolable : PoolableObject
    51.     {
    52.         var gameObject = RetrieveFromPool<TPoolable>();
    53.         gameObject.transform.SetParent(parent);
    54.         if (location.HasValue)
    55.         {
    56.             if (relativePosition) gameObject.transform.position = location.Value;
    57.             else gameObject.transform.localPosition = location.Value;
    58.         }
    59.         else gameObject.transform.position = parent.position;
    60.         if (rotation.HasValue)
    61.         {
    62.             if (relativeRotation) gameObject.transform.localRotation = rotation.Value;
    63.             else gameObject.transform.rotation = rotation ?? Transform.rotation;
    64.         }
    65.         else gameObject.transform.rotation = Transform.rotation;
    66.         gameObject.Activate();
    67.         return gameObject;
    68.     }
    69.  
    70. #region Get<T>() shortcuts
    71.     /// <summary>
    72.     /// Sets the parent to the given transform and sets a new location and rotation relative to the new parent
    73.     /// </summary>  
    74.     /// <param name="parent">The new parent</param>
    75.     /// <param name="location">The new location for the object relative to the new parent</param>
    76.     /// <param name="rotation">The new rotation for the object relative to the new parent(default: no rotation)</param>
    77.     /// <returns>The requested object as a child of the given transform</returns>
    78.     public static TPoolable GetRelative<TPoolable>(Transform parent, Vector3 location, Quaternion? rotation = null) where TPoolable : PoolableObject
    79.     {
    80.         return Get<TPoolable>(parent, location, rotation, true, true);
    81.     }
    82.     /// <summary>
    83.     /// Sets the parent to the given transform and sets a new location and rotation relative to the new parent
    84.     /// </summary>  
    85.     /// <param name="parent">The new parent</param>  
    86.     /// <param name="rotation">The new rotation for the object relative to the new parent</param>
    87.     /// <param name="location">The new location for the object relative to the new parent(default: parent.position)</param>
    88.     /// <returns>The requested object as a child of the given transform</returns>
    89.     public static TPoolable GetRelativeToParent<TPoolable>(Transform parent, Quaternion rotation, Vector3? location = null) where TPoolable : PoolableObject
    90.     {
    91.         return Get<TPoolable>(parent, location, rotation, true, true);
    92.     }
    93.     /// <summary>
    94.     /// Sets the parent to the given transform and sets the rotation relative to the new parent with a new location in world space
    95.     /// </summary>
    96.     /// <param name="parent">The new parent</param>
    97.     /// <param name="rotation">The new rotation for the object relative to the new parent</param>
    98.     /// <param name="location">The new location for the object in world space(default: parent.position)</param>
    99.     /// <returns>The requested object as a child of the given transform</returns>
    100.     public static TPoolable GetRelativeRotation<TPoolable>(Transform parent, Quaternion rotation,  Vector3? location = null) where TPoolable : PoolableObject
    101.     {
    102.         return Get<TPoolable>(parent, location, rotation, false, true);
    103.     }
    104.     /// <summary>
    105.     /// Sets the parent to the given transform and sets the location relative to the new parent with a new rotation in world space
    106.     /// </summary>
    107.     /// <param name="parent">The new parent</param>
    108.     /// <param name="location">The new location relative to the new parent</param>
    109.     /// <param name="rotation">The new rotation in world space(default: no rotation)</param>
    110.     /// <returns>The requested object as a child of the given transform</returns>
    111.     public static TPoolable GetRelativePosition<TPoolable>(Transform parent, Vector3 location, Quaternion? rotation = null) where TPoolable : PoolableObject
    112.     {
    113.         return Get<TPoolable>(parent, location, rotation, true, false);
    114.     }
    115.     /// <summary>
    116.     /// Sets the parent to the given transform and sets its position and rotation to match its new parent in world space
    117.     /// </summary>
    118.     /// <param name="parent">The new parent</param>
    119.     /// <returns>The requested object as a child of the given transform</returns>
    120.     public static TPoolable GetMatchparent<TPoolable>(Transform parent) where TPoolable : PoolableObject
    121.     {
    122.         return Get<TPoolable>(parent, null, parent.rotation, false, false);
    123.     }
    124. #endregion  
    125.  
    126.     /// <summary>
    127.     /// Called by poolable objects when they have completed their task and are ready to be returned to the pool
    128.     /// (This will not change an object's position. It will stay put until it's pulled out of the pool.)
    129.     /// </summary>
    130.     /// <param name="sender">The object triggering the event</param>
    131.     protected virtual void OnAvailabilityChange(PoolableObject sender)
    132.     {
    133.         sender.transform.parent = transform;//reset the parent just in case
    134.         Pools[sender.GetType()].Enqueue(sender);//put it back in the pool
    135.     }
    136.     /// <summary>
    137.     /// Instantiate all of the objects and fill up the queues
    138.     /// </summary>
    139.     private IEnumerator Initiate()
    140.     {
    141.         foreach (PoolObject pool in Prefabs)
    142.         {  
    143.             //create a pool
    144.             var type = pool.Prefab.GetType();
    145.             Pools.Add(type, new Queue<PoolableObject>());
    146.             for (int i = 0; i < pool.Quantity; i++)
    147.             {  
    148.                 //fill the pool with objects
    149.                 var gameObject = Instantiate(pool.Prefab, transform); //create the object
    150.                 Pools[type].Enqueue(gameObject);                      //add it to the pool
    151.                 gameObject.AvailabilityChange += OnAvailabilityChange;//tell it how to get back to the pool
    152.             }
    153.             yield return null;//create one complete pool per cycle
    154.         }
    155.     }
    156.     /// <summary>
    157.     /// Used to add objects the pool through the inspector
    158.     /// </summary>
    159.     [Serializable]
    160.     public class PoolObject
    161.     {
    162.         public int Quantity;
    163.         public PoolableObject Prefab;
    164.     }
    165. }
    Code (CSharp):
    1. public abstract class PoolableObject : MonoBehaviour
    2. {
    3.     /// <summary>
    4.     /// Call this when the object has completed its task and is ready to be returned to the pool.
    5.     /// </summary>
    6.     public event Action<PoolableObject> AvailabilityChange;
    7.     /// <summary>
    8.     /// The pool's position in the game world
    9.     /// Send the object here when you want to remove it from view.
    10.     /// </summary>
    11.     public static Vector3 PoolStorageLocation => ObjectPool.Transform.position;
    12.  
    13.     /// <summary>
    14.     /// Triggered when this object is pulled from the pool
    15.     /// (Use this to tell monsters to start hunting or bullets to start flying.)
    16.     /// </summary>
    17.     public virtual void Activate() { }
    18.     /// <summary>
    19.     /// Call this when the object is ready to return to the pool.
    20.     /// (Does not change its position)
    21.     /// </summary>
    22.     protected virtual void ReturnToPool() => AvailabilityChange?.Invoke(this);
    23.     /// <summary>
    24.     /// Removes the object from view and puts it back in the pool
    25.     /// </summary>
    26.     protected virtual void ReturnToStorage()
    27.     {
    28.         transform.position = PoolStorageLocation;
    29.         ReturnToPool();
    30.     }
    31. }
    32.  
     
  2. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,337
    I would suggest to never visit the sites with tutorials like that. You shouldn't need arrays, but you would need a hashmap/dictionary.

    Regarding your code;
    Code (csharp):
    1.  
    2. [LIST=1]
    3. [*]private void Awake()
    4. [*]    {
    5. [*]        Instance = this; // create a staic reference
    6. [*]        StartCoroutine("Initiate"); //build the pools
    7. [*]    }
    8. [/LIST]
    9.  
    Implementing initialization of the pool in awake means that the game will throw exception if something initializes before the pool and requests the pool. Lazy initialization (where static property instance upon first requests created an instance of object pool) is likely a better approach.

    Using function names is a bad practice. Use function itself. It is a bad practice, because if you rename the function, you'll forget to update the point where the function is called by name, and the program will break.

    Initializing via a coroutine, also means that there will be a whole frame to throw an exception if something requests the pool. See:
    https://docs.unity3d.com/Manual/ExecutionOrder.html
    Because Coroutines are executed After update and everything else.

    Next...

    You're keying object pools by TYPE for some reason, and that does not make much sense. There's no point in using type as a key, at all. Different objects can be of same type, so you don't need it.
    Additionally, there is also no point in storing pool location, as ideally you're supposed to make unused pool objects inactive, which will hide them.

    I also have no idea why the hell would people use arrays and looping through them when making object pools, so if there's a site with such amazing idea, avoid it in the future.

    --------

    Last time I implemented an object pool its function was to replace Instantiate method. Because of that, the pool itself was implemented as a dictionary (Dictionary<Object, List<Object>> OR Dictionary<Object, List<GameObject>>), with prefab being a key. That makes it universal.

    Upon first request, pool would check the dictionary for existing objects, and if none were found, it would instantiate an object normally, ATTACH a component to it (if it does not already exist) with some book keeping information, and that is it. Upon "destruction" (which has to be done through pool methods), the object would be reparented to pool root (to make managing it easier in hierarchy view) and that would disable it. And tha'ts all.

    If the object was in the list, the pool would simply grab the last one registered and reuse it. Any object would be supported, although some care would be necessary to ensure the object reinitializes properly (for example, particle systems restart).
     
    HyperBully likes this.
  3. HyperBully

    HyperBully

    Joined:
    Sep 9, 2017
    Posts:
    25
    Thanks for your time. I really appreciate your advice. You've given me a lot to think about. I have no idea how hashmaps work so I guess that's what I'm doing today. One question I have though is that I was thinking about having the pooled objects do all the work of in activating and inactivating themselves. (I stored the pool location because I was struggling with some physics timings and teleporting an object away was a miracle/crutch.) I figure a crater should never disable itself and should sit on the ground looking like a crater until it's needed somewhere else while a bullet or an explosion might need to disable themselves as soon as they are done with some task. I don't see why the pool should even have the concept of disabling an object. I should definitely put more of that work in the PoolableObject class so I don't have to repeat myself a ton. That feels like a big oversight on my part now. Is there a reason the pool should know how to disable the object rather the object disabling itself?
     
  4. HyperBully

    HyperBully

    Joined:
    Sep 9, 2017
    Posts:
    25
    A hashmap is not different than a dictionary right? I think I misunderstood. I am using a Dictionary. When you're as new to this as I am all the tutorials try to stay pretty basic. I love that so many people are trying to teach me to make games. I just wish there were more real world examples of the basic things that everybody needs like state machines and object pools. There are a million articles and videos telling me why I need these things and showing me the most basic examples of them but I still have no idea what these things look like in real games.
     
  5. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,337
    Hashmap is a Dictionary for all practical reasons. HashMap is a type of generic container with specific internal organization, and in C# Dictionary is implemented a hashmap.

    I believe you should not be doing that.
    The purpose of the pool is to provide a replacement for Instantiate/Destroy.
    When your pooled object starts deactivating itself (it can't reactivate itself, by the way), it is overstepping bounds and goes beyond what pooled obejct is supposed to do.

    You're talking about self-destructing object, like an explosion particle system. The principle is the same regardless of whether it uses pool or not. Once it is done it either destroys itself or return itself to the pool.

    Unused pooled object must be deactivated, otherwise it will be still part of the game world. Meaning it will be possible to collide with it, if it is a rigid body, it will roll around the scene, and if it is a sound effect it will continue playing, and if it is a mesh it will be rendered. Additionally all Update() methods will be still called. To "remove" it from the scene, you need to deactivate it until it is needed and until it is the time to reuse it.

    Pool provides replacement for Instantiate/Destroy. Except it keeps objects around to reuse them.
    The reason why pool disables object because if they are not disabled, they'll remain active in the scene.
    https://docs.unity3d.com/ScriptReference/GameObject.SetActive.html
    So objects that are not currently used, should be disabled.

    The reason why object shouldn't be disabling itself, because disabling it is a part of implementation of the pool, and therefore should be done in the pool. Additionally having an "unspoken agreement" that all pooled objects should disable themselves is asking for a bug. At some point somebody will forget to adhere to the agreement, hence disabling should be done by the pool in a method where object is being returned to the pool.

    Play around with unity scene hierarchy, organize objects into a tree, disable tree root, see what happens.
     
    HyperBully likes this.
  6. HyperBully

    HyperBully

    Joined:
    Sep 9, 2017
    Posts:
    25
    I'm more thinking about the sort of object that just exists to hold a sprite. A crater would sit on the ground after the explosion effect and just look like a crater. It would sit there forever unless the player had created a certain number of of them, then the pool would start reusing the oldest ones as the player created more. I guess the pool could use one event for OnReadyToBeRepooled and another for OnReadyToBeDestroyed but isn't it ultimately up to the PoolableObject whether or not it even needs to be destroyed?
     
  7. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,337
    That's a decal/debris system. This would be separate from object pool.

    In this situation Debris object would be managed by some sort of Debris Manager, and that would decide what to do if there are too many decals/debris objects in the scene. Debris management does not need to be a part of ObjectPool.
     
    HyperBully likes this.
  8. HyperBully

    HyperBully

    Joined:
    Sep 9, 2017
    Posts:
    25
    Ok I think I get it. "Decal" is a term I was unfamiliar with. Thinking about a Debris Manager has me seeing this from a whole different perspective. It makes sense that it should be separate from my Object Pool. A bullet system is definitely different than a crater system. So now it seems to me that since my object pool is basically a decal system too, the object pool I end up with could probably just inherit a lot of stuff from my decal system. Instantiating objects and storing them in a collection will basically be the same. There will be a lot of the same functionality when retrieving objects as well. So, in my head right now, having spent zero time learning about decal systems, I feel like my object pool should extend my decal pool. I'm going to learn more about this but I'm wondering if this seems like the right approach from your experience.
     
  9. HyperBully

    HyperBully

    Joined:
    Sep 9, 2017
    Posts:
    25
    After some thought, they don't seem like they are that similar after all. I am now thinking about an entirely different type of system. Thank you so much for help. This was exactly the sort of feedback I was hoping for.
     
  10. Xiromtz

    Xiromtz

    Joined:
    Feb 1, 2015
    Posts:
    65
    Have a look at this:
    https://gameprogrammingpatterns.com/object-pool.html

    I'm not a pro on c# specifics, but the game programming patterns book is a great reference on getting the gist of things in a really well explained manner, even though its C++ specific. I love the free list implementation though, it's so elegant and works perfectly, though probably unusable in C#.

    In my opinion the object pool pattern is a rather simple one and you shouldn't be overthinking things. It depends very much on how you want to use the object pool.
    Do you want a single object pool as a singleton that creates pools of any type per request?
    Are pools maybe instantiable classes, with multiple instances existing, one for each type of component/object?
    Do I create pools on a per-level basis, maybe during loading screens?
    Do I create a pool at the start of the game and keep it forever?
    Should the pool expand or not?

    Additionally, if you're creating a single pool object as a singleton in the Awake() function, a solution to the problem described would be to start the game in an empty scene with only the pool object. Set the pool object to be persistent through scenes and load the next scene ones setup is complete. Personally, I'm not a fan of the lazy intialization in this case, since the object pool setup could take a lot of time and if the first call to the pool is in a runtime crucial part of the game, you would get a frame spike.

    On to your implementation:

    Simply having a quick gist over your implementation, I have one question for you: Is Unity even able of Instantiating anything other than GameObjects? I don't think so. Aren't you overcomplicating things here?

    Maybe a simpler implementation would be to make the Pool NOT a singleton, make it instantiable. Have a single pool container object that persists throughout scenes. For each prefab you need a pool for, add a child gameobject with the pool component, have a
    public GameObject Prefab
    and
    public int Quantity
    field. The Instances are created as children of the pool gameobject and are easily sortable that way. If you destroy the corresponding pool gameobject, all children are destroyed.
    The container gameobject might get one additional pool manager component, which would hold a dictionary <GameObject, Pool>, where one lookup gets you the Pool of the corresponding prefab to do all of your necessary calculations.
    You can create and remove pools during runtime at will with this implementation.

    But this is just my idea to split up your code and simplify your implementation a bit. Hope I could help a bit!
     
  11. HyperBully

    HyperBully

    Joined:
    Sep 9, 2017
    Posts:
    25
    Thanks for your feedback and your time.
    This is similar to what I started with . I was using a queue of integers to track indexes of available objects. Somewhere along the way I decided to drop the indexes and just the objects themselves in a queue. I think I am using this "free list" idea in a C# way. It works very similar to what's described in the link.

    I definitely agree. I have spent way more time on this tiny problem than it deserves. But that's kind of the point of this exercise. I want to spend some time trying to master some small thing so that I can get better at Unity and C# in general. Even this conversation is helping me learn to communicate about these topics that can still feel pretty alien at times.

    Code (CSharp):
    1. void Awake()
    2. {
    3.    Instance = this
    4. }
    5.  
    Is the easiest way to create a static instance of a component I've seen since I can't use new() to create a MonoBehaviour. Lazy initialization isn't even really possible here. I need an instance that exists to create a static copy of itself. If the object pool object extends MonoBehaviour and I add it as a component of a GameObject Unity creates the instance and calls Awake() for me. I'm really not sure what the best way to do this is but I saw it in a tutorial and it has worked well for what I've needed. It doesn't feel "right" at all but it works like magic in Unity. lol


    This will exist as a component of persistent object. I have a GameManager object that contains stuff like this.


    I hadn't really thought about creating and destroying pools at runtime but that actually seems super useful. i definitely like this idea.

    You've helped a ton. Thanks
     
  12. Xiromtz

    Xiromtz

    Joined:
    Feb 1, 2015
    Posts:
    65
    Using the Awake function as a constructor to set the singleton instance is definetely a common Unity use-case.
    If you want to see a lazy initialization singleton, I'm currently working on an ML-Agents toolkit project and remember seeing Unity implementing one of the classes as a singleton:
    https://github.com/Unity-Technologies/ml-agents/blob/master/com.unity.ml-agents/Runtime/Academy.cs

    There's a bit too much going on for me to understand the implementation 100%, but I believe their class just doesn't inherit from MonoBehavior, but instead acts like a static class. In this case, you couldn't set any variables through the inspector, but it's really nice code either way.
     
    HyperBully likes this.
  13. HyperBully

    HyperBully

    Joined:
    Sep 9, 2017
    Posts:
    25
    I really like having the option to set variables in the inspector. But that ML-Agents looks way fun to play with. I'm going to spend some time learning that for sure. Thanks for the link.
     
  14. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,337
    Don't. This will cause heisenbugs.

    What you're trying to do is an equivalent of a static initialization order bug in C++.
    https://isocpp.org/wiki/faq/ctors#static-init-order

    It is.

    Code (csharp):
    1.  
    2. public class PoolSample: MonoBehavior{
    3.     static private PoolSample cachedInstance_ = null;
    4.     static public PoolSample instance{
    5.         get{
    6.             if (cachedInstance != null){
    7.                 return cachedInstance;
    8.             }
    9.          
    10.             var poolObject = new GameObject("Pool Node");
    11.             poolObject.hideFlaggs = HideFlags.DontSave;
    12.             var pool = poolObject.AddComponent<PoolSample>() as PoolSample;
    13.             cachedInstance = pool;
    14.             return cachedInstance;          
    15.         }
    16.     }
    17. }
    18.  
    You can't create MonoBehavior in a vacuum, but you can spawn a gameobject in a scene, and can add a component to it.

    You also don't need a pool object in the scene, and can have it spawned on the first request.
     
    csofranz and HyperBully like this.
  15. HyperBully

    HyperBully

    Joined:
    Sep 9, 2017
    Posts:
    25
    I had tried to do that using something like
    var poolSample = new PoolSample()
    and Unity threw errors at me. I didn't realize I needed to do it that way. Thanks again.
     
  16. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,337
    Components have to be attached to GameObjects and cannot exist without them.
     
    HyperBully likes this.
  17. HyperBully

    HyperBully

    Joined:
    Sep 9, 2017
    Posts:
    25
    I'm finding out that my whole concept of what is a Component verus what is a GameObject was totally wrong. I was thinking of a Monobehaviour as something that could be a GameObject or a Component depending on where it was inspector and the hierarchy. But I'm now thinking of a GameObject as a thing that holds a set of Monobehaviours. I feel like a lot of confusing concepts just became clear all at once. lol
     
  18. Xiromtz

    Xiromtz

    Joined:
    Feb 1, 2015
    Posts:
    65
    Have a look at the component pattern:
    https://gameprogrammingpatterns.com/component.html

    Honestly all patterns described in there are real useful and interesting, you should have a look at the whole book! Ican't recommend it enough, since the guy put everything online for free.
     
    HyperBully likes this.
  19. HyperBully

    HyperBully

    Joined:
    Sep 9, 2017
    Posts:
    25
    That link helped me understand the idea of a component for sure. That whole website seems like it will help me understand Unity way better. I'm so new to this that I thought terms like "component" and "transform" were Unity specific jargon. It's cool to see that they are just following some basic design patterns that I can learn and think about separately from Unity. I think this will help me a ton. You guys have really helped out a ton. I feel like I've leveled up because of both of you.
     
  20. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    You should definitely use an interface that is known by the pools, rather than a concrete base class constraint. You can still use a base class, even though it doesn't really do much at all in your case, and is currently pretty lightweight which speaks for itself. The interface and some ideas mentioned below will probably render the base class useless.

    Pool: GET, PUT, optionally CREATE, but that's actually not its business. You can be pragmatic and do that. Later on, you can plug in a factory/generator/builder.

    How does interaction with that pool look like? In my opinion, poolables should not know that they are poolable at all. That's an abstract property which messes things up. I can throw cups away, or I can keep them and re-use them. The pool is only the container, but the items stored never know where, when and how they're stored.

    If you were to do that, you'd basically give any inheriting type the responsibility to store itself back in its pool. This is too pragmatic IMO. Because you've got a dependency that goes into the wrong direction. More than that, you now have bi-directional dependencies. Pool <---> Poolable. This is often not desired. There should be a mechanism to decouple things (which you actually have already, but isn't really used that way).

    Now your event "AvailabilityChange" becomes important.
    Let me recommend AvailabilityChanged and an additional parameter that indicates which transition was made. That'd extend the usefulness and expresses the intent of the event much better:

    Compare:
    AvailabilityChange (no arg)
    :
    Is it now available, or not?
    Is it even raised for both? Or more than two states?
    Is it ABOUT TO CHANGE, is it IN PROGRESS, or HAS it ALREADY CHANGED?

    AvailabilityChanged(availability state information) // this could be "new state", or "old+new state" depending on your number of phases/states.
    Argument suggests there's state information indicating which state it transitioned to (optionally which it transition from, if you really need that)
    You can definitely tell it's raised for at least two states.
    Past tense suggests the state HAS CHANGED.

    The latter can be documented, but it's a matter of intuitive naming convetions (yes, it's actually a recommendation made by Microsoft, but it makes sense IMO).

    Back to topic.
    I said I wouldn't let the poolables know that they're poolable.
    Instead of letting the object return itself to a pool that it knows, the event can be used to subscribe to "AvailabilityChanged" and check for the state transition that lets subscribes know "its no longer available".

    How do dependencies and responsibilities look like?
    Pool doesn't even know "poolables" in that sense, but things that can - once handed out - eventually turn "unavailable" to their context/world, or in different words "available for new requests" issued towards the pool.

    The objects that provide the interface no longer know anything like a pool. They need to handle their lifecycle instead, in order to be recycable and put themselves (or let a third party put them) into an "initial" state.

    I can tell that I use the same event name among other events and methods (currently only in my UI system in order to abstract the direct coupling to the gameobject's API away) and it definitely fulfills many purposes. Hence I do not call them "poolable" anywhere. They're implementing an interface that suggests these objects handle their lifecycles and lifetime, which a pool can make use of (that's just one of many uses).
     
    Last edited: Apr 13, 2020
  21. HyperBully

    HyperBully

    Joined:
    Sep 9, 2017
    Posts:
    25
    I started out that way but realized that I was going to be adding pool specific code into every every creature/fireball I make. PoolableObject implemented IPoolable and did the basic stuff I thought all poolables would have. By using inheritance I was able to only modify the methods I needed to modify and not have to tell each specific object how to get back to the pool. once I was doing that the interface seemed like it didn't need to exist. Maybe I'm just using interfaces wrong but they don't seem right when the specific implementation of each method will be identical for all classes using it. It seems like I'd be copy and pasting a lot of stuff. The first thing I would do every time I create a poolable is type
    void AvailabilityChanged()
    ...


    This is due to my lack of experience. You make a lot of sense.


    I like the idea of it being completely decoupled from the pool but I don't think I'm understanding how to do this without inheritance. When I want to create new type of projectile, if I'm using an interface, I tell my projectile to implement it and now have to start telling the projectile how to get back to the pool. An interface just feels like a ton of extra work here.
     
  22. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    I could start telling that this wouldn't be the problem if you had objects that CAN be pooled because the exposed API allows it, but ARE NOT aware of it. That'd probably eliminate some of the concerns, but the concerns you mentioned apply to general API design.

    The assumption that such seemingly redundant interfaces are superfluous is just wrong. The abstract class is still more specific than an interface. Remove the interface, and all of a sudden the code base is bound to a very specific base type. This can be very, very limiting, especially in general purpose APIs.

    An interface adds much more freedom, as you can set up any type without the need to have a very specific base type. What if you suddenly wanted to pool instances of non-MonoBehaviour types? Well, your generic pool is constrained to "PoolableObject", which is a MonoBehaviour...

    Once again. That's the wrong way to think about it. There's nothing preventing you from having both, an interface and a convenient base-class implementation.

    Your pool is just one of the potential entities that wants to do something, when an object raises its notification saying "I'm no longer available in this context/world". There could be one system that listens and stores timestamp, instance ID, does analytics... another one could react to that and spawn something... and then, there can be your pool, that thing which picks up wasted instances, and then stocks these objects and returns them when a new one is needed. All of that is not of any interest for the instance of types that can transition to being "unavailable".
     
    HyperBully likes this.
  23. HyperBully

    HyperBully

    Joined:
    Sep 9, 2017
    Posts:
    25
    I think I'm convinced. I can imagine a situation where I would want to pool some kind object that is already inheriting from some other type of MonoBehaviour and I wouldn't even be able to use inheritance. An interface would definitely work better for that. Then I can can create a different abstract convenience class for any kind of poolable object I need and get the best of both worlds. I get it now. That makes total sense to me. Thank you so much for helping with that. I feel like I understand how interfaces work pretty well but I never really understood why exactly they were useful. Even with this project, it felt like a waste of space. But now I think I can start to see why I should have just left it there. It cost me only a few lines of code in a file but gave me an infinite amount of flexibility.
     
  24. jamespaterson

    jamespaterson

    Joined:
    Jun 19, 2018
    Posts:
    391
    Hi. For what it's worth i implemented my own pooling system for gameobjects (e.g. bullets enemies etc) in my WIP game. Probably by most people's standards i did a shoddy job.

    However, the main comment i would make is largest amount of my pain came from cleanly resetting the state of gameobjects so that they can be reused without residual state from the last use. An example of this might be a projectile with rigidbody. Velocity and torque need to be manually reset to zero before each use. Good luck!
     
    HyperBully likes this.
  25. Xiromtz

    Xiromtz

    Joined:
    Feb 1, 2015
    Posts:
    65
    Yeah, typically pools are only used for simple objects that need a lot of instances, such as particles. The reset code won't be too complicated if the object itself doesn't have too much functionality.

    In many cases, a pool might not be necessary at all unless you're making a heavy console game that needs to squeeze out the memory budget perfectly..
    As always remember if it's really necesssary - "Premature optimization is the root of all evil"
     
    HyperBully likes this.
  26. HyperBully

    HyperBully

    Joined:
    Sep 9, 2017
    Posts:
    25
    I have definitely spent some frustrated time learning that lesson. lol