DataStores are pretty weird indeed, but it’s a simple concept at it’s core.
Getting started
A game can create many different data stores. What is a data store? Think of it like a map-style table stored in the cloud. Through the Data Store API, you can use string keys to access and set different values in your data store.
The first thing you need is a name for your data store. This will be used when getting your data store. For example, we can use the name “PlayerData” to refer to our data store containing player data.
For some kinds of data stores, you might also want a scope. Think of scopes like sub-categories inside your name; for example, we might use our player’s UserId as a scope inside “PlayerData”. Scopes are optional though, so if you don’t need a scope, feel free to leave it out.
With our name and scope, we can now get a data store like this:
--DataStoreService contains the function we need to create data stores!
local DataStoreService = game:GetService("DataStoreService")
local userId = 1670764 -- that's my user id c:
-- Here we get our data store (this lets us access it's contents)
local storeName = "PlayerData"
local storeScope = tostring(userId)
local playerDS = DataStoreService:GetDataStore(storeName, storeScope)
We now have a data store inside our playerDS
variable, what can we do with it?
You can easily set and get values in your data store using string keys, a bit like setting and getting values in a table using string keys.
To set a value, we use SetAsync(keyName, value)
and to get a value, we use GetAsync(keyName)
, as demonstrated below:
-- coins = the value stored in Coins in our data store, or nil if we never set a value in Coins before
local coins = playerDS:GetAsync("Coins")
-- we now set the value of Coins in our data store to 5
playerDS:SetAsync("Coins", 5)
-- since we just set the value of Coins to 5, coins = 5
coins = playerDS:GetAsync("Coins")
If you're struggling
These methods are a bit like setting values in a table:
-- (magicTable represents a fictional table, which we can pretend stores our player data)
local coins = playerDS:GetAsync("Coins")
-- is similar to
local coins = magicTable["Coins"]
playerDS:SetAsync("Coins", 5)
-- is similar to
magicTable["Coins"] = 5
There’s plenty of other functions you can use with your data store to do some special tasks. For example, what if we wanted to add 5 more coins?
A naive solution might be:
local coins = playerDS:GetAsync("Coins")
playerDS:SetAsync("Coins", coins + 5)
However, if you’re using the previous value to set the new value, like we’re doing here, Roblox recommends you use the UpdateAsync function. It’s designed to do this kind of thing in a way that prevents a lot of bugs and errors:
function updateCoins(oldCoins)
local newCoins = oldCoins + 5
return newCoins
end
-- The function you pass to UpdateAsync is given the old value, and should return the new value
playerDS:UpdateAsync("Coins", updateCoins)
If you want, you can include the function directly in the call to UpdateAsync:
playerDS:UpdateAsync("Coins", function(oldCoins)
local newCoins = oldCoins + 5
return newCoins
end)
This is a much better solution than we had before! However, the best way of doing this, designed specifically for our case, is making use of an even more specialised function, IncrementAsync, designed specifically to work with numbers:
playerDS:IncrementAsync("Coins", 5) -- the simplest, most efficient way to add 5 more coins!
This might seem like a lot of functions, but you can always check the Developer Hub to see all of the different ones you can use! Generally you should follow these simple steps when choosing which function to use for setting values:
- Is there a function that’s specifically designed to do this task? (e.g. IncrementAsync for adding/subtracting numbers)
- Is the new value based on the old value/do I need the old value? (UpdateAsync is best when you need the old value)
- Using SetAsync should be fine if the above two criteria aren’t met
Limits and throttling
Up until this point, we’ve pretty much assumed that this data store is just like a hidden table that saves across play sessions. In reality, we’re actually sending requests over the internet to Roblox’s servers! For that reason, to stop people overloading their servers, there are limits to data stores.
The names, scopes and keys you use with data stores must be under 50 characters long. You can check this using string.len
if you’re unsure.
It’s a bit more complicated for values;
- Numbers don’t have limits; all numbers can be stored
- Same for boolean values!
- Strings have a maximum length of 260,000 characters
- Tables are more complex; they’re sent through
HttpService:JSONEncode()
internally so that the data store can save it as a string, meaning that result should also be under 260,000 characters.
- Other stuff like functions, Instances, etc. can’t be stored at all
On top of that, to stop you spamming Roblox’s servers with requests, you’re limited in the amount of times you can use each function of your data stores per minute. The two important limits are:
- You can use GetAsync (60 + numPlayers * 10) times per minute.
- You can use (any combination of) SetAsync, IncrementAsync, UpdateAsync and RemoveAsync (60 + numPlayers * 10) times per minute.
(numPlayers = the number of players online in your server)
You can find all limits here if you need them.
In addition, since these requests go over the internet, it’s going to take a bit of time before our data is saved. This means that your code will stop and wait for your data to save before continuing - it’s good to keep this in mind! If you need to do other stuff at the same time, you can use coroutines, spawn()
or delay()
to run your saving code separately to your other code.
Finally, since we’re sending these requests to Roblox’s servers over the internet, plenty of things could go wrong either on the internet or on their servers. When a problem occurs with your data store function, it will error. The full list of errors it can produce are also found at the link above.
To handle these errors, you should always put your data store functions inside pcalls:
local success, err = pcall(function()
playerDS:SetAsync("Coins", 5)
end)
if success then
print("yay you have 5 coins now")
else
print("oof, you don't have 5 coins because something went wrong")
print("the error is: " .. err)
end
How you deal with these errors is up to you. Some ideas might be to retry after a short while (30 seconds?) or to show an error to the player saying their stuff couldn’t be saved.
Advanced techniques
There’s a few things you can do to help protect against these kinds of errors.
The simplest thing you can do is back up your player data to a separate data store. The easy way of doing this is copying your values over from your data store into a separate ‘backup’ data store:
local backupStoreName = "PlayerDataBackup"
local backupDS = DataStoreService:GetDataStore(backupStoreName, storeScope)
...
-- every once in a while
local coins = playerDS:GetAsync("Coins")
backupDS:SetAsync("Coins", coins)
More advanced methods include berezaa’s backing-up method, which is slightly out of the scope of this post, but you can read more here if you’re interested.
Finally, you mentioned DataStore2 in your post - that’s one of the many modules made by other developers to help you more easily deal with data stores, especially when they go wrong. Just for your information, these aren’t actually part of Roblox! Those modules will likely have a page describing how they work, and the one for DataStore2 specifically can be found here.
Applying this to your problem
At last, getting around to your question specifically. Suppose we have a table with player data in it;
local playerData = {
[1670764] = {
coins = 5,
health = 100,
items = {
{type = "awesome_katana", quantity = 1},
{type = "cheezburger", quantity = 10},
{type = "gravity_coil", quantity = 2}
}
}
}
The first thing that’d be useful to do is to create the data store for our player data using the name “PlayerData” and the user ID as the scope. We’ll put all the following code in a function too:
function savePlayerData(userId)
local playerDS = DataStoreService:GetDataStore("PlayerData", tostring(userId))
-- to be continued below
end
Now, it’s as simple as taking the player’s data and saving it using SetAsync:
-- now being continued
playerDS:SetAsync("PlayerData", playerData[userId])
That’s it - you can now call savePlayerData(userId)
to save a given player’s data at any time!
However, we need to deal with errors and limitations too! We’ll make savePlayerData
return true
if the save was successful, and false
+ the error message if something goes wrong - which is coincidentally exactly what pcall returns!
local function savePlayerData(userId)
local playerDS = DataStoreService:GetDataStore("PlayerData", tostring(userId))
local success, err = pcall(function()
playerDS:SetAsync("PlayerData", playerData[userId])
end)
return success, err
end
This would be useful if you want to show an error message to the player!
Of course, we also should keep in mind the limits for how many requests we can send per minute. This is simple enough to do by keeping track of the last time we called the function for each user ID:
local lastSaveTimes = {} -- keeps track of our last save times for us
local saveCooldownTime = 30 -- how many seconds required between saves
local function savePlayerData(userId)
-- compare the last save time with the current time
local lastSaveTime = lastSaveTimes[userId]
if lastSaveTime ~= nil and os.time() - lastSaveTime < saveCooldownTime then
return false, "You're saving too fast, please slow down!"
end
-- set the last save time to the current time
lastSaveTimes[userId] = os.time()
-- saving code from before
local playerDS = DataStoreService:GetDataStore("PlayerData", tostring(userId))
local success, err = pcall(function()
playerDS:SetAsync("PlayerData", playerData[userId])
end)
-- return the success status and any error messages
return success, err
end
Finally, if you’d like to have a retrying function, you can easily implement one! If you’d like, you could tweak the following implementation however you’d like (spacing out requests over longer times after multiple errors, or adding a maximum number of retries, etc.):
-- be careful; you might end up retrying forever!
local function savePlayerDataWithRetry(userId)
while not savePlayerData(userId) do
wait(saveCooldownTime)
end
end
Something to keep in mind about these implementations I’ve just shown is that they are yielding - that is, when you call these methods, your code will stop and wait for them to finish. As I said before, it’s often a good idea to run your saving code separately if you need to do multiple saving operations at once!
Hopefully some of this is of use to you and to anyone else browsing these forums