Cleaning Up Event Listeners

A common challenge I’ve seen in building bigger and more complex games is how event listeners can build up and affect performance. It’s not necessarily the number of event listeners, but more about what each of those event listeners is doing. A relatively fresh server instance will perform smoothly at first, but after players have been active in it for a long time, it can start to get sluggish due to the build-up of event listeners.

Event listener buildup isn’t the only way that this can happen, so I recommend using the Performance Profiler to see if this may be affecting your game.

Listeners

A game can have listeners that listen for specific events that are happening in your game. Listeners subscribe to events and so don’t actively listen for anything. The listener function gets called when the event fires. For example, listening for when a player joins the game or listening for when a specific event gets broadcasted.

local function OnPlayerJoined(player)

	-- playerJoinedEvent fired

end

local function OnMyEvent()

	-- myevent fired

end

Game.playerJoinedEvent:Connect(OnPlayerJoined)
Events.Connect("myevent", OnMyEvent)
Code language: Lua (lua)

As your game systems get bigger and more complex, then listeners can build up, and this is fine. The problem is listeners will hang around even if the script that setup the listener gets destroyed. For the most part, this isn’t a big deal, but it can cause issues by introducing undesired side effects and become a potential performance problem for your game.

For example, in Overrun, a pod can drop and give buffs to the zombies (for example, damage, or health). Periodically the pod script would broadcast to the zombies and give them random buffs that could stack. When a pod got destroyed by players, zombies were not killable because they continued to receive buffs. This was because of a broadcast listener that was still getting called even though the script didn’t exist.

As someone who was new to Core at the time, it confused me, and it wasn’t until I placed a print statement inside the broadcast that I figured out the issue and how to solve it.

Example Problem

In this example, a damageable object has a child script listening for when the broadcast event alive gets fired. For any crate that is still alive, the player receives 100 money when the player interacts with the console. Only 1 crate should ever be alive. So each time the player uses the console, they should get awarded 100 money.

local crate = script:GetCustomProperty("DamageableCrate")

script.parent.diedEvent:Connect(function()
    local pos = script.parent:GetWorldPosition()
    
    Task.Wait(.5)
    World.SpawnAsset(crate, { position = pos })
end)

Events.Connect("alive", function()
    Events.Broadcast("give_money")
end)
Code language: Lua (lua)

The script above is the child script of the damageable object. The important part is the event connection at the bottom of the script. It’s listening for the alive event. When the event gets fired, it will be broadcast to another script and award money to the player.

See the video below of it in action, and watch the amount of money that gets added when the console gets activated. You may expect the total money to go up by 100 each time.

So What is the Problem?

The problem is when the object (along with the child script) gets destroyed, the listener is still subscribed to the alive event. This means the listener function will continue to get called.

Solving the Problem

The easiest way to solve the problem above is to disconnect the event when we know it is no longer needed. So when a crate gets destroyed, we know that the event should not fire, and should get disconnected.

local crate = script:GetCustomProperty("DamageableCrate")

script.parent.diedEvent:Connect(function()
    local pos = script.parent:GetWorldPosition()

    Task.Wait(.5)
    World.SpawnAsset(crate, { position = pos })
end)

local alive_evt = Events.Connect("alive", function()
    Events.Broadcast("give_money")
end)

script.destroyEvent:Connect(function()
    if(alive_evt.isConnected) then
        alive_evt:Disconnect()
    end
end)
Code language: Lua (lua)

The script has been updated so the EventListener returned from connecting the alive event is stored in the variable alive_evt so we have a reference to it later on.

script.destroyEvent:Connect(function()
    if(alive_evt.isConnected) then
        alive_evt:Disconnect()
    end
end)
Code language: Lua (lua)

When the script is destroyed, we can then check if the alive_evt is connected, if so, disconnect it. In doing so, we clean up the broadcast listener. Now when using the console, it will only award the player 100 money, which is now correct.

Solution

If an object that the event is on gets destroyed, then those events will get disconnected for you. However, in other cases, then you will need to handle the disconnect yourself. It’s good practice to get into the habit of cleaning up your event listeners yourself.

Events that can connect a listener, will return an EventListener that can be used for disconnecting later.

local my_event = Events.Connect("some_event", some_func)

if(my_event.isConnected) then
    my_event:Disconnect()
end
Code language: Lua (lua)

There may be cases where you want to disconnect a listener from within the listener function. In this case, you need to handle storing the EventListener a little differently by having a global variable that can be referenced from within the listener function.

local my_event

my_event = Events.Connect("some_event", function()
    if(my_event.isConnected) then
        my_event:Disconnect()
    end
end)
Code language: Lua (lua)

Disconnecting Multiple Events

When you have a lot of event listeners, a good way to store them is in a table and then loop over them when they need to be disconnected.

local my_events = {}

-- Using table insert to add a new item to the table

table.insert(my_events, Events.Connect("some_event", some_event))
table.insert(my_events, Events.Connect("some_other_event", some_other_event))
table.insert(my_events, Events.Connect("other_event", other_event))

script.destroyEvent:Connect(function()
    for index, evt in ipairs(my_events) do
        if(evt.isConnected) then
            evt:Disconnect()
        end
    end

    my_events = nil
end)
Code language: Lua (lua)

The above example will push all event listeners into the my_events table, and when the script is destroyed, loop through the table and disconnect them. This is a good way to handle a large number of events that you may have in a script.

Summary

Get in the habit of disconnecting your events. As your project gets bigger and more complex, strange behavior or bugs may occur when listeners get left hanging around.

If you have any questions, feedback, or additional information that other creators would benefit from, please comment below.

Leave a Comment

Scroll to Top