How to create Generics Pooling System for components/scripts?
Asked Answered
R

2

7

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?

Rutheruthenia answered 8/11, 2018 at 22:17 Comment(11)
Do BumperFX and LandingFX have a common parent class?Quirinal
Not really. They're both MonoBehaviours, which is a Unity base class sort of thing, but not in the normal sense of common parent class. And I'm not really familiar with hierarchies in C#, so have my entire project as flat. I kind of think this is something I need to learn to overcome, to use generics. I'm coming from Swift.Rutheruthenia
Is the generic pool supposed to be used for Components/Scripts or GameObjects prefabs?Deserved
Hey @Programmer, you legend! The pools are of GameObjects, that often have many sub-objects. The parent usually has the activation components and controller component for the FX that the whole little tree of objects is.Rutheruthenia
In the above example Bounce() is a controller type script on the parent GameObject of LandingFX tree of objects, and it takes those values (from the collision) and uses them to shape, direct and determine amounts of particles, volumes of sounds, and size of other animations around the bounce.Rutheruthenia
If the pool is supposed to store GameObjects, there is really no need to make it generic. Just make a pool system that stores a type of GameObject.....Deserved
Sorry. I don't understand what that means is possible. Or how to even conceive of doing it. Assume extreme ignorance. The kind only prevalent in designers.Rutheruthenia
I wasn't notified on your comment. What I meant is that when you have two objects, for example BulletObject, GrenadeObjects, you can create make the pool take GameObject as param and create pool of BulletObject and GrenadeObjects with two different pool instances. Do you understand me? Should I put code to show you?Deserved
@Deserved Sorry about not @-ing you. And now for my density to display itself. I think you mean that a function that creates pools could take a "GameObject", and that could be any of my objects, as they're all descended from GameObjects, despite their increased complexity and scripts, etc. And a pool can then return a GameObject... which means I then need to get the references to the Script Component on the GameObject in order to call the FX activations... have I got this right? If so, my only concern is how to cache the lookup of the script component for minimal overhead during activation.Rutheruthenia
It will cache the gameobjects not scripts. You can then use GetComponent to get the component from each GameObject in the pool. You just want to cache the scripts so that you don't need GetComponent to access them later on?Deserved
@Programmer, yes. Caching and pooling all done to absolutely avoid garbage collection and get every other performance gain possible. Target = mobile.Rutheruthenia
D
5

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();
Deserved answered 17/11, 2018 at 1:57 Comment(6)
Perhaps unrelated question. Having gotten the component, and using that as the thing to pool and access, is it a big climb up the hierarchy to access the parent transform to position each pooled object when it's needed?Rutheruthenia
Sorry, I don't understand the comment. Can you rephrase it?Deserved
Sure. @Programmer. Every one of these pooled objects I need to position to where they're going to be effective effects. So I need to grab the parent transform, somewhere, and tell it where to go. Is there much overhead in doing that? Or is it just normal "transform.position = goHereDoYourStuff;"?Rutheruthenia
I really don't know about it's performance hit on doing that but it would make sense to change transform.position than the parent because the changing the parent pos will unnecessary change/update all of the child object transform too.Deserved
I was overthinking the transform problem. Was super simple. The component has access to its host's transform... DOH! THANK YOU!!!!Rutheruthenia
I've just realised I can use this for scoring and lap times, too, with a bit of modification. THANK YOU!!! You LEGEND!!!Rutheruthenia
B
3

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.

Beefsteak answered 8/11, 2018 at 22:57 Comment(3)
Sorry for my ignorance (it knows no bounds), what's the problem with the instantiation of classes with new?, and where and how am I using a factory method? I don't even really know how I came up with my way. I understand arrays, and chose to use them. If a pooled object is in use, I simply deactivate it, thereby resetting it, and use it. Kind of like how polyphony on a good keyboard steals the note played earliest when it runs out of polyphony.Rutheruthenia
There's nothing with using new, the thing is that new() requires that type T provides public parameterless constructor. I assumed you're using factory method because of call to _poolOfLndngFX[i] = Instantiate( LndngFX, transform );, where I assume Instantiate is some wrapper around Activator.CreateInstance.Beefsteak
argh. I think I see. No. Instantiate is a Unity system thingy. It makes copies of things, so... yes. Probably is a factory function.Rutheruthenia

© 2022 - 2024 — McMap. All rights reserved.