Saving Player Data

ROBLOX allows information to be saved on its servers. One of the primary uses for this feature is to store player data between sessions. This allows players to keep progressing in a game even if they leave and come back. In this tutorial, we will create statistics for a player's money and experience. We will then setup a system that automatically stores this data so it can be retrieved when the player joins the game again. To do this, we will utilize a ROBLOX service called DataStoreService.

Setting up player data[edit]

Before we dive into data storage, let's first setup the system that keeps track of the player's money and experience in the game session. We'll start by creating a ModuleScript for our game. A ModuleScript is a special type of script that can be referenced from other scripts. The ModuleScript can be setup to return a table. If it does, the other scripts that reference the ModuleScript can access the values in that table such as functions, variables, even other tables.

ModuleScript in ServerStorage:

-- Setup table that we will return to scripts that require the ModuleScript.
local PlayerStatManager = {}
 
-- Table to hold all of the player information for the current session.
local sessionData = {}
 
-- Function the other scripts in our game can call to change a player's stats. This
-- function is stored in the returned table so external scripts can use it.
function PlayerStatManager:ChangeStat(player, statName, changeValue)
	sessionData[player][statName] = sessionData[player][statName] + changeValue
end
 
-- Function to add player to the sessionData table.
local function setupPlayerData(player)
	sessionData[player] = {Money = 0, Experience = 0}
end
 
-- Bind setupPlayerData to PlayerAdded to call it when player joins.
game.Players.PlayerAdded:connect(setupPlayerData)
 
-- Return the PlayerStatManager table to external scripts can access it.
return PlayerStatManager

Script in ServerScriptService:

-- Require ModuleScript so we can change player stats
local PlayerStatManager = require(game.ServerStorage.PlayerStatManager)
 
-- After player joins we'll periodically give the player money and experience
game.Players.PlayerAdded:connect(function(player)
	while wait(2) do
		PlayerStatManager:ChangeStat(player, 'Money', 5)
		PlayerStatManager:ChangeStat(player, 'Experience', 1)
	end
end)

Saving player data[edit]

Now that we have a system that keeps track of player data, let's start storing this information to ROBLOX's database using DataStores. The first thing we need to do is add a variable for the DataStore in our ModuleScript. We can use the GetDataStore function to get a DataStore for our game. This function takes a string parameter which will be the name of our DataStore. If no DataStore exists with that name ROBLOX will create one automatically.

local DataStoreService = game:GetService('DataStoreService')
local playerData = DataStoreService:GetDataStore('PlayerData')
You can name your DataStores whatever is convenient for you. Note that DataStores are shared between all places in a game. If your game has multiple places, they can all access the same DataStore by having them all request the same name.

Now that we have a variable holding our DataStore let's change how our setupPlayerData function works. Before, it just created new data for a player every time they joined the game. Now, we can check to see if the DataStore is holding any information for that player with the GetAsync function. If that function doesn't return anything then we haven't saved anything for that player yet and we need to create a new save for them using SetAsync.

local function setupPlayerData(player)
	local data = playerData:GetAsync(player.UserId)
	if not data then
		data = {Money = 0, Experience = 0}
		playerData:SetAsync(player.UserId, sessionData[player])
	end
	sessionData[player] = data
end

We now need to make sure that the player's data is saved when they leave the game. We can create a new function and bind it to the PlayerRemoving event.

local function setupPlayerData(player)
	local data = playerData:GetAsync(player.UserId)
	if not data then
		data = {Money = 0, Experience = 0}
		playerData:SetAsync(player.UserId, sessionData[player])
	end
	sessionData[player] = data
end
 
game.Players.PlayerRemoving:connect(function(player)
	savePlayerData(player)
	-- We don't need to keep track of the data anymore
	sessionData[player] = nil
end)

Lastly, we want to make sure our system can handle unfortunate events like game crashes. These should be infrequent, but we definitely don't want players to lose any of their progress! While it is tempting to save player data any time there is a change, DataStores have a limit to how often they can be accessed. Access a DataStore too often and it will slow down how fast it can retrieve and store data. A simple solution is to automatically save player's data on a timer. To do this, let's make a function that waits a set amount of time in a loop. In the loop we'll cycle through all of the players in our sessionData table and use the savePlayerData function we made earlier to save the current information. Once this function is defined we can use Spawn to have the function called in the background of the game.

local AUTOSAVE_INTERVAL = 60
 
local function autosave()
	while wait(AUTOSAVE_INTERVAL) do
		for player, data in pairs(sessionData) do
			savePlayerData(player)
		end
	end
end
 
spawn(autosave)

With all of the above implemented we now have a simple stat saving system which will automatically save our player's data! The below script shows the ModuleScript in full with changes from before highlighted in yellow:

-- Setup table that we will return to scripts that require the ModuleScript.
local PlayerStatManager = {}
 
-- Create variable for the DataStore.
local DataStoreService = game:GetService('DataStoreService')local playerData = DataStoreService:GetDataStore('PlayerData') 
-- Create variable to configure how often the game autosaves the player data.
local AUTOSAVE_INTERVAL = 60 
-- Table to hold all of the player information for the current session.
local sessionData = {}
 
-- Function the other scripts in our game can call to change a player's stats. This
-- function is stored in the returned table so external scripts can use it.
function PlayerStatManager:ChangeStat(player, statName, changeValue)
	sessionData[player][statName] = sessionData[player][statName] + changeValue
end
 
-- Function to retrieve player's data from the DataStore.
local function getPlayerData(player)	return playerData:GetAsync(player.UserId)end 
-- Function to save player's data to the DataStore.
local function savePlayerData(player)	playerData:SetAsync(player.UserId, sessionData[player])end 
-- Function to add player to the sessionData table. First check if the player has
-- data in the DataStore. If so, we'll use that. If not, we'll add the player to
-- the DataStore.
local function setupPlayerData(player)
	local data = getPlayerData(player)	if not data then		-- DataStores are working, but no data for this player		sessionData[player] = {Money = 0, Experience = 0}		savePlayerData(player)	else		-- DataStores are working and we got data for this player		sessionData[player] = data	endend
 
-- Function to run in the background to periodically save player's data.
local function autosave()	while wait(AUTOSAVE_INTERVAL) do		for player, data in pairs(sessionData) do			savePlayerData(player)		end	endend 
-- Bind setupPlayerData to PlayerAdded to call it when player joins.
game.Players.PlayerAdded:connect(setupPlayerData)
 
-- Call savePlayerData on PlayerRemoving to save player data when they leave.
-- Also delete the player from the sessionData, as the player isn't in-game anymore.
game.Players.PlayerRemoving:connect(function(player)	savePlayerData(player)	sessionData[player] = nilend) 
-- Start running autosave function in the background.
spawn(autosave)
 
-- Return the PlayerStatManager table to external scripts can access it.
return PlayerStatManager

Handling DataStore failures[edit]

ROBLOX DataStores are very reliable but if you are saving player data you should always prepare for the worst. If your game cannot connect to the DataStore service then your game should be able to handle that and inform the player. A classic way to handle connection failures is to make sure every DataStore access call (SetAsync, GetAsync, UpdateAsync) can be retried several times. If all of those tries fail, you can then inform the player that their data may not be saved this session. Then, depending on how dependent your game is on the saved data, you can encourage players to play without saving data or encourage them to try again later.

If a DataStore cannot be communicated with, any script that calls GetAsync or SetAsync will create an error and stop execution of the script it is inside of. Your scripts should be prepared for this case by wrapping such calls in a pcall. If code inside a pcall function fails then instead of crashing the script, the pcall function will catch the error and cause the pcall function to return false. If the code runs without error then the pcall function will return true. Here is a simple example of how we could rewrite our savePlayerData function to check for this error.

local function savePlayerData(player)
	local success, message = pcall(function()
		playerData:SetAsync(player, sessionData[player])
	end)
	if not success then 
		print(message) 
	end
end

Since we want to retry the function several times, we can put this code inside of a loop that finishes only when the call succeeds or it has tried too many times. When we define this function, we can also have one of the parameters be the function we want to retry. This way this code can be used for both setting and getting values from the DataStore.

local DATASTORE_RETRIES = 3
 
local function dataStoreRetry(dataStoreFunction)
	local tries = 0	
	local success = true
	local data = nil
	repeat
		tries = tries + 1
		success = pcall(function() data = dataStoreFunction() end)
		if not success then wait(1) end
	until tries == DATASTORE_RETRIES or success
	if not success then
		error('Could not access DataStore! Warn players that their data might not get saved!')
	end
	return success, data
end
 
local function getPlayerData(player)
	return dataStoreRetry(function()
		return playerData:GetAsync(player.UserId)
	end)
end
 
local function savePlayerData(player)
	return dataStoreRetry(function()
		return playerData:SetAsync(player.UserId, sessionData[player])
	end)
end

If the initial load of data fails, there are several things we can do to safeguard our players. A very simple and conservative approach is to not permit any saving if the initial load failed. That way the game will not overwrite any old info. The way we'll do this is we will set the sessiondata of a player to false if it fails to load. Then, when setting data, we will check if the value is false or not. If the value exists, then we can save.

local function setupPlayerData(player)
	local success, data = getPlayerData(player)
	if not success then
		sessionData[player] = false
	else
		if not data then
			sessionData[player] = {Money = 0, Experience = 0}
			savePlayerData(player)
		else
			sessionData[player] = data
		end
	end	
end

We can integrate all this code with our code from before. Now we have a robust player statistic system that automatically saves data and gives us the ability to warn the player if anything is going wrong.

-- Setup table that we will return to scripts that require the ModuleScript.
local PlayerStatManager = {}
 
-- Create variable for the DataStore.
local DataStoreService = game:GetService('DataStoreService')
local playerData = DataStoreService:GetDataStore('PlayerData')
 
-- Create variable to configure how often the game autosaves the player data.
local AUTOSAVE_INTERVAL = 60
 
-- Number of times we can retry accessing a DataStore before we give up and create
-- an error.
local DATASTORE_RETRIES = 3 
-- Table to hold all of the player information for the current session.
local sessionData = {}
 
-- Function the other scripts in our game can call to change a player's stats. This
-- function is stored in the returned table so external scripts can use it.
function PlayerStatManager:ChangeStat(player, statName, changeValue)
	sessionData[player][statName] = sessionData[player][statName] + changeValue
end
 
-- Function to retry the passed in function several times. If the passed in function
-- is unable to be run then this function returns false and creates an error.
local function dataStoreRetry(dataStoreFunction)	local tries = 0		local success = true	local data = nil	repeat		tries = tries + 1		success = pcall(function() data = dataStoreFunction() end)		if not success then wait(1) end	until tries == DATASTORE_RETRIES or success	if not success then		error('Could not access DataStore! Warn players that their data might not get saved!')	end	return success, dataend 
-- Function to retrieve player's data from the DataStore.
local function getPlayerData(player)
	return dataStoreRetry(function()		return playerData:GetAsync(player.UserId)	end)end
 
-- Function to save player's data to the DataStore.
local function savePlayerData(player)
	if sessionData[player] then		return dataStoreRetry(function()			return playerData:SetAsync(player.UserId, sessionData[player])		end)	endend
 
-- Function to add player to the sessionData table. First check if the player has
-- data in the DataStore. If so, we'll use that. If not, we'll add the player to
-- the DataStore.
local function setupPlayerData(player)
	local success, data = getPlayerData(player)	if not success then		-- Could not access DataStore, set session data for player to false.		sessionData[player] = false	else		if not data then
			-- DataStores are working, but no data for this player
			sessionData[player] = {Money = 0, Experience = 0}
			savePlayerData(player)
		else
			-- DataStores are working and we got data for this player
			sessionData[player] = data
		end
	end	
end
 
-- Function to run in the background to periodically save player's data.
local function autosave()
	while wait(AUTOSAVE_INTERVAL) do
		for player, data in pairs(sessionData) do
			savePlayerData(player)
		end
	end
end
 
-- Bind setupPlayerData to PlayerAdded to call it when player joins.
game.Players.PlayerAdded:connect(setupPlayerData)
 
-- Call savePlayerData on PlayerRemoving to save player data when they leave.
-- Also delete the player from the sessionData, as the player isn't in-game anymore.
game.Players.PlayerRemoving:connect(function(player)
	savePlayerData(player)
	sessionData[player] = nil
end)
 
-- Start running autosave function in the background.
spawn(autosave)
 
-- Return the PlayerStatManager table to external scripts can access it.
return PlayerStatManager