Connecting to a Specific Property Through Instance.Changed

When someone connects to .Changed, they rarely need to connect to every single property. The way .Changed is set up closes doors for potential C++ optimizations, where some properties might only need constant updating if someone is listening to or getting the value. The number of unnecessary function calls can be massive.

Pretty much every time .Changed is used, it will look something like this:

part.Changed:connect(function(property)
	if property == "BrickColor" then
		-- stuff
	end
end)

I think the problem is in how events/signals are set up. Currently, we have RBXScriptSignals, but there are plenty of features that would benefit from a specialized signal type. Here are some rough ideas:

###Instance.Changed
part.Changed:connectProperty(“BrickColor”, function()
– stuff
end)

RunService.BindToRenderStepped

If something can be disconnected from, or “unbound” then it should always return a connection.

local priority = Enum.RenderPriority.Input.Value
game:GetService("RunService").RenderStepped:connectPriority(priority, function(delta)
	-- stuff
end)

###ContextActionService.BindAction
User input is tricky, not sure how to improve this one, but it’s related.

game.OnClose

This is a problem for an analytics module I’ve been working on, because it’s not possible for multiple scripts to connect to this.
Ideally, multiple scripts would be able to connect, and when the game closes, the connected functions would all be called, and the game would close once all of them are finished.

game.Closing:connect(function()
	wait(1)
end)
4 Likes

I like the requested change to Instance.Changed because then it could possibly allow us to listen for Position, Size and CFrame changes, which would be so helpful.

1 Like

I completely agree with your RunService thing, where it returns a listener object that can be disconnected like other event listeners. I’m not sure why they didn’t go with that. The binding by name is so odd compared to the rest of the API.

Didn’t physics-related cframe changes stop getting fired?

It hurts to listen to something like a guiObject’s visibility, and have your function called 120 times per second as it gets tweened.

Yeah no position, size or CFrame property changes call the Changed event. What I said is that adding this argument would then add the ability for us to listen to those specific properties.

1 Like

I ended up writing a wrapper module for BindToRenderStepped because it was painfully awkward to use:

local self = {}

local RunService = game:GetService("RunService")

local uniqueIds = {}

local function ConnectWithPriority(priority, func)
	local id = #uniqueIds + 1
	
	local name = "_auto:" .. id
	
	RunService:BindToRenderStep(name, priority, func)
	
	uniqueIds[id] = true
	
	local disconnected = false
	
	return function()
		if disconnected then
			return
		end
		disconnected = true
		
		RunService:UnbindFromRenderStep(name)
		uniqueIds[id] = nil
	end
end

-- Returns Connection
function self:connect(priority, func)
	if priority == nil then
		return RunService.RenderStepped:connect(func)
	end
	
	return {
		disconnect = ConnectWithPriority(priority, func)
	}
	
end

return self
1 Like

You basicly want a RBXSignal:connectArguments(…) that works like:

  • The actual function that has to be called is the last argument
  • The function only gets called if the first argument of the event equals the first argument of the …
  • Same for the 2nd, 3rd, …
  • If the event has (for example) 5 arguments and you only specified 4, the 5th can be anything
  • If you “specified” an argument as nil, it can be anything

Would be nice, but it may be more logical if the first argument of connectArguments would be the function.
(Which might actually look better in the code?)
I might write a Lua version in a few minutes, I’m just in school being bored.

Both versions cuz bored:

local function connectArguments(ev,fu,...)
	local pls = {...}
	local n = select("#",...)
	return ev:connect(function(...)
		local args = {...}
		for i=1,#n do
			if args[i] ~= pls[i] and pls[i] ~= nil then
				return
			end
		end fu(unpack(args,1,selec("#",...))
	end)
end
local connection = connectArguments(part.Changed,function(prop,value)
	print("huehuehue")
end,"BrickColor")

local function connectArguments(ev,...)
	local n = select("#",...)
	local pls = {...}
	local fu = pls[n] n = n - 1
	return ev:connect(function(...)
		local args = {...}
		for i=1,#n do
			if args[i] ~= pls[i] and pls[i] ~= nil then
				return
			end
		end fu(unpack(args,1,selec("#",...))
	end)
end
connection = connectArguments(part.Changed,"BrickColor",function(prop,value)
	print("huehuehue")
end)
1 Like

I’m just outlining the general behavior, not the exact argument ordering.

I’ve been using custom events like these for quite a while, and I usually make the function the final argument because the extra arguments can get seemingly lost behind a massive anonymous function

Not very useful considering one of the main reasons we need it is for performance and simplicity :stuck_out_tongue:

If you use it as final argument, it would be easier to see what you connected for.
You would immediatly see stuff(event,arg1,arg2,…).
I prefer it as second argument (event being first) because it just looks nicer to me.
(And I don’t really have a problem with not immediatly seeing what I connect for)

Oh well, it works and performance wise it isn’t that bad…

Okay now I have no idea what we’re talking about x)

It’s only bad if it’s being called thousands of times

Function as last argument, like:

connect(event,arg1,arg2,function)

event:connect or a custom connect function? I don’t normally use the latter. I mainly use custom event objects

It’s just a few comparisons, of which a few you would’ve done anyway.
(As in prop = “BrickColor” for example)
The most expensive part is the function call, and just that single extra call… not that bad.
There’s probably a lot worse code in your game. (general speaking)

Sometimes, if I need a way to disconnect all events I ever created for some reason, I use a connect-function.
It’s basicly something stupid that works like:

local cons = {}
local function connect(ev,fu)
	local c = ev:connect(fu)
	cons[c] = fu return c
end

It’s also more readable and simplistic to do it the old fashioned way.

I do this, but per lua object I make, so if I need to dispose of something I call a dispose method which disconnects if from whatever it’s connected to. Hard to explain but it’s similar to what you’re doing, but it isn’t done globally.

Thaaaaat looks like a memory leak

I thought about using __mode, and I should, but eh, I wrote this in 10s as an example.
(And I can always clear the table when disconnecting everything, not a real leak)

__mode still wouldn’t work, because I assume you usually don’t store a reference to the connection every time you call your connect function.

Not sure a connection gets GCed if it’s still connected.
You could always go trough the table now and then and clear those where .connected is false.

Why do you need disconnecting everything anyways?