Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 459
Confused Avatar asked Nov 08 '18 22:11

Confused


2 Answers

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();
like image 168
Programmer Avatar answered Nov 09 '22 07:11

Programmer


I'm not that pleased with the solution, but combining a nice object pool with a use of simple Dictionary<K, V> yields the following:

// pool of single object type, uses new for instantiation
public class ObjectPool<T> where T : new()
{
    // this will hold all the instances, notice that it's up to caller to make sure
    // the pool size is big enough not to reuse an object that's still in use
    private readonly T[] _pool = new T[_maxObjects];
    private int _current = 0;

    public ObjectPool()
    {
        // performs initialization, one may consider doing lazy initialization afterwards
        for (int i = 0; i < _maxObjects; ++i)
            _pool[i] = new T();
    }

    private const int _maxObjects = 100;  // Set this to whatever

    public T Get()
    {
        return _pool[_current++ % _maxObjects];
    }
}

// pool of generic pools
public class PoolPool
{
    // this holds a reference to pools of known (previously used) object pools
    // I'm dissatisfied with an use of object here, but that's a way around the generics :/
    private readonly Dictionary<Type, object> _pool = new Dictionary<Type, object>();

    public T Get<T>() where T : new()
    {
        // is the pool already instantiated?
        if (_pool.TryGetValue(typeof(T), out var o))
        {
            // if yes, reuse it (we know o should be of type ObjectPool<T>,
            // where T matches the current generic argument
            return ((ObjectPool<T>)o).Get();
        }

        // First time we see T, create new pool and store it in lookup dictionary
        // for later use
        ObjectPool<T> pool = new ObjectPool<T>();
        _pool.Add(typeof(T), pool);

        return pool.Get();
    }
}

Now, you can simply do the following:

pool.Get<A>().SayHello();
pool.Get<B>().Bark();

This however still leaves a room for improvements as it instantiates classes with new rather than your factory method, as well as does not provide a way to customize pool size in a generic way.

like image 3
orhtej2 Avatar answered Nov 09 '22 06:11

orhtej2