Introduction to Object Pools

a guide written by tkcmdr

Hey there!

As I worked on this year's Game Jam submission, a buddy of mine noted that if you started the game too quickly, objects would be spawned, but you couldn't see them for quite some time. The result was a very abrupt death at the hands of cloaked UFOs -- oh no!

A problem you might often find yourself running into on particularly demanding games is that items do not load quite as fast as they ought to. Large objects like battleships, buildings, and so on have to be loaded completely from scratch. You might find this to be a problem even for smaller things; such is especially true on mobile devices, where performance is notably diminished. In both cases, few optimizations have a more noticeable effect than that of object pools.

What are object pools?

Object pools are collections of things usually meant to be loaded onto the game at high frequencies. They’re initialized as the game loads, well before gameplay begins. The idea is simple: the objects are already loaded onto memory, they just aren’t visible in the game yet. When the time comes to use them, you just flip a switch and they’re there. Obviously, it’s slightly more complicated than that, but you get the idea. Object pools save time and make your game feel more responsive.

Good examples of situations where object pools are advantageous might be when spawning bullets and spent cartridges for firearms, large objects such as vehicles when quick loading is pertinent, or even TextLabels meant to display point gains during intense fights.

How do I implement them?

To implement object pooling, we must first create all the objects we need and save references to them. We might do so by writing the following script:

local Prefab = path.To.Prefab
local PoolFolder = Instance.new("Folder")
PoolFolder.Parent = game.ReplicatedStorage

local ObjectPool = {}

-- Returns a table that can be used to manage the object and its state
local function GetObjectManager(object)
    assert(typeof(object) == "Instance", "First argument of GetObjectManager invalid!")

    return
    {
        Active = false;
        Instance = clone;

        -- Sets self.Active to false if parent is ObjectPool, then sets self.Instance.Parent accordingly.
        SetParent = function(self, parent)
            assert(parent and typeof(parent) == "Instance", "First argument of SetParent invalid!")

            self.Active = parent == ObjectPool

            self.Instance.Parent = parent
        end;

        -- Calls self:SetParent, supplying PoolFolder as the parent.
        Deactivate = function(self)
            assert(typeof(self.Instance) == "Instance", "Instance property not set to an instance!")

            self:SetParent(PoolFolder)
        end;
    }
end

for i = 1, 30 do
    local clone = Prefab:Clone()
    clone.Parent = PoolFolder

    table.insert(ObjectPool, GetObjectManager(clone))
end

Alright! Pretty sweet. We now have a little system that creates 30 copies of a prefabricated object and then references them in tables that store the objects themselves as well as their status. They even have their own little Deactivate and SetParent methods to streamline usage.

It is worth noting that the pool folder was parented to ReplicatedStorage. That was a deliberate decision; objects in ReplicatedStorage are stored on the server and then replicated to each client, which makes it ideal for storing objects in a more general object pool. If you need only for objects in a certain pool to be kept on the clients or server, you should use ReplicatedFirst and ServerStorage, respectively.

Back to the code, we might retrieve an instance from a pool by doing something like to this:

-- Iterates over the object pool to find an inactive object
local function GetFromObjectPool(pool)
    for k, object in pairs(pool) do
        if not object.Active then
            return object
        end
    end
end

local myPrefab = GetFromObjectPool(ObjectPool)

-- In the case that all prefabs are currently in use, myPrefab is nil!
if myPrefab then
    myPrefab:SetParent(game.Workspace)

    -- ... and when you're done, you can simply do this:

    myPrefab:Deactivate() -- Poof! Gone for now!
end

Implementing such a system wouldn't take long in most applications and can be done in a variety of ways. All said and done, object pooling might vastly improve your game's performance. Next time you find yourself being wasted by invisible UFOs, remember to use object pooling to save the day!

  • tkcmdr