At this point it’s probably better if you circle back and just do task.delay(1, obj.Destroy, obj)
as the namecall optimization won’t be saving you any time when using a closure means you’d be doing 2 function calls anyways.
Benchmark time. (Note: this benchmarks task.spawn
, but task.delay
should be about the same, minus the scheduling cost.)
local n = 500
local instance = Instance.new('Folder')
instance:Destroy()
function spawnLookup()
for i = 1, n do
task.spawn(instance.Destroy, instance)
end
end
local destroy = game.Destroy
function spawnCached()
for i = 1, n do
task.spawn(destroy, instance)
end
end
function closure()
for i = 1, n do
task.spawn(function() instance:Destroy() end)
end
end
function closureNoUpvalues()
for i = 1, n do
task.spawn(function(instance) instance:Destroy() end, instance)
end
end
Caching game.Destroy
yields the best performance, while looking up game.Destroy
every time results in the slowest performance. However everything overlaps so heavily that in practice it doesn’t really matter.
Try me.
Here’s what happens if you call namecall directly using some implementation-specific Hot Garbage. Please note: this is scary as hell. You don’t want to see how I did this.
So namecall
is a normal instance:Destroy()
call, without using the task library. __namecall
is a direct __namecall
metamethod call, without using the task library, and without setting the namecall method at all (it is set once, to Destroy, using a newproxy __namecall, and then not set again).
cursed
uses the task library, and is the most efficient way to schedule a (precisely) delayed destruction of an Instance.
Alright so scheduling namecall ~directly using the task library is absolutely possible and it has huge performance benefits. That’s literally what I’m doing.
Okay I’ll be honest this is funny. I was complaining about how this wasn’t possible and here it is, right in front of me.
I might make a community resource, you’ll see how I did this soon.
There is also an edge case where connections connected to an instance after the instance is destroyed are not disconnected, this should be fixed with this new update as well.
local b = workspace.Baseplate
local c1 = b.Touched:Connect(function()
end)
b:Destroy()
task.defer(function()
print(c1.Connected) --> false
end)
local c2 = b.Touched:Connect(function()
end)
while c2.Connected do
print("connection still connected...")
task.wait(1)
end
Note that in certain cases (garbage collection, etc) you won’t observe the Connected
property turn false until the next time the event is fired. Your callback won’t get called and the Connected
property will get set to false. That may be what you’re observing.
Of course, you won’t be able to trigger the Touched event after something is destroyed, try with GetPropertyChangedSignal.
I misread your reply. After something is destroyed, connections that are added should not be disconnected. Some Instances are still somewhat functional after being destroyed, so sometimes this is wanted for one reason or another.
Connections linked to an instance still being kept alive even if the instance is destroyed is bad and unexpected behavior in an almost every single case. Once an instance is destroyed, there is no reason for you to use it in almost any cases (except clean up references to it, etc…). Maids and janitors make this process easy and cleaning up in general.
Even the developer hub recommends so and this isn’t surprising anyways:
There’s a reason that you can still call methods, get and set fields, etc. on destroyed objects. Destroyed Instances are permanently removed from the DataModel and all connections are cleaned up, but they aren’t removed from all userland code.
You can actually call Destroy multiple times on an Instance.
local inst = Instance.new('Folder')
local conn = inst.AncestryChanged:Connect(function() end)
print(conn.Connected) -- true
inst:Destroy()
print(conn.Connected) -- false
conn = inst.AncestryChanged:Connect(function() end)
print(conn.Connected) -- true
inst:Destroy()
print(conn.Connected) -- false
Yes, I’m the author of one of the fastest maids (which I haven’t published because nobody will care).
The reason you can do so is primarily because of backwards compatibility with old games who have bad code like that but at the same time many developers sometimes use certain properties of an instance to clean references of that instance, which isn’t a problem!
Ideally for destroyed instances, their data should be read only and should be kept immutable (an error can be raised when trying to mutate it), this also encourages proper usage of the data of an instance. Now this may bring up another problem with having references to these data (those which are of reference types) but that is okay since they would be collected when those references are dead.
Newly created connections should never be alive on an destroyed instance, and the behavior of Destroy
should be kept consistent (i.e an instance shouldn’t be destroyed multiple times – all the steps needed to do destroy an instance should be run once and not repeatedly, the current behavior of Destroy
doesn’t respect this properly which concerns me slightly due to lack of precaution. This also allows you to check if an instance is already destroyed by having just a simply code like:
local function IsInstanceDestroyed(inst)
return not (inst.AncestryChanged:Connect(function() end)).Connected
end
According to what I said above, cleaning up references to an destroyed instance properly may look like so:
local instanceRefs = {...}
local function Cleanup(instance)
-- We're just fetching the instance's unique id (notice
-- we aren't modifying it, but only using the data to
-- clean the reference)
instanceRefs[instance:GetAttribute("UniqueId")] = nil
-- In another case, just this is fine..
instanceRefs[instance] = nil
-- BAD code - mutating an attribute of a destroyed instance!
instanceRefs[instance:GetAttribute("UniqueId")] = nil
instance:SetAttribute("UniqueId", nil) -- don't need it anymore..
end
instance.Destroying:Connect(function()
Cleanup(instance)
end)
I’m not sure if this is directly related since I’ve never tested this before, but I recently tested this so I’ll report it here (I’m not regular).
It seems that the Destroying
event isn’t fired for children when their parent is destroyed.
Just drop this into a game (just two scripts and 10 lines total)
Script.rbxm (723 Bytes)
This calls Destroy
on the parent script and its Destroying
event is fired properly, but its child’s Destroying
event isn’t called it seems.
Not sure if this is intentional or not, or if this update causes this, but the documentation on Destroy
states that it “calls Destroy on all children” which doesn’t seem to be the case as the event is not fired. This just makes it difficult to do stuff on destroy of scripts that don’t have Destroy
called on them directly. In my case this is in ScreenGuis that have ResetOnSpawn
enabled as the event is only fired on the ui itself.
I should expect either none of those to print or both, but I did do some testing and found out that it seems to just be scripts which are listening to their own Destroying
event and are being automatically cleaned up because their parent was destroyed. But the first print shows that scripts are capable of listening to their own destroy, just not if they’re children, which I find inconsistent.
This is actually consistent with the behavior that the Event Listeners a Script connects will not run once the Script is removed from the DataModel. If you read the documentation here: Script you will find that we say a Lua Script will run if
- Disabled property is false
- The Script object is a descendant of the
Workspace
or
ServerScriptService
and terminate if these conditions are no longer met.
To be more specific, let’s go through the example you provided: When we have the following in the DataModel
ServerScriptService
-Script1
-Script2
and the Scripts look like
--Script1
script.Destroying:Connect(function()
print("Destroying")
end)
task.delay(5, function()
script:Destroy()
end)
--Script2
script.Destroying:Connect(function()
print("Destroying 2")
part1:Destroy()
end)
and Script1
calls Destroy()
, Script1
's parent gets set to nil
. Afterwards, Script2
's Destroying
event will fire.
Note that by the time Script2
's Destroying
signal fires, Script1
has already been reparented to nil
. This means that, Script2
, as the child of Script1
, is also no longer part of the DataModel. More importantly, Script2
is also no longer a descendant of Workspace
or ServerScriptService
. Thus, the Event Listeners connected by Script2
will no longer run.
So it’s not that Script2:Destroying
doesn’t fire - the issue is that the event listener connected by Script2
got disconnected when its ancestor changed from ServerScriptService
to nil
, when Script1
was destroyed.
I figured this was the case when I saw it was firing and I could listen to it from another script, however my use case was with a PlayerGui which gets reset on spawn, so the only way to be able to listen to it being destroyed is listening to the Destroying
event of the UI itself? Mainly because connections were still being listened to from the destroyed script because it wasn’t cleaned up (something listening to Destroying
allows me to do).
This is because the script listening is actually a module script which even when destroyed runs its code (or at least still responds to events):
Also this was a weird way to handle it and I already changed a lot of how this works so that this doesn’t affect it at all, but I just thought it was a little odd. Thanks for the reply!
(To anyone reading this, don’t put module scripts in UI’s that reset on spawn, that’s the root cause of my issues)
No worries, glad I could help and glad you’ve already got a solution!
Your use case is still somewhat confusing to me, so I’m not really sure I can give you useful tips for how to address it. That being said, writing games on Roblox is your expertise, so your solution is likely better than what I could come up with
and yes, I totally agree that the engine sometimes behaves in surprising ways xD
I have an question will wait()
be fixed because like it is slower and laggy it should be speed as task.wait
.
Here are some results:
No. wait
is deprecated, the reason it sticks around is to support legacy scripts and if we changed the behavior it would break some of those legacy scripts. If you want the better behavior use task.wait
What do you mean? Just use task.wait
instead of wait
, it is way more reliable and fast.
The Destroying event doesn’t fire when you delete objects in the explorer in Studio, which is a problem for my plugin. I can’t listen to users deleting specific things in Studio, only with some hacky workarounds.
Greetings Developers,
It’s finally time for Instance:Destroy()
to be replicated by default! We will roll this change out May 16, 2022
This means that setting ReplicateInstanceDestroySetting
under Workspace
to Default
will replicate Instance:Destroy()
. If you don’t want your experience to replicate Instance:Destroy()
, you will have to set ReplicateInstanceDestroySetting
under Workspace
to Disabled
.
Remember, the plan is to eventually enable Instance:Destroy()
for all experiences, so please update your experiences to take advantage of this feature, or let us know what’s blocking you from enabling this feature!
Will Destroy be called on a Character when it resets?
Will Destroy be called on parts when they fall too low on a map?
Cool stuff, updating the roblox functions.
Is there a way to check if the parent property is locked or any other property is locked?
I’d like to destroy all objects within a folder upon one of them being destroyed but it errors whenever the first object being destroyed tries to get destroyed again, checking it’s parent being nil doesnt work since it’s activated on destroying event
This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.