Destroy() Can Now Replicate to Clients

Greetings Developers,

Coming soon, calls to :Destroy() on the server will replicate to clients. What exactly does this mean?

TL;DR: Try changing the ReplicateInstanceDestroySetting under Workspace to Enabled for your experiences and see how they are affected, as we plan to enable it by default in the future.

Current Behavior

You may have noticed that currently when you call :Destroy() on an Instance, that Instance will be destroyed, meaning that it and all its descendants:

  • Will have their parent set to nil.
  • Will have their parent locked, meaning they cannot be reparented to anything else.
  • Any connections made on them will be disconnected for you.

However, what you may not have noticed before is that the same is not true on the clients connected to that server. Roblox’ automatic replication will remove the Instances on the clients as well (setting their Parent to nil), but it will not destroy those Instances. That means that on the client, the Instances:

  • Can still be reparented to another object if you kept a reference to them.
  • Will not have their connections automatically disconnected.

The Planned Change

That all changes soon, we’ve upgraded the automatic network replication code such that when you call :Destroy() on the server, the destroyed Instances will actually be destroyed on the client rather than just being removed.

In addition to the new replication behavior, since the server will now Destroy objects on the client, it may be important for code on the client to interact with Destroy in more subtle ways, in particular performing some specialized cleanup on an object before it gets destroyed. In order to facilitate this, we’ve added a dedicated Destroying event on Instance which allows you to jump in and do such cleanup.

We recognize that this change could potentially break some experiences, especially those containing custom replication code. That’s why we’re rolling out this change carefully in a way that gives you time to adapt your code if it would be impacted…

Rollout Schedule

Now: The Destroying event will be available in all experiences as it will not impact existing code.

Also now: You will see a ReplicateInstanceDestroySetting under Workspace. You may optionally turn on the new replication behavior by changing this setting to Enabled. Please give this a try and let us know how your experience is affected!

A few months from now: We will change the default for ReplicateInstanceDestroySetting to Enabled, but you will still be able to opt out by manually changing to Disabled if you need more time to adapt your experiences. Before we do this we will make another DevForum post to keep everyone informed, so please stay tuned!

Later on: We would like to ultimately end up at a place where we can remove the setting and enable Destroy() replication for all Roblox experiences to make memory clean-up easier for all Devs. Please help us get there!

New Behavior Technical Details

How it will work

When Destroy() gets called on the Server, an explicit “destroy” message will also get replicated so that the same Destroy() gets called on Instances on the client.

Before each Instance is destroyed, the Instance.Destroying event will be fired. This gives you a chance to run additional cleanup code on the client before the Instances are removed from the hierarchy (Note that Destroying handler cannot “cancel” the Destroy(), so make sure you only call Destroy() on things you’re sure you want gone). Timing may change with Deferred Lua enabled - see Deeper Technical Details for more info.

Destroy() will disconnect all connections on Instance, so you don’t have to worry about manually disconnecting connections.

Let’s go through an example!

-- Experience contents
Workspace
→ Folder X
  → Folder Y
    → Part Z

X:Destroy() --> Called by a ServerScript

Then the following events will occur:

First, on the server

  1. X.Parent property gets locked (So that Destroying handlers can’t attempt to “rescue” it)
  2. X.Destroying event gets fired
  3. X.Parent gets set to nil
  4. Y.Parent property gets locked
  5. Y.Destroying event gets fired
  6. Y.Parent gets set to nil
  7. Z.Parent property gets locked
  8. Z.Destroying event gets fired
  9. Z.Parent gets set to nil
  10. X, Y, and Z are “locked-in” as the set of Instances to be destroyed, and replicated to the client

Then, on the client

  1. Client receives X, Y, and Z as a set of Instances to Destroy().
  2. Events 1-9 happen on client-side exactly as they did on server-side even if the client hierarchy is arranged differently due to LocalScript modifications.

Deeper Technical Details

Destroy replication is server → client

As with all property replication, Destroy() will only be replicated from server → client and not the other way around as that would create security issues.

Non-destroying parent changes

If you’re writing server scripts that specifically set X.Parent = nil instead of calling Destroy(), your code won’t be affected. Only code called Destroy() is affected… however you should consider migrating your code to use Destroy() if it doesn’t already.

Interaction with Deferred Lua

If an Instance X gets destroyed, the DataModel will fire the Instance.Destroying event. If Deferred Lua is enabled, the listeners of Instance.Destroying will not run immediately, but after X and its subtree have been fully removed from the hierarchy.

Interaction with Streaming Enabled

If an Instance X gets destroyed on the server, X:Destroy() will get replicated to all clients which the server has ever replicated X to. This logic helps ensure that the Destroy call frees as much memory as possible.

This matters in a streaming-enabled experience: Imagine the client receives part X under Workspace, hooks up a bunch of signal listeners to X, and then sets X.Parent = nil. If the server does not replicate X:Destroy() to the client because X isn’t currently in the client’s hierarchy, then the client will never be able to clean up those listeners without separate handling code.

Interaction with additional client-local content

Only the specific Instances that were destroyed on the server will be destroyed on the client, additional client-local Instances parented to the destroyed Instances will be left alone to avoid unnecessarily interfering with client logic. Imagine a situation where (thanks to local modifications) the DataModels look different on the client and server:

Server Workspace
→ Folder X --> Server Script destroys this
  → Folder Y

Client Workspace
→ Folder X
  → Folder Y
    → Part Z --> A LocalScript has created this

Now, a ServerScript calls X:Destroy(), recursively destroying X and Y on the server-side. Only the Destroy() of X and Y will be replicated, thus part Z will not be destroyed on the client-side.

Note that since Part Z is a descendant of the destroyed Folder Y, and Folder Y was destroyed, Part Z will still end up with a nil parent (but it won’t have been destroyed so it can be reparented). You may hook up a listener to the Destroying event of Folder Y to handle cleanup or reuse of Part Z if needed.

Interaction with client-local hierarchy changes

All instances that were destroyed on the server-side will also be destroyed on the client-side, even if a LocalScript moved them somewhere else other than the parent which was destroyed on the server.

E.g.: If you have a situation where the DataModels are:

Server Workspace 
→ Folder X --> Server Script destroys this
  → Folder Y
    → Part Z

Client Workspace 
→ Part Z --> has been reparented to Workspace by a LocalScript
→ Folder X
  → Folder Y

If a server Script calls X:Destroy() (recursively destroying the X, Y, and Z on the server), then X, Y, and Z will all be destroyed on the client-side upon replication even though Z isn’t a descendant of X anymore on the client.

Wrapup

Please let us know how the change affects your code.

[Update] May 12, 2022

296 Likes

This topic was automatically opened after 10 minutes.

Does this apply to :ClearAllChildren() too? I remember there being some inconsistency in the past between it and :Destroy(), and hopefully that’s been considered! Good change, though; I didn’t realize this behavior wasn’t already the case.

32 Likes

Yes, :ClearAllChildren() destroys the cleared Instances.

Generally any time the engine removes Instances for you it will be via a :Destroy(). In the past some APIs just set .Parent = nil but they’ve all been migrated to :Destroy() over the years.

51 Likes

This is well needed upgrade, improved a lot of performance issues with many games. Thanks for adding this even though it feels a bit overdue.

A quick question is, if a client changes an Instance’s parent and that parent gets destroyed on the server. Will this replicate to the instance that has been reparented?

16 Likes

This is awesome!

Will this apply to every location a client can see other than Workspace? ReplicatedStorage, Lighting, …?

I do presume that server-side items destroyed in ServerScriptService & ServerStorage will continue to not be replicated to the client.

7 Likes

finally people have been complaining about this for years glad to see roblox is close to changing the behaviour

8 Likes

Yes, it will - if Instance X and its child X.Y is Destroy()-ed on the Server, then X and X.Y will also be destroyed on the Client, even if Y is reparented on the Client side.

10 Likes

Super happy to see we finally have an event for instance destruction! After a quick test, I did notice that .Destroying does not fire when instances fall out of the world, it’d be great if that could be changed. Otherwise, works great.

10 Likes

Yup! Destroy() will only be replicated for Instances that are descendants of replicated services. So like you said, Destroy() will be replicated for Instances under Workspace, ReplicatedStorage, etc.

However, Destroy() will not be replicated for instances under ServerScriptService and ServerStorage.

7 Likes

What about Debris:AddItem()? Will it have the new behavior too?

7 Likes

I have good news for you, it already does :Destroy()! In fact, it’s one of the ones that didn’t in the past but was upgraded to do so (relatively recent change, I think it was changed to destroy a couple years ago)

18 Likes

You know what would be even better? If Destroy replicated to the server. Not in a way where the client can destroy instances on the server, but rather send a signal telling the server to stop replicating data to that client.

3 Likes

I think that behavior should be kept away from the Destroy() function and added to some future feature where you have more fine grained control of the replication behavior of stuff.

4 Likes

Edit: Never mind, I just saw the answer when rereading the original post. It fires on both client and server.

Will this fire on the server also?

1 Like

Know what would be even better? Stop allowing clients to remove ANY instance from their character model, even scripts, values and their own humanoid -_-

11 Likes

There’s a good reason for this: Roblox uses distributed physics. That means that in some rare edge cases one physics simulating peer may decide that something fell off of the map, while the actually authoritative peer does not agree, so it has to be possible to “undo” the thing falling off the map.

5 Likes

Great to hear! I heavily rely on Debris, so it using the new behavior is almost a must for me.

1 Like

Since when is the server authoritative at all when it comes to physics? Roblox doesn’t seem to do any sort of latency correction, prediction, anticheat etc. just interpolation.

1 Like

I adjusted the wording, really the new authoritative peer. Basically the problem scenario may occur during physics handoff when the server is adjusting which client should be handling the simulation for the falling object.

3 Likes