Generic object pooler implementation for Unity prefabs 2 scripts: 
    - ObjectPooler
    - PoolableObject 

The ObjectPooler acts as a singleton manager of poolable objects. Any prefab can be instantiated using the pooler via:

    GameObject go = GetPooledObject(prefabReference)


You would use this rather than instantiate the object manually. If an object of that prefab type already exists and is inactive, it is returned instead of instantiating a new one. Currently I am unsure if I should SetActive(true) the gameobject when it is returned from the pool, as that would invoke Start() on that GameObject before its values can be updated.

ObjectPooler.cs:

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

/* This class is used for pooling objects and reusing objects for performance
 * This works best when lots of the same object are spawned multiple times
 * such as projectiles from a gun */
public class ObjectPooler : MonoBehaviour
{
    public static ObjectPooler instance;
    public List&lt;PrePooledObjects&gt; prePooledObjects;
    private Dictionary&lt;string, Queue&lt;GameObject&gt;&gt; dict = null;

    [Serializable]
    public struct PrePooledObjects
    {
        public GameObject gameObject;
        public int count;
    }

    void Awake()
    {
        if (instance == null)
        {
            instance = this;
            dict = new Dictionary&lt;string, Queue&lt;GameObject&gt;&gt;();
        }
        else
        {
            Destroy(this);
        }
    }

    private void Start()
    {
        List&lt;GameObject&gt; pooledObjects = new List&lt;GameObject&gt;();
        foreach (PrePooledObjects prePoolObj in prePooledObjects)
        {
            for (int i = 0; i &lt; prePoolObj.count; i++)
            {
                pooledObjects.Add(GetPooledObject(prePoolObj.gameObject));
            }
        }

        foreach (GameObject go in pooledObjects)
        {
            go.SetActive(false);
        }
    }

    public GameObject GetPooledObject(GameObject go)
    {
        if (!dict.ContainsKey(go.name))
        {
            dict.Add(go.name, new Queue&lt;GameObject&gt;());
        }

        if (dict[go.name].Count &gt; 0)
        {
            return dict[go.name].Dequeue();
        }
        else
        {
            GameObject newGo = Instantiate(go);
            PoolableObject po = newGo.GetComponent&lt;PoolableObject&gt;();
            if (po == null)
            {
                po = newGo.AddComponent&lt;PoolableObject&gt;();
            }
            po.prefab = go;
            return newGo;
        }

    }

    public void ReleasePooledObject(PoolableObject po)
    {
        if (!dict.ContainsKey(po.prefab.name))
        {
            dict.Add(po.prefab.name, new Queue&lt;GameObject&gt;());
        }
        dict[po.prefab.name].Enqueue(po.gameObject);
    }
}


PoolableObject.cs:

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

public class PoolableObject : MonoBehaviour
{
    [SerializeField]
    public GameObject prefab = null;

    private void OnDisable()
    {
        if (prefab != null)
            ObjectPooler.instance.ReleasePooledObject(this);
    }
}
```
Interesting code! Am about to write something similar for a quite extreme case (decoration elements for procedurally generated terrain) so I could share a few thoughts I had:

That mechanism for prePooledObjects feels a bit too entwined with the pooling system itself. Why not provide a single function like:

PrePoolObjects(GameObject go, int count);


That would directly prepool a number of objects without the need for that extra struct and all that. Your concept is only necessary if you really want to provide the information what needs to be prePooled a single time and also clear the pool regularly for some reason.

Consider adding more than one new object when the queue is empty. For memory access reasons, it tends to be faster to instantiate multiple instances one after another than to instantiate multiple times at separate times.

Together with the above, it might make sense to shrink the number of pooled objects over time. At least if there are rare moments when a ton of a particular object is needed.

A really, really smart pooling system would evaluate the size of the pool constantly in the background (for example once every 5 physics updates) and ensure that there are not too few and not too many objects held "in stock". Important for that is a hysteresis, because of course you do not want to immediately grow the pool whenever a single instance was removed from it and vice versa. Only when it is too full or too empty for a certain period of time, it would start destroying or creating instances. Such a system would nicely adapt to the current need and not load up the memory when a single level progresses.
Admittedly this concept might be more necessary for my infinite-terrain game than for a game that's based on levels where you can flush the pool when switching scenes.

Last but not least: Using .name and perform that dictionary search every time could be replaced with custom constants that just hold an int and are the indices in an array/list of pool queues.
Then instead of providing the prefab to the GetPooledObject call, you provide that int constant and it takes the relevant queue from the array/list without any lookup operation.
The downside of this concept is the slight inconvenience of not being able to use prefabs immediately but first needing to define that constant. Also that might make using drag and drop with the prefabs more complicated or impossible.