You've been trying to save player data to a data store upon your players leaving but the script doesn't seem to save? This is the tutorial for you.
First, I will start by explaining why the data isn't being saved. Next, I will show you my own solution, and explain how it differs from the way developers commonly go about saving their players' data. Let's get started.
What is happening?
Well first, consider this piece of code:
local Players = game:GetService("Players") local function loadPlayerData(player) -- ... end local function savePlayerData(player) -- ... end Players.PlayerAdded:Connect(function(player) loadPlayerData(player) end) Players.PlayerRemoving:Connect(function(player) savePlayerData(player) end)
Without entering too much into the details, this is how game developers typically save their players' data. The player joins and we load their data, and then sometime later they leave and their data is saved, simple.
That's all fine, right? Wrong.
This code would fail to save the players' data in some circumstances. When a game server closes, it stops everything it is doing and then focuses solely on closing. I can think of three possible ways this can happen:
- The last connected player in the server leaves.
- All game servers are shutdown manually by the developer from the game's page.
- The game server encounters a network error and is forced to shutdown or crashes.
This means in all these cases the code within the
PlayerRemoving event listener is never executed.
So what can we do to fix this?
There is a simple fix. However, there is a catch which we will see later.
The solution is, in addition to saving a player's data upon leaving, to use the
game:BindToClose() method to register a function to be called when the game shuts down and from there save the remaining connected players' data. You can bind multiple functions, in which case Roblox will call them seemingly in parallel in separate Lua threads.
It's pretty straightforward:
-- code from before... game:BindToClose(function() for _, player in pairs(Players:GetPlayers()) do savePlayerData(player) end end)
And there we have it, we're saving the data of all connected players upon the server closing. Easy, right? There's just a slight problem with this.
You see, Roblox only allows functions bound with
game:BindToClose() 30 seconds to do their thing. 30 seconds for all bound functions, to be precise, not 30 seconds each. When the 30 seconds are over, the game shuts down regardless of if they are finished with their work or not.
Now that wouldn't be a problem on its own if it weren't for data store requests taking an indeterminate amount of time to complete. Indeed, data store requests are web requests, and web requests can take a lot of time to go through depending on latency and other factors.
As our code is right now, we have to wait for one data store request to complete before proceeding to the next, resulting in a waste of time. Because of that, it's possible the function will take more than the allowed 30 seconds and thus not all connected players' data will be saved.
What we could do is utilize this time to process the other players' data. We can do that by initiating data store requests in different, new threads.
If you're not familiar with
spawn() or multi-threading in general on Roblox, know that only a single Lua thread (aka coroutine) can execute at a given time. They do not truly run in parallel, however they give the illusion of it by switching flawlessly between each-other. Coroutines can be "resumed" by some code in another coroutine. A coroutine can also "yield" to pause midway through its code and return control back to whoever resumed it. Threads created via the
delay() Roblox functions are entirely managed by Roblox and do not need to be resumed manually.
That behavior is enough for us. When we call Roblox functions that take an indeterminate amount of time to finish (what Roblox labels as "Async" functions on the Wiki), Roblox pauses the current thread until the request completes and selects a new Roblox-managed thread to resume. That is, a script's thread or a thread that was previously created with
Using that, we can parallelize our code:
-- code from before... game:BindToClose(function() for _, player in pairs(Players:GetPlayers()) do spawn(function() savePlayerData(player) end) end end)
And there we have it! Our players' data should now save appropriately.
Hope this helped you all. If you have any problems or suggestions, feel free to reply below. And please be sure to leave some feedback. This is my first ever forum tutorial, and I'm sure there's room for improvement. It might not show, but english isn't my native language either. Let me know also if you'd like to see more of these. I don't have much free time on my hands, but I'll consider writing more if I ever have some.