Saving and Loading Data with DataStores

a guide written by NoahWillCode

If you are playing a game, such as Twisted Murderer or Mad Paintball, and you buy in-game currency or a character, or level up, you expect that to save. You expect that, next time you play this game, your progress, your purchases, will not be gone.

So now, you're making your own game. And you want to make sure your game saves some important data for the player, too. How do you do that?

You use the DataStoreService.

The DataStoreService is an awesome API specifically meant for saving and loading data for your game. Literally in its name - it stores data! (No it doesn't mean it is a store to buy data from.)

How do you use the DataStoreService? I'll teach you.

The first step is to get the DataStoreService into your script. You do this by loading it in, just like any other ROBLOX Service.

local DSS = game:GetService("DataStoreService")

After this, you get your specific DataStore. See, the DataStoreService is not actually where you save your data, it is simply an API to get your DataStores. The DataStore is actually where all your data is saved and loaded from. And here is how to get a DataStore:

local datastore = DSS:GetDataStore("GeneralSaveData", "Players")

The DataStoreService:GetDataStore() function has two parameters, name and scope. The name parameter is required, and it just means the actual name of your DataStore. You can name your DataStore anything, even if it is "BillyBobJoeManLikesUnicorns". However, it helps to name your DataStore according to its purpose. The reason I have mine named "GeneralSaveData" is that it is where I am saving the general data for each player, so the name makes perfect sense.

The second parameter, scope, is a bit more tricky. Thankfully, this parameter is not required, so you can just leave it empty and only provide a name argument. But, if you're feeling adventurous, you can use the scope argument to have different, sort of, groups of DataStores. Let's say you saved general data for every player, but that you also saved general data for your game itself. You could then have two DataStores named "GeneralSaveData", but each in a different scope (e.g. "Players" and "Game"). You can sort of see it as a folder structure on your computer. You can have two different folders, each with files that have the same name. But because the files are in different folders, there isn't a problem!

Two folders

So, great, now you know how to make and have a DataStore! How do you use it?

Let's start with how to save data. There are couple things you're going to need in order to save your data. The first thing is, you're going to need a key. I don't mean a key to unlock a door or even a fancy wireless key for one of those push-to-start cars. No, in this case, the key is a string, and it is sort of a name for the data itself. Since we're working with data for players, we're going to want to make this key specific to the player. The best way to do this is to create the key based off of the player's userId, because this is the only thing we can be sure won't change for each player's account. What I like to do is create a function inside of my data script that generates a key by just giving it a player as an argument. Here's what I normally do:

function generateDataKey(player)
    local ret = "uid_" .. player.userId
    return ret
end

For my account, my key from this function would be "uid_3276148".

The next thing you need is the data itself. The data has to be in the form of primitives (e.g. numbers, strings, and booleans), but these primitives CAN be stored in a table. Typically, this is how I store data, because it is easy to access. So let's create a mock table, assuming the player has two leaderstats - Points and Wins.

function generateDataTable(player)
    local dataTable = {
        Points = player.leaderstats.Points.Value,
        Wins = player.leaderstats.Wins.Value
    }
    return dataTable
end

The last part of saving data is to actually tell your DataStore to save it! This is done using your key and your data, in that order. And then all you have to do is call the SetAsync method on your DataStore. I like to put this in its own function that takes a player as an argument, and then it calls the generateDataKey and generateDataTable functions right there. Here's what I normally do:

function saveDataForPlayer(player)
    local key = generateDataKey(player)
    local data = generateDataTable(player)
    datastore:SetAsync(key, data)
end

What this function does, is it saves whatever data you give it with the assigned key. Now it safely stored away in ROBLOX's systems, ready to be loaded back in at any time! We'll discuss loading data in my next blog post, since this one is so long already. So make sure to check back on Thursday, May 21 for my next post! For now, here is the script we've made, in total:

local DSS = game:GetService("DataStoreService")

local datastore = DSS:GetDataStore("GeneralSaveData", "Players")

function generateDataKey(player)
    local ret = "uid_" .. player.userId
    return ret
end

function generateDataTable(player)
    local dataTable = {
        Points = player.leaderstats.Points.Value,
        Wins = player.leaderstats.Wins.Value
    }
    return dataTable
end

function saveDataForPlayer(player)
    local key = generateDataKey(player)
    local data = generateDataTable(player)
    datastore:SetAsync(key, data)
end

So, from this, how can we load in our data again? Say, when the player joins the game on a new server? It's actually sort of simple. We will use our datastore variable's :GetAsync(key)' method. Keep in mind, we used ourgenerateDataKey` function to generate a key to store our entire data table with. So we will use this function again to get that data table back! Here's what I mean:

function loadDataForPlayer(player)
    local key = generateDataKey(player)
    local data = datastore:GetAsync(key)
end

So, our function now loads the data in. But how do we go about using this data and giving the player their stats back? My answer would be the same way we got the data in the first place - just setting one value to another. In our generateDataTable function, we wrote this:

Points = player.leaderstats.Points.Value

So, now, we will write something like this:

player.leaderstats.Points.Value = data.Points

Make sense? We're taking the value we saved FROM the Points leaderstat, and we're just putting it back there! So let's make our new inputDataToPlayer function.

function inputDataToPlayer(player, data)
    player.leaderstats.Points.Value = data.Points
    player.leaderstats.Wins.Value = data.Wins
end

And now, let's alter our loadDataForPlayer function to call inputDataToPlayer

function loadDataForPlayer(player)
    local key = generateDataKey(player)
    local data = datastore:GetAsync(key)
    inputDataToPlayer(player, data)
end

Voila! All of our functions are written!

But one problem.

Nothing really works...

Why?

Well, we've written a lot of functions. But we haven't actually called our save or load functions anywhere! But, when SHOULD we call these functions? Let's discuss this.

When to load data...Well, probably only need to load a player's data once - when they join the game.

What about saving? Times to save data are more complicated. At least when the player leaves the game...And probably more often than that, too. Maybe we should save every time the player's Win stat changes, since that one seems important and will only happen once every round. Yeah, let's go with that!

So here is how we implement this (if you don't know about Events, please refer to this blog post):

game.Players.PlayerAdded:connect(function(player)
    loadDataForPlayer(player) --Load first thing when they join
    player.leaderstats.Wins.Changed:connect(function()
        saveDataForPlayer(player) --Save the data when Wins changes value
    end)
end)

game.Players.PlayerRemoving:connect(saveDataForPlayer) --Save data when the player leaves the game

And that's it! Our saving and loading data idea works now that we've made all our functions and implemented them! Keep in mind, you will have to alter some of these functions to make them work for your game, but the general concept is the same.

And, to close out, here is all the code compiled into one:

local DSS = game:GetService("DataStoreService")

local datastore = DSS:GetDataStore("GeneralSaveData", "Players")

function generateDataKey(player)
    local ret = "uid_" .. player.userId
    return ret
end

function generateDataTable(player)
    local dataTable = {
        Points = player.leaderstats.Points.Value,
        Wins = player.leaderstats.Wins.Value
    }
    return dataTable
end

function saveDataForPlayer(player)
    local key = generateDataKey(player)
    local data = generateDataTable(player)
    datastore:SetAsync(key, data)
end

function inputDataToPlayer(player, data)
    player.leaderstats.Points.Value = data.Points
    player.leaderstats.Wins.Value = data.Wins
end

function loadDataForPlayer(player)
    local key = generateDataKey(player)
    local data = datastore:GetAsync(key)
    inputDataToPlayer(player, data)
end

game.Players.PlayerAdded:connect(function(player)
    loadDataForPlayer(player) --Load first thing when they join
    player.leaderstats.Wins.Changed:connect(function()
        saveDataForPlayer(player) --Save the data when Wins changes value
    end)
end)

game.Players.PlayerRemoving:connect(saveDataForPlayer) --Save data when the player leaves the game