How to Properly save Player Data to Data Stores

  • Global Moderator

    Oh no.

    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 spawn() and 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 spawn() or delay().

    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.

    Conclusion

    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.


  • @Link150 I give this a big thumbs up. Great tutorial! This is exactly how it should be for the audience it is created for. It is also simple (not simplistic) and easy to understand. I have seen far too many developers struggling with this issue. I hope to see more of these in the future. I can only think of one improvement, and that is adding some links where people can find more information on a specific topic.

  • Global Moderator

    @Phlegethon5778 Thanks for your comment. About the additional information links, that's a good idea. I didn't think of it when I originally wrote the tutorial, but I'll see what I can do later.

    I wrote this while I'm at work, during my free time, so I can't really search the Internet as I want for helpful links right now.


  • @Link150, that's ok. I was just suggesting some improvements as you requested in your conclusion.


  • @Link150 tbh the "oh no" in bold capital letters was reasonable since it helps convey the problem better but everywhere else just makes this post feel very "loud" if you know what I'm saying. Other than that this is a very helpful post since it makes people more aware of the data loss issue

  • Global Moderator

    @abnotaddable Unfortunately it seems I can't choose the case of the titles. The forums decided to capitalize them without me having any say in it.


  • Mistake(s)

    Players.PlayerRemoving:Connect(function(player)
        loadPlayerData(player)
    end)
    
    Players.PlayerRemoving:Connect(function(player)
        savePlayerData(player)
    end)
    

    I think you meant "PlayerAdded" for the first one

    Some things you could include

    • When a server crashes, bindtoclose (might) not run, you should always save (atleast) once/2 minutes in addition to using bindtoclose
    • Because roblox's datastores don't always work, a (small) backup system isn't bad to add either

    Overall a nice (little) tutorial, although it could be improved upon

  • Global Moderator

    @RedcommanderV2 Thanks for pointing out this mistake.

    As for backups and autosaves, I'm leaving that for the reader to implement. This tutorial isn't a complete guide to using data stores, it's only intended to help with this one issue.


  • @Link150 https://devforum.roblox.com/t/does-bindtoclose-fire-on-crash/193434

    Looks like I'm not the only one who thinks differently, which is logical as if something crashes it shouldn't be able to perform a task, I've included "could" though as I do know that the tutorial wasn't about it, but it's always good to include these things as people tend to not care and make awfull datastores, which can kill a game very easily


  • This post is deleted!

  • Certainly an advancement from the initial problem. Bit still rather expensive for the BindToClose execution.
    If anyone's looking to step it up a notch below I've written an over-simplified queue system. It might not be entirely efficient and is certainly not fully fleshed, but hopefully it gets the point across.

    local data = {}
    local queue = {}
    local datastores = {}
    
    spawn(function()
        while true do
            if #queue > 0 then
                -- some functionality here to detect duplicate entries in queue
                
                local stat = queue[#queue]
                --lack of pcalling ftw
                if not datastore then
                    datastores[stat.Name] = dataStoreService:GetDataStore(stat.Name)
                end
                local datastore = datastores[stat.Name]
    
                datastore:SetAsync(stat.Key, data[stat.Key][stat.Name])
                
                table.remove(queue, #queue)
    
                for id, plrData in pairs(data) do
                    if plrData.CanDelete then
                        local dontDelete
                        for i = 1, #queue do
                            if queue[i].Key == id then
                                dontDelete = true
                            end
                        end
                        
                        if not dontDelete then
                            data[id] = nil
                        end
                    end
                end
            end
            wait()
        end
    end)
    
    
    local module = {
        SetStat = function(key, name, value)
            data[key][name] = value
            table.insert(
                data,
                {
                    Key = key,
                    Name = name
                }
            )
        end,
        GetStat = function(key, name)
            return data[key][name]
        end
    }
    
    game.Players.PlayerAdded:Connect(function(plr)
        data[plr.UserId] = {}
        --load data
    end)
    
    game.Players.PlayerAdded:Connect(function(plr)
        data[plr.UserId].CanDelete = true
    end)
    
    game:BindToClose(function()
        repeat wait() until #queue < 1
    end)
    
    return module
  • Global Moderator

    @Pejorem That would not work. As I've stated in my tutorial,

    @Link150 said in How to Properly save Player Data to Data Stores:

    When a game server closes, it stops everything it is doing, and focuses solely on closing. I can think of three possible ways this can happen:

    • The last 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.

    This means the code within the PlayerRemoving event listener is never executed.

    Just like any other piece of code, your spawn()ed thread would stop when the game shuts down. It is not simply suspended, it's a complete stop.

    Also you're never inserting anything to your queue.

  • Global Moderator

    @RedcommanderV2 Okay, I suppose maybe for actual crashes it wouldn't, but there's nothing else we can do about that outside of implementing autosaves, and again that's not the goal of this tutorial.

    Besides, autosaves aren't a requirement. While autosaving is now adopted by a lot of video-games, games have relied on the user to manually save for a very long time, and some still do. Plus, there are games where an autosave system simply would not work.

Log in to reply
 

Looks like your connection to Scripting Helpers was lost, please wait while we try to reconnect.