Destroy() Can Now Replicate to Clients

This has been brought up in the past by a since-anonymized user, but never got any replies.

1 Like

I assume Destroy() is not called on characters when the player leave? Because I keep a reference to their characters and I was able to reparent their character, after they left, despite having this new setting enabled.

Since destroy can now replicate to clients which is great, how would I destroy it on the server after it’s deleted in the client?

also it’s pretty simple to make this system

Clients don’t replicate to the server. Unless the instance that it destroyed was inside their character… But in all other cases, a client destroying something won’t affect the server.

Sure, that’s definitely correct that such a check will throw a false positive on a newly created instance, and on paper that’s a problem. But the thing i have to ask is when would this actually matter? I’m having trouble thinking of a case of where you specifically need to see when an object was called via :Destroy().

Furthermore, while I understand this won’t fit every case it should fit most of them, which is a change in approach; instead of checking if the instance is destroyed yet, you can switch to a more linear design and hook onto the .Destroyed event and then run whatever behavior was meant to happen after checking if it was destroyed or not.

I will give you an example, I literally don’t like when people come at me and say that a feature is just NOT needed just because they don’t see the use case for it. When developers don’t see why it would be useful to have it is because they haven’t come across a situation where is needed.

This property in my opinion should be easy to implement and is better to have it available than the developer itself connecting to destroying and then changing a variable to true to know when the object is destroyed.


Here Are Some of The Use Cases:

  • You have a loop that uses a certain Instance, this loop runs every 60 seconds. You want to terminate this loop when the object is destroyed. (Currently you will have to check if the parent is nil which is not accurate)
  • You have a table on client full of objects that can be destroyed at any time from the server. Then you have a for loop on the client that iterates through this table and is required to check if the object is destroyed before executing some code, else just remove the object from the table. (Weak tables don’t work with Instances and I am also not willing to connect to Destroying for a bunch of objects, assume you have like 500)
  • You want to update how a Gui looks like if is not destroyed, if is destroyed then do nothing.(Currently you will have to check if the parent is nil which is not accurate)
  • Do some stuff to an object while is not destroyed, for example, you want an arrow to point at the object. You make this arrow point at it while is parented to Workspace, if not then remove, if is added back then point at it again. (If you were to check if this object is parented to nil to assume is destroyed it will stop the arrow from pointing at the object when is parented back to workspace, you might say, focasds just parent the object somewhere else, yes I can do that, but I prefer just parenting to nil).

Now I won’t make any feature request because I can’t create topics and because this is something that should have been done when Destroy() was first added.

I use the IsDestroyed method (a function I use to know if an object is destroyed which basically is a wrapper to check if is not a descendant of game) 167 times in my game:
image

That said I will really appreciate a property provided by default to know when an Instance is already destroyed.

4 Likes

I honestly thought this was already a thing. I was thinking only a few hours ago on how I could utilize this for one of my games, and now I know it will be a thing!

Oh man, I didn’t know this wasn’t already the case. That explains some memory problems I was having before

Sure, exploiters will take advantage of the ability for the server to free up some of its bandwidth. To that exploiter.

I found a bug with this new feature, previously had this bug happening when calling :Destroy() on the character on the client and we fixed it by moving the :Destroy() over to the server (should’ve done that in the first place anyways i guess)

but now that it replicates the char:Destroy() to the client the warning is back
Player:Move called, but player currently has no humanoid.

how to replicate:
disable characterautoloads and then on a server script:

game.Players.PlayerAdded:Connect(function(player)
	player.CharacterAdded:Connect(function(char)
		wait(10)
		char:Destroy()
		wait(10)
		player:LoadCharacter()
	end)
	wait()
	player:LoadCharacter()
end)

it would spam the warning every 10 seconds

4 Likes

Just use a closure

task.delay(1, function()
   obj:Destroy();
end);

Someone correct me if I’m wrong but using an anonymous function for something like this in Luau is supposedly cheap compared to regular Lua. The only reason I could ever think of doing what you’re suggesting is to cut down on file size … which should not be an issue if you’re modularizing properly

You’re correct. The Luau implementation does a substantially better job of making small lambdas like that cheap. Using such a lambda will generally only incur a single memory allocation compared to it incurring several in vanilla Lua.

Awesome! The moment I first heard that this behavior existed my immediate thought was why it hadn’t already been addressed as it didn’t seem like the type of behavior that developers really depended on or knew of in the first place and I’m pretty glad that I’m not part of the team responsible for rolling out these sorts of changes as I would have definitely inadvertently squandered a lot of people’s hard work put into making custom replication. Really awesome to see you guys considering even the most obscure use cases here and a big kudos to how carefully this change is being rolled out. Great job.

I realize that this reply isn’t entirely necessary but I felt that the praise was needed.

I do.

I’m curious, would removing the upvalue and using a function argument instead have a smaller cost? (i.e. task.delay(1, function(obj) obj:Destroy() end, obj))

Yes indeed, that will allow the Luau VM to avoid any memory allocation in most cases, but it’s generally not worth the reduced readability.

1 Like

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.

1 Like

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.

4 Likes

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
1 Like

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: