DataStoresService

Often, when making games, we want to let players keep some of their data when they leave. For instance: in-game currency, purchases, upgrades, points, level. Well, ROBLOX provides a simple, easy, and safe way to do this, called DataStores. Unlike the deprecated old system: Data Persistance, wich had alot of data losses, a tiny data saving limit, and could only be called from a player currently in the game, making it imposible to save data for Servers, Data Stores are way safer, and can be used for quite anything, they can even be used to save data on a whole game universe, and be used in ROBLOX Studio, if you Enable it, as well as be used with all games you own and games you have access from your account in group games. Keep in mind they can only be accessed from a SERVER SCRIPT

Heres how DataStore works:

First, Data Stores require using DataStoreService, to get the service, we must use this:

game:GetService("DataStoreService")

As we may use DataStoreService many times in a same script, i recomend making it a variable. Also, In DataStoreService, Theres 3 kinds of DataStores, but 2 of them are quite simular. They are:

-GlobalDataStore(From GetGlobalDataStore)

-DataStore(From GetDataStore, Can also return a GlobalDataStore)

-OrderedDataStore(From GetOrderedDataStore)

Ok, lets begin with GlobalDataStores and DataStores:

Here is a graph about GlobalDataStore and DataStore, In a GlobalDataStore, NameSpace(Scope), is ALWAYS Global:

NameSpace(Or Scope) Data Store Key/Value Pairs

Global________________DataStore 1___Key 1--535

Custom 1 DataStore 2 Key 2--"Lol"

Custom 2 DataStore 3 Key 3--Table_______Array Table(tablename[x] = value) OR Dictionary Table(tablename["String"] = value)

Custom 3 DataStore ... Key ...--True

Custom ...

Ok, now here is Explanation, Scopes are like, places you would like to save a DataStore. in GlobalDataStore, it will always be Global, but not in DataStore. a Scope name can have a limit of 50 characters, and must be a string. you can name it whatever you want, and you can create as many as you wish. A good use of scope, could be to use them as Backups in case of any issue. Lets say you update yourr Data Store system one day, it is possible that you created a glitch that could make data losses/corruptions, A safe way to prevent most of the losses, is to save all new data in a new scope, and load from the new scope. If a user does not have any data to load in new scope, we could load it in the previous one. That way, if it takes you like an hour to fix the bug, alot of users could have lost their data meanwhile, and it would be better to revert to the previous scope. By loading and saving back to old scope. Keep in mind data cannot be deleted, so new scope will stay. Make sure next time you do that, name your scope differently than the last one you reverted from. Next, when you retreive data, you must use a DataStore name as a parameter, and a scope is required when we want to get other than Global: Here is our basic code so far:

GlobalDataStore and DataStore:

In a global data Store:

local ds = game:GetService("DataStoreService"):GetGlobalDataStore("DataStoreName")

Or

local ds = game:GetService("DataStoreService"):GetDataStore("DataStoreName")

Or

local ds = game:GetService("DataStoreService"):GetDataStore("DataStoreName","Global")

All of thoses works, and it is not required to make it a local variable, it can simply not be a variable at all, except a variable is usefull as a shortcut. GetGlobalDataStore() requires 1 parameter: DataStore name(Again it must be a string with a limit of 50 characters), same goes with GetDataStore(), except dataStore haves an OPTIONAL second parameter: Scope(Or Namespace), here, i made an example while using it as Global, to let you knew this would work too. Below I will explain how to get a Custom Scope DataStore, keep in mind Scopes are not requires in Data Saving systems, they are just a bonus:

local ds = game:GetService("DataStoreService"):GetDataStore("DataStoreName","Custom Scope Name")

Now, this would only get you to your DataStore, So now i think it is time to explain how a DataStore name should be. Theres many ways...Id first recommend Something representing what it containers(Like UserData, UserProfiles, or even, InterServerChat), but you may also direcly use the userID(It is important to save a user data under UserID, never under UserName, as usernames can change but not userId, if you use username and user changes username, all his data would be lost.

Now. we have a line that gets the datastore, but that is useless if we do not do something with it...So here are 4 methods and 1 event of a DataStore:

Methods:

GetAsync(key)

GetAsync() is how we usually get a data(String, Boolean, Table, or Number value), It requires 1 parameter: Key Name, So here is a script that gets data when a player joins

local ds = game:GetService("DataStoreService"):GetDataStore("UserProfiles","MyScope")

game.PlayerAdded:connect(function(plr)

plr:WaitForDataReady()

local value = Instance.new("NumberValue,plr)

value.Name = "Data"

local key = ds:GetAsync(plr.UserId)

if key ~= nil then

plr.Data.Value = key

end

end)

Explanation: local ds variable is the datastore we will work in. The function creates a local variable of the player and, waits for his data to be ready. Then, a value is created, and inserted in the player. As data stores have a limit of loading, we will save data in his player, because if he dies, data would have to reload. This is safer, easier, simpler. the key is the UserId(IF THE USERID IS NOT THE DATASTORE(Or the scope), then the USERID MUST BE IN THE KEY, end) then a if statements makes sure key already have data saved as it before, if yes, the data will overwrite the value.

SetAsync(key, value)

The most simple way to save data is to use SetAsync() (works with Strings, Booleans, Tables, and Number Values), heres how it works:

SetAsync() requires 2 parameters: The key, and the value. It will overwrite previous key, and haves no way to check if the data was correcly saved(UNLESS YOU USE A PCALL FUNCTION. Now, continuing our previous script, we would like the script to save the data once every 60 seconds:

local ds = game:GetService("DataStoreService"):GetDataStore("UserProfiles","MyScope")

game.PlayerAdded:connect(function(plr)

plr:WaitForDataReady()

local value = Instance.new("NumberValue,plr)

value.Name = "Data"

local key = ds:GetAsync(plr.UserId)

if key ~= nil then

plr.Data.Value = key

end

while wait(60) do

ds:SetAsync(plr.UserId,plr.Data.Value)

end

end)

This loop, after the GetAsync, will save the data to that user once ever minute, overwriting his old data with the value of 2nd parameter

UpdateAsync(key, function())

UpdateAsync() should usually be used when another server could have updated the value at the same time, for instance, Total number of rounds of minigames in a whole game starting of the script was added. As GetAsync() can return an outdated value(if another server updates at save time that value), using SetAsync() from GetAsync() on that value could overwrite it and cause data losses. This is not required in a script that only 1 server could save it at a time(like a user data). It requires 2 parameters: The key name you want to save into, and a function(Works with tables, numbers, strings, and boolean values):

local ds = game:GetService("DataStoreService"):GetDataStore("CityMap","MurderMinigame")

workspace.RoundEnded:connect(function()

ds:UpdateAsync("TotalRounds",function(oldvalue)

newvalue= 1

if oldvalue ~= nil then

newvalue = newvalue + oldvalue

end

return newvalue

end

)

end)

This will create a function on the event of a round ending, and will Update async key "TotalRounds" from the function value...function haves a parameter that is used like GetAsync() to get the current value, wich you can use to create a new value(like by stacking), return is required to return newvalue, return will convert function into a variable called: newvalue, and it will save that value. NOTE: the function cannot yield(no wait(), waitforchild() and such thing), also, if the key is nil, then oldvalue will be nil, just like with GetAsync(), and if the function returns nil, the update is canceled, to prevent data loss.Another good point: if another server updates same value betweem the key is retrieved and save by UpdateAsync, the function will call again to prevent data overwriting, this will be called back until data is successfully saved without any overwriting.

IncrementAsync(key, interger)

IncrementAsync() is good to increase a value key easily, but it only works on intergers. It requires 2 parameters: Key name, and a interger, this will add the interger number to the key by doing key+interger. The following code will add 10 points to a player every 60 secs:

local ds = game:GetService("DataStoreService"):GetDataStore("UserId","Points")

game.PlayerAdded:connect(function(plr)

plr:WaitForDataReady()

while wait(60) do

ds:IncrenentAsync(plr.UserId,10)

end

end)

Here, after making a connection and waiting for data to be ready, the script will add 10 points to the player every minute by having interger 10 as 2nd parameter

Events:

OnUpdate

Lets say you would like to make a inter-server chat, here is a good way to make a connection for it using DataStores, it will fire whenever a specifix key is updated, and run a function(Works on Tables, Numbers, Boolean, and String values):

local ds = game:GetService("DataStoreService"):GetDataStore("GlobalChat","Chats")

local event = ds:OnUpdate("CurrentChatKey", function(chat)

CreateChatInChatBox(chat)

event:disconnect()

end)

It is important to disconnect the function when you do not need it, because there is a limit on number of times we use all of thoses methods/events of DataStores, and we do not want to exceed it when the value changes when we do not need it.

Tables - A simple way to use less often the events/methods:

Because the Events and Methods of DataStores have limits on the use, it is not only better to load data once every time a player joins and save once in a while, but also to use a Table...OnUpdate Event, UpdateAsync(), SetAsync() and GetAsync() (not IncrementAsync() ) Can support Tables, using a table lets you save all the data into 1 key(like currency, points, level, inventory and such) There are 2 kinds of Tables:

-Array-Style Tables

-Dictionary-Style Tables

I would recomend Dictionary-Style for DataStores, but if you want to save items of an Inventory, I think you should use Array-Style for it.

Array-Style Tables:

Array-Style Tables are ordered, they are best for something that must keep a specific order(Like an Inventory) ,because you can give a position to each values, they are working like this: tablename[number position] = value

Dictionnary-Style Tables:

Dictionnary-Style tables are not ordered. They are best for something that must be saved in a specific name(like a whole user data) because you can give a name to each values, they are working like this: tablename["String Name"] = value

The good point about tables: They can work with every kinds of values. For more information about tables, read our table tutorial(Not created yet)

Limits:

Request Limits:

Type _____________ Limit Per Minute

Gets(GetAsync()) -------------------------------------------60+NumberOfPlayers*10

Sets(SetAsync(),IncrenentAsync(),UpdateAsync())-----60+NumberOfPlayers*10

OnUpdate(OnUpdate)--------------------------------------30+NumberOfPlayers*5

Request Rejected due to Request Limits, What to do:

A good way to verify your saving and make sure it was successfully saved,INSTEAD USE CLASSIC SetAsync(), Use this:

local ds = game:GetService("DataStoreService"):GetDataStore("UserId","Stuff")

repeat wait(1)

local result,errors = pcall(function()

ds:SetAsync("Points",500)

end)

if result == false then

print("Save Failed, Trying Again in 1 second: "..errors)

wait(1)

else print("Save Success")

end

until result == true

This will check is the save was a success when making the user points 500, if not, it will wait 1 second before trying again. Use this system for IMPORTANT SAVES

This works with IncrementAsync(),UpdateAsync(),SetAsync(), GetAsync() too,Usefull to make sure a save is saved/loaded properly in case of the request limit reached or any other problems

Data Limits:

Type Character Limit

Key-------------50

Name----------50

Scope----------50

Data-----------64998

Unlike Data Persistance: Cannot Save Instances(You could still save the Instance Data)

Use UpdateAsync() for saves that are hosted on many servers, it is safer.

Tables can only use 1 style, a table cannot be BOTH Array and Dictionnary Styles

OrderedDataStore:

As DataStores and GlobalDataStores do not let you save dat in order from their value, and as you cannot load the data of alot players when required, the best way to create a in-game global leaderboard that shows top players, is to use OrderedDataStores:

OrderedDataStores can ONLY USE INTERGERS, and can pull MANNY REQUESTS at a time, saving alot in request limits. THEY ARE NOT SAVED IN THE SAME PLACE AS NORMAL DATA STORES!

They work just like Regular DataStores but with some more options...

Again they follow same system with Scopes, DataStores, Keys, and Values, explained in the DataStore/GlobalDataStore tutorial. They also have same methods/functions but with some more added to them, to get a GLOBAL OrderedDataStore:

local ds = game:GetService("DataStoreService"):GetOrderedDataStore("DataStoreName")

Or

local ds = game:GetService("DataStoreService"):GetOrderedDataStore("DataStoreName","Global")

to get a CUSTOM OrderedDataStore:

local ds = game:GetService("DataStoreService"):GetOrderedDataStore("DataStoreName","Scope Name Here")

Methods:

GetAsync(key)

View GetAsync() Method in the DataStore/GlobalDataStore section EXCEPTION of normal DataStores: Only accepts Intergers!!

SetAsync(key, value)

View SetAsync() Method in the DataStore/GlobalDataStore section EXCEPTION of normal DataStores: Only accepts Intergers!!

UpdateAsync(key, function())

View UpdateAsync() Method in the DataStore/GlobalDataStore section EXCEPTION of normal DataStores: Only accepts Intergers!!

IncrementAsync(key, interger)

View GetAsync() Method in the DataStore/GlobalDataStore section

GetSortedAsync(isAscending,PageSize,minValue,maxValue)

GetSortedAsync() will create a set pages with all the keys Keys(Or the ones in min/max values if they are there):

As here we get many keys at once, we do not require a Key Parameter, isAscending and PageSize parameters are REQUIRED. isAscending is a boolean, make it false if you want the values to be top numbers first, last numbers down. PageSize is the number of keys per pages. min and max values are not required, but they are usefull to put either a minimum value to be on leaderboard, OR,a minimum AND a maximum(Cannot be just a max)

Heres how it works, following script will create a page object from the OrderedDataStore and update it every minutes:

local ds = game:GetService("DataStoreService"):GetOrderedDataStore("TopPlayers","Kills")

while wait(60) do

pages = ds:GetSortedAsync(false,5,0,5000)

end

Here a new set of pages is created to update the old one every minute

pages:AdvanceToNextPageAsync()

this will advance to the next page of your pages object. Now the following script will print the page number we current are on then wait 1 sec before advancing for the 10 first pages:

pagenum = 1

local ds = game:GetService("DataStoreService"):GetOrderedDataStore("TopPlayers","Kills")

while wait(60) do

pages = ds:GetSortedAsync(false,5,0,5000)

print(pagenum)

for i = 1,10 do

wait(1)

pages:AdvanceToNextPageAsync()

pagenum = pagenum+1

print(pagenum)

end

end

Here the script advanced of 1 page every second and print the page number, for first 10 pages...

pages:GetCurrentPage()

GetCurrentPage() is a method that will return a list of all the keys from a page,here is how it works,This script will be the same as our previous one but it will also update a leaderboard every minutes(also it doesnt wait 1 sec between pages):

pagenum = 1

list = script.Parent.ScrollingFrame

base = script.Parent.TextLabel

local ds = game:GetService("DataStoreService"):GetOrderedDataStore("TopPlayers","Kills")

while wait(60) do

pages = ds:GetSortedAsync(false,5,0,5000)

print(pagenum)

list:ClearAllChildren

keys = pages:GetCurrentPage()

for i,v = in pairs(keys) do

newbase = base:Clone()

newbase.Parent = list

newbase.Position = UDim2.new(-0.02+0.02*i,0,0,0)

newbase.Text = keys.key..":"..keys.value

end

for p = 1,9 do

pages:AdvanceToNextPageAsync()

pagenum = pagenum+1

print(pagenum)

keys = pages:GetCurrentPage()

for i,v = in pairs(keys) do

newbase = base:Clone()

newbase.Parent = list

newbase.Position = UDim2.new(0.8+0.02*i*p,0,0,0)

newbase.Text = keys.key..":"..keys.value

end

end

Here the script created a scrollingframe of 50 players by combining 10 pages togueter, it first does a first page, then after, it repeats 9 times a page advancement with adding the values...The loop is used to put each values 1 by 1 from a page

Events:

OnUpdate

View OnUpdate Event in the DataStore/GlobalDataStore section EXCEPTION of normal DataStores: Only accepts Intergers!!

Properties:

pages.IsFinished

Boolean value telling if current page is the last page of the pages, returns true if the page is the last of the DataStore, READ: Considering that the Previous leaderboard code may attempt to create pages that doesnt exist(If there are less than 50 players in total for the pages), it is important to make a check for that shown as the following:

pagenum = 1

list = script.Parent.ScrollingFrame

base = script.Parent.TextLabel

local ds = game:GetService("DataStoreService"):GetOrderedDataStore("TopPlayers","Kills")

while wait(60) do

pages = ds:GetSortedAsync(false,5,0,5000)

print(pagenum)

list:ClearAllChildren

keys = pages:GetCurrentPage()

for i,v = in pairs(keys) do

newbase = base:Clone()

newbase.Parent = list

newbase.Position = UDim2.new(-0.02+0.02*i,0,0,0)

newbase.Text = keys.key..":"..keys.value

end

for p = 1,9 do

if pages.IsFinished == false then

pages:AdvanceToNextPageAsync()

pagenum = pagenum+1

print(pagenum)

keys = pages:GetCurrentPage()

for i,v = in pairs(keys) do

newbase = base:Clone()

newbase.Parent = list

newbase.Position = UDim2.new(0.8+0.02*i*p,0,0,0)

newbase.Text = keys.key..":"..keys.value

end

end

end

This add, right on top of AdvanceToNextPageAsync() will make sure that the page is not the last, and the there is atleast 1 more before continuing. It will skip if the page is last.

Limits:

Request Limits:

Type _____________ Limit Per Minute

Gets(GetAsync()) -------------------------------------------60+NumberOfPlayers*10

Sets(SetAsync(),IncrenentAsync(),UpdateAsync())-----60+NumberOfPlayers*10

GetSorted(GetSortedAsync())------------------------------5+NumberOfPlayers*2

OnUpdate(OnUpdate)--------------------------------------30+NumberOfPlayers*5

Request Rejected due to Request Limits, What to do:

A good way to verify your saving and make sure it was successfully saved,INSTEAD USE CLASSIC SetAsync(), Use this:

local ds = game:GetService("DataStoreService"):GetOrderedDataStore("Data","Points")

repeat wait(1)

local result,errors = pcall(function()

ds:SetAsync("Points",500)

end)

if result == false then

print("Save Failed, Trying Again in 1 second: "..errors)

wait(1)

else print("Save Success")

end

until result == true

This will check is the save was a success when making the user points 500, if not, it will wait 1 second before trying again. Use this system for IMPORTANT SAVES

This works with IncrementAsync(),UpdateAsync(),SetAsync(), GetAsync(), and GetSortedAsync() too,Usefull to make sure a save is saved/loaded properly in case of the request limit reached or any other problems

Data Limits:

Type Character Limit

Key-------------50

Name----------50

Scope----------50

Data-----------64998

OrderedDataStore can only save INTERGERS

Use UpdateAsync() for saves that are hosted on many servers, it is safer.

-iiDevelopers