DataStores with Object-Oriented Programming

USING AND MAKING DATASTORES IN OOP (OBJECT-ORIENTED PROGRAMMING)

0. Quick Info

  • 0.1
    I’ve had problems with doing this, and I have made several attempts, but then I eventually figured it out, with the help of JonByte. So I thought I’d share my knowledge. Also keep in mind that none of this needs to be the exact same thing, you’re free to use your own methods on how you do it.
  • 0.2
    This article will cover on how you can utilise Object-Oriented Programming, for DataStores specifically. And there will be 2 segments: Saving it, and loading it. Or in OOP term, creating it, saving it, and loading it.

You’ll need to know these things in order to be able to follow up and understand this tutorial properly.

  • Lua (obviously, but in Roblox form though)

  • How ModuleScripts work

  • Object-Oriented Programming (Check out JonByte’s tutorial)

  • DataStores

  • That’s all!

INTIALIZATION


First you’ll want to create a ModuleScript and put it in ServerScriptService or ReplicatedStorage. Call it whatever you want, but I’ll be naming mine [color=red]PlayerData[/color].

Oh, hey, you’ve opened the ModuleScript, you’re one step ahead (or you’re not…)

If you have experience with Object-Oriented Programming, you’ll probably know what to do at the start.

local PlayerData = {};
PlayerData.__index = PlayerData;

function PlayerData.new(player)
    --empty
end

Now that’s a good way to set it up. Of course, we’ll also want to set its dictionaries/properties and put in the datastore.

local PlayerData = {};
PlayerData.__index = PlayerData;

function PlayerData.new(player)
    local LocalData = {};
    LocalData.Player = player;
    LocalData.Key = player.UserId.."-cash"; --concatenate it to not overwrite other datasaves
    LocalData.Value = player.leaderstats.Cash.Value --get the leaderstats cash's value
    LocalData.DataStore = game:GetService("DataStore"):GetDataStore("Cash");
    setmetatable(LocalData, PlayerData);
    return LocalData;    
end

We’ll of course be indexing our datastore inside the player data function for more accessibility (and so that we don’t occupy space for code, haha)

Now what next? Oh you know what’s next - our custom methods.

CREATING THE METHODS AND SETTING IT UP


Exciting, right? Has to be one of the most fun aspects I find in Object-Oriented Programming. You get to make your own methods and set what it does, what happens etc. etc.

We’ll want these:

A Load method,
A Save method,
Parameters for the methods (easy, don’t know why I had to add this in)

So let’s make it, starting with the Save function.

function PlayerData:Save()
    local savedData;
    local success, err = pcall(function()
        savedData = self.DataStore:SetAsync(self.Key, self.Player.leaderstats.Cash.Value);
    end)

    if success then
        if savedData then
            print("Data saved.");
        end
    else
        warn("There was a problem with saving your data");
        warn(err);
    end
end

We don’t really need to wrap this in a pcall (at least I think so) so feel free to not, I just did it to be safe I guess.

Then we have to create the Load method, of course, which should be easy.

function PlayerData:Load()
    local Result;
    local success, err = pcall(function()
        Result = self.DataStore:GetAsync(self.Key);
    end)

    if success then
        if Result then
            print("Current cash: "..Result);
            self.Player.leaderstats.Cash.Value = Result;
        end
     else
        warn("There was an error with fetching your data");
        warn(err);
    end
end

Now this one [color=yellow]has[/color] to be wrapped in a pcall. You never know, getting the data might fail for some reason and the whole script might break. But then, this is how we’ll load the data and apply the loaded data to the cash. This should be self-explanatory if you know how DataStores work or if you’ve worked with them before.

SETTING THE SCRIPT

Now that we’ve done almost everything, it’s time to move onto a regular script, which I’ll be putting in ServerScriptService. Name it anything you want, I’ll be naming it psadfjlkasjdflkjxd;lcvkjlkjsldkf.

Now you may think, the first thing we’d want to do here is to require the ModuleScript. Yes, that’s [color=red]one[/color] of what we’re going to prioritize, but let me remind you: we haven’t made leaderstats or cash for the player yet. So let’s do that.

local Players = game:GetService("Players");
local PlayerData = require(script.Parent.PlayerData);

Players.PlayerAdded:Connect(function(player)
    local leaderstats = Instance.new("Folder", player);
    local cash = Instance.new("IntValue", leaderstats);
    cash.Name = "Cash";
    
end)

Now that that’s done, let’s now use the PlayerData module for when the player is added.

local Players = game:GetService("Players");
local PlayerData = require(script.Parent.PlayerData);

Players.PlayerAdded:Connect(function(player)
    local leaderstats = Instance.new("Folder", player);
    local cash = Instance.new("IntValue", leaderstats);
    cash.Name = "Cash";
    
    local newPlayerData = PlayerData.new(player);
    newPlayerData:Load() --load in case
end)

Easy, and in just a few lines! But then, we [color=red]forgot something[/color]. We haven’t made anything that removes the player’s data upon leaving. Don’t skim out on this, this is important if you don’t want any memory leaks.

Now you may ask, how would we get the specific PlayerData object that we’ve created that the player owns? It’s simple. We’ll create a dictionary that will store it as a key, with the player’s user id being the name of that key.

local Players = game:GetService("Players");
local PlayerData = require(script.Parent.PlayerData);

local allPlayerData = {};

Players.PlayerAdded:Connect(function(player)
    local leaderstats = Instance.new("Folder", player);
    local cash = Instance.new("IntValue", leaderstats);
    cash.Name = "Cash";
    
    local newPlayerData = PlayerData.new(player);
    newPlayerData:Load() --load in case
    allPlayerData[player.UserId] = newPlayerData; --make a new key, set newPlayerData as the value
end)

Now we have access to that specific data module just by using the player’s ID! Nothing complicated, nothing hard, just simple. Now let’s save the data then delete the data off the current game session/server to prevent memory leaks or any unwanted unintentional issues in the future.

local Players = game:GetService("Players");
local PlayerData = require(script.Parent.PlayerData);

local allPlayerData = {};

Players.PlayerAdded:Connect(function(player)
    local leaderstats = Instance.new("Folder", player);
    local cash = Instance.new("IntValue", leaderstats);
    cash.Name = "Cash";
    
    local newPlayerData = PlayerData.new(player);
    newPlayerData:Load() --load in case
    allPlayerData[player.UserId] = newPlayerData; --make a new key, set newPlayerData as the value
end)

Players.PlayerRemoving:Connect(function(player)
    local pData = allPlayerData[player.UserId]; --search for player's data using user id
    if pData then
        pData:Save() --save data
        allPlayerData[player.UserId] = nil; --remove it
    end
end)

CONCLUSION

Thanks for reading my horrible and poorly-written guide. Hope it helped!

[color=lime]Feel free to PM me if you’re having an issue somewhere[/color].

20 Likes

The DataStore should be held as a private access variable in the PlayerData module separated from class objects, or just as one of the upvalues in the script itself.

local DataStoreService = game:GetService("DataStoreService")
local DataStore = DataStoreService:GetDataStore("Cash")

local PlayerData = {}

function PlayerData:Save()
    local success, result = pcall(function ()
        return DataStore:UpdateAsync(self.Key, function(oldCash)
            local newValue = (oldCash or 0) + leaderstatsValue
            return newValue
        end)
    end)

    if success then
        print("Data saved for " .. self.Key)
    else
        warn("Save failed for " .. self.Key .. ": " .. result)
    end
end

As player data will often be more than one value, it’s also always good to account for that fact by allowing a table of values to be managed by your object which can then be pushed down into the DataStore. A single-value DataStore will often not be what games now need.

Just the most immediate feedback I could think of.

7 Likes

Hey @colbert2677, can you explain how would I implement the :UpdateAsync() method for a table instead of a single value?

thank you.

2 Likes

You first need to understand how to use UpdateAsync before you can get to work on this. The main principle of UpdateAsync is that you return a value that becomes the new value in the DataStore (or return nil to cancel the update). Return a table instead of a single value.

3 Likes

Here is a good resource if you want to get into learning about how you can use and implement UpdateAsync.

3 Likes

Holy hell, jesus christ… Never thought the man would reply to any of my posts! The man, the fox! colbert2677!

1 Like

This is a really nice tutorial! One thing I would suggest is to stray away from using the second parameter Instance.new(). Instead, it would be best practice to set your instance’s properties first and then set the parent, as such:

local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"

local cash = Instance.new("IntValue")
cash.Name = "Cash"

cash.Parent = leaderstats
leaderstats.Parent = player
3 Likes