Hey y’all, today I am going to teach you how to do some data stores in Roblox!
BOOOOOOOOOOOOOOOOOO, another Data Store tutorial, smh.
Why can’t this guy teach us something better?
I am tired of learning all of these stupid data storing tutorials, please make something more unique.
Okay, we are not going to use Roblox’s integrated DataStoreService for this tutorial. Instead, we are going to use 2 useful modules which are made by the legendary man himself, @loleris!
ProfileService
ProfileService serves as a module for loading and saving data, without worrying anything that could cause item loss or item duplication, because of its feature known as Session Locking. This is actually slightly better than DataStore 2.0 which was made by another developer.
ReplicaService
ReplicaService basically allows you to replicate almost anything or states which are server-side. The reason why we need this module is because ProfileService is designed for server side, meaning that our data is server-side, which means clients are unable to receive their own data to present. ReplicaService was designed to overcome this, as stated in the ProfileService module introduction post!
Why are you making a tutorial of this?
Well, although there are videos about teaching how to use ProfileService, there isn’t any tutorials that teach you how to use ReplicaService, excluding loleris himself giving us a basic usage of it in the API page. So, after I done some testing with it, I am confident myself have the ability to teach you guys how to use it. To any experience users of both modules, do correct me if I made any mistakes!
Before continuing, you must have a basic understanding of DataStoreService, even though ProfileService backends would handle this. This is because we are going to talk about some of DataStoreService’s function which leads to why sometimes item loss or item duplication can happen.
You also need to get the modules as well.
Let's start learning!
Setting up player added and removing events
First things first, we must parent the modules in their own respective parents.
For ReplicaService, follow the following hierachy stated by loleris.
https://madstudioroblox.github.io/ReplicaService/images/HowItsMade.jpg
You can just remove the examples folder if you don’t wanna test it out first.
Next, parent your ProfileService module script somewhere you like, I like to put it in ServerStorage.
Don’t worry about the ReplicaService folder.
Create a module script named DataManager, place it somewhere else you like. Again, I like ServerStorage.
Okay, now we can finally start scripting this big thing. To make things easier for you to learn, we will start by learning the basic usages of ProfileService first.
- First, define the following services and modules which we need to setup in the
DataManager
module script that you just created.
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ProfileService = require(ServerStorage.ProfileService)
- Next, we need to get a data store, or known as ProfileStore, from ProfileService. To do that, we will use a function called
.GetProfileStore()
.
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ProfileService = require(ServerStorage.ProfileService)
local CashProfileStore = ProfileService.GetProfileStore("CashProfileStore")
CashProfileStore
is the name of our ProfileStore, there is a second argument we need to provide inside this function, which is known as the template for that ProfileStore. So, if we access to this ProfileStore, we would get a data template containing all of the things that are available inside the template. For our template, we will just create a dictionary and add a key value pair where the key is named as Cash and the value, acting as the starting cash, is 100.
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ProfileService = require(ServerStorage.ProfileService)
local CashProfileStore = ProfileService.GetProfileStore("CashProfileStore",{
Cash = 100
})
- Next, we need something to refer the player’s profile so we could modify their data. A profile in ProfileService just basically means a unique thing to refer ones’ data. You get the idea, it’s pretty straightforward. So, we have to create an empty table containing all of the player profiles, and we will index it using the player instance itself.
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ProfileService = require(ServerStorage.ProfileService)
local CashProfileStore = ProfileService.GetProfileStore("CashProfileStore",{
Cash = 100
})
local Profiles = {}
- Now, we will need to return something from this module script right? So just make it that this module will return our functions.
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ProfileService = require(ServerStorage.ProfileService)
local CashProfileStore = ProfileService.GetProfileStore("CashProfileStore",{
Cash = 100
})
local Profiles = {}
local DataManager = {}
return DataManager
- Next, we wanna make it so that whenever a player joins, we load out their data, and when a player leaves we save it. So, create 2 functions and connect them to their own respective events as stated below:
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ProfileService = require(ServerStorage.ProfileService)
local CashProfileStore = ProfileService.GetProfileStore("CashProfileStore",{
Cash = 100
})
local Profiles = {}
local DataManager = {}
local function OnPlayerAdded(player)
end
local function OnPlayerRemoved(player)
end
Players.PlayerAdded:Connect(OnPlayerAdded)
Players.PlayerRemoving:Connect(OnPlayerRemoved)
return DataManager
- Let’s start scripting the
PlayerAdded
event’s connected function first. In here, we first need to get their profile from the ProfileStore, using the:LoadProfileAsync()
function.
local function OnPlayerAdded(player)
local PlayerProfile = CashProfileStore:LoadProfileAsync("CashProfileStore"..player.UserId,"ForceLoad")
end
Just like the DataStoreService, you need an unqiue identifier key in order to retrieve one’s data. So in this case, the key will be "CashProfileStpre"..player.UserId
. Then, we put in a second argument which tells ProfileService what to do if the profile is not released. You can pass in a function that will run what happens if the profile is unable to be retrieved. The reason why sometimes the profile is unable to be retrieved is because that profile is being session locked.
Allow me to explain what is session lock. Let’s say, you have Player A and Player B. Player A trades with Player B, Player A puts a super cool item, and if Player A somehow can keep that cool item even after it is being traded, then they would have infinite copies of that cool item, and this is a problem known as item duplication.
Consider that Player B is Player A’s alt account. The way how this works is that right after the trade is completed, Player A will immediately leave and rejoin the game instantly. When Player A left the game, the DataStoreService:SetAsync()
function will take some time in order to save the data, but once they join the game, the DataStoreService:GetAsync()
function will run right before the DataStoreService:SetAsync()
finished saving the player’s data. And if everything goes well and everything completes in under a second, item duplication will occur. This is called a race condition.
So, how do we prevent this happening? DataStore 2.0 is unable to solve this because they cannot handle item loss/duplication, neither can LuckyDataStore although they do have the session locking feature, I believe some people said that the code inside it isn’t that great and optimized which I could be wrong, but this is where ProfileService is useful! It has the session locking feature, which basically if a data is being modified, for exampled being saved, loaded or etc, ProfileService will make sure to ‘lock’ that profile to prevent any other functions to overwrite it. For example, if a profile is being loaded or already loaded in Server A, and Server B is trying to load the data, Server B will unable to do it because Server A locked the profile in order to prevent disruptions while Server A is modifying the data. With this, item loss/duplication is solved! But that’s not it.
We passed "ForceLoad"
in the second argument of the function, which basically means that if Server B uses this option, it will indefinitely load the data, meaning it will try to load that profile regardless of whether or not the profile is being session locked. This sometimes can help if the profile is being dead session locked, which basically means the profile is locked forever, this is called stealing.
There are more options if you would like to try, but we are going for "ForceLoad"
.
- Now, sometimes the function will return
nil
, mostly because of a Roblox server trying to load the same profile at the same time when another server is doing the same thing. This case should be extremely rare to occur, as stated in the API page. If it returnsnil
, we need to:Kick()
the player and let them rejoin in order to load their own profile again.
local function OnPlayerAdded(player)
local PlayerProfile = CashProfileStore:LoadProfileAsync("CashProfileStore"..player.UserId,"ForceLoad")
if PlayerProfile ~= nil then
else
player:Kick("Unable to load your data. Please rejoin.")
end
end
- Next, if we successfully get their profile, we are going to do a sanity check, we need to check if the player is still in the game after the profile is being loaded, we can’t just leave it hanging around there. So, we will save their data by using a function called
:Release()
.:Release()
in simple terms, just saves the data of the profile that called this function.
local function OnPlayerAdded(player)
local PlayerProfile = CashProfileStore:LoadProfileAsync("CashProfileStore"..player.UserId,"ForceLoad")
if PlayerProfile ~= nil then
if player:IsDescendantOf(Players) then
else
PlayerProfile:Release()
end
else
player:Kick("Unable to load your data. Please rejoin.")
end
end
Now, what if I tell you, you can make a function to run whenever we call Profile:Release()
? This is because we need a function to remove the key value pair inside the Profiles
dictionary that we created at the beginning of our script. As I’ve said, once we successfully load the player’s profile, we can put it in a dictionary so we could refer it more easily, that’s why we need a function to run so. To do this, we can connect this function inside Profile:ListenToRelease()
.
local function OnPlayerAdded(player)
local PlayerProfile = CashProfileStore:LoadProfileAsync("CashProfileStore"..player.UserId,"ForceLoad")
if PlayerProfile ~= nil then
PlayerProfile:ListenToRelease(function()
Profiles[player] = nil
end)
if player:IsDescendantOf(Players) then
else
PlayerProfile:Release()
end
else
player:Kick("Unable to load your data. Please rejoin.")
end
end
Profile:ListenToRelease()
will run whenever Profile:Release()
is called for a profile, allowing us to run a function that is attached inside it and in this case, it will remove our Profile instance from the table.
Now if everything goes smooth (AKA if the player is still in the game after joining), then we need to put their Profile instance to the Profiles
table, adding an unique key along with the player’s Profile to.
local function OnPlayerAdded(player)
local PlayerProfile = CashProfileStore:LoadProfileAsync("CashProfileStore"..player.UserId,"ForceLoad")
if PlayerProfile ~= nil then
PlayerProfile:ListenToRelease(function()
Profiles[player] = nil
end)
if player:IsDescendantOf(Players) then
Profiles[player] = PlayerProfile
else
PlayerProfile:Release()
end
else
player:Kick("Unable to load your data. Please rejoin.")
end
end
And that should be it for our OnPlayerAdded
function!
- Alright, now we can start scripting our
OnPlayerRemoved
function, which is pretty short considering we are only going to save our player’s data.
local function OnPlayerRemoved(player)
local PlayerProfile = Profiles[player]
if PlayerProfile then
PlayerProfile:Release()
end
end
In here, we basically just check if the player’s profile is in the table, if it is save their data. Sometimes it could be nil
if our ProfileStore failed to load their data.
- Now, sometimes
Players.PlayerRemoving()
event may not get fired due to the server shutting down itself too quick before the event can get fired, in this case you should implement the same function ingame:BindToClose()
.
game:BindToClose(function()
table.foreach(Players:GetPlayers(),function(_,player) -- table.foreach() basically iterates the dictionary.
OnPlayerRemoved(player)
end)
end)
CHECKPOINT
Congratulations on reaching here! You should have this code now:
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ProfileService = require(ServerStorage.ProfileService)
local CashProfileStore = ProfileService.GetProfileStore("CashProfileStore",{
Cash = 100
})
local Profiles = {}
local DataManager = {}
local function OnPlayerAdded(player)
local PlayerProfile = CashProfileStore:LoadProfileAsync("CashProfileStore"..player.UserId,"ForceLoad")
if PlayerProfile ~= nil then
PlayerProfile:ListenToRelease(function()
Profiles[player] = nil
end)
if player:IsDescendantOf(Players) then
Profiles[player] = PlayerProfile
else
PlayerProfile:Release()
end
else
player:Kick("Unable to load your data. Please rejoin.")
end
end
local function OnPlayerRemoved(player)
local PlayerProfile = Profiles[player]
if PlayerProfile then
PlayerProfile:Release()
end
end
Players.PlayerAdded:Connect(OnPlayerAdded)
Players.PlayerRemoving:Connect(OnPlayerRemoved)
game:BindToClose(function()
table.foreach(Players:GetPlayers(),function(_,player)
OnPlayerRemoved(player)
end)
end)
return DataManager
You can rest now if you want, give yourself a big pat on your shoulder.
Adding a function to modify our data
Alright, the next thing we are going to add in our DataManager module is a function which can modify our data, in this tutorial our 'Cash" data.
- Sometimes it maybe useful for us to get a player’s Profile for something else. So, we are going to create a function named
GetData()
which will return a player’s profile. To do so, we simply index their Profile by using their Player instance, after that if it exists then we will return the Profile, otherwise we will use give out a warning.
function DataManager:GetData(player)
local PlayerProfile = Profiles[player]
if PlayerProfile then
return PlayerProfile
else
warn("[DataManager]: CANNOT GET DATA FOM"..player.Name..".")
end
end
- Next, we are going to make a function named as
SetData()
, which basically helps us to set a specified data with the data we want to set to. First off, we are going to create 3 parameters, namely the player’s profile we are going to modify, the name of the data we are going to modify and lastly the new data value for that data value
function DataManager:SetData(player,dataToSet : string,newDataValue)
local PlayerProfile = self:GetData(player)
if PlayerProfile then
end
end
Now, this step is optional for you, but I like to make sure the type of the data we are setting is the same as the type of the data value that should be stored inside the data. For example, since Cash
can only be measured or described in numbers/integers, if we are setting a new data value for it, we wanna make sure the new data value is a number/integer. So we can do an if
statement along with the typeof()
function to do so.
function DataManager:SetData(player,dataToSet : string,newDataValue)
local PlayerProfile = self:GetData(player)
if PlayerProfile then
local OldDataValueType = typeof(PlayerProfile.Data[dataToSet])
if OldDataValueType == typeof(newDataValue) then
end
end
end
After this, if everything goes right, we can safely set the data to something else.
function DataManager:SetData(player,dataToSet : string,newDataValue)
local PlayerProfile = self:GetData(player)
if PlayerProfile then
local OldDataValueType = typeof(PlayerProfile.Data[dataToSet])
if OldDataValueType == typeof(newDataValue) then
PlayerProfile.Data[dataToSet] = newDataValue
end
end
end
Perfect! This should conclude our DataManager module! Now, we are going to use it from a server script and start modifying a player’s data.
For the sake of this tutorial, we are going to make a part so that whenever you click on it, it will increase your cash value by 1. So first off, spawn a part, get a click detector inside it and get a script inside the click detector and let’s begin declaring a few variables first.
- First off, require the module and connect the
ClickDetector.MouseClick
event.
local DataManager = require(game.ServerStorage.DataManager)
script.Parent.MouseClick:Connect(function(plr)
end)
- Now, when someone clicked the ClickDetector instance, we first get their profile to retrieve their current cash value, and then we are calling
DataManager:SetValue
to set the player’s cash value, in this case increment the current cash value by 1.
local DataManager = require(game.ServerStorage.DataManager)
script.Parent.MouseClick:Connect(function(plr)
local Profile = DataManager:GetData(plr)
print(Profile.Data["Cash"])
DataManager:SetData(plr,"Cash",Profile.Data["Cash"] + 1) -- if Profile.Data["Cash"] = 10, then 10 + 1 = 11.
print(Profile.Data["Cash"])
end)
I also added two print statements before and after the change of the data to compare.
And this should be it! You may publish this game and test it in a real game and see the magic works! Here’s an example of it working:
Now when I rejoin, look at that!
CHECKPOINT
You are amazing! You finally have a working data storing code using ProfileService! But that’s not it, in the next part, we are going to use ReplicaService to replicate and present the data to their respective cleints.
Here is the full code:
DataManager module script:
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ProfileService = require(ServerStorage.ProfileService)
local CashProfileStore = ProfileService.GetProfileStore("CashProfileStore",{
Cash = 100
})
local Profiles = {}
local DataManager = {}
local function OnPlayerAdded(player)
local PlayerProfile = CashProfileStore:LoadProfileAsync("CashProfileStore"..player.UserId,"ForceLoad")
if PlayerProfile ~= nil then
PlayerProfile:ListenToRelease(function()
Profiles[player] = nil
end)
if player:IsDescendantOf(Players) then
Profiles[player] = PlayerProfile
else
PlayerProfile:Release()
end
else
player:Kick("Unable to load your data. Please rejoin.")
end
end
local function OnPlayerRemoved(player)
local PlayerProfile = Profiles[player]
if PlayerProfile then
PlayerProfile:Release()
end
end
function DataManager:GetData(player)
local PlayerProfile = Profiles[player]
if PlayerProfile then
return PlayerProfile
else
warn("[DataManager]: CANNOT GET DATA FOM"..player.Name..".")
end
end
function DataManager:SetData(player,dataToSet : string,newDataValue)
local PlayerProfile = self:GetData(player)
if PlayerProfile then
local OldDataValueType = typeof(PlayerProfile.Data[dataToSet])
if OldDataValueType == typeof(newDataValue) then
PlayerProfile.Data[dataToSet] = newDataValue
end
end
end
Players.PlayerAdded:Connect(OnPlayerAdded)
Players.PlayerRemoving:Connect(OnPlayerRemoved)
game:BindToClose(function()
table.foreach(Players:GetPlayers(),function(_,player)
OnPlayerRemoved(player)
end)
end)
return DataManager
On click detector clicked event:
local DataManager = require(game.ServerStorage.DataManager)
script.Parent.MouseClick:Connect(function(plr)
local Profile = DataManager:GetData(plr)
print(Profile.Data["Cash"])
DataManager:SetData(plr,"Cash",Profile.Data["Cash"] + 1) -- if Profile.Data["Cash"] = 10, then 10 + 1 = 11.
print(Profile.Data["Cash"])
end)
Pairing up with ReplicaService
Just to get everyone on the right track, ReplicaService is basically a module that allows you replicate anything especially only server-sided data to the client. The reason why we need it is because if you try to send the player’s Profile.Data
, throuhg a remote event or any cross-server communication instances from Roblox, the client would see it as nil.
BUT, if we use ReplicaService, it will make a copy of the data and make it so that it is available to our client!
Before, we get started, I would like to teach you guys how to use ReplicaService first.
In ReplicaService, you have to create something called a token, in which this handles as a replication unit to clients and allowing them too retrieve data inside each token. This makes it easier for you to create certain replication for two different things, such as one token to replicate profile data, and the other one maybe replicate other things. We first have to create a token through the function ReplicaService.NewReplica()
, in which we have to provide a dictionary as its parameters. The pattern of the dictionary must be like this:
{
ClassToken = ReplicaService.NewClassToken("TokenExample"), -- TokenExample will be the name of this token.
Data = {0}, -- the data that will be replicated to the clients listening for this token
Replication = "All" -- this token will be replicated to everyone in the game and will also replicate to players that joined the game after the creation of this token. We will be able to choose which player can only listen for this token.
}
You can read more about each of these properties here from the API page. You don’t have to worry about the Tags
, Parent
, and WriteLib
variable.
- We first wanna go to our DataManager module script’s
OnPlayerAdded()
function, which we wanna make it so that whenever a player joins the game, we create a token with a unique name for them to getProfile.Data
to their screen and parent their replica token to a table containing all of the player’s replica token in a game so we can later refer it. Along with that, we wanna make sure to destroy their replica token when they leave the game, because we can’t just let their replica token hanging around there in the game until the server shuts down. So first off, add a new variable at the starting of the module script namedDataReplicas
. Then, we will need to callReplica:Destroy()
upon a player leaving the experience.
Don’t forget to require the ReplicaService module itself too!
local ProfileService = require(ServerStorage.ProfileService)
local ReplicaService = require(ServerScriptService.ReplicaService)
local CashProfileStore = ProfileService.GetProfileStore("CashProfileStore",{
Cash = 100
})
local Profiles = {}
local DataReplicas = {}
local DataManager = {}
local function OnPlayerAdded(player)
local PlayerProfile = CashProfileStore:LoadProfileAsync("CashProfileStore"..player.UserId,"ForceLoad")
if PlayerProfile ~= nil then
PlayerProfile:ListenToRelease(function()
Profiles[player] = nil
DataReplicas[player] = nil
DataReplicas[player]:Destroy
end)
if player:IsDescendantOf(Players) then
Profiles[player] = PlayerProfile
local DataReplica = ReplicaService.NewReplica({
ClassToken = ReplicaService.NewClassToken("DataToken_"..player.UserId), -- since a player's user id is unique, we will name it according to their user id to create a unique name for the class token.
Data = PlayerProfile.Data,
Replication = player -- this player that joined the game can only access to this replica!
})
DataReplicas[player] = DataReplica
else
PlayerProfile:Release()
end
else
player:Kick("Unable to load your data. Please rejoin.")
end
end
NOTE: BEFORE YOU CONTINUE, GO TO REPLICASERVICE MODULE SCRIPT AND FIND LINE 373 AND REPLACE THE WHOLE FUNCTION WITH THIS LINE:
(loleris did told me this isn’t a bug, and I should just use other properties of the replica to correctly create the replica, but I’m too lazy and don’t wanna learn how so)
local function DestroyReplicaAndDescendantsRecursive(replica, not_first_in_stack)
-- Scan children replicas:
for _, child in ipairs(replica.Children) do
DestroyReplicaAndDescendantsRecursive(child, true)
end
local id = replica.Id
-- Clear replica entry:
Replicas[id] = nil
-- Cleanup:
replica._maid:Cleanup()
-- Remove _creation_data entry:
replica._creation_data[tostring(id)] = nil
-- Clear from children table of top parent replica:
if not_first_in_stack ~= true then -- ehhhh... Yeah.
if replica.Parent ~= nil then
local children = replica.Parent.Children
table.remove(children, table.find(children, replica))
else
TopLevelReplicas[id] = nil
end
end
CreatedClassTokens[replica.Class] = nil
-- Swap metatables:
setmetatable(replica, LockReplicaMethods)
end
- Next, we are going to also modify our
SetData
function so that whenever a player’s data value is changed, their replica will also set the data to the data value that is going to be set to the player’sProfile.Data
. TheReplica
itself has a function calledReplica:SetValue()
. It takes 2 argument, the first argument is the name of the data we are going to set specified in a table form, and then the next argument is going to be the new value to be updated.
function DataManager:SetData(player,dataToSet : string,newDataValue)
local PlayerProfile = self:GetData(player)
local DataReplica = DataReplicas[player]
if PlayerProfile and DataReplica then
local OldDataValueType = typeof(PlayerProfile.Data[dataToSet])
if OldDataValueType == typeof(newDataValue) then
PlayerProfile.Data[dataToSet] = newDataValue
DataReplica:SetValue({"Cash"}, newDataValue)
end
end
end
Perfect! Now, we are going to do some scripting on the client side, where we will make our client to listen for any changes from this replica token. So go ahead, insert a LocalScript in StarterPlayerScripts.
- Firstly, define
Players
service for referring the local player’sUserId
to get the name of their respective replica token, then defineReplicatedStorage
in order to getReplicaController
module script.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local Player = Players.LocalPlayer
local ReplicaController = require(ReplicatedStorage:WaitForChild("ReplicaController"))
- Next, we will refer a function of
ReplicaController
, which is theReplicaController.ReplicaOfClassCreated()
, where the first argument is the name of the replica token we want our client to listen for and attach a function at the second argument to be ran after the creation of the requested replica token is completed. In this case, we must make it so that they only listen for their own replica token, how? Remember where I said we will create a unique name for each player’s replica token using theirUserId
? This is where it is handy!
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local Player = Players.LocalPlayer
local ReplicaController = require(ReplicatedStorage:WaitForChild("ReplicaController"))
ReplicaController.ReplicaOfClassCreated("DataToken_"..Player.UserId,function(replica) -- the ReplicaOfClassCreated function will pass in the requested replica token in the attached function's argument.
end)
After that, we will need to make our replica to listen for its changes of Data
, in this case we put Profile.Data
as the data we want to replicate in this token, and Profile.Data = {Cash = 100}
, so it would make sense for us to listen for the changes of this Cash’s value. To listen the change, we can do Replica:ListenToChange()
where the first argument is the name of the key/data we want to listen for and a function which it will also pass in an argument which contains the newly/updated value of the key/data we are listening for.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local Player = Players.LocalPlayer
local ReplicaController = require(ReplicatedStorage:WaitForChild("ReplicaController"))
ReplicaController.ReplicaOfClassCreated("DataToken_"..Player.UserId,function(replica) -- the ReplicaOfClassCreated function will pass in the requested replica token in the attached function's argument.
replica:ListenToChange({"Cash"},function(newVal) -- listening for our cash value to change.
print("Data changed!")
-- your logic to show the updated value
end)
end)
Now, this is the part where you get creative and start scripting your logic on how you will present the updated data to the player’s screen. You can present it on a ScreenGui using a TextLabel, which is what I am gonna do. So go ahead and create a simple ScreenGui to show out the updated data along with its value.
And here is the logic of presenting the cash using the TextLabel.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local Player = Players.LocalPlayer
local PlayerGui = Player.PlayerGui
local CashTextLabel = PlayerGui:WaitForChild("ScreenGui").TextLabel
local ReplicaController = require(ReplicatedStorage:WaitForChild("ReplicaController"))
ReplicaController.ReplicaOfClassCreated("DataToken_"..Player.UserId,function(replica) -- the ReplicaOfClassCreated function will pass in the requested replica token in the attached function's argument.
replica:ListenToChange({"Cash"},function(newVal) -- listening for our cash value to change.
print("Data changed!")
-- your logic to show the updated value
CashTextLabel.Text = "Cash: "..newVal
end)
CashTextLabel.Text = "Cash: "..replica.Data["Cash"] -- it is a good idea to also set the value outside the scope of Replica:ListenToChange() so that when the replica token's creation is finished, we will instantly present the data from Replica.Data
end)
- ONE LAST STEP! Make sure you call
ReplicaController.RequestData()
at the last line of this local script, always. This is stated in the API page:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local Player = Players.LocalPlayer
local PlayerGui = Player.PlayerGui
local CashTextLabel = PlayerGui:WaitForChild("ScreenGui").TextLabel
local ReplicaController = require(ReplicatedStorage:WaitForChild("ReplicaController"))
ReplicaController.ReplicaOfClassCreated("DataToken_"..Player.UserId,function(replica) -- the ReplicaOfClassCreated function will pass in the requested replica token in the attached function's argument.
replica:ListenToChange({"Cash"},function(newVal) -- listening for our cash value to change.
print("Data changed!")
-- your logic to show the updated value
CashTextLabel.Text = "Cash: "..newVal
end)
CashTextLabel.Text = "Cash: "..replica.Data["Cash"] -- it is a good idea to also set the value outside the scope of Replica:ListenToChange() so that when the replica token's creation is finished, we will instantly present the data from Replica.Data
end)
ReplicaController.RequestData()
We are finally done with everything! Give it a test in the real game and see the magic happens!
When I rejoin, the cash is saved and loaded to us!
Congratulations! You successfully learnt how to use ProfileService and replicate server-sided things to the client using ReplicaService! You are amazing, keep up the good work and enjoy this beautiful system working for you. Here are the final scripts:
DataManager module script:
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local ProfileService = require(ServerStorage.ProfileService)
local ReplicaService = require(ServerScriptService.ReplicaService)
local CashProfileStore = ProfileService.GetProfileStore("CashProfileStore",{
Cash = 100
})
local Profiles = {}
local DataReplicas = {}
local DataManager = {}
local function OnPlayerAdded(player)
local PlayerProfile = CashProfileStore:LoadProfileAsync("CashProfileStore"..player.UserId,"ForceLoad")
if PlayerProfile ~= nil then
PlayerProfile:ListenToRelease(function()
Profiles[player] = nil
DataReplicas[player] = nil
DataReplicas[player]:Destroy
end)
if player:IsDescendantOf(Players) then
Profiles[player] = PlayerProfile
local DataReplica = ReplicaService.NewReplica({
ClassToken = ReplicaService.NewClassToken("DataToken_"..player.UserId), -- since a player's user id is unique, we will name it according to their user id to create a unique name for the class token.
Data = PlayerProfile.Data,
Replication = player -- this player that joined the game can only access to this replica!
})
DataReplicas[player] = DataReplica
else
PlayerProfile:Release()
end
else
player:Kick("Unable to load your data. Please rejoin.")
end
end
local function OnPlayerRemoved(player)
local PlayerProfile = Profiles[player]
if PlayerProfile then
PlayerProfile:Release()
end
end
function DataManager:GetData(player)
local PlayerProfile = Profiles[player]
if PlayerProfile then
return PlayerProfile
else
warn("[DataManager]: CANNOT GET DATA FOM"..player.Name..".")
end
end
function DataManager:SetData(player,dataToSet : string,newDataValue)
local PlayerProfile = self:GetData(player)
local DataReplica = DataReplicas[player]
if PlayerProfile and DataReplica then
local OldDataValueType = typeof(PlayerProfile.Data[dataToSet])
if OldDataValueType == typeof(newDataValue) then
PlayerProfile.Data[dataToSet] = newDataValue
DataReplica:SetValue({"Cash"}, newDataValue)
end
end
end
Players.PlayerAdded:Connect(OnPlayerAdded)
Players.PlayerRemoving:Connect(OnPlayerRemoved)
game:BindToClose(function()
table.foreach(Players:GetPlayers(),function(_,player)
OnPlayerRemoved(player)
end)
end)
return DataManager
The script to set our data:
local DataManager = require(game.ServerStorage.DataManager)
script.Parent.MouseClick:Connect(function(plr)
local Profile = DataManager:GetData(plr)
print(Profile.Data["Cash"])
DataManager:SetData(plr,"Cash",Profile.Data["Cash"] + 1) -- if Profile.Data["Cash"] = 10, then 10 + 1 = 11.
print(Profile.Data["Cash"])
end)
Local script:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local Player = Players.LocalPlayer
local PlayerGui = Player.PlayerGui
local CashTextLabel = PlayerGui:WaitForChild("ScreenGui",10).TextLabel
local ReplicaController = require(ReplicatedStorage:WaitForChild("ReplicaController"))
ReplicaController.ReplicaOfClassCreated("DataToken_"..Player.UserId,function(replica) -- the ReplicaOfClassCreated function will pass in the requested replica token in the attached function's argument.
replica:ListenToChange({"Cash"},function(newVal) -- listening for our cash value to change.
print("Data changed!")
-- your logic to show the updated value
CashTextLabel.Text = "Cash: "..newVal
end)
CashTextLabel.Text = "Cash: "..replica.Data["Cash"] -- it is a good idea to also set the value outside the scope of Replica:ListenToChange() so that when the replica token's creation is finished, we will instantly present the data from Replica.Data
end)
ReplicaController.RequestData()
Frequently Asked Questions
Q: Why do you solely use ProfileService for the data storing and not something else like DataStore2 or LuckyDataStore?
A: As I’ve mentioned, DataStore2 does not have the session locking feature(if I am correct), while LuckyDataStore does has this feature, the code and safety behind it isn’t that promising.
LuckyDataStore - An Easy to Use Saving Module with Session Locking and Auto-Saving - #14 by angrybino
ProfileService will handle everything at its backend so you don’t have to worry about any errors, plus it is easy as DataStore2.
Q: I wanna use this tutorial to replace my current data storing script. How can I do so? Do I need to move all my current data store to ProfileService’s ProfileStore?
A: I did had this issue when I was transferring my game’s data store script to ProfileStore, but one thing I noticed is that whatever function you called to get a data store from any custom data storing module script, the backends will always use DataStoreService:GetDataStore()
to get the data store, so you don’t have to worry about moving everything to a new data store, just call the function with the same exact name of your current data store name and it should load out any existing saved data out!
will add more Q&As soon
Thank you guys so much for lending me your eyes to read this tutorial, I hope you are benefitted from it. Please leave a feedback if you wanna ask anything or having any doubts.
Edits
EDIT 1: 2/10/2021
I forgot to mention that when removing the replica, you have to call :Destroy()
on the player’s replica when they leave, thanks for @RobloxWobot first bringing up this issue!
Once again, thank you, @loleris for making these 2 legendary modules!
Tutorial writted by,
notzeussz.