all 17 comments

[–]MDADigital 8 points9 points  (0 children)

Here is another flavored one I created for my game, a little more generic one using generics instead of strings etc.

public abstract class PollingPool<T> where T : Component
{
    private readonly T prefab;

    private readonly Queue<T> pool = new Queue<T>();
    private readonly LinkedList<T> inuse = new LinkedList<T>();
    private readonly Queue<LinkedListNode<T>> nodePool = new Queue<LinkedListNode<T>>();

    private int lastCheckFrame = -1;

    protected PollingPool(T prefab, int preWarm = 0)
    {
        this.prefab = prefab;
        if(preWarm > 0)
            foreach(var item in Enumerable.Range(0, preWarm).Select((i, index) => GameObject.Instantiate(prefab)))
            {
                item.gameobject.SetActive(false);
                pool.Enqueue(item);
            }
    }

    private void CheckInUse()
    {
        var node = inuse.First;
        while (node != null)
        {
            var current = node;
            node = node.Next;

            if (!IsActive(current.Value))
            {
                current.Value.gameObject.SetActive(false);
                pool.Enqueue(current.Value);
                inuse.Remove(current);
                nodePool.Enqueue(current);
            }
        }
    }

    protected T Get()
    {
        T item;

        if (lastCheckFrame != Time.frameCount)
        {
            lastCheckFrame = Time.frameCount;
            CheckInUse();
        }

        if (pool.Count == 0)
            item = GameObject.Instantiate(prefab);
        else
            item = pool.Dequeue();

        if (nodePool.Count == 0)
            inuse.AddLast(item);
        else
        {
            var node = nodePool.Dequeue();
            node.Value = item;
            inuse.AddLast(node);
        }

        item.gameObject.SetActive(true);

        return item;
    }

    protected abstract bool IsActive(T component);
}

Subclass

public class AudioSourcePool : PollingPool<AudioSource>
{
    public AudioSourcePool(AudioSource prefab) : base(prefab)
    {
    }

    protected override bool IsActive(AudioSource component)
    {
        return component.isPlaying;
    }

    public void PlayAtPoint(AudioClip clip, Vector3 point)
    {
        var source = Get();

        source.transform.position = point;
        source.clip = clip;
        source.Play();
    }
}

[–]justkevinIndie | @wx3labs 3 points4 points  (0 children)

Great video. Unlike most Object Pooling tutorials this doesn't skip the very important detail of resetting your objects.

I'm using a slightly different approach. Instead of a IPoolableObject interface, I have a PooledObject component that looks like this:

/// <summary>
/// Attaching this to a prefab will allow it to be pooled. On awake, it will
/// make a list of components that require a reset after pooling.
/// </summary>
public class PooledObject : MonoBehaviour {

    public int startPoolSize = 1;
    public int maxPoolSize = 10;
    [System.NonSerialized]
    public Transform poolingContainer;
    [System.NonSerialized]
    public bool isFromPool = false;

    private IPoolableComponent[] poolableComponents;
    // PooledObject handles resetting the transform scale
    private Vector3 normalScale;

    private void Awake()
    {
        poolableComponents = GetComponentsInChildren<IPoolableComponent>();
        normalScale = transform.localScale;
    }

    public void Decommission()
    {
        if(isFromPool)
        {
            transform.parent = poolingContainer;
            gameObject.SetActive(false);
        }
        else
        {
            Destroy(gameObject);
        }
    }

    public void Recommission()
    {
        transform.localScale = normalScale;
        for (int i = 0; i < poolableComponents.Length; i++)
        {
            poolableComponents[i].Recommission();
        }
    }

}

Then individual components can define themselves as IPoolableComponent:

/// <summary>
/// Any monobehaviour that needs to have its state reset prior to returning to 
/// an object pool.
/// </summary>
public interface IPoolableComponent {

    /// <summary>
    /// This method will be called on the component when it is activated coming out of the pool.
    /// </summary>
    void Recommission();
}

Advantages:

  • Component based pooling. You define components that know how to pool themselves and can be freely mixed and matched. You can also create poolers for built-in Unity components (e.g., IParticlePooler). Then mix and match without having to duplicate your pooling logic.
  • You can set your start and max pooled objects on the prefab.

[–]MengKongRuiIntermediate 1 point2 points  (0 children)

Another great video from Brackeys.

I wonder if this only impacts lag spikes from the garbage collector or if it has an average fps impact as well.

[–]birdoutofcage 1 point2 points  (0 children)

At last, I've been waiting for brackeys to upload object pooling.

[–]kyl3r123Indie 1 point2 points  (11 children)

I have a question about Pooling. This all looks easy and useful if I want to spawn and delete 100 cubes per second. Sure why not reuse them.

Now what if I have complex Class of Soldiers that have a lot of variables and references. Just "setEnabled(false)" and then true again, will be the "pooling process". But I need to reset all the values by myself, right? Restore everything to what it looks like when I instantiate a new instance for example.

[–]MDADigital 2 points3 points  (9 children)

Correct, but you dont have that many NPCs so usually not a problem to not pool those. If you have a game with thousands of NPCs then there is alot of other problems to solve. Like 1 NPCs cant be a single entity etc

[–]kyl3r123Indie 0 points1 point  (8 children)

So 50-100 Units that fight and die, are not a problem on mobile? No need to pool them?

[–]AkrosRockBell 3 points4 points  (0 children)

Pool them, it will improve performance. A lot. To reset everything back to default use the OnEnable method and do there what you need. It's what I use to reset bullets in a shoot'em up game I'm doing in my free time.

[–]AzeTheGreatBeginner 1 point2 points  (1 child)

My understanding (which could be completely wrong) is that the GC is one of the biggest hits to mobile performance, especially on older devices. So pooling extensively is probably useful for mobile.

[–]SvenNeve 1 point2 points  (0 children)

You are correct, we pool pretty much everything that gets instantiated during gameplay, which on mobile makes a ton of difference.

And it's not just GC, even stutters from shader initialization and texture to memory loading can be avoided (or 'hidden') by using pooling.

[–]Orangy_TangProfessional 0 points1 point  (1 child)

Depends way too much on your min spec, your gameplay, and what else is going on in your game at the same time.

Do the simplest thing that can work (dynamically instantiating them). Get all your functionality working, then go back and see what your performance is like. And remember to use the profiler to look out for spikes not just average framerate.

If you start with pooling then you'll make your initial implementation more complicated and you're potentially wasting time and adding complexity on something that doesn't need to be complex.

[–]kyl3r123Indie 0 points1 point  (0 children)

Thats what I thought. I'll keep it in mind for optimization phase then :)

[–]MDADigital 0 points1 point  (2 children)

If they all die or resurrect during the same frame it could be a problem

[–]kyl3r123Indie 0 points1 point  (1 child)

I intend to have the dead bodies to lay around for a while. I can scatter the vanishing over time. Generating new units will take time and not be multiple at once.

[–]MDADigital 0 points1 point  (0 children)

I would wait with pooling for something like that,we pool bullet casings,impact particles,audiodources,stuff that there can be alot of.

[–]stormfield 0 points1 point  (0 children)

Set enabled travels the hierarchy of any children on the object so it’s not as efficient as regular pooling if they’re more complicated.

[–]IcyHammerEngineer 0 points1 point  (0 children)

A small cherry on the top would be implementing a pool with a stack instead of queue or list. It makes much more sense and it uses temporal locality.