Intermediate
Understanding DataStores
Save and load player data that persists between sessions. Master DataStoreService, handle errors properly, and build a complete coins save system from scratch.
understanding-datastores.lua
-- PLACE IN: ServerScript (inside ServerScriptService)
-- REQUIRES: A "leaderstats" folder will be created automatically by this script
-- No RemoteEvents needed - leaderstats are handled by Roblox automatically
-- ============================================================
-- COINS SAVE SYSTEM - Fully Commented for Complete Beginners
-- Every single line is explained so you know exactly what
-- is happening and WHY it's written that way.
-- ============================================================
-- ══════════════════════════════════════════════
-- SECTION 1: GET THE SERVICES WE NEED
-- ══════════════════════════════════════════════
-- "Services" are Roblox's built-in tools. We ask Roblox to give
-- us access to them using GetService().
-- DataStoreService lets us SAVE and LOAD data between sessions.
-- Without this, all coins would disappear when the player leaves!
local DataStoreService = game:GetService("DataStoreService")
-- Players service lets us detect when players join or leave the game.
local Players = game:GetService("Players")
-- ══════════════════════════════════════════════
-- SECTION 2: SET UP THE DATA STORE
-- ══════════════════════════════════════════════
-- GetDataStore() creates (or opens) a "database" with this name.
-- Think of it like a filing cabinet. "CoinsData" is the label on the cabinet.
-- All players' coin data will be stored inside this one cabinet.
-- You can rename "CoinsData" to anything you like.
local CoinsDataStore = DataStoreService:GetDataStore("CoinsData")
-- ══════════════════════════════════════════════
-- SECTION 3: CONFIGURATION (Easy to change!)
-- ══════════════════════════════════════════════
-- These are the settings you're most likely to want to change.
-- Putting them at the top makes them easy to find.
-- How many coins a brand new player starts with.
local STARTING_COINS = 0
-- How often (in seconds) the system auto-saves every player's data.
-- 60 seconds = save every 1 minute while they play.
local AUTO_SAVE_INTERVAL = 60
-- ══════════════════════════════════════════════
-- SECTION 4: THE SAVE FUNCTION
-- ══════════════════════════════════════════════
-- This is a "function" - a block of reusable code we can call by name.
-- We give it a player, and it saves that player's coins to the DataStore.
-- We'll call this function when a player leaves AND on a timer.
local function saveData(player)
-- First, we find the "leaderstats" folder inside the player.
-- leaderstats is a special Roblox folder that shows stats on the leaderboard.
-- FindFirstChild() looks for something by name - it returns nil if not found.
local leaderstats = player:FindFirstChild("leaderstats")
-- If leaderstats doesn't exist yet (e.g., data still loading), stop here.
-- The "if not" means "if this thing is missing / doesn't exist".
if not leaderstats then
-- "return" exits the function immediately. Nothing below this runs.
return
end
-- Now we find the "Coins" value inside the leaderstats folder.
local coinsValue = leaderstats:FindFirstChild("Coins")
-- Same check - if Coins doesn't exist for some reason, stop here safely.
if not coinsValue then
return
end
-- Each player has a unique "UserId" number that NEVER changes,
-- even if they change their username. We use this as the "key"
-- (the label on the folder) to store their data in the DataStore.
local playerKey = "Player_" .. player.UserId
-- Example result: "Player_1234567890"
-- The ".." operator joins two strings together.
-- pcall stands for "protected call". It runs code that might fail
-- (like saving data) and CATCHES any errors instead of crashing the game.
-- It returns two things:
-- success = true if it worked, false if it failed
-- errorMessage = the error text if it failed (nil if success)
local success, errorMessage = pcall(function()
-- SetAsync() saves data to the DataStore.
-- "playerKey" is WHERE to save it (which player's folder).
-- "coinsValue.Value" is WHAT to save (the actual number of coins).
CoinsDataStore:SetAsync(playerKey, coinsValue.Value)
end)
-- Now we check if the save worked or failed.
if success then
-- The save worked! Print a message to the Output window (only visible in Studio).
print("✅ Saved " .. coinsValue.Value .. " coins for " .. player.Name)
else
-- The save failed. Print a warning so you know something went wrong.
-- "warn()" prints in orange/yellow in the Output - easier to spot than print().
warn("❌ Failed to save data for " .. player.Name .. " | Error: " .. errorMessage)
end
end
-- ══════════════════════════════════════════════
-- SECTION 5: THE LOAD FUNCTION (runs when player joins)
-- ══════════════════════════════════════════════
-- This function runs when a player joins the game.
-- It creates their stats on the leaderboard AND loads their saved coins.
local function onPlayerAdded(player)
-- ── STEP 1: Create the leaderboard stats ──
-- Instance.new() creates a new Roblox object.
-- "Folder" is the type of object - we're making a folder.
local leaderstats = Instance.new("Folder")
-- Give the folder the special name "leaderstats".
-- Roblox automatically shows any values inside a folder named exactly
-- "leaderstats" on the in-game leaderboard. The name must be exact!
leaderstats.Name = "leaderstats"
-- IMPORTANT RULE: Always set Parent LAST.
-- Setting Parent causes Roblox to "register" the object in the game world.
-- If we parent it first and then set properties, it can cause tiny slowdowns.
-- Here we parent the folder to the player so they own it.
leaderstats.Parent = player
-- Now create an "IntValue" - this is a Roblox object that holds a whole number.
-- It will show up on the leaderboard because it's inside leaderstats.
local coinsValue = Instance.new("IntValue")
-- Name it "Coins" - this is the label that appears on the leaderboard.
coinsValue.Name = "Coins"
-- Set the starting value to our config variable from Section 3.
-- This will be overwritten below if we find saved data.
coinsValue.Value = STARTING_COINS
-- Parent it to leaderstats so it appears on the leaderboard.
coinsValue.Parent = leaderstats
-- ── STEP 2: Load saved data from the DataStore ──
-- Build the same key we use in the save function.
-- It must match exactly, or we'll look in the wrong "folder"!
local playerKey = "Player_" .. player.UserId
-- Use pcall again to safely attempt loading data.
-- If the DataStore is down or there's a network error, this won't crash the game.
local success, savedCoins = pcall(function()
-- GetAsync() retrieves data from the DataStore using the key.
-- It returns whatever was saved there, or "nil" if nothing was saved yet
-- (e.g., a brand new player who has never played before).
return CoinsDataStore:GetAsync(playerKey)
end)
if success then
-- The load worked! Now check if we actually got data back.
-- "savedCoins" will be nil for brand new players (never saved before).
-- The "~=" operator means "not equal to".
if savedCoins ~= nil then
-- We have saved data! Overwrite the default starting value
-- with the player's actual saved coin count.
coinsValue.Value = savedCoins
print("📂 Loaded " .. savedCoins .. " coins for " .. player.Name)
else
-- No saved data found = this is a brand new player!
-- They'll keep the STARTING_COINS value we set earlier.
print("🆕 New player! Gave " .. STARTING_COINS .. " starting coins to " .. player.Name)
end
else
-- The load failed. savedCoins actually holds the error message here
-- because when pcall fails, the second return value is the error.
warn("❌ Failed to load data for " .. player.Name .. " | Error: " .. savedCoins)
-- We still let the player in with default coins - never block a player
-- from playing just because the DataStore had a hiccup!
end
end
-- ══════════════════════════════════════════════
-- SECTION 6: HANDLE PLAYERS LEAVING (SAVE ON EXIT)
-- ══════════════════════════════════════════════
-- This function runs automatically when a player leaves the game.
-- It's critical we save here so their progress is never lost!
local function onPlayerRemoving(player)
print("👋 " .. player.Name .. " is leaving - saving their data...")
-- Call the save function we wrote in Section 4.
-- We pass the player as an "argument" (an input to the function).
saveData(player)
end
-- ══════════════════════════════════════════════
-- SECTION 7: HANDLE SERVER SHUTDOWN (Emergency Save)
-- ══════════════════════════════════════════════
-- "game:BindToClose()" runs a function when the SERVER is shutting down.
-- This is important because if the server closes suddenly (e.g., Roblox updates),
-- PlayerRemoving might not fire for every player in time.
-- This is our safety net!
game:BindToClose(function()
print("🔴 Server shutting down - saving all players...")
-- Get a list of ALL players currently in the game.
-- Players:GetPlayers() returns a table (list) of player objects.
local allPlayers = Players:GetPlayers()
-- "for" loop goes through every item in the list one by one.
-- "_" is used when we don't need the index number (just the player object).
for _, player in allPlayers do
-- Save each player's data.
saveData(player)
end
print("✅ All players saved! Server closing safely.")
end)
-- ══════════════════════════════════════════════
-- SECTION 8: AUTO-SAVE LOOP (Saves while playing)
-- ══════════════════════════════════════════════
-- Even if the server never shuts down cleanly, we auto-save every player
-- periodically. This means players lose at most AUTO_SAVE_INTERVAL seconds
-- of progress if something goes horribly wrong.
-- task.spawn() runs a block of code in the "background".
-- This means the auto-save loop runs on its own without
-- freezing or blocking the rest of the script.
task.spawn(function()
-- "while true do" creates a loop that runs forever.
-- The script will keep repeating this block of code endlessly.
while true do
-- task.wait() pauses the loop for the number of seconds we set.
-- This is like setting a timer. The loop "sleeps" and then wakes up.
-- IMPORTANT: We use task.wait(), NOT wait() - the old wait() is deprecated!
task.wait(AUTO_SAVE_INTERVAL)
-- After waiting, save every player currently in the server.
local allPlayers = Players:GetPlayers()
-- Only print the auto-save message if there's at least 1 player.
-- "#allPlayers" gives us the number of items in the list.
if #allPlayers > 0 then
print("💾 Auto-saving " .. #allPlayers .. " player(s)...")
end
for _, player in allPlayers do
saveData(player)
end
end
end)
-- ══════════════════════════════════════════════
-- SECTION 9: CONNECT EVENTS (Make everything actually run!)
-- ══════════════════════════════════════════════
-- Up until now we've only DEFINED functions. Nothing has actually run yet!
-- We now "connect" our functions to Roblox events so they fire at the right time.
-- PlayerAdded fires whenever a new player joins the game.
-- We tell it to call our onPlayerAdded function, passing the player automatically.
Players.PlayerAdded:Connect(onPlayerAdded)
-- PlayerRemoving fires whenever a player leaves the game.
-- We tell it to call our onPlayerRemoving function.
Players.PlayerRemoving:Connect(onPlayerRemoving)
-- ── Handle players already in the game ──
-- If this script runs AFTER some players have already joined
-- (can happen in Studio when testing), PlayerAdded won't fire for them.
-- This loop manually calls onPlayerAdded for anyone already in the server.
for _, player in Players:GetPlayers() do
-- task.spawn() runs onPlayerAdded for each existing player
-- without blocking - each one loads independently.
task.spawn(onPlayerAdded, player)
end
print("🚀 Coin Save System loaded successfully!")
-- ══════════════════════════════════════════════
-- BONUS: HOW TO GIVE/TAKE COINS FROM OTHER SCRIPTS
-- ══════════════════════════════════════════════
--
-- From any other ServerScript, you can change a player's coins like this:
--
-- local player = -- (however you get the player)
-- local coins = player.leaderstats.Coins
--
-- -- Give 50 coins:
-- coins.Value = coins.Value + 50
--
-- -- Take 30 coins (with a check so it doesn't go below 0):
-- coins.Value = math.max(0, coins.Value - 30)
--
-- -- Set coins to exactly 100:
-- coins.Value = 100
--
-- The save system handles saving automatically - you just change the Value!
-- ══════════════════════════════════════════════
📍 **Setup:**
- **Place this script** inside `ServerScriptService` as a `Script` (not a LocalScript)
- **Enable Studio API access** → Go to `Game Settings → Security → Enable Studio Access to API Services` (required for DataStores to work in Studio testing)
- **Test it** → Hit Play, check the Output window for the `✅` and `📂` messages confirming it's working, and watch the leaderboard appear automatically
- **Change `STARTING_COINS`** at the top of Section 3 if you want new players to begin with coins (e.g., `100` for a starter bonus)
- **Give coins from other scripts** using `player.leaderstats.Coins.Value += 50` from any ServerScript — the auto-save handles the rest every 60 seconds
Want this written for your specific game?
Describe exactly what you need and get working Luau code in seconds.
Try the AI Generator →