Generic Object Pool

I propose generic object pooling functionality. Object pooling aims to alleviate the performance hit of instantiating/destroying many objects by activating/deactivating the objects instead, reusing objects when needed. Several systems could benefit from object pooling: Enemy, projectile, and item spawns, on-demand audio, and on-demand effects.

If a generic object pool were implemented, these systems could have consistent object pooling functionality and would not have to implement system-specific pools.

If this were to become a desired feature, I would like to work on it and would implement something along these lines: https://liamederzeel.com/a-generic-object-pool-for-unity3d/

I would like to hear from other collaborators about whether generic object pool functionality is desirable and what they would like to see in one from the perspective of systems they're envisioning.

Hey, I think this would be useful! A lot of systems use pooling so having a generic solution we can just plug in would be really convenient.

It's a nice idea. The biggest problem in practice i would expect is how to achieve resetting of complex object state in a generic way. Having implemented an object pool for my own game this was the biggest challenge and actually for some classes of object e.g. compound physics objects proved impossible AFAIK.

1 Like

@jamespaterson Do you mean triggering a reset on a pooled object from a generic pool? Or specifically resetting the complex data you were working with? Because if it's the former, you simply restrict the poolable types to those that implement an IPoolable.Reset. If it's the latter, do you have a repo or example I could look at to see?

Hi. Really i refer to the difficulties in resetting some forms of gameobjects, particularly those involving the physics engine. I am afraid i don't have a specific example to hand but here are some examples of things very difficult to reset to an initial condition without destroying and reinstatiating from prefab:

1) multipart objects e.g. an object which fractures (achievable)
2) objects with configurable physics joints (achievable with some weird hacks)
3) an object like (1) where the sub parts have rigidbodies (impossible afaik)

In addition there are challenges around assuming a single atomic reset call can be carried out in a single frame, e.g particle systems which must "die out" and then be returned to pool (not impossible but implies a kind of 'deferred' reset action)

Obviously for (1) to (3) the objects must have been used, e.g. split apart / had forces applied. I suppose really this is an issue with an inability to zero the physics engine more than anything else. But there may well be other examples where resetting to initial state is difficult.

Here's another tricky scenario. Spawning pooled objects on other pooled objects. Imagine you have a pooled enemy in game. A projectile hits the enemy, so you spawn a pooled damage effect e.g. particle system inside the enemies hierarchy to allow it to move with the enemy. Now the enemy dies and is reset. The pooled damage effect needs to be detached and returned to the pool seperately otherwise next time to enemy is used from the pool it will already have the damage effect on it.

1 Like

I think having such a system is a good idea, however, I don't think we need to complicate it too much.
In this game, I can see it being used for AudioSources and Particle effects for sure.

@jamespaterson your example of potential issues are great. We can use them to try and navigate around them. For instance: in the case of particles, it would be good to think of setting the pooled object only if the particle system reached its full playback (?)

So for instance, in this case it would be responsibility of the pool to track and check if all of its particle systems have finished playback. This is arguably "lighter" than having a MonoBehaviour on each particle system that checks itself.

Can we think of these specialised pools?

Also, @drod7425 , I like that tutorial you posted as a base to start with. We can start with something simple and build on it as we go. If anyone else wants to participate, feel free to collab in one PR?

1 Like

Specialized pools was exactly the sort of thing I was hoping to avoid by proposing generic pools. I think the logic for how a poolable object resets should live on the poolable object. Then the pool only has to do one thing - be a pool. I see it as a sort of collection like list. You wouldn't bake logic into List that says when or how T should be added/removed.

However, I agree the issues @jamespaterson listed are things we definitely want to avoid. I've been prototyping some things and I'll certainly be sure to test against these scenarios. Perhaps it will be impossible to keep it generic, but I'd like to give it a try.

3 Likes


Yes, how the object resets... but when is the object available, I think that could totally be in the hands of the pool.

Why are you against specialised pools? They could be inheriting from a base type, and implement their own "CheckIfAvailable".

Anyway, I'll leave it to you!

1 Like

Ah, ok. Yes I agree, absolutely the pool should determine object availability.

Oops, my mistake. For some reason, "specialized pools" triggered some idea of a different architecture not using a generic base. I am not against specialized pools. That sounds like a excellent solution.

Hello all,

I've started a draft PR here: https://github.com/UnityTechnologies/open-project-1/pull/117

Please tear it apart :)

Note: I did not use an override-able CheckIfAvailable method in this first iteration, because the reset callback I implemented to address the "reset state over time" goal kind of naturally lets the pool know when an object is available. If we don't like the callback, I'm more than open to going down that road.

1 Like

Made some changes to the PR that @deivsky suggested. Mostly a paring down of functionality. Pool no longer maintains a reference to unavailable objects, which both simplifies the class a bit and ensures that we don't maintain a reference to an improperly destroyed object, hindering GC. ComponentFactory was pared down considerably and now does the bare minimum of what it needs to do.

Some new features include batch Request and Return for Pool.

I've also been experimenting with ScriptableObject versions of the Pool and Factory. Factory works no problem. Pool has proved a little more complex, but promising.

Please take a look and test it out if this stuff interests you!
https://github.com/UnityTechnologies/open-project-1/pull/117

EDIT: Added experimental scriptable objects as well. I hope to make some progress on them this weekend.

Looks good, just please keep it simple for the first iteration. The readme you provided in the PR is great though.

We should start thinking of this kind of mini-manuals for all systems, so people know how to put them to use! Where should they go?! A wiki on the repo?

2 Likes

Yes, aiming for simple. I promise! Not planning new functionality. Just rethinking how you create/use pools. I think being able to use pools/factories as scriptable object assets is in line with the videos linked in the Conventions doc and will be better for systems in the long run.

After removing the example, this PR should only be 7 files and 188 lines of new code.

As for documentation, I was actually thinking of including a local README in the folder, but a repo wiki would be even better!

I was checking the implementation of the pooling system in the Audio System by elocutura and the problems they had with it, and it made me think IPoolable is doing more harm than good.

The pool doesn't need to know whether something needs to do anything when requested or returned, so there's no need to couple them together. As far as the pool's concerned, we could have a pool of integers with a factory that simply calls Random.Next(). If an object needs to be reset, it can implement some IResettable interface and we can leave the responsibility of resetting it to the pool owner, instead of the pool. Same thing goes with initialization after requesting the object from the pool.

Maybe it isn't more foolproof, because now you have to implement extra steps after requesting and before returning objects to the pool, but I think it's a cleaner design. Like, an object shouldn't need to be explicitly poolable just so it can be pooled, just as it doesn't need to be IListable to be in any IList. What do you think?

1 Like

I'll need to look into it some more once I have some time, but I'm starting to get that feeling. Would remove the need to wrap Unity components and built in types as well.

On a semi-related note, I'm also finding while writing documentation that ComponentFactorySO doesn't bring much to the table while assuming/enforcing a prefab. You could get the same functionality from FactorySO without enforcing prefab and just override to Instantiate. Considering removing.

Semi-related because IPoolable and ComponentFactory are kind of these remnants from the tutorial I started from. We've kind of moved away from that implementation. So maybe it's time to rethink them.

I was going to post some thoughts about it so finally I decided to show you how I do it.

All prefabs that are clients of pooling must have component derived from IPoolClientSiteComponent in its root transform. This component responsibility is, being a site for IPoolClientComponent , Releasing (object to the pool client) , and Reseting object (refreshing when retrieved from pool, objects in pool are dirty when waiting on a pool).

Usually component deriving from IPoolClientSiteComponent should destroy object instead of returning it to the pool when it not poses valid PoolClient (inside) this lets you use object even without pooling.
The only thing that needs to be remembered is that you do not destroy object by using Object.Destroy but by calling GameObject.Get().Release().

Now IPoolClientComponent is a connection between IPoolClientSiteComponent and IPoolComponent.
It got only two members Release and ResetObject.

Lastly IPoolComponent this component is responsible for actually managing the pool.
It holds pooled objects and allows you to retrieve them. It got two members.

public bool TryAcquireObject(GameObject prefab, out GameObject go)
public void RegisterObject(GameObject prefab, GameObject go)

So before instantiating object you "TryAcquireObject"
if it failed you instantiate object and call RegisterObject.
Argument "prefab" of this methods let's you identify what kind of object we want to obtain, so we can have multiple types of pooled object on one pool.

So to finish it with a short exemplar story:

  • We want to spawn Bullets from our Rifle.

  • Create our concrete PoolComponent on our Rifle. When object is pooled it will disable the object and parent it, so when we destroy pool all pooled objects are going to be destroyed with it. (but it also might be global pool shared between rifles)

  • When firing Rifle first check if it can acquire Bullet from pool if not it will PoolComponent.RegisterObject.

During registeration IPoolComponent adds it's dedicated concrete IPoolClientComponent to Bullet. ()
Than search for IPoolClientSiteComponent and do IPoolClientSiteComponent.SetPoolClient(IPoolClientComponent).

If object was Acquired from pool than registration was already made and we only have to place object when we want and call GameObject.SetActive(true). We do not worry about reseting object, PoolComponent should call IPoolClientComponent.ResetObject (during acquiring) and this should call IPoolClientSiteComponent.ResetObject this is why IPoolClientComponent should know what IPoolClientSiteComponent it is connected to.

public abstract class IPoolClientSiteComponent : MonoBehaviour{

    public void SetPoolClient(IPoolClientComponent poolClient) {
        OnSetPoolClient(poolClient);
    }

    protected abstract void OnSetPoolClient(IPoolClientComponent poolClient);


    public IPoolClientComponent GetPoolClient() {
        return OnGetPoolClient();
    }

    protected abstract IPoolClientComponent OnGetPoolClient();


    public void ResetObject() {
        OnResetObject();  
    }

    protected abstract void OnResetObject();



    public void Release() {
        OnRelease();
    }

    protected abstract void OnRelease();


}
public abstract class IPoolClientComponent : MonoBehaviour{

    public void ResetObject() {
        OnResetObject();
    }

    protected abstract void OnResetObject();


    public void Release() {
        OnRelease();
    }

    protected abstract void OnRelease();

}
public abstract class IPoolComponent : MonoBehaviour
{

    public bool TryAcquireObject(GameObject prefab, out GameObject go) {
        return OnTryAcquireObject(prefab, out go);
    }

    protected abstract bool OnTryAcquireObject(GameObject prefab, out GameObject go);



    public void RegisterObject(GameObject prefab, GameObject go) {
        OnRegisterObject(prefab, go);
    }

    protected abstract void OnRegisterObject(GameObject prefab, GameObject go);

}

I know it sounds quite convoluted and might be hard to swallow but it is quite flexible system.
As you can see I have not posted concrete implementations I don't want to make this post even longer.

@deivsky Alright, I've submitted a PR to remove IPoolable and ComponentFactorySO: https://github.com/UnityTechnologies/open-project-1/pull/159

Hey @koirat , glad to see other ideas in this thread. I am having a bit of a struggle figuring out what that code is doing though. I think it might be helpful to post a concrete implementation if you've got one (a gist or a branch on github if it's too long to post here?).

I have a few questions:

  • Why are you using interface naming convention for abstract classes?
  • What would the advantage be to having dedicated pool components over the current scriptable object variant?
  • Why does there need to be a IPoolClientComponent layer between IPoolClientSiteComponent and IPoolComponent? (this might be answered by a concrete implementation)
  • W̶h̶a̶t̶ ̶w̶o̶u̶l̶d̶ ̶t̶h̶e̶ ̶a̶d̶v̶a̶n̶t̶a̶g̶e̶ ̶b̶e̶ ̶t̶o̶ ̶t̶h̶i̶s̶ ̶o̶p̶t̶i̶o̶n̶ ̶o̶v̶e̶r̶ ̶t̶h̶e̶ ̶c̶u̶r̶r̶e̶n̶t̶ ̶i̶m̶p̶l̶e̶m̶e̶n̶t̶a̶t̶i̶o̶n̶?̶ ̶A̶l̶w̶a̶y̶s̶ ̶o̶p̶e̶n̶ ̶t̶o̶ ̶n̶e̶w̶ ̶i̶d̶e̶a̶s̶,̶ ̶b̶u̶t̶ ̶n̶o̶t̶ ̶y̶e̶t̶ ̶s̶e̶e̶i̶n̶g̶ ̶s̶o̶m̶e̶t̶h̶i̶n̶g̶ ̶h̶e̶r̶e̶ ̶t̶h̶a̶t̶ ̶t̶h̶e̶ ̶c̶u̶r̶r̶e̶n̶t̶ ̶i̶m̶p̶l̶e̶m̶e̶n̶t̶a̶t̶i̶o̶n̶ ̶d̶o̶e̶s̶n̶'̶t̶ ̶d̶o̶.̶ Ah I think I see now. Looks like you trade having to write Factory/Pool SOs for each new type for having to GetComponent to get the target type.
  1. I know it breaches a little the c# convention but since I cannot use interface for component I'm simulating it with abstract class derived from MonoBehaviour. I could use some different prefix but I decided to use "I" in the end. When I see I*****Component than I'm 100% sure it is an abstract class and the most generic one for particular set of types.

  2. Dedicated pool component might implement almost any kind of pooling system. You may create pool component that will use ScriptablerObject pooling for example. It is a matter of flexibility.
    Now when you got PoolComponent you also get events like OnDestory.

It is also more convenient for my own system where I use special InstantiatorComponent instead of raw GameObject.Instantiate. This instantiators can modify prefab (creating intermediate prefab) before final instantiation so I cannot have one shared pool for one type of prefab.

  1. Theoretically we could do it without it. And just connect IPoolCompoent with IPoolClientSiteComponent directly but making it current way gives us some advantage. First of all "contract" IPoolClientSiteComponent does not need to have direct access to IPoolComponent. Also when we add IPoolClientComponent to created object we get access to events like OnDestroy etc..

  2. Yes I'm not writing any specified object for types, my system can swallow any prefab. You can have random list of prefabs and use the same PoolComponent for all of them.

@drod7425 I might be misunderstanding something, but why the method InitializePool is private?
I see it's called by the pool itself the first time a Request is called. But this defeats the purpose of the pool: now in the middle of the game I want an object from the pool, and instead of instantiating one I am instantiating the whole size of the pool in one frame.

Logically I'd expect that whoever is responsible for the pool (in my case, the AudioManager) pre-warms the pool by calling InitializePool at the beginning of the game/scene, etc.