Datastore Tutorial (Reduce data loss!)

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! :slight_smile: - 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

26 Likes

Should be ipairs(playerService:GetPlayers())

Also you forgot to link the place

2 Likes

Thanks, sorry I knew I’d forgotten something :joy:

Nice tutorial but please use task.wait() wait is outdated

3 Likes

You should do a tutorial on Profile Service. These types of data stores just reduce data loss, but what about REMOVING data loss?

2 Likes

I do like to use ProfileService, but this tutorial just focuses on Roblox’s default Datastore implementation. I agree Profile Service is more secure but sometimes it can take a while for data to load :slight_smile:

Using profileservice does not “remove data loss”, its only enemy is stuff like pet cloning via trading etc. not data loss


Generalized loops also work well, using ipairs or pairs does no difference.

3 Likes

ipairs is actually faster then pairs. ipairs should only be used when all objects that are being looped through are the same type.

because they are iterated, generalized pairs will either iterate pair or pairs if its a dictionary

also micro optimization is not good

edit

Edit: i hardly even call this optimization. you are only saving 0.01 seconds using ipairs

0.04	pairs
0.03	ipairs

code

local clock = os.clock
local function benchmark(benchmarks, times)
   for i,v in pairs(benchmarks) do
      local first = clock()
      for i = 1, times do
         v()
      end
      print(clock() - first, i)
   end
end
benchmark({
   ["pairs"] = function()
      for _,v in pairs({"hi", "mango"}) do
         v = nil
      end
   end,
   ipairs = function()
      for _,v in ipairs({"hi", "mango"}) do
         v = nil
      end
   end
}, 100000)

Just a quick suggestion when benchmarking, try to make the benchmark as light as possible to narrow in on what needs to be tested. I made a post about pairs and ipairs a while ago and benchmarked the two:

If we use that benchmark we can see the difference between the two is actually 0.000000000000014551915228 per iteration.

So you’re absolutely correct, it’s micro optimization; it’s just even more micro than what was shown.

2 Likes

I think they did, by using the DataId in the load function and checking it while saving.

2 Likes

It doesn’t save my data. :confused:
I followed everything I think the tutorial was not completed.

1 Like

You should try DataStore2 it’s easier to learn and doesn’t have any data loss (if used correctly)
Tutorial
Module

2 Likes

Did you get any error messages in the output?
Sorry for the late reply.

ProfileService works too and is more modern

2 Likes

I don’t even remember but I tried to follow the tutorial. It wasn’t easy, I had to cut somethings because I was not making an obby so I didn’t want to save the position of the player.

1 Like

I don’t know how to use it sadly.

2 Likes

Oh, I made a tutorial about it where you don’t have to set it up the repetitive way, unlike what YouTubers teach you to do:

I’m sorry about that, which part can you not get to work? :frowning:

Saving and giving the data I think. I had to remove a part of the script because I am not making an obby.