Best way to deal with events and cleaning up post-Roblox event order update?

So after the Roblox event update the order and way which events work has pretty much changed.

It offers better performance but comes at the cost of no longer being able to execute code or functions after object deletion which makes it rather tricky to make clean-up scripts and what not.

But long story short, I plan to make a module where a function returns a bindable event to which other scripts can bind their own functions and whatnot.

But when that bindable event (created and returned by the module script), I’m concerned about connections staying around or not getting cleaned up properly.

This is for an object-oriented input module with easy rebindable keys.

So there’s a function that returns a bindable event object.
This bindable event fires whenever the user presses whatever mouse button or key they’ve set up to be the “attack” button.

local attackButton = inputModule.getInput("attack")

attackButton.Event:Connect(functionThatDoesWeaponLogic)

But with how events have changed, it is no longer possible to do things like…

script.Parent.Changed:Connect(function()
 if script.Parent.Parent == nil then event:Disconnect() end
end)

I’m unsure how Roblox handles event connections behind the scenes and under what conditions they are garbage collected.
I also do not know where in the micro profiler or other analytical tools I can see where event connections are or in what state they are.
I want to clean up things as efficiently as possible.

Any help or tips is greatly appreciated, if there are any documentation on how events are memory managed I’d gladly look into that as well.
I want to ensure no memory leaks so I don’t have to restart the server after a while.

3 Likes

Cleaning up connections and instances are where the “maid pattern” shines best. There are a couple of options available as standalone packages (e.g. Maid, Janitor and Trove) while other resources may implement their own cleanup pattern depending on their design and needs.

Whether you’re using Immediate mode or planning to transition to Deferred, the “maid pattern” helps your code be more predictable and explicit about what you’re tracking and what you’ll want to clean up when you’re finished working with something. Particularly, the pattern is used a lot with class libraries out there where each object has its own instance manager to store created objects in.

2 Likes

Interesting.

Do you by any chance also know a bit about how Roblox events work?
Like I’ve always wondered what happens with events when a script is Destroy()'d that holds the function that the event is connected to.

I know for a fact that events are disconnected if the object that has them is destroyed.
But what about service events?
Because services of course are never destroyed and I don’t want lingering event connections that can’t activate any function.

3 Likes

They will be automatically cleaned up by the garbage collector in the next cycle.

3 Likes

All RBXScriptConnections are disconnected when the script where they were spawned in is destroyed or parented to nil. You can test this by parenting any script to nil using the command bar. What you are doing in codeblock two is unnecessary.

Maids are only useful for deconstruction of classes when the script that they were constructed in will continue to run (such as in single-script architectures). It would be nice if you learned how to utilize them, but they are not needed in the scenario.

4 Likes

Does this also go for module scripts by any chance?
I mostly work with module scripts and a lot of events are created inside module scripts.

I don’t have to track or manually disconnect events I assume then?

2 Likes

Sadly, no, since if all the users of the module were destroyed it would still be possible that another script would need to require it again.

Although you shouldn’t worry about connections to an instance that you are going to destroy, since they get disconnected when that happens. For example, this would free both the Instance and Connection:

Part.Changed:Connect(function()
	print(Part.Name)
end)

Part:Destroy()

While the following won’t, because it contains a strong reference to the part and the connection is never disconnected.

RunService.Heartbeat:Connect(function()
	print(Part.Name) -- Still Prints
end)

Part:Destroy()
2 Likes

So to give a bit of context.

My input module creates a event connection to UserInputService and then returns a bindable event that is fired whenever a specific keybind is activated.

This means that there is a script signal connected to UIS as well.

Destroying the BindableEvent would of course result in the events of that object getting disconnected as well.
But UIS never gets destroyed because it’s a service and I make all the event connections inside the module script.

I doubt if destroying the BindableEvent also breaks the connection with UIS in the module script and my concern is that signals never getting cleaned cause memory leaks.

Even if the BindableEvent is destroyed in an unexpected way, I have to somehow clean or get rid of the UIS signals as well.

1 Like

Assuming you are already “reusing” the BindableEvents (Storing them in a dictionary and then returning the same event), you would just have to make sure every connection to them is disconnected when they are no longer needed, like in a cleanup method that runs every time you disable actions and etc.

Don’t worry about the connections to UserInputService, though. That is meant to be a singleton connection that always is running, because it macro-manages the rest of the events. It won’t cause memory leaks; it just uses more memory in general.

2 Likes

The bindable events are destroyed after use.

I’ve wanted to simply script items/weapons that call a inputModule.getInput("fire") function, receive a bindable event and then simply parent it under the script that called it or the tool.

If the player respawns or drop the tool, the real tool that the player normally holds is destroyed and instead replaced by a “dropped item entity”, a interactable entity that serves no other purpose than despawning upon interaction and giving the player said item right in their hands.

I don’t reuse bindable events as I think that’d be too tedious and some objects might get destroyed (BindableEvent objects included) in a unexpected way, rendering the bindable event inaccessible and unusable.

Very true on behalf of the maid pattern being potentially overkill in this scenario! I’ve been subscribed to single script architecture for years now that I tend to forget that single script architecture and power tools are not in fact the standard of programming on Roblox. I’m really out of touch, sorry. :sweat_smile:

I think the important point to behold is that whichever method is most appropriate for your use case, you’ll want to make sure that you’re disconnecting connections and destroying instances that you don’t need anymore. In the case of indestructible instances like services, you should be manually tracking and disconnecting those connections when they’re no longer relevant.

1 Like

Yea it’s kinda what I was thinking.
Designing a whole janitor system JUST to clean up events feels like a overkill solution but I appreciate your input.

Roblox event update simply changed how events work and now I have to adapt and clean up in different ways than I’m used to.
Maybe its unnecessary to clean up, could be that Roblox garbage collector is smart enough to find unused events in module scripts.

But something just doesn’t feel right and I don’t want to make assumptions either since that can lead to problems later.

I’m trying to figure out how Roblox’ event system works to see if I actually have to manually disconnect events in a module script once the BindableEvent object stops existing.

Like I’ve stated before, even when the BindableEvent is destroyed, there is still logic tied to UserInputService that would then be trying to fire a non-existent BindableEvent likely.

Bumping it because I still don’t really have a clear answer to this.

Is this also true for signals connected to descendants of instances that get destroyed or parented to nil? For example, in the following scenario, would the connection automatically get GC’d in the next cycle or should these be manually disconnected?

Group.Part.Changed:Connect(function()
	print(Part.Name)
end)

Group:Destroy()

Yes, because descendants get destroyed atomically with their parents. Thus, the connection disconnects, and the instance can be GC’d:

local Baseplate = workspace.Baseplate
local Texture = Baseplate.Texture

local Connection = Texture.Changed:Connect(function()
	print(Texture.Name)
end)

Baseplate:Destroy()
task.wait(0.5)
print(Connection.Connected) -- false
1 Like

Okay so real question I have here.
Let’s say I do this:

UserInputService.InputBegan:Connect(function(input)
 -- Some code that checks what key it is.
 MyBindableEvent:Fire(Some_args)
end)

But the BindableEvent is later Destroy()'d.
Do I still have to worry about that UIS connection that now tries to fire a non-existent BindableEvent?

This happening inside of a module script in ReplicatedStorage that is used by a car script somewhere in workspace.
The car is later Destroy()'d and thus also destroying the BindableEvent.

But the UIS connection was made in a module script.
Would that become a problem?

Since you destroy the BindableEvent, all of its connections are disconnected and can no longer hold any strong references to instances. However, there is still a strong reference to the BindableEvent itself, which will continue to exist.

Instead of treating BindableEvents as expendable components, you should instead treat them as immutable constants that will always exist. In your case, since you are trying to make custom events for certain inputs, you could create a map of these events correlating to their input.

For example:

local InputMap = {
	Action = Enum.UserInputType.MouseButton1
}
local BindMap = {}

local function OnInputBegan(Input)
	local Bind = BindMap[Input.UserInputType]
	
	if Bind then
		Bind:Fire(Input)
	end
end

local function GetInput(Action)
	if Action == "Attack" then
		local Input = InputMap[Action]
		
		if not BindMap[Input] then
			BindMap[Input] = Instance.new("BindableEvent")
		end
		
		return BindMap[Input]
	end
end

I see.

I’ve considered taking this approach, but the idea behind my input module actually is that when a object (such as a car or tool) wants input, they’ll request an event object that fires whenever said input is activated.

So a gun or a sword is supposed to request a BindableEvent object that will be parented to it.
The reason I also do this is because I might want to implement a custom priority system later so every BindableEvent has a certain priority and maybe even BindableEvents that only fire if 2 or more buttons are pressed simultaneously.

But I need a simple and clean way to deal with references and connections that still refer to the BindableEvent after it has been destroyed.

I’ve considered using a .Changed event to check if the BindableEvent has been parented to nil or loop over a table of objects every once in a while to check which BindableEvents are still present but unsure of how practical that would be.