Object pooling is a classic optimization technique. The basic theory is simple, creating and destroying things is relatively expensive. This is especially true in a .net based language that uses a Garbage Collector (GC). In the case of Unity if the GC decides it needs to collect up the unused objects it will suspend Unity’s thread while it cleans up. For games that live and die by their frame-rate such a suspension is something that you really want to avoid. Typically when we talk about pooling in Unity we think about GameObjects. Fortunately Unity provides some built in Pooling support.
However, pooling does not have to be confined to visual elements. Another classic optimization for Unity is the WaitForSeconds yield instruction.
In the above code I am ‘new’ing WaitForSeconds instances. This means that .net will allocate them and then the GC will be seeking to eventually clean these up. One solution to this is to create them before-hand. Once cached they will not require additional allocations and the GC cannot release them because the references are still live.
There are a couple of drawbacks with this approach.
- The cached instance is not shared elsewhere. E.g. if 500 other components also want to wait for 4 seconds then we will have 501 cached instances of WaitForSeconds(4f).
- It does not support variable times, e.g. new WaitForSeconds(Random(1, 10))
A Pool of WaitForSeconds
There are different ways around these problems, but the method I chose for Xevious was to create a Pool of WaitForSeconds.
It seems to be a nice solution for the majority of the needs. One potential issue is if you have a large random range where the chance of hitting a preexisting cached instance is low. In this case I would not use this method as the pool will grow too large and put unnecessary pressure on the GC. I.e. we could end up forcing the GC to collect more often, the very thing we wanted to avoid.
Sometimes when you write code you have a dilemma. In this case mine was should I use the in-built pooling or write my own? My problem with the inbuilt methods is that they require the pooled object to have an empty public constructor and guess what, WaitForSeconds does not have one. Now I could get around that this by having a pool of YieldInstruction which WaitForSeconds derives from. But if I do that then it gets a bit muddy because other YieldInstructions do not require a float and I would need to write code to wrap it and…well it just feels a little icky to me.
The net result was I wrote my own.
To drag out the old ‘Peter Parker principle’, pooling is a great power but it is a great responsibility too. It can be used for lots of situations where you find yourself wanting to reuse something rather than continually destroying and recreating it. However, you have to monitor it carefully because it can end up accelerating the problem you were trying to avoid.