Hello everyone,
I have been working on Data Stores a lot lately, and have been improving as the times go by. Before I have been wondering how to save an ObjectValue, then I realized it was too complicated until recently I have found the answer.
Data Serializer Legacy: Marketplace | GitHub | API Documentation
This is a folder-based data store module that is capable of saving objects under a folder.
Notable Features
- Capable of saving any Instance type. This includes Scripts, Models, MeshParts, ObjectValue, Parts, CFrameValue, Motor6D, along with the rest of the objects that are present in the “Insert Object” widget.
- All attribute types are supported.
- Save-fail retries.
- Offline Studio support.
The structure is as follows:
DataSerializer \ -- ModuleScript
LoadData | -- ModuleScript
SaveData | -- ModuleScript
Why use it?
-
Unlike some other community modules, this one allows full control for the developer. You can choose when you want to save by using your prefered methods.
-
The data structure is organized so you can easily edit it using a plugin.
-
Capable of serializing all data objects, including ObjectValues.
How to Setup (and Limitations)
Initial Setup
- A folder named “PresetPlayerData” must be present under the
ServerStorage
. This will be used as the default folder for each new player.
This is the part where you want to start customizing PresetPlayerData, it can be achieved by adding Value objects and folders within the preset folder.
> Changing Values
Changing Values
The Data Serializer Legacy version allows only Folders and ValueBase to be stored under PresetPlayerData, alongside their attributes. Two or more objects under the same Parent cannot share the same name.
Instances supported:
- Folder
- IntValue
- NumberValue
- BoolValue
- CFrameValue
- Vector3Value
- Color3Value
- BrickColorValue
- RayValue
- ObjectValue
Attribute data types supported:
- All data types are supported.
Note
ObjectValue features unrestricted saving, meaning that storing any instance type is possible under this value object. This includes: Models, Beams, Full NPCs, Scripts, LocalScripts, MeshParts, Highlight, and everything else featured in the “Insert Object” menu.
> ObjectValue Limitations
ObjectValue Limitations
ObjectValue.Value
itself has no limitations, but the objects within it do.
Trying to save a property that references an Instance can be risky if the path to instance is not unique.
Examples of objects with a property referencing an Instance:
SurfaceGui.Adornee
WeldConstraint.Part0
Motor6D.Part0
Beam.Attachment1
The path to an Instance can be brought up by calling Instance:GetFullName()
. If the path is not unique enough, the objects within the path should have their names changed to be unique. For example, if the path to Beam.Attachment0
is, Workspace.Part.Part.Part.Attachment
, it should be changed to Workspace.Part.Part2.Part3.Attachment
.
> MeshParts and Scripts
MeshParts and Scripts
MeshParts, Scripts, and SurfaceAppearance can persist, but they need to be present within the ServerStorage
service.
- For scripts to save, the name must match with the one in ServerStorage.
- MeshParts and SurfaceAppearances must have matching copies containing the same Properties as the one in ServerStorage. Name will not matter.
Post-Setup (Practical Usage)
Post-Setup
Play test in studio, look in Players service to see if you have the PlayerData folder for your user. If it exists under the player, that means it’s working.
> If it's not working, here is what to do
If the PlayerData folder did not appear under the player, you first need a script that requires this module. DataSerializer:GetStore()
will return a DataStore
class object that you will need later on.
DataStore:Get()
is the magic that copies that PresetPlayerData folder over to the Player with it renamed to “PlayerData”. It returns the PlayerData
folder.
DataStore:CleanUpdate()
is a function that can only be used when the player is removing because it will destroy ObjectValue.Value
s within the PlayerData folder. This is the magic that saves the PlayerData when you rejoin studio.
DataStore:Update()
(optional) performs the same action as :CleanUpdate()
except without the cleaning. This function will not destroy ObjectValue.Value
s, therefore it is safe to use when you need to auto-save. Just be aware of Roblox’s DataStore limitations.
This is how it would look like in code:
local Players = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")
local dsMod = ServerScriptService:WaitForChild("DataSerializer")
local DataSerializer = require(dsMod)
local DataStore = DataSerializer:GetStore("DataStore_name")
Players.PlayerAdded:Connect(function(Player)
local plrKey = "plr_" .. Player.UserId
local PlayerData = DataStore:Get(Player, plrKey, {Player.UserId})
end)
Players.PlayerRemoving:Connect(function(Player)
local plrKey = "plr_" .. Player.UserId
DataStore:CleanUpdate(Player, plrKey)
end)
For more information on how to use these functions, see the api docs.
> Attributes info
When you inspect your player’s attributes, you will notice two named “DSLoaded” and “IsSaving”. DSLoaded
fires when PlayerData is fully loaded, this attribute returns a string
with the name of the folder (which in this case is “PlayerData”). IsSaving
is true
whenever this module is saving, and turns false
when finished.
To retrieve the PlayerData folder in code, you would put:
local Players = game:GetService("Players")
local Player = Players.LocalPlayer
if not Player:GetAttribute("DSLoaded") then
Player:GetAttributeChangedSignal("DSLoaded"):Wait()
end
local PlayerData = Player:FindFirstChild(Player:GetAttribute("DSLoaded"))
This method is better over local PlayerData = Players:WaitForChild("PlayerData")
because it waits for data to fully load.
To change values, the test session needs to be in “Server” mode. It is possible by going to the “Home” tab (at the top bar), and clicking this button for it to say “Server” instead of “Client”. If you were to change data values from the Client side, it will not save because a server Script is handling data-stores. Feel free to learn more about Client-Server model.
Let’s say I want my player to buy something by spending coins. First you will need an IntValue named “Coins” somewhere in the PlayerData folder. If it’s not there, then you can simply make a new one.
Now change the value to whatever you prefer. I will also be adding an attribute named “TestColor” on the Coins value object to check if that will save when I test the game again.
Stop testing, then test it again. Repeat the steps to find the PlayerData folder under the player, and see if the “TestColor” attribute and the Coin’s value saved to how you changed it.
> How to change PlayerData in scripts
Client or LocalScripts cannot access data stores, instead you will need to use RemoteEvents
to transfer data to the server and make the Script
change the values.
Here is my PlayerData folder.
Now, I want to give the player additional Coins after clicking a button on a LocalScript. In order to do that, I will need to define the RemoteEvent
and call RemoteEvent:FireServer()
to tell the server to update my coins.
In this little tutorial, the requirements are:
- “ServEvents”
Folder
needs to be in ReplicatedStorage - “ChangeData”
RemoteEvent
needs to be in ServEvents folder. - The
LocalScript
provided below, must be placed under aTextButton
orImageButton
. - The
Script
provided below must be placed in ServerScriptService.
-- local script
-- replicated
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServEvents = ReplicatedStorage:FindFirstChild("ServEvents")
-- defined remote event
local evChangeData = ServEvents:FindFirstChild("ChangeData")
local button = script.Parent
-- on mouse button click, give 100 coins to the player
button.MouseButton1Click:Connect(function()
evChangeData:FireServer()
end)
Then, on a Script, evChangeData
will get fired and change the Coins value to Coins.Value + 100
. Basic requirements: PlayerData needs to be defined, then the coins. Just for fun, I will also add 1
Gems.
-- server script
-- replicated
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServEvents = ReplicatedStorage:FindFirstChild("ServEvents")
-- defined remote event
local evChangeData = ServEvents:FindFirstChild("ChangeData")
evChangeData.OnServerEvent:Connect(function(plr: Player)
-- check if Player loaded data yet.
local loadedName = "DSLoaded"
if not plr:GetAttribute(loadedName) then
plr:GetAttributeChangedSignal(loadedName):Wait()
end
-- player data folder
local PlayerData = plr:FindFirstChild(plr:GetAttribute(loadedName))
-- wallet
local WalletFile = PlayerData:FindFirstChild("Wallet")
local charCoins = WalletFile:FindFirstChild("Coins")
local charGems = WalletFile:FindFirstChild("Gems")
-- give coins and gems
charCoins.Value = charCoins.Value + 100
charGems.Value = charGems.Value + 1
end)
Now, after you are done spamming the button that adds + 100 Coins
and + 1 Gems
per click, rejoin Studio to see if the data saved.
Prebuilt Script
This script and the DataSerializer module must be parented to ServerScriptService
.
--[Made by Jozeni00]--
--settings
local DataSettings = {
--{DATA}--
--Any changes made below are susceptible to a clean data wipe, or revert data to its previous.
["Name"] = "DS_TestLV0-0-0"; --DataStore name for the entire game.
["Key"] = "Plr_"; --prefix for key. Example: "Player_" is used for "Player_123456".
--{FEATURES}--
["AutoSave"] = true; --set to true to enable auto saving.
["SaveTime"] = 1; --time (in minutes) how often it should automatically save.
["UseStudioScope"] = true; --set to true to use a different Scope for Studio only.
["DevName"] = "DEV/DS_TestLV0-0-0"; --Name of the Data Store for Studio if UseStudioScope is true.
["DevKey"] = "Dev_"; --Key of the Data Store for Studio, if UseStudioScope is true.
}
--scripts
local ServerScriptService = game:GetService("ServerScriptService")
local dataModule = ServerScriptService:FindFirstChild("DataSerializer") -- DataSerializer Module Script.
local DataSerializer = require(dataModule)
--players
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
--set scope
if DataSettings.UseStudioScope then
if RunService:IsStudio() then
DataSettings.Name = DataSettings.DevName
DataSettings.Key = DataSettings.DevKey
end
end
local DataStore = DataSerializer:GetStore(DataSettings.Name)
--on entered
function onPlayerEntered(Player)
local key = DataSettings.Key .. Player.UserId
--player data
local PlayerData = DataStore:Get(Player, key, {Player.UserId})
if DataStore and DataSettings.AutoSave then
local isGame = true
local plrRemove = nil
if DataSettings.SaveTime < 1 then
DataSettings.SaveTime = 1
end
local saveTimer = DataSettings.SaveTime * 60
plrRemove = Players.PlayerRemoving:Connect(function(plr)
if plr == Player then
isGame = false
end
end)
while Player and isGame do
task.wait(saveTimer)
--update
DataStore:Update(Player, key)
end
if plrRemove and plrRemove.Connected then
plrRemove:Disconnect()
end
end
end
--on removing
function onPlayerRemoving(Player)
local key = DataSettings.Key .. Player.UserId
DataStore:CleanUpdate(Player, key)
end
for i, v in pairs(Players:GetPlayers()) do
if v:IsA("Player") then
local onEnter = coroutine.wrap(function()
onPlayerEntered(v)
end)
onEnter()
end
end
--events
Players.PlayerAdded:Connect(onPlayerEntered)
Players.PlayerRemoving:Connect(onPlayerRemoving)
game:BindToClose(function()
print("Closing...")
for i, v in pairs(Players:GetPlayers()) do
if v:IsA("Player") then
v:Kick()
end
end
task.wait(3)
print("Name:", DataSettings.Name)
end)
--[Made by Jozeni00]--
Solving the memory or RAM issue
The downside of having a folder-based datastore in your game is that it can take up memory space at least for the client because every player will have their PlayerData folder replicated to all players in the server. To only have the LocalPlayer’s data folder, the other player data folders must be deleted as well as their ObjectValues using a LocalScript.
ObjectValue.Value
's are stored in a folder under ReplicatedStorage named “DataTempFile”. Imagine if your game that has 200 players in a server, and a feature where players can build custom houses, and they all get put under ReplicatedStorage, it may lag the client while causing their game to crash.
To assist in solving the memory issue, I have provided some code that detects objects that do not belong to the LocalPlayer’s data folder and deletes the object, as well as deleting the other PlayerData folders that are not a part of the Local Player.
--[Made by Jozeni00]--
--Put this LocalScript into "game.StarterPlayer.StarterPlayerScripts"
--It destroys the PlayerData of other players in exchange for lighter memory usage.
--Detects other objects that do not belong to the player and deletes it.
--plr
local Players = game:GetService("Players")
local Player = Players.LocalPlayer or script.Parent.Parent
--replicated
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local DataTempFile = ReplicatedStorage:WaitForChild("DataTempFile")
--data folder
local loader = string.char(68, 83, 76, 111, 97, 100, 101, 100) -- "DSLoader"
if not Player:GetAttribute(loader) then
Player:GetAttributeChangedSignal(loader):Wait()
end
--check temp file
function tempObjAdded()
task.wait(0.1)
local PlayerData = Player:FindFirstChild(Player:GetAttribute(loader))
if not PlayerData then
return nil
end
--scan the temp file
for i, v in pairs(DataTempFile:GetChildren()) do
local foundObj = false
--find matching obj
for _, data in pairs(PlayerData:GetDescendants()) do
if data:IsA("ObjectValue") then
if data.Value then
if data.Value == v then
foundObj = true
break
end
end
end
end
if not foundObj then
v:Destroy()
end
end
end
--player added function
function onPlayerEntered(plr)
if plr == Player then
return nil
end
--get plr data
if not plr:GetAttribute(loader) then
plr:GetAttributeChangedSignal(loader):Wait()
end
local PlrData = plr:FindFirstChild(plr:GetAttribute(loader))
--scan for object values and destroy it
for i, v in pairs(PlrData:GetDescendants()) do
if v:IsA("ObjectValue") then
if v.Value then
v.Value:Destroy()
end
end
end
PlrData:Destroy()
end
--check existing players
for i, v in pairs(Players:GetPlayers()) do
if v:IsA("Player") then
local thread = coroutine.wrap(function()
onPlayerEntered(v)
end)
thread()
end
end
--check existing objects
local checkObj = coroutine.wrap(function()
tempObjAdded()
end)
checkObj()
--events
Players.PlayerAdded:Connect(onPlayerEntered)
DataTempFile.ChildAdded:Connect(tempObjAdded)
--[Made by Jozeni00]--
What if this module receives an update?
Do I need to update this script?
If you only care about the basic needs, then no. Although, this repository will still be maintained to support newer object classes Roblox may add in the near future.
Showcase
The place files should include:
- DataSerializer module
- DSHandler, a script that uses this module.
- ClearDebris, a local script that optimizes performance for the client.
- MarketService, a script that handles the process receipt, edited for this module.
- PresetPlayerData folder
Place Link:
– or –
RBXL File:
DS_Legacy_Place.rbxl (83.9 KB)
Update Log
Update 1.3
- Fixed PresetPlayerData folder getting renamed to “PlayerData” on require. It now stays as “PresetPlayerData” on require.
- Added type-checking in main module.
- Font attribute can be saved now.
Update 1.2
- Reorganized the SaveData script’s code to make it neater.
Update 1.1
- Changed from Script to ModuleScript.
This module will be available on the marketplace and maintained side by side with the GitHub repository.
Feedback
- What are your thoughts on this post?
- Is this module helpful to you?
- What are some areas you think I should look to improve on?