Object Pooling and when to use it?

Hello!

I have recently learned about object pooling and it’s benefits.

My game has lots of bullets and lots of enemies.

The bullets are an obvious candidate for this, and I have started to create a pool system for them, as it’s only one game object with an initializer function to change it’s behaviour based on which weapon fires it.

Would the enemies also be a candidate for this? There can be up to 200 on the screen at a time, so it seems like a large pool? I say this with no knowledge, is 200 a large pool? Would it be reasonable to have a pool of 200 of each enemy type to call from, as it could be any combination of any of the enemies on screen at a time? Currently there are 6 enemy types, 4 of which are “common” meaning a pool of 800?

Would love some insight on this.

Thanks

1 Like

Pooling helps when you need to destroy and recreate a lot of game objects of the same type, in a small period of time, it essentially avoids allocating memory for the new objects, which would need to be freed (Garbage collected) when destroying them, which is a relatively costly operation. So having x number of units on screen per se is not a reason to use pooling at all.

Also Unity offers a robust pooling system starting from v2021, so no need to reinvent the wheel.

And remember, you always need to profile before doing any optimization. The gains could be not worth it at all.

5 Likes

Thanks for the reply.

So the bullet is a candidate as there’s as many as 50+ per second, and they’re only alive for 5 seconds max.

Regarding the enemies in my case, the spawn rate varies from 3 per second to a peak of 78 per second.

Currently instantiated within a loop.

Would this be a candidate?

Another related question, would a prefab containing X enemies be the same as instantiating X enemies? or would it be the same as instantiating just 1 object. Say, a square prefab of 100 enemies surrounding the outside of the cameras view, instantiated.

I’d say 3/s is not too much and don’t need pooling. 78/s is relatively high and could be a candidate for pooling. But as I suggested earlier, never presume, profile! The golden rule in programming is: if it ain’t broken, don’t fix it. Start with a simple Instantiate/Destroy, and if you notice performance problems, profile. If the profiler reveals that the performance problem comes from instantiating/destroying, you can then switch to pooling. To make the transition seamless, just make sure you create one central class that is responsible for managing/creating/destroying enemies/entities, this will make switching on/off pooling very easy:

// Singleton
public class EnemiesManager : MonoBehaviour
{
    private readonly List<GameObject> _enemies = new();

    public IReadOnlyList<GameObject> ActiveEnemies => _enemies; 

    public static EnemiesManager Instance { get; private set; }

    private void Awake()
    {
        Instance = this;
    }

    public GameObject CreateEnemy(GameObject prefab, Vector3 position, Quaternion rotation, Transform parent = null, string name = null)
    {
        // Direct instantiation, no pooling. Later we can switch to pooling if needed
        var enemy = Instantiate(prefab, position, rotation, parent);

        if (!string.IsNullOrEmpty(name))
            enemy.name = name;

        _enemies.Add(enemy);

        return enemy;
    }

    public void DestroyEnemy(GameObject enemy, float delay = 0)
    {
        // Direct destroy, no pooling. Later we can switch to pooling if needed
        Destroy(enemy, delay);

        _enemies.Remove(enemy);
    }
}

You may ask why not using Pooling from the beginning for enemies? well the reason is that pooling introduce some complexities for entities state management. This could not be a problem for simple entities with no advanced state/logic like bullets (fire and forget with just a simple collision detection logic), but for enemies this could lead to some serious, hard to debug problems, since you’ll need to always reset the entity state before reusing it (health, AI state machine, capabilities, animations…), the more complex your enemy is, the more complex will be your state reset. Destroying and recreating enemies won’t have this problem since you always start with a fresh enemy state. That’s why I advised you to always profile and use pooling only when necessary.

For instantiating X enemies vs 1 enemy with X objects, it all boils down to the number of Unity.Object (MonoBehaviours/Components…etc). I don’t think it’ll have a big difference, unless you’re instantiating thousands of objects, in this case you may need to think about switching to ECS alltogether.

6 Likes

This! If you can get away without a pool, do so. They arent just a free improvement to any project. Instead they can cause real problems too, for a usually miniscule benefit. For example, keep in mind that you will have to reset the state of each object returning to the pool. For some things, like the transform, this can easily be automated. More complex objects will need their own clean up function, which you have to remember to adjust whenever you add new fields or want to adjust some related behavior. It’s a quick way to introduce semihard to trace bugs.

The main benefit of object pooling is the removal (or reduction) of garbage collection, which could otherwise cause spikes in frametimes, ie cause jitter or stutter in gameplay. However, there is other ways to smooth these out aswell. Unity has a setting to run garbage collection over multiple frames, for example. Considering this there is a very small amount of cases where object pooling would be the appropriate solution, in my experience. You either dont need it, or smoothing out garbage collection is sufficient, … or it wouldnt be enough anyways as you work with too many objects and should approach the topic using more efficient approaches, such as DOTS.

So when in doubt the most likely answer for whether you should implement an object pool would be “no”. In practice obviously “it depends”, but do definitely profile what is actually causing your issues, and if an object pool even addresses those in the first place. If you dont have an issue, dont implement a solution “just because you can”. It will add problems without solving any. If your issues are not related to uneven frametimes due to garbage collection, but rather the amount of gameobjects unity has to handle (which can quickly become a problem), look into DOTS, which is orders of magnitudes faster (up to factor 100) and doesnt create garbage in the first place. That said, if you dont have to… dont use DOTS either, as it can be a huge pain to work with still. Not a beginner topic either, and the documentation, while improving, still sucks a bit.

4 Likes

There’s a free asset on the store called Master Object Pooler 2. Check that out. No need to re-invent the pool. :wink:

1 Like

When pooling and reusing bullets you can cycle/rotate through an array of bullets like this:

    if (Input.GetButtonDown("Fire1"))
        bullets[bullet++%bullets.Length].transform.position=transform.position;
1 Like

Avoid pooling at all costs. Consider it a last possible resort to be used when you’ve exhausted all your other optimization choices.

Pooling is that bad.

The costs and issues associated with object pooling / pools:

In very rare extremely-high-count object circumstances I have seen small benefits from pooling.

In MOST circumstances, object pooling is a source of complexity, bugs and edge cases while not giving any measurable benefit.

For all performance and optimization issues, ALWAYS start by using the Profiler window:

Window → Analysis → Profiler

Generally optimization is:

  • avoid doing the thing that is slow
  • do it fewer times and store its result
  • do the slow thing less frequently over time
  • do the slow thing when nobody cares much (eg, during level loading)
  • find a faster way to do the thing (hardest)

DO NOT OPTIMIZE “JUST BECAUSE…” If you don’t have a problem, DO NOT OPTIMIZE!

If you DO have a problem, there is only ONE way to find out: measuring with the profiler.

Failure to use the profiler first means you’re just guessing, making a mess of your code for no good reason.

Not only that but performance on platform A will likely be completely different than platform B. Test on the platform(s) that you care about, and test to the extent that it is worth your effort, and no more.

Remember that you are gathering information at this stage. You cannot FIX until you FIND.

Remember that optimized code is ALWAYS harder to work with and more brittle, making subsequent feature development difficult or impossible, or incurring massive technical debt on future development.

Don’t forget about the Frame Debugger window either, available right near the Profiler in the menu system.

Notes on optimizing UnityEngine.UI setups:

At a minimum you want to clearly understand what performance issues you are having:

  • running too slowly?
  • loading too slowly?
  • using too much runtime memory?
  • final bundle too large?
  • too much network traffic?
  • something else?

If you are unable to engage the profiler, then your next solution is gross guessing changes, such as “reimport all textures as 32x32 tiny textures” or “replace some complex 3D objects with cubes/capsules” to try and figure out what is bogging you down.

Each experiment you do may give you intel about what is causing the performance issue that you identified. More importantly let you eliminate candidates for optimization. For instance if you swap out your biggest textures with 32x32 stamps and you STILL have a problem, you may be able to eliminate textures as an issue and move onto something else.

This sort of speculative optimization assumes you’re properly using source control so it takes one click to revert to the way your project was before if there is no improvement, while carefully making notes about what you have tried and more importantly what results it has had.

“Software does not run in a magic fairy aether powered by the fevered dreams of CS PhDs.” - Mike Acton

2 Likes

Interesting stuff. I don’t actually have any issues at the minute, I stumbled across the concept whilst reading about other things and assumed I would be doing myself a favour by implementing it early, evidently not.

Thanks everyone.

2 Likes

I would always use pooling/caching for mobile games. Not pooling will hitch the game constantly. This includes any strings you want to use for UI.

Here’s a pre-allocated List. Unity has a bunch of pool classes for Objects/etc.

1 Like

[u]Hobbyist/Learner’s Advice:[/u] Don’t worry about it until it becomes an obvious problem. It’ll add lots of extra work that could be spent making stuff. Quantity of games made is more important than quality at this point. Quantity IS quality when learning.

Professional Advice: Ultimately if you are constantly destroying and re-creating then you should be pooling. Unity’s garbage collector is ancient - like 1980s technology. By not pooling you are a) consuming more memory than you would otherwise be due to fragmentation and potentially even more if the incremental system can’t keep up with ethe rate of your allocations, b) potentially causing huge lag spikes unless using the new incremental garbage collector c) making future allocations all the slower as the result of the previous two points d) making future collections slower as the result of the above two points. While it’s probably not very likely unless you have a serious runaway issue it could potentially even be possible to enter a death spiral where the rate of allocations outpaces the incremental system so much that it can never free enough memory to keep up. Admittedly that last one is pretty unlikely.

I think most any serious project will try to avoid runtime allocations as much as humanly possible.

6 Likes

Interesting. I’m a software engineer by career title and a graphic designer by trade, so I think I sit somewhere in between, so would your recommendation be to explore pooling? Thanks for the detailed reply.

I am also concerned that this contradicts the above posts, as Kurt said it’s something to avoid at all costs.

Kurt says a lot of things that going strongly against the majority. Whether you agree with it or not is up to you. Ideally you make a decision through your own experience and experimentation.

I’ve only used object pooling a handful of times, but it was definitely necessary in those cases. And it wasn’t really an affair in book-keeping either. Design it well enough and you can make it invisible to the outside user.

var addRequest = ItemAddRequest.GetItemAddRequest(itemInstance);
ItemAddResult result = contents.TryAddItem(addRequest);

if (result.Success == true)
{
    // stuff
}

With something like this, you’d never know that ItemAddRequest is pooled (ItemAddResult is a struct).

5 Likes

The thing a lot of people forget is that Kurt is taking into account the average demographics of the Unity forums userbase. Almost none of the questions posted on these forums are coming from experienced industry professionals and are instead from hobbyists and beginners. The kinds of questions they ask and the answers that serve them best are going to be vastly different what veterans and experts would be asking and looking for.

EDIT: I realize I didn’t actually address your main question: Yes you absolutely will need to understand the concept of pooling. Whether you use it or not is entirely based on what you are making. Doing a turn-based combat system or a card game? Or perhaps a small game with only a handful of persistent characters in the level? Probably don’t need it. Doing a bullet hell? Bullet-hell yeah you’re gonna need it.

In some contexts it’s actually very trivial to implement. For me I literally only need to use one line of code and it just works. Usually the issues get more complex when you have to consider interfacing with 3rd party tools that don’t take your system into consideration: Case in point, I’ve found that most networking libraries for Unity acknowledge pooling but generally it’s always a pain to implement since they really can’t know how I’m specifically going to do it.

One other point that Kurt doesn’t always point out but it can be relevant is overpooling. Counterintuitively, having too many pools that are excessively large can in fact hurt performance. Mostly due to the garbage collector as well as generally just cache-coherency - the exact same very things that pooling can also help with… see how this gets tricky? You could write a whole book on this stuff and every chapter would start with ‘But wait, that things I said last chapter isn’t always the case…’

One interesting point to note is that if you go all-in on Unity’s ECS (admittedly not something I do myself or I would probably recommend to your average developer anyway) this kinda all just goes away since ECS is inherently pooled on the backend anyway. That’s part of how you can get away with creating and destroying so many entities so fast without any real impact. Entities are basically pre-allocated one Chunk at a time and handed out as requested.

6 Likes

Funny you should mention that… just heard of some teammates finding out pooling was gobbling 100mb of runtime RAM (in mobile) because of a subtle implementation detail, not even really a bug, just a detail.

Thanks for that callout Sluggy. I do indeed tailor my message to match what I estimate to be the capabilities of the person asking the question. I think that is just basic Communication101.

4 Likes

Thanks for the replies, all. I’m taking away from this that I should implement it, but to be aware of the potential complexity it could cause. I will likely leave this until I’m further in, as I am not currently experiencing any performance issues, and my computer is getting older now so there’s hope, but with future development I expect the complexity of each scene to increase at least by a factor of 3 or 4, so we shall see.

1 Like

Pooling is often conflated with preloading.

They are not the same.

Preloading involves getting the data that you know you will need out of the bundle and decompressing that data into RAM. That will almost always cause a hitch.

That’s why putting references to everything in your scene guarantees it is loaded all at once with the scene, which is always the biggest hitch.

But you generally don’t care because scene loading happens all at once, and if you want, it happens behind your async loading screen, if need be.

Shoving little bits and bobs around in RAM causes far less hitch, especially just creating fresh instances of GameObjects and MonoBehaviours.

Ok, so if I break this down Kurt maybe you could provide some guidance.

Instantiated:
Upwards of 50 bullet objects per second.
each one instantiating a muzzle flash too

Up to 78 enemies per second.
Each one animated, and each one instantiating a blood particle system and an animated blood decal on death. (the decals are static sprites, animated with a sprite mask).

up to 20 explosions or so per second, 2 particle systems and a circle trigger, (explosive ammo actually causes each bullet to explode too).

I am not getting any issues at the minute, I have 16gb ram and an i7-7700.

I am yet to test it on my laptop properly, although it ran fine when I last tested it, which was before I implemented the blood decal…

It’s a lot of instantiation and destroying, but it’s working…

My professional recommendation: Ship it.

Otherwise, like any performance issue, start with the profiler (Window-> Analysis → Profiler)

If you find a hotspot, don’t knee-jerk reach for pooling. On average object pooling is unlikely to improve your game, and it WILL make your game harder to work with, 100% of the time.

Instead, understand the hotspot and reason about HOW to fix it.

1 Like