Update Notice 2022
If you want to setup reliable datastores quickly and without having to worry about all the quirks which come with the datastore API, definitely check out ProfileService.
This thread is reasonably outdated whereas a resource like ProfileService is actively maintained by @loleris. It covers scenarios you might not typically account for such as preventing the duplication of items when trading with session-locking.
Quick Disclaimer 2021
This post is a few years old now and my attitudes and practises have slightly changed - I aim to create a re-write of this thread sometime in the future. There will likely be many updates to DataStores by Roblox itself since the time of writing this too. Nevertheless this is still a useful tutorial and I’d highly recommend reading the responses from people such as @colbert2677 and @filiptibell who arguably provide equally if not more important tips and discussion.
Background
For the majority of games, there comes a point where data about an individual needs to be saved (e.g. level, items, cash, etc). To achieve this, we use DataStores.
Now ask any developer, what did you find most challenging about creating a game, and you’ll find a good majority respond “DataStores”. There are various reasons for this: they require a good understanding of lua, take significant time to initially learn about and setup, and are difficult to test compared to typical features. There is one issue however which dominates all; one with real-world consequences and potentially disastrous results: the loss of player data.
Consequences of data-loss
The loss of player data can have severe repercussions, both for the user and developer:
- Significant time spent trying to restore data and refund players.
- Loss of players/reputation. This is fairly self explanatory; if a player loses their data there’s a good chance of them quitting for good and leaving a negative review in the process.
- Ethical consequences. To some, a player might just be text and a character on the screen, however you have to realise each individual is a real human. That 1000 R$ might mean nothing to you, but for the player was 10-weeks of saving up pocket money to afford it. Some players may have also invested huge amounts of time into your game. The loss of many-months of progress may cause them personal distress.
What can be done to limit this?
Obviously no developer desires this, however speaking with others this is something almost everyone, including myself, experience at one point or another. No need to fret though, there are measures you can take to prevent or at least minimise data-loss and its effects:
- Use a separate datastore to specifically handle developer product purchases. If worst comes to worst, this will make refunding players enormously quicker and easier to do. It doesn’t take long to implement and could really pay-off in the long run.
- Consider setting up your datastores using DataStore2. In simple, it provides an easy way for you to setup datastores without having to worry about the technical side to saving data. It utilises the DataStore and OrderedDataStore services so that data can’t be overwritten and gives you the ability to retrieve previous data versions. You can find out more here.
- Verifying data before writing it to the datastore. The data retrieved using GetAsync() is not always accurate or correct. For example, if GetAsync() fails then the players data may be set to false or nil. If this value is written to the datastore, the player’s data will be completely overwritten. GetAsync() can also retrieve incorrect previous data if a player hops between servers too quickly. To prevent this, we use UpdateAsync().
Writing data
There are four methods of writing data:
Now there’s a good chance you’ve heard of SetAsync(), but what about UpdateAsync()? If you haven’t, shame on you as the main page even highlights it in yellow for you:
As the warning explains, SetAsync simply writes a new value to a key without any consideration for the previous value, whereas UpdateAsync retrieves the value of a key from the datastore and updates it with a new value - this gives you the ability to compare the data about to be saved with the previous and act accordingly.
Writing data correctly
So obviously we should all be using UpdateAsync() to save player data, right? Yes! However there is still a handful of people using SetAsync() when they really shouldn’t. Even an official tutorial by Roblox on Saving Player Data uses this incorrectly.
Here’s an example of UpdateAsync() and incremental-values used to effectively save data:
You vs the guy she says not to worry about
Source code
if playersData then
datastore:UpdateAsync(key, function(oldValue)
local previousData = oldValue or {DataId = 0}
if playersData.DataId == previousData.DataId then
playersData.DataId = playersData.DataId + 1
return playersData
else
return nil
end
end)
end
Note: nil is returned for scenarios where we don’t want the data to be updated. It effectively cancels the save. You should also wrap your functions in pcalls when retrieving and setting data.
Epilogue
I’m guilty myself of using SetAsync(). Back in 2017 I had a game which blew up to front page. This was an awesome experience but also created a datastore nightmare at the time too. We were having reports of data-loss at least 20 times a day which, as you can imagine, created a huge headache; the next week of my time was completely devoted to rewriting the datastore and refunding as many players as possible.
In-a-way, this was a blessing in disguise as it forced me to develop good practices when saving data. Now-a-days, I will always use UpdateAsync() with an incremental-value when saving important data. More recently, I released a multi-place game where players are constantly teleporting between servers, yet to this day have had 0 reports of data-loss.
Summary
Here’s a brief overview by @colbert2677:
- UpdateAsync is the canonical way to update data. Get should be used to retrieve data and set should be used when you need to force data. This is also an official recommendation as stated on the Developer Hub.
- UpdateAsync respects previous data in the DataStore. If you don’t use this, you have to tread fairly carefully when doing a Get-Set method.
- UpdateAsync respects conflicting calls. Data won’t force itself as the new value or try to overstep other calls that are also attempting to write to the DataStore.
- Good practice
Thanks for reading. Please get in touch if you have any question.
If you’re interested in setting up your own datastore, feel free to check out my other brief tutorial here: How would you use ROBLOX's datastorage API? - #3 by ForeverHD