ROBLOX Scripting How To: Data Persistence
Welcome to one of the first of many to come guides from ROBLOX on cool new scripting features!
In this guide I'll be showing you how to use Data Persistence in your place. As a general warning, you'll probably want to be a least somewhat proficient at scripting; in other words, this shouldn't be your first script.
Now with that out of the way, let's start with the basics: What is Data Persistence?
Have you created a game that players earn money in? That has checkpoints (like an obby)? Maybe you've made a building place where people make their own creations.
These are all great examples of that places that can benefit from Data Persistence. Data Persistence is the ability to save some data (like a number, or a string, or a brick) for a player in your game, so when they come back you can load in what they had last time they visited your place (such as the amount of money they earned, or a level they gained, etc.)
So, now the next question: How do I use Data Persistence?
Persistence is completely done via Lua, with calls on the individual player to save/load a piece of data for a particular place, as this is a Per User Per Place (PUPP) paradigm. In simpler terms, this means for each place, each player can have saved data. The model for this data is key/value pairs, with the key being a string you specify (more on this in the example code), and the value is determined by the call.
The following are our current calls for saving different types of data, and each of these calls can be found under Player:
SaveBoolean(String key, Bool value)
SaveNumber(String key, Number value)
SaveString(String key, String value)
SaveInstance(String key, Instance value)
All of these calls do essentially the same thing, except they each will save different types of data, in accordance with the call name. Let's look at the arguments we pass these functions:
String key - Think of this as a literal key that you would use to unlock a door. Whatever you call this key - "Money", "Experience", maybe even "Pink Elephant" will be the key for loading (unlocking) any data you save later.
Value - this is the actual thing that you want to save for the player. What this is depends on the functions you call, if you call SaveBoolean(), you'll want to use a boolean (true/false) value. If you use some data type other than the one specified by the function, you will get an error!
For each save function, we have a corresponding load function:
LoadBoolean(String key)
LoadNumber(String key)
LoadString(String key)
LoadInstance(String key)
If you use the same key you defined while saving a value from above, you will now get the same value you saved returned from each of these functions.
Things to be Aware Of
What is save/load Instance all about?
These functions will save any object that can be placed under workspace (including all of the object’s descendants), which also brings up a key point: All of the save functions have a data cap on how large the saves can be, if this cap is reached no new data will be saved to that particular key (we believe the size of each to be sufficient currently, but if we see a need to raise it in the future, we will consider raising).
What's with this Player.DataReady stuff?
This is a very important part of data persistence, which will we show more of in the Examples below. On the Player object there is now a read-only Boolean value called DataReady. This value indicates whether the player is ready to have save/load functions called on him or her. When a player initially joins a game, DataReady is false. After a few seconds, DataReady will typically become true (at this point you can freely call save/load functions). The best way to determine when DataReady goes from false to true is as follows:
function waitForProperty(instance, name) while not instance[name] do instance.Changed:wait() end end waitForProperty(player,"DataReady"
This waitForProperty() function will wait until DataReady on player is true. This solution is much more optimal than polling, as it relies on events, which use less processing power. Note: if DataReady never becomes true, we will sit and wait indefinitely on this property, effectively stopping the script from executing.
Why you should use pcall with Data Persistence
If you're not familiar with pcall, you definitely will be after using persistence. Pcall stands for "protected call", and rightfully so. Any code executed inside of a pcall will not cause a script to crash. Rather, the pcall will return with the error message the failed code has output. So, why is this important in respect to Data Persistence? You should always wrap your save/load calls in a pcall, just in case the call fails, you can handle the error in your script and continue on. Here is an example:
local playerScore if not pcall(function() playerScore = player:LoadNumber("Score") end) then playerScore = 0 end
The above code tries to load a number into playerScore with the key "Score" on the player object. If for some reason we can't load the number (maybe we have never saved something to the key "Score", maybe Data Persistence goes down in the middle of the game, etc.), we can now recover by checking if the pcall succeeds. In this example, if we fail we just assign a player score of 0 and allow the script to keep running.
Examples
The first place that used persistence was [1], so naturally we thought it would be a good piece of example code. Models that players build are saved, so when they come back they can continue to build on their creations. Consider the following code snippet:
function waitForProperty(instance, name) while not instance[name] do instance.Changed:wait() end end waitForProperty(player,"DataReady") local savedBuildingArea local success = pcall(function() savedBuildingArea = player:LoadInstance("PlayerArea") end) if not success or savedBuildingArea == nil or not savedBuildingArea:FindFirstChild("BasePlate") then createNewPlayerArea(area) else local offset = area.BasePlate.CFrame.p - savedBuildingArea.BasePlate.CFrame.p area.BasePlate.Parent = nil savedBuildingArea:TranslateBy(offset) savedBuildingArea.Parent = area savedBuildingArea:MakeJoints() end
Walkthrough
So, (in case you skipped the other sections and went to straight to examples) why are we waiting for this property in Player called DataReady? Under each player is a bool called “DataReady”, when this property is true, this means persistent data is ready to be saved or loaded for that player.
Next, we try to load in a key titled “PlayerArea” for our particular player. Putting this in a pcall is a good idea, if we get an error (maybe this is the player’s first visit to your place, see Things to be Aware Of for more info) we can handle that situation. If we fail at loading "PlayerArea", our script then creates a new playerArea. Otherwise, if we succeed in loading a previous playerArea, we set it up for use by the player.
Maybe it would make more sense to talk about saving data first? The following is the corresponding save mechanism for the above example:
function removePlayer(player) if not pcall(function() player:SaveInstance("PlayerArea", takenAreas.PlayerArea) end) then print("error saving") end end game.Players.PlayerRemoving:connect(function(player) removePlayer(player) end)
Walkthrough
The above script simply tries to save an Instance "takenAreas.PlayerArea" to the key “PlayerArea” for the player simply called “player” when he or she leaves the game (typically it is good practice to only save when a player leaves your game, saves done in game don't actually get pushed out until the player leaves, thus saving in the middle of playing doesn't do much). Once again we do this with a pcall, just in case persistence isn’t working properly we can handle the error. Saving for bools, numbers and strings work the same way, except it will throw an error if you try to pass the functions anything except for the type they specify respectively.
I hope that was pretty straightforward, if you have any questions please feel free to pm me (jeditkacheff). Happy persisting!