Hi there!
In this tutorial I am going to show you how to create a saving system using Roblox’s default DatastoreService. This is aimed for people looking to reduce data loss, or those who are new to datastores.
I know there are alternatives such as ProfileService and Datastore2, but this tutorial focuses on Roblox’s API, and how to improve the security of your players’ data.
If there are any improvements I could make please let me know, I appreciate you taking the time to read this tutorial! - Shepp2004
1. Creating our stats
First we need our players to have stats that we can save and load the next time they join the game.
In this tutorial, we will have 2 types of stats: leaderstats and playerstats.
Leaderstats are the stats that show up on the leaderboard when you are in a game:
Playerstats are the stats that we don’t want to show on the leaderboard. Let’s create a server script to give players these stats when they join the game. Inside this script, add two folders renaming them to “playerstats” and “leaderstats”.
It is very important you name “leaderstats” correctly as Roblox looks for this name inside the player when deciding what should go on the leaderboard.
Server script (In ServerScriptService):
local playerService = game:GetService("Players")
-- This will run when a player joins the game.
local function onPlayerAdded(player)
-- Clone a leaderstats folder
local leaderstats = script.leaderstats:Clone()
leaderstats.Parent = player
-- Clone a playerstats folder
local playerstats = script.playerstats:Clone()
playerstats.Parent = player
end
-- This will run when a player leaves the game
local function onPlayerRemove(player)
-- Here we want to save the data, we will add this soon
end
-- This function will run when the script loads
local function initialise()
--[[ Here, we want to iterate through all players that
may have joined before this script loaded.
]]--
for _, player in ipairs(playerService:GetPlayers()) do
onPlayerAdded(player)
end
end
-- Connect events for when players leave / join
playerService.PlayerAdded:Connect(onPlayerAdded)
playerService.PlayerRemoving:Connect(onPlayerRemove)
Now that this player script is done, we can add some values to the folders we just created. For this example I am going to add “Cash” and “Level” to leaderstats, and I’m going to add “XP” and “SpawnPoint” to playerstats.
Make sure “SpawnPoint” is a Vector3Value and all the others are IntValues.
If you have different values, you can change the code later on to fit your game.
[Important] Once you have all your values, you should add an intValue called DataId. (You don’t have to name it exactly this, but make sure to called it something along these lines!)
2. Saving data
Next, in our player script, we are going to add a few functions to let us save our data. Just underneath where we defined “playerService”, add the code:
local dataStoreService = game:GetService("DataStoreService")
-- You can replace the "Data" in the brackets with anything you'd like.
-- It is the name of the data store
local gameDataStore = dataStoreService:GetDataStore("Data")
local function serialiseData(player)
-- I will explain these next 2 functions shortly
end
local function deserialiseData(data)
end
local function saveData(player)
end
local function loadData(player)
end
Roblox’s data stores only allow the certain types to be saved:
- Numbers
- Strings
- Bools (True / false)
- Arrays
- Null
This means we must change (serialise) our data so that is in a saveable format. But first, we will add some code to the saveData function.
SaveData function:
local function saveData(player)
-- First we need to find their leaderstats / playerstats
local leaderstats = player:FindFirstChild("leaderstats")
local playerstats = player:FindFirstChild("playerstats")
if not (leaderstats and playerstats) then
-- Warn the script
return false, "No leaderstats/playerstats found for " .. player.Name
end
-- Next, serialise the data into a saveable format
local data = serialiseData(player)
-- Get their userId
local userId = player.UserId
-- Now we can attempt to save to Roblox
-- We retry up to 10 times if something goes wrong
local attemptsLeft = 10
local success, errorMessage
repeat wait()
pcall(function()
-- We should use UpdateAsync here instead of
-- SetAsync() as we can compare to previous data
success, errorMessage = gameDataStore:UpdateAsync(userId, function(old)
if not (old and old.DataId) then
-- If no old data was found, we should overwrite the data
return data
elseif (old.DataId > data.DataId) then
-- The data didn't load correctly! Do not overwrite
error("Data did not load correctly!")
else
-- The data is safe to overwrite
return data
end
end)
end)
attemptsLeft -= 1 -- Decrease the number of attempts left
until (attemptsLeft == 0) or (success)
-- Check that the data was saved
if success then
print(player.Name .. "'s data was saved successfully!")
-- We should increment their data Id so that their data
-- can be saved again in this server
playerstats.DataId.Value += 1
else
-- Display the error message
warn(player.Name .. "'s data wasn't saved! :", errorMessage)
end
-- Return the result
return success, errorMessage
end
Now that we’ve got our function for saving data, we need a way of loading data! This is what we’ll do soon in step 4.
3. BindToClose()
This next step is very important as it ensures that players’ data tries to save if a server gets shutdown. Just add this piece of code to the bottom of the server script:
game:BindToClose(function() -- Runs when a server shuts down
-- Iterate through every player in the server and save their data
for _, p in ipairs(playerService:GetPlayers()) do
saveData(p)
end
end)
4. Loading Data
The following code should go in the “loadData” function. It makes up to 10 attempts to retrieve the data, and is wrapped in a ‘pcall’ to handle errors that could occur due to communication issues with the Roblox servers.
local function loadData(player)
-- Get their user Id
local userId = player.UserId
local data = nil
local attemptsLeft = 10
-- Repeat 10 times, or until the script has communicated successfully
local success, errorMessage
repeat task.wait()
success, errorMessage = pcall(function()
data = gameDataStore:GetAsync(userId)
end)
attemptsLeft -= 1 -- Reduce the number of attempts left
until (attemptsLeft == 0) or success
-- Check if there was a problem with the Roblox servers
if (not success) then
warn("Error Loading player data:", errorMessage)
return
end
-- Check whether there is any data or they're a new player
-- Also, we should get their leaderstats and playerstats folders
local leaderstats = player:WaitForChild("leaderstats")
local playerstats = player:WaitForChild("playerstats")
if data then
-- Returning player
-- Here we can load in the values
leaderstats.Cash.Value = data.Cash or 50
leaderstats.Level.Value = data.Level or 1
playerstats.SpawnPoint.Value = data.SpawnPoint or Vector3.new(0, 0, 0)
playerstats.XP.Value = data.XP or 0
-- Set their data Id to what it previously was
playerstats.DataId.Value = data.DataId or 0
else
-- New player!
-- Here we can set default values e.g cash = 50
leaderstats.Cash.Value = 50
end
-- Finally we want to increment their dataId, to indicate that the data loaded
playerstats.DataId.Value += 1
return true -- Success!
end
Notice where I wrote:
leaderstats.Level.Value = data.Level or 1
This line means that if there is no ‘Level’ saved in the datastore, the value will default to 1.
5. Serialising data
We are now near the end of this tutorial!
The function we are going to write is responsible for collecting all the player’s data from their folders and putting it all in a saveable format. In the SerialiseData() function, we can add this:
local function serialiseData(player)
-- Collects a player's data into a table
-- Find the player's leaderstats/playerstats
local leaderstats = player:FindFirstChild("leaderstats")
local playerstats = player:FindFirstChild("playerstats")
if not (leaderstats and playerstats) then
warn("No leaderstats/playerstats found for " .. player.Name)
return
end
-- Create a table for the data to go in
local data = {
Cash = leaderstats.Cash.Value;
Level = leaderstats.Level.Value;
XP = playerstats.XP.Value;
-- Because 'SpawnPoint' is a vector3 value which can't be saved,
-- we need to convert it to a dictionary
SpawnPoint = nil;
DataId = playerstats.DataId.Value;
}
-- We will convert spawn point here:
data.SpawnPoint = {
X = playerstats.SpawnPoint.Value.X;
Y = playerstats.SpawnPoint.Value.Y;
Z = playerstats.SpawnPoint.Value.Z;
}
return data -- This data can now be saved to a DatastoreService
end
6. Deserialising Data
This step is only necessary if you have data types like Vector3 (e.g Color3) that can’t be saved to Datastores. If you don’t you can skip to the next and final step.
The deserialiseData() function takes the array we made before and converts it back into Vector3 or whatever you need.
local function deserialiseData(data)
-- In this case, we only need to deserialise "SpawnPoint"
-- We take the dictionary and convert to a Vector3 Value
if data.SpawnPoint then
local dict = data.SpawnPoint
local vector = Vector3.new(dict.X, dict.Y, dict.Z)
-- Replace the dictionary with the Vector3
data.SpawnPoint = vector
end
-- Return the updated data
return data
end
Finally, in the loadData() function, just under where we checked if the data had loaded, add this:
if data then
-- Returning player!
-- Here we can deserialise their data
data = deserialiseData(data)
-- Here we can load in their values
...
7. Connecting everything up
The final step is to actually run these (saving / loading) functions when a player leaves or joins the game!
In the onPlayerAdded() function we made at the beginning, add the highlighted line:
local function onPlayerAdded(player)
-- Clone a leaderstats folder
local leaderstats = script.leaderstats:Clone()
leaderstats.Parent = player
-- Clone a playerstats folder
local playerstats = script.playerstats:Clone()
playerstats.Parent = player
> -- **Load their data**
> loadData(player)
end
And finally, in the onPlayerRemove() function, we can call the saveData() function:
local function onPlayerRemove(player)
-- Save their data
saveData(player)
end
You are now done with the tutorial! Thanks for reading, I hope this gave you some useful tips on saving player data using UpdateAsync(), as well as pcalls and using the :BindToClose() method.
Here is the link to the place with all the code:
Thank you for reading! ~ Shepp2004