Right now your leaderstats reset to zero every single time a player leaves. That is fine for a quick round, but a tycoon needs memory. A player should earn Studs, log off, come back tomorrow, and find their progress waiting. That is what a DataStore is for: a little vault Roblox keeps on its servers, one value per key, that survives between visits.
First, the honest part: this is the spicy lesson, and it has setup that the earlier ones did not. DataStores only work on a place that is actually published to Roblox, and you have to switch on API access. So before any code:
File,Publish to Roblox, and save your place online.Game Settings,Security, turn ONEnable Studio Access to API Services.
Skip either of those and your saves silently fail, which is a classic head-scratcher, so check them first if something is not saving.
You reach the vault through a service called DataStoreService. You ask it for a named store (think of the name like a labeled drawer), then you use two functions: SetAsync to write a value, and GetAsync to read it back. Each value is stored under a key, and the natural key for a player is their UserId, which never changes.
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local playerStore = DataStoreService:GetDataStore("PlayerData")
Now the load. When a player joins, you read their saved coins and pour that number into their leaderstat. But talking to Roblox’s servers can fail, a hiccup, a timeout, so you wrap the risky call in a pcall. A pcall is a “protected call”: it tries the code and, instead of crashing your script if it errors, it hands you back a true/false plus whatever was returned. It is a safety net.
Players.PlayerAdded:Connect(function(player)
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
local coins = Instance.new("IntValue")
coins.Name = "Coins"
coins.Parent = leaderstats
local success, savedCoins = pcall(function()
return playerStore:GetAsync(player.UserId)
end)
if success and savedCoins then
coins.Value = savedCoins -- found a save, use it
else
coins.Value = 0 -- brand new player, or the load failed
end
end)
Then the save. When the player is leaving, you write their current coins back under the same key, again wrapped in pcall:
Players.PlayerRemoving:Connect(function(player)
local coins = player.leaderstats.Coins
local success, err = pcall(function()
playerStore:SetAsync(player.UserId, coins.Value)
end)
if not success then
warn("Could not save data for " .. player.Name .. ": " .. tostring(err))
end
end)
A few things worth burning into memory. Always use the same key for loading and saving (here, player.UserId), or you will save to one drawer and read from another and wonder where your coins went. Always wrap DataStore calls in pcall, because the one time the server hiccups, you want a logged warning, not a dead script. And the load checks if success and savedCoins because a first-time player has nothing saved yet, so GetAsync returns nil, and nil is not an error, it just means “new player, start at zero.”
Now make it yours: get coins persisting across a leave-and-rejoin, then level up the save. Instead of saving one number, try saving a whole table (coins AND level together) as a single value, which is exactly how real tycoons remember everything a player has unlocked.