Save Your Pets
Lesson 18 of 18 in Coddy's Roblox Game: Pet Simulator course.
Coins, the Desert and every pet should still be there next time the player joins. A DataStore keeps a table per player, under their UserId. Pets are instances, which a DataStore cannot hold, so save a plain table of each pet's name and whether it is out, and build the pets again from it on join:
{ Coins = 120, Desert = true, Pets = { { Name = "Fox", Equipped = true } } }Save when the player leaves (PlayerRemoving) and when the server closes (game:BindToClose); load in PlayerAdded. Both calls can fail on Roblox's side, so wrap them in pcall. In Roblox Studio, DataStores work once the place is published and Enable Studio Access to API Services is on.
Challenge
Hard- Get
DataStoreServiceand a DataStore namedPets. - Write
save(player): saveCoins,Desertand a list of{ Name, Equipped }for every pet, under the player's UserId, in a pcall. Call it onPlayerRemovingand for every player ingame:BindToClose. - In
PlayerAdded, after making the folders, load the save (in a pcall) and restore the coins, the Desert attribute and the pets (skip names not inpetInfo).
Press Play, hatch, press Stop and Play again: your pets are back. Then Check.
Try it yourself
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local RunService = game:GetService("RunService")
local Settings = require(game.ReplicatedStorage.Settings)
local remotes = game.ReplicatedStorage.Remotes
local piles = workspace.Piles
local petParts = workspace.PetParts
local pileSpot = {} -- each pile -> the spot it stands on
local lastHit = {} -- each player -> when they last hit
print("A Meadow pile gives " .. Settings.PileValue .. " coins")
-- Settings.Pets by name
local petInfo = {}
for _, info in Settings.Pets do
petInfo[info.Name] = info
end
-- A pet: a StringValue in the player's Pets folder holding its name, with
-- its rarity and power as attributes
local function givePet(player, name, equipped)
local info = petInfo[name]
local pet = Instance.new("StringValue")
pet.Name = "Pet"
pet.Value = name
pet:SetAttribute("Rarity", info.Rarity)
pet:SetAttribute("Power", info.Power)
pet:SetAttribute("Equipped", equipped)
pet.Parent = player.Pets
return pet
end
-- how many pets a player has out
local function equippedCount(player)
local count = 0
for _, pet in player.Pets:GetChildren() do
if pet:GetAttribute("Equipped") then
count += 1
end
end
return count
end
-- what a player's pets add to each hit
local function petPower(player)
local power = 0
for _, pet in player.Pets:GetChildren() do
if pet:GetAttribute("Equipped") then
power += pet:GetAttribute("Power")
end
end
return power
end
-- Every player gets Coins on the leaderboard
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
-- the pets they own
local pets = Instance.new("Folder")
pets.Name = "Pets"
pets.Parent = player
end)
-- A coin pile on a spot: a copy of ServerStorage.CoinPile with its health
-- and value as attributes
local function spawnPile(spot)
local desert = spot.Parent.Parent.Name == "Desert"
local health = desert and Settings.DesertHealth or Settings.PileHealth
local pile = ServerStorage.CoinPile:Clone()
pile.Position = spot.Position
pile:SetAttribute("MaxHealth", health)
pile:SetAttribute("Health", health)
pile:SetAttribute("Value", desert and Settings.DesertValue or Settings.PileValue)
pile:SetAttribute("Zone", spot.Parent.Parent.Name)
pile.HealthGui.Label.Text = pile:GetAttribute("Health") .. "/" .. pile:GetAttribute("MaxHealth")
pileSpot[pile] = spot
pile.Parent = piles
return pile
end
-- A hit takes 1, plus the power of the player's pets, off a pile's health;
-- at 0 the pile breaks and pays its value
local function hitPile(player, pile)
local health = pile:GetAttribute("Health") - (1 + petPower(player))
if health > 0 then
pile:SetAttribute("Health", health)
pile.HealthGui.Label.Text = health .. "/" .. pile:GetAttribute("MaxHealth")
return
end
player.leaderstats.Coins.Value += pile:GetAttribute("Value")
local spot = pileSpot[pile]
pileSpot[pile] = nil
pile:Destroy()
-- a new pile on the same spot, a few seconds later
task.delay(Settings.RespawnTime, spawnPile, spot)
end
-- A pile on every spot, in every zone
for _, zone in workspace.Zones:GetChildren() do
for _, spot in zone.Spawns:GetChildren() do
spawnPile(spot)
end
end
-- The client asks to hit a pile. Never trust the client: anyone can fire
-- this remote with anything, from anywhere, as fast as they like.
remotes.Hit.OnServerEvent:Connect(function(player, pile)
-- a real pile
if typeof(pile) ~= "Instance" or pile.Parent ~= piles then
return
end
-- in reach
local character = player.Character
local root = character and character:FindFirstChild("HumanoidRootPart")
if not root or (root.Position - pile.Position).Magnitude > Settings.Reach then
return
end
-- not too fast
local now = time()
if lastHit[player] and now - lastHit[player] < Settings.HitCooldown then
return
end
lastHit[player] = now
-- in a zone the player has opened
if pile:GetAttribute("Zone") == "Desert" and not player:GetAttribute("Desert") then
return
end
hitPile(player, pile)
end)
-- A weighted roll: each pet's Chance (out of 100) is its share of the line
local function rollPet()
local roll = math.random() * 100
for _, info in Settings.Pets do
roll -= info.Chance
if roll <= 0 then
return info.Name
end
end
return Settings.Pets[1].Name
end
-- Hatching: the egg's prompt, paid for in coins
workspace.Egg.ProximityPrompt.Triggered:Connect(function(player)
local coins = player.leaderstats.Coins
if coins.Value < Settings.EggPrice then
return
end
coins.Value -= Settings.EggPrice
local name = rollPet()
-- out at once, if there is room
local pet = givePet(player, name, equippedCount(player) < Settings.MaxEquipped)
print(player.Name .. " hatched a " .. name)
-- everyone hears about the rarest pets
if pet:GetAttribute("Rarity") == "Legendary" then
remotes.Announce:FireAllClients(player.Name, name)
end
end)
-- Pets out follow their owner: each one has a part in the world, kept in a
-- row behind the player every frame
local petModels = {} -- each pet -> its part in the world
RunService.Heartbeat:Connect(function()
for _, player in Players:GetPlayers() do
local root = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
local pets = player:FindFirstChild("Pets")
local slot = 0
for _, pet in pets and pets:GetChildren() or {} do
local model = petModels[pet]
if pet:GetAttribute("Equipped") and root then
slot += 1
if not model then
model = ServerStorage.PetModels[pet.Value]:Clone()
model.Parent = petParts
petModels[pet] = model
end
model.CFrame = root.CFrame * CFrame.new((slot - 2) * 3, -1.5, 4)
elseif model then
model:Destroy()
petModels[pet] = nil
end
end
end
end)
-- The client asks to put a pet out or away. The server checks: the pet is
-- the player's own, and no more than MaxEquipped are out.
remotes.Equip.OnServerEvent:Connect(function(player, pet)
if typeof(pet) ~= "Instance" or pet.Parent ~= player.Pets then
return
end
if pet:GetAttribute("Equipped") then
pet:SetAttribute("Equipped", false)
elseif equippedCount(player) < Settings.MaxEquipped then
pet:SetAttribute("Equipped", true)
end
end)
-- The Desert gate: pay once and it opens, for that player only
workspace.Gate.ProximityPrompt.Triggered:Connect(function(player)
local coins = player.leaderstats.Coins
if player:GetAttribute("Desert") or coins.Value < Settings.GatePrice then
return
end
coins.Value -= Settings.GatePrice
player:SetAttribute("Desert", true)
end)
This lesson includes a short quiz. Start the lesson to answer it and track your progress.