I recently started developing a plugin and noticed that while using ChangeHistoryService, undo works normally, until you try to undo an action that involved destroying an instance. Because destroy locks an instances parent property, when you attempt to undo the action, you get a warning in the output, and the action is not undone. I have seen posts talking about similar problems, however I don’t feel any had reasonable solutions. Setting the instances parent to nil is a bad practice, because it would just be left there right? Meaning once you leave enough objects in there, memory leaks could start to occur. Does anyone know a better method to make this work?
This really depends on your plugin and what behavior you want when from undo-ing a destructive action. Here are some thoughts and ideas which may help you.
Cloning
Note that if you have a reference to an Instance which has :Destroy() called on it, you can still :Clone() it and the clone’s Parent will not be locked.
local x = Instance.new("Part")
x.Parent = workspace
x:Destroy()
local clone = x:Clone()
clone.Parent = workspace
Using this fact could allow undos to get you back in a state you wish to be. However, if you need the exact same Instance back, this is obviously not the answer. Also, :Destroy() disconnects all connections from an Instance and its Children, and :Clone() does not bring these connections back.
Parenting to nil
I will point out you cannot “undo” an action that “destroys” an Instance, without keeping a reference to that Instance (if you truly wish to use the EXACT same Instance). This means it is necessary for the instance to be “just left there” somewhere.
Parenting to nil does not disconnect all event connections from the Instance, but this may be beneficial if you want to preserve those connections. However, this can in fact lead to memory leaks if you don’t eventually disconnect them.
Do not destroy
If these destructive actions that you want the user to be able to undo are completely under your plugin’s control: do you really need to destroy the Instance immediately? Much like Windows’ Recycling Bin, you could delay the :Destroy() until a later time (like after 10 other actions occur, and/or when your plugin is closed). So your action could simply Parent the Instance to a special folder or to nil, then undo would move it back.
I appreciate the in depth reply! The third option was what I have been considering, however I worry if someone is interacting with large instances, and undoing large actions, it can lead to issues with the sheer amount of memory being used to keep these instances loaded in case an undo is done. Perhaps I should just try to see how many descendants a destroyed object has, and decide how long to keep it based on that information. Thank you!
No problem!
I did a little experimenting in Studio, and it appears at the moment Roblox does something similar.
If you delete a part in Studio, it sets the Parent to nil (retaining its event connections). Undoing sets the Parent back to its previous Parent.
(You can try this for yourself by creating a part in workspace, run a command like:
workspace.Part.AncestryChanged:Connect(function(c, p) print(`Part: {c and c:GetFullName()},`, `Parent: {p and p:GetFullName()}`) end)
then delete the part, then undo it.)
Further, if you try to undo a :Destroy() in Studio, it will give a warning like The Parent property of Part is locked, current parent: NULL, new parent Workspace
. This shows it is trying to set the Destroyed part’s Parent to Workspace. Which means Roblox must have kept a reference to the part.
So even if you Destroy your Instances and drop all references to them, Roblox Studio may be keeping them in memory anyway.
Thats very interesting. Weird how this all works, thanks for sharing that!