Resource Manager and Object Pooling

[UPDATE]

I wrote this blog post quite a while ago. Since then I have learned a lot about memory management, and I would like to discuss the drawbacks of this pattern.

One of the reasons Unity doesn’t recommend using the Resources folder is that it “makes fine-grained memory management more difficult.” Indeed, the assets which are in the Resources folders are loaded into the memory when the application launches, regardless of them being referenced anywhere in the project. And they are included in the build.

When the pattern described in this post is used, the assets are not loaded into the memory on runtime. But they are loaded whenever a scene with ResourceManager is loaded. So when you have a scene with a few small assets, this pattern should work fine. But if you try to use it with lots of big models, the loading time for that particular scene may take a long time.

The best solution, of course, is the Addressables system. However, it is somewhat harder to implement. So my advice would be:

  • Avoid Resources folder like a plague (unless it is a throwaway prototype)
  • Use ResourceManager in small doses, and with care
  • Use Addressables for memory critical projects

(I don’t have any experience with asset bundles, potentially they could be used instead of Addressable system)

[ORIGINAL POST]

You know the Resources folder right? The place where you can put an asset and instantiate it whenever you need them with the Resources.Load<T>() function. Yeah, don’t use it. “But how should I instantiate the prefabs?” I hear you ask. Don’t worry! I will tell you about a very simple pattern I usually use for such purposes. And you will have a simple object pooling system as a bonus.

The first thing we are gonna need is the ResourceManager class. I will make it a Singleton, using this base class (use Singletons with caution though!). Of course, this class doesn’t have to be a Singleton. You may want to have multiple resource managers for different set of assets.

namespace ExtraTools
{
    public class ResourceManager : Singleton<ResourceManager>
    {
    }
}

Next, we need a class to hold the prefab and a list of instantiated objects.

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

namespace ExtraTools
{
    public class ResourceManager : Singleton<ResourceManager>
    {
        [Serializable]
        private class ObjectHolder
        {
            [SerializeField] private GameObject prefab;
            [SerializeField] private List<GameObject> instantiated = new List<GameObject>();
        }
    }
}

We won’t need the ObjectHolder class outside of the ResourceManager, so I made it a private class. The [Serializable] part is important because we want to see what’s in it from the editor. The [SerializeField] attribute forces Unity to serialize private fields, making them viewable in the inspector. We want to set the prefab from the editor but don’t want anyone to actually access it so it is a perfect use case. Serializing the instantiated list is not mandatory but I will keep it for now. So let’s add a method to actually instantiate the prefab. I will call it Get().

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

namespace ExtraTools
{
    public class ResourceManager : Singleton<ResourceManager>
    {
        [Serializable]
        private class ObjectHolder
        {
            [Tooltip("The object to instantiate"), SerializeField]
            private GameObject prefab;
            [Tooltip("The pool of instantated objects"), SerializeField]
            private List<GameObject> pool = new List<GameObject>();

            /// <summary>
            /// Returns a game object from the pool. Instantiate a new one if none is available
            /// </summary>
            /// <param name="position">Position of the object</param>
            /// <param name="rotation">Rotation of the object</param>
            public GameObject Get(Vector3 position = default(Vector3), Vector3 rotation = default(Vector3))
            {
                for (int i = 0; i < pool.Count; i++)
                {
                    if(!pool[i].activeInHierarchy)
                    {
                        pool[i].transform.position = position;
                        pool[i].transform.rotation = Quaternion.Euler(rotation);
                        pool[i].SetActive(true);
                        return pool[i];
                    }
                }

                pool.Add(Instantiate(prefab, position, Quaternion.Euler(rotation)));
                return pool[pool.Count - 1];
            }
        }
    }
}

Finally, define an ObjectHolder field to the ReourceManager folder like this:

public ObjectHolder testPrefab;

Set the prefab from the inspector and you are good to go!

Instead of instantiating object like this:

Instantiate(prefab);

Instantiate it like this:

ResourceManager.Instance.testPrefab.Get();

Disable the game object whenever you are done with it and it will be ready to be reused.

This pattern will make managing prefabs much easier. In fact, if you drop the Singleton, you can have multiple resource managers for different scenes or states. I will add a couple more functions but this is the core of this pattern. Here is the final version:

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

namespace ExtraTools
{
    public class ResourceManager : Singleton<ResourceManager>
    {
        [Serializable]
        public class ObjectHolder
        {
            [Tooltip("The object to instantiate"), SerializeField]
            private GameObject prefab;
            [Tooltip("The pool of instantated objects"), SerializeField]
            private List<GameObject> pool = new List<GameObject>();

            /// <summary>
            /// Returns a game object from the pool. Instantiate a new one if none is available
            /// </summary>
            /// <param name="position">Position of the object</param>
            /// <param name="rotation">Rotation of the object</param>
            public GameObject Get(Vector3 position = default(Vector3), Vector3 rotation = default(Vector3), Transform parent = null)
            {
                for (int i = 0; i < pool.Count; i++)
                {
                    if (!pool[i].activeInHierarchy)
                    {
                        pool[i].transform.SetParent(parent);
                        pool[i].transform.position = position;
                        pool[i].transform.rotation = Quaternion.Euler(rotation);
                        pool[i].SetActive(true);
                        return pool[i];
                    }
                }

                pool.Add(Instantiate(prefab, position, Quaternion.Euler(rotation)));
                pool[pool.Count - 1].transform.SetParent(parent);
                return pool[pool.Count - 1];
            }

            /// <summary>
            /// Destroyes currently disabled objects
            /// </summary>
            public void DestroyUnused()
            {
                for (int i = 0; i < pool.Count; i++)
                {
                    if(!pool[i].activeInHierarchy)
                    {
                        Destroy(pool[i]);
                        pool.Remove(pool[i]);
                    }
                }
            }

            /// <summary>
            /// Destroyes all objects
            /// </summary>
            public void DestroyAll()
            {
                for (int i = 0; i < pool.Count; i++)
                    Destroy(pool[i]);

                pool.Clear();
            }

            /// <summary>
            /// Instantiates new objects to the pool
            /// </summary>
            /// <param name="count">Number of objects to prepare</param>
            public void Prepare(int count = 0)
            {
                for (int i = 0; i < count; i++)
                {
                    pool.Add(Instantiate(prefab));
                    pool[pool.Count - 1].SetActive(false);
                }
            }
        }

        //A transform to store inactive objects
        private static Transform pool;
        private static Transform Pool
        {
            get
            {
                if (!pool)
                    pool = new GameObject("Pool").transform;

                return pool;
            }
        }

        public ObjectHolder testPrefab;

        /// <summary>
        /// Disables the object and moves it under the Pool object
        /// </summary>
        /// <param name="obj">Object to disable</param>
        public static void Remove(GameObject obj)
        {
            obj.SetActive(false);
            obj.transform.SetParent(Pool);
        }
    }
}

You can download this project from Github.

So what do you think? Is it useful? Is there an alternative solution? Any suggestions? Optimizations? Let me know!

Leave a Reply

Your email address will not be published. Required fields are marked *