I’m trying to lock an instance’s property to a certain value and caching any changes to it that are not made by my code so I can return it to the user set value. A problem I’m having with this is that the deffered signal behavior types cause my code to not work (I understand why).
I can’t just set the signal behavior to immediate because the code I’m writing is for a plugin. What I’m asking is if anyone knows a different / better way to do this that works with deffered events?
Here’s an example of my problem where I force the CanCollide property to alway be off.
--!strict
local function forceNoCollision(part: BasePart)
local rawSet = false
local function refreshCanCollide()
rawSet = true
part.CanCollide = false
rawSet = false
end
local wasCanCollide = part.CanCollide
local canCollideChanged = part:GetPropertyChangedSignal("CanCollide"):Connect(function()
if not rawSet then
wasCanCollide = part.CanCollide
refreshCanCollide()
end
end)
refreshCanCollide()
return function()
canCollideChanged:Disconnect()
part.CanCollide = wasCanCollide
end
end
local part = Instance.new("Part")
part.CanCollide = false
local unforce = forceNoCollision(part)
-- the user set this to true so when we unforce it will be true
part.CanCollide = true
unforce()
-- if signal behavior == immediate then true (correct!)
-- if signal behavior == deferred then false (wrong!)
print(part.CanCollide)
I was in a very similar situation when trying to modify TextBox.Text with each change the user typed. Deferred signals broke ‘rawSet’, so I started counting the changes where each even change would be made by users.
Code
local function forceNoCollision(part: BasePart)
local wasCanCollide = part.CanCollide
local n = 0
part.CanCollide = false
local canCollideChanged = part:GetPropertyChangedSignal("CanCollide"):Connect(function()
n += 1
if n % 2 == 0 then return end
wasCanCollide = part.CanCollide
part.CanCollide = false
end)
return function()
canCollideChanged:Disconnect()
part.CanCollide = wasCanCollide
end
end
local part = Instance.new("Part")
part.CanCollide = false
local unforce = forceNoCollision(part)
part.CanCollide = true
task.defer(function()
unforce()
print(part.CanCollide)
end)
An alternative is to (re)connect :Once() upon modification.
Code
local function forceNoCollision(part: BasePart)
local wasCanCollide = part.CanCollide
local canCollideChanged
local function OnCanCollideChanged()
wasCanCollide = part.CanCollide
part.CanCollide = false
canCollideChanged = part:GetPropertyChangedSignal("CanCollide"):Once(OnCanCollideChanged)
end
OnCanCollideChanged()
return function()
canCollideChanged:Disconnect()
part.CanCollide = wasCanCollide
end
end
local part = Instance.new("Part")
part.CanCollide = false
local unforce = forceNoCollision(part)
part.CanCollide = true
task.defer(function()
unforce()
print(part.CanCollide)
end)
Some details
unforce() and print() are deferred because otherwise, when testing in the same script, the listener is disconnected before it gets the chance to catch the signal. A user who manages that should consider themselves extremely lucky.
Signals are optimised to not fire if a property is set to the same value, thus ‘false’ to ‘true’ works but ‘true’ to ‘false’ doesn’t, making counting and reconnecting no different than simply setting wasCanCollide to ‘true’ if the change occurs.
→ I’d consider adding some sort of a separate gui switch or shortcut for modifying the state, at least while it’s locked.