New Player and Character Destroy Behavior Enabled by Default

Hi! I create a PlayerData folder within each Player, which contains various subsets, and within those subsets, there are further subsets depending on the type of data being stored, such as Inventory, Loadout or Settings. Some folders (usually the last child) have attributes assigned to them.

Upon the PlayerRemoving event, I serialize the entire PlayerData folder, including all its descendants, into a table and save it to DataStore. However I can’t reference the descendants during the PlayerRemoving event.

One approach I considered was saving the data into a table using custom read/write functions, but this would require a significant amount of recoding.

Is there a way to reference or replicate the PlayerData folder upon the PlayerRemoving event? Alternatively, would a change in the process itself be more optimal? I’d appreciate any advice on improving this workflow.

3 Likes

Is there any convincing reason as to why these destroyed instances need to have their descendants parented directly to nil in the first place? An unreferenced instance tree outside of the datamodel, is surely still perfectly eligible for GC. It seems as though we could have a variant of destroy that still treats these niche memory leakage concerns, without introducing unnecessary conflicts with the old player/character removal behavior, which only set the parent of the root instance to nil.

Hi, Why not just Clone() your PlayerData folder then reference the values in the cloned instance? It seems to work in my tests. Looks like it could be a good way to deal with this if you have everything in a Player.PlayerData folder (unlike myself) :frowning: .

1 Like

i don’t see how this is an issue, keep a table with updates that reflect whenever the backpack changes then save that, don’t do it when the player leaves… that’s how you get duplication glitches :sweat_smile:

1 Like

Different developers code differently, not to mention how simple games used to be. Making the setting default to be Enabled is not the issue, but sunsetting it completely.

Also this same argument could be used for why this implementation was added in the first place, since you could possibly code the destruction of the player by yourself and clear all cache.

The backpack was used as an example, since it’s the most used type of code that would normally have this issue, but you can also refer to Values that are unique to a player, Folders with values that are used as (although poor) saving systems, you cannot simply say that the whole platform should adhere a change just because it can be coded in a different way.

Talking about duping, until a year ago (not sure now) you could enter two different servers with two different instances of the Roblox Player, making any protection utterly useless since the same account could be at different servers at the same time. Obviously this could be fixed with code, but relied completely on the developer themselves.

1 Like

Can the property instead be changed to a number that is the delay before the player and characters are destroyed

2 Likes

Welp I’ve tried this method before, but it doesn’t work because all the parents of PlayerData and its descendants are set to nil when the PlayerRemoving event triggers. This means that if I attempt to Clone() the PlayerData folder at that point, I can’t access its descendants.

That said, I can Clone() the PlayerData folder before the PlayerRemoving event. However, how would I detect when the player is about to leave in order to handle this properly? :thinking:

Are you sure? It seems to work for me when I tested it.

local players = game:GetService("Players")

local function playerAdded(player)	
	local playerData = Instance.new("Folder")
	playerData.Name = "PlayerData"
	playerData.Parent = player
	
	local cash = Instance.new("IntValue")
	cash.Name = "Cash"
	cash.Value = 50
	cash.Parent = playerData
	
	local cash = Instance.new("IntValue")
	cash.Name = "Gems"
	cash.Value = 100
	cash.Parent = playerData		
end

local function playerRemoving(player)		
	local clonedPlayerData = player.PlayerData:Clone()	
	
	task.wait(1) -- to allow parents of the original values to be set to nil by the new behavior
	
	print(clonedPlayerData.Cash, clonedPlayerData.Cash.Value)
	print(clonedPlayerData.Gems, clonedPlayerData.Gems.Value)	
end

players.PlayerAdded:Connect(playerAdded)
players.PlayerRemoving:Connect(playerRemoving)


game:BindToClose(function() -- to allow player removing function to run
	task.wait(2)
end)





2 Likes

To anyone currently wondering, the best alternative way to save data with these new changes would be to parent every piece of data that you want to save under a folder in another instance or service. Recommendations for such would be ReplicatedStorage (so that the client can access data too) and the ServerStorage (to ensure your data is save there).

When the player is removed, you can then first call for a save of all the data within the folder and than afterwards destroy the folder.

If anyone has alternative ways, please share them :slight_smile:

1 Like

I was hoping my game was immune to this change, I tested today and it turns out my datastore script won’t work the way its coded. I’m testing it now. I don’t save much data, but the workaround that I’m using that seems to be working is to just create local variables for all of the data items before calling any async functions.

Its confusing to me what is meant by " This task is deferred and occurs after bound event connections are invoked, including PlayerRemoving ."

What does deferred mean? From my testing you don’t get much time to access the player before its gone. It seems any async call will finish after the player is gone(so if you have more than one async call to save data, the subsequent reference to the player’s children will throw an error)

If cycle through 4 or 5 values that I save in the player and put those into a table (or individual variables would probably work also), then use those variables to make my database async calls then it seems to work. (I guess its deferred long enough to save the values to variables, but not long enough to make more than one async call).

Somebody let me know if i’m doing this wrong or can explain what deferred means, how much time does this give you to access the instance.

abstract pseudo code with Enabled before fix and after ‘fix’:

So I reckon if we have it Enabled we don’t have to do any garbage collection of events connected to Player or Character instances when they leave or character is removed?

like I don’t need to do this:

PLAYERS.PlayerRemoving:connect(function(player)
	task.defer(player.Destroy, player)
end)

or

PLAYERS.PlayerAdded:Connect(function(player)
	player.CharacterRemoving:Connect(function(character)
		task.defer(character.Destroy, character)
	end)
end)

anymore?

Why this update? You could’ve at least set it to disabled for us to make some changes on our script. After that, we can just enabled it. But no, it literally wiped out all of my player’s data. My datastore fires a save function whenever an item is added or destroyed from their inventory. Because of this, the script save function fires after all the items from their inventories being removed or destroyed which ended up saving an empty inventory. Damage has already been done and all of my players are not happy.

Datastores have version history going back to a week iirc. You might still be able to recover from this!

Can you explain more? I’m using the DataStore Editor and I don’t see that setting where I could revert the entire datastore rather than doing one by one on each player.

I was curious about this as well but I haven’t been able to find any examples of how it works or how to use it.

All I can find is how datastore2 has automatic versioning, well that’s great… unless you can’t figure out how to use it!

If it requires you to rewrite your data saving scripts then its not going to be very useful for most people.

edit: this thread has some examples but I’m still not seeing how you would do this for all players

Apologies on the confusion earlier. I mixed up PlayerRemoving with AncenstryChanged. The initial issue was related to AncenstryChanged hence the inability to clone PlayerData upon the event signal fired. Thank you for the reassurance regarding PlayerRemoving. I’ve applied that but ultimately decided to go with DevFofo’s suggestion on handling player’s data entirely within ReplicatedStorage instead.

As DevFofo mentioned, if anyone has alternative methods, please share them—I’d love to find out if there areany more efficient ways to handle this :slightly_smiling_face: :+1:

1 Like

My suggestion is to revert the data as the player loads in, but that would require being able to tell that they were impacted by the issue.

Maybe check if the last save falls after things started to break, and if a save before that exists. If so, revert.

Could you let us know what this issue is? Are live games still safe to have PlayerCharacterDestroyBehavior = enabled with this issue?

1 Like

The ‘Enabled’ option is still safe to use.

The issue was that making ‘Enabled’ the Default option negatively affected more experiences than we anticipated, where the experience code was not prepared for destroyed Character or Player, but did not select ‘Disabled’ mode.

If the ‘Enabled’ option doesn’t work for you today, consider switching it to ‘Disabled’ until you have time to adjust to changes instead of leaving the ‘Default’.

We are also in the process of developing a different transition option which will provide most of the benefit, but will keep the Character/Player connected to the children (but will destroy connections).
We will post an update once we select the best path to continue.

3 Likes

Awesome to hear it, although we are all very heated about it, as long we are indeed being heard and given fair options, no real damage should be caused.

Thanks for everything and keep up the good work.