Search code examples
c#unity-game-enginegenericspooling

How to create Generics Pooling System for components/scripts?


My awareness of Generics is that they can help me streamline my pooling, but can't figure out how.

My pooling system is minimalistic, but messy. And now getting unwieldy and messy, and MESSY. It doesn't scale nicely...

My FXDistribrutor.cs class is a component attached to an object in the initial scene, designed to permanently exist throughout all scenes of the game. It has a static reference to itself, so that I can call into it from anywhere easily. More on that contrivance at the end. I'm not even sure if that's the 'right' way to do this. But it works nicely.

FXDistributor has a public slot for each type of FX Unit it is able to distribute, and an array for the pool of this type of FX, and an index for the array and a size of the pool.

Here are two examples:

    public BumperFX BmprFX;
    BumperFX[] _poolOfBumperFX;
    int _indexBumperFX, _poolSize = 10;

    public LandingFX LndngFX;
    LandingFX[] _poolOfLndngFX;
    int _indexLndngFX, _poolSizeLndngFX = 5;

In the Unity Start call, I fill the pools of each FX Unit:

void Start(){

    _poolOfBumperFX = new BumperFX[_poolSize];
    for (var i = 0; i < _poolSize; i++) {
    _poolOfBumperFX[i] = Instantiate(BmprFX, transform );
    }

    _poolOfLndngFX = new LandingFX[_poolSizeLndngFX];
    for ( var i = 0; i < _poolSizeLndngFX; i++ ) {
    _poolOfLndngFX[i] = Instantiate( LndngFX, transform );
    }
}

And in the body of the class I have a bunch of methods for each FX type, to provide them to wherever they're needed:

public LandingFX GimmeLandingFX ( ){
    if ( _indexLndngFX == _poolSizeLndngFX ) _indexLndngFX = 0;
    var lndngFX = _poolOfLndngFX[_indexLndngFX];
    _indexLndngFX++; return lndngFX;
}
public BumperFX GimmeBumperFX ( ) {
    if ( _indexBumperFX == _poolSize ) _indexBumperFX = 0;
    var bumperFX = _poolOfBumperFX[_indexBumperFX];
    _indexBumperFX++;   return bumperFX;
}

So when I want one of these FX, and to use it, I call like this, from anywhere, into the static reference:

    FXDistributor.sRef.GimmeLandingFX( ).Bounce(
            bounce.point,
            bounce.tangentImpulse,
            bounce.normalImpulse 
            );

How do I streamline this approach with Generics so I can easily and less messily do this sort of thing for a couple of dozen types of FX Units?


Solution

  • In Unity, the Instantiate() and Destroy() functions are used to create copy of objects especially prefabs and destroy them. When it comes to pooling, the pool object is usually represented in the pool as a Type of GameObject. When you need to access a component from the pool you first retrieve the pool GameObject then use the GetComponent function to retrieve the component from a GameObject.


    Reading your question and comments carefully, you want to avoid the GetComponent section and represent just the components not the GameObject so that you can also access the components directly.

    If this is what you want then this is where Unity's Component is required. See below for steps required to do this.

    Note that when I say component/script, I am referring to your scripts that derive from MonoBehaviour that can be attached to GameObjects or built-in Unity components such as Rigidbody and BoxCollider.

    1. Store the components/scripts to a List of Component.

    List<Component> components;
    

    2. Store the List of Components in a Dictionary with Type as the key and List<Component> as the value. This makes it easier and faster to group and find components by Type.

    Dictionary<Type, List<Component>> poolTypeDict;
    

    3. The rest is really easy. Make the function that adds or retrieves the pool items from and to the Dictionary to be generic then use Convert.ChangeType to cast between the generic type to Component type or from generic to what ever type that is requested to be returned.

    4. When you need to add item to the Dictionary, check if the Type exist yet, if it does, retrieve the existing key, create and add new Component to it with the Instantiate function then save it to the Dictionary.

    If the Type doesn't exist yet, no need to retrieve any data from the Dictionary. Simply create new one and add it to the Dictionary with its Type.

    Once you add item to the pool de-activate the GameObject with component.gameObject.SetActive(false)

    5. When you need to retrieve an item from the pool, check if the Type exist as key then retrieve the value which is List of Component. Loop over the components and return any component that has a de-activated GameObject. You can check that by checking if component.gameObject.activeInHierarchy is false.

    Once you retrieve item from the pool activate the GameObject with component.gameObject.SetActive(true)

    If no component is found, you can decide to either return null or instantiate new component.

    6. To recycle the item back to the pool when you're done using it, you don't call the Destroy function. Simply de-activate the GameObject with component.gameObject.SetActive(false)*. This will make the component able to be found next time you search for available components in the Dictionary and List.

    Below is an example of minimum generic pool system for scripts and components:

    public class ComponentPool
    {
        //Determines if pool should expand when no pool is available or just return null
        public bool autoExpand = true;
        //Links the type of the componet with the component
        Dictionary<Type, List<Component>> poolTypeDict = new Dictionary<Type, List<Component>>();
    
        public ComponentPool() { }
    
    
        //Adds Prefab component to the ComponentPool
        public void AddPrefab<T>(T prefabReference, int count = 1)
        {
            _AddComponentType<T>(prefabReference, count);
        }
    
        private Component _AddComponentType<T>(T prefabReference, int count = 1)
        {
            Type compType = typeof(T);
    
            if (count <= 0)
            {
                Debug.LogError("Count cannot be <= 0");
                return null;
            }
    
            //Check if the component type already exist in the Dictionary
            List<Component> comp;
            if (poolTypeDict.TryGetValue(compType, out comp))
            {
                if (comp == null)
                    comp = new List<Component>();
    
                //Create the type of component x times
                for (int i = 0; i < count; i++)
                {
                    //Instantiate new component and UPDATE the List of components
                    Component original = (Component)Convert.ChangeType(prefabReference, typeof(T));
                    Component instance = Instantiate(original);
                    //De-activate each one until when needed
                    instance.gameObject.SetActive(false);
                    comp.Add(instance);
                }
            }
            else
            {
                //Create the type of component x times
                comp = new List<Component>();
                for (int i = 0; i < count; i++)
                {
                    //Instantiate new component and UPDATE the List of components
                    Component original = (Component)Convert.ChangeType(prefabReference, typeof(T));
                    Component instance = Instantiate(original);
                    //De-activate each one until when needed
                    instance.gameObject.SetActive(false);
                    comp.Add(instance);
                }
            }
    
            //UPDATE the Dictionary with the new List of components
            poolTypeDict[compType] = comp;
    
            /*Return last data added to the List
             Needed in the GetAvailableObject function when there is no Component
             avaiable to return. New one is then created and returned
             */
            return comp[comp.Count - 1];
        }
    
    
        //Get available component in the ComponentPool
        public T GetAvailableObject<T>(T prefabReference)
        {
            Type compType = typeof(T);
    
            //Get all component with the requested type from  the Dictionary
            List<Component> comp;
            if (poolTypeDict.TryGetValue(compType, out comp))
            {
                //Get de-activated GameObject in the loop
                for (int i = 0; i < comp.Count; i++)
                {
                    if (!comp[i].gameObject.activeInHierarchy)
                    {
                        //Activate the GameObject then return it
                        comp[i].gameObject.SetActive(true);
                        return (T)Convert.ChangeType(comp[i], typeof(T));
                    }
                }
            }
    
            //No available object in the pool. Expand array if enabled or return null
            if (autoExpand)
            {
                //Create new component, activate the GameObject and return it
                Component instance = _AddComponentType<T>(prefabReference, 1);
                instance.gameObject.SetActive(true);
                return (T)Convert.ChangeType(instance, typeof(T));
            }
            return default(T);
        }
    }
    
    public static class ExtensionMethod
    {
        public static void RecyclePool(this Component component)
        {
            //Reset position and then de-activate the GameObject of the component
            GameObject obj = component.gameObject;
            obj.transform.position = Vector3.zero;
            obj.transform.rotation = Quaternion.identity;
            component.gameObject.SetActive(false);
        }
    }
    

    USAGE:

    It can take a any prefab component script. Prefabs are used for this since pooled objects are usually prefabs instantiated and waiting to be used.

    Example prefab scripts (LandingFX, BumperFX) :

    public class LandingFX : MonoBehaviour { ... }
    

    and

    public class BumperFX : MonoBehaviour { ... }
    

    Two variables to hold the Prefabs references. You can either use public variables and assign them from the Editor or load them with the Resources API.

    public LandingFX landingFxPrefab;
    public BumperFX bumperFxPrefab;
    

    Create new Component Pool and disable auto-resize

    ComponentPool cmpPool = new ComponentPool();
    cmpPool.autoExpand = false;
    

    Create 2 pools for LandingFX and BumperFX components. It can take any component

    //AddPrefab 2 objects type of LandingFX
    cmpPool.AddPrefab(landingFxPrefab, 2);
    //AddPrefab 2 objects type of BumperFX
    cmpPool.AddPrefab(bumperFxPrefab, 2);
    

    When you need a LandingFX from the pool, you can retrieve them as below:

    LandingFX lndngFX1 = cmpPool.GetAvailableObject(landingFxPrefab);
    LandingFX lndngFX2 = cmpPool.GetAvailableObject(landingFxPrefab);
    

    When you need a BumperFX from the pool, you can retrieve them as below:

    BumperFX bmpFX1 = cmpPool.GetAvailableObject(bumperFxPrefab);
    BumperFX bmpFX2 = cmpPool.GetAvailableObject(bumperFxPrefab);
    

    When you're done using the retrieved component, recycle them back to the pool instead of destroying them:

    lndngFX1.RecyclePool();
    lndngFX2.RecyclePool();
    bmpFX1.RecyclePool();
    bmpFX2.RecyclePool();