How to script a basic instance based DataStore

Introduction

What is a Instance based datastore?

A Instance based datastore is a datastore where you make use of Instances to easily replicate the changes for the client & server using functions & events like Instance:GetPropertyChangedSignal() or Instance.Changed.

Are Instances better than data manager modules?

They aren’t. Using Instances can cause a big performance impact for your game, depending on how many data you’ll have to create for each player.

Example: your server has 30 players playing and each player has a total of 50 Instances, then you have created 1,500 Instances in total.

Summarizing, you should only use Instance based data stores for small projects. players with a good PC will mostly never notice the difference lol…

Coding

We always start by getting the services we’ll need to make our data store.

--# services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local DataStoreService = game:GetService("DataStoreService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local PREFIX= "DataStore:"

local DataStore = DataStoreService:GetDataStore("game")

local JobId = game.JobId

local DefaultData = {
    Cash = 0,
    Kills = 0,
    Deaths = 0
}

local function say(action, ...)
	if action == "warn" then
		warn(PREFIX, ...)
	elseif action == "print" then
		print(PREFIX, ...)
	end
end

local function createInstances(folder, data)
	for name, value in pairs(data) do
		if type(value) == "number" then
			local NumberValue = Instance.new("NumberValue")
			NumberValue.Name = name
			NumberValue.Value = value
			NumberValue.Parent = folder
		elseif type(value) == "string" then
			local StringValue = Instance.new("StringValue")
			StringValue.Name = name
			StringValue.Value = value
			StringValue.Parent = folder
		elseif type(value) == "boolean" then
			local BoolValue = Instance.new("BoolValue")
			BoolValue.Name = name
			BoolValue.Value = value
			BoolValue.Parent = folder
		end
	end
end

local function getStoredTools(player, toolsData)
	for _, toolName in ipairs(toolsData) do
		local tool = game.ServerStorage.Backpack:FindFirstChild(toolName)
		if tool then
			tool:Clone().Parent = player.Backpack
			tool:Clone().Parent = player.StarterGear
		end
	end
end

local function GetData(player)
	say("warn", ("attempting to get %s's data."):format(player.Name))
	
	local key = player.UserId
	local tries = 0
	
	local success, err = nil, nil
	
	repeat
		success, data = pcall(DataStore.GetAsync, DataStore, key)
		if not success then
			tries = tries + 1
			wait(1)
		end
	until tries >= 4 or success
	
	if success then
		
		local folder = Instance.new("Folder")
		folder.Name = player.Name
		folder.Parent = ReplicatedStorage
		
		if data then
			createInstances(folder, data)
			getStoredTools(player, data.Backpack)
		else
			createInstances(folder, DefaultData)
			getStoredTools(player, DefaultData.Backpack)
		end
		
		say("print", ("finished to set %s's data."):format(player.Name))
		
	else
		player:Kick("failed to get your data, please rejoin after 3-5 minutes.")
	end
end

local function SaveData(player)
	if RunService:IsStudio() then
		warn("cannot save in studio")
		return false
	end
	
	local folder = ReplicatedStorage:FindFirstChild(player.Name)
	
	if not folder then
		say("warn", ("%s's data folder was not found, cannot proceed to save."):format(player.Name))
		return false
	end
	
	local key = player.UserId
	local tries = 0
	
	local success, err = nil, nil
	local saveTable = {
	    ["Backpack"] = {}
	}
	
	for _, stat in ipairs(folder:GetChildren()) do
		saveTable[stat.Name] = stat.Value
	end
	
	for _, tool in ipairs(player.Backpack:GetChildren()) do
		if tool:IsA("Tool") then
		    table.insert(saveTable.Backpack, tool.Name)
		end
	end
	
	repeat
		if saveTable == nil then
			err = "saveTable is nil"
			break
		end
		
		success, err = pcall(DataStore.UpdateAsync, DataStore, key, function(data)
			if data == nil then
				return saveTable
			elseif data ~= nil and data.SessionJobId == nil or data.SessionJobId == JobId then
				data.SessionJobId = JobId
				
				if data ~= saveTable then
					return saveTable
				end
				
				return nil
			end
			return nil
		end)
		
		if not success then
			tries = tries + 1
			wait(6)
		end
	until tries >= 5 or success
	
	if success then
		say("warn", ("%s's data has been saved!"):format(player.Name))
	else
		warn(err, 2)
	end

	folder:Destroy() -- destroy folder and everything inside it
end

for _, player in ipairs(Players:GetPlayers()) do
	coroutine.wrap(GetData)(player);
end

Players.PlayerAdded:Connect(GetData)
Players.PlayerRemoving:Connect(SaveData)

game:BindToClose(function()
	for _, player in ipairs(Players:GetPlayers()) do
		coroutine.wrap(SaveData)(player);
	end
end)
7 Likes

One small note I would say is I would recommend using warn in place of print simply for prettier colours, not only is it bad practice and not using it as intended but it could also mess with peoples error detection systems.

1 Like

I don’t understand the wording of this reply, you say you recommend it but also say it’s bad practice? So which one is better, warn or print?

1 Like

The way your doing it should be split slightly. The say function will always warn, or always print. It should be a good combo of both. You should have a say function and a warn function, as I want to warn for errors, but I don’t want to warn for everything else. Besides, I don’t even see a use case for the function. Why use it over just printing or warning normally?