Hey there, I’m asyncable, and I have seen people approach this topic many times, usually just to create something simplistic (as I’ve also done here) to show off skills and to provide a resource for others to use, but most people tend to avoid explaining how they made their Pet Systems, so that’s exactly what I am here to do!
To start off I’d like to make some things clear, this is NOT some professional pet system you should use right off the bat without any touching up, as it is more of a resource for people learning how to make pet systems to dive more into a simple process, although with a bit of touching up you could easily implement this system.
(download at the end)
To start off with making a pet system, I’d suggest creating some sort of datastore with a way to store tables, this could be done with ProfileService or any other ‘fancy’ datastore module, in this instance I used a custom method which just makes it easy to access a folder containing the pets (as seen below)
--MAIN DATA SCRIPT
local DataStoreService = game:GetService("DataStoreService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local DataTemplate = require(script.Parent.DataTemplate)
local PlayerDataUtil = require(game.ReplicatedStorage.Packages.PlayerData)
local DataStore = DataStoreService:GetDataStore("PetSys_1.7")
local StatTypes = {
number = "NumberValue",
string = "StringValue",
boolean = "BoolValue",
table = "Folder"
}
local function createStatsFolder(data, parent, playerId)
for key, value in pairs(data) do
local instanceType = StatTypes[type(value)]
if instanceType == "Folder" then
local subFolder = Instance.new("Folder")
subFolder.Name = key
subFolder.Parent = parent
createStatsFolder(value, subFolder, playerId)
else
local stat = Instance.new(instanceType)
stat.Name = key
stat.Value = value
stat.Parent = parent
end
end
end
local function loadStats(Player)
local Data
local success, errorMessage = pcall(function()
Data = DataStore:GetAsync("Player_"..Player.UserId)
end)
local tries = 1
while not success and tries < 3 do
success, errorMessage = pcall(function()
Data = DataStore:GetAsync("Player_"..Player.UserId)
end)
tries += 1
end
print(Data)
if not success then
warn("Data Loading Error: "..errorMessage)
Player:Kick("There was a problem loading your data. Please try again")
return
end
local playerFolder = Instance.new("Folder")
playerFolder.Name = tostring(Player.UserId)
playerFolder.Parent = ReplicatedStorage:FindFirstChild("PlayerDatas") or ReplicatedStorage:WaitForChild("PlayerDatas")
if Data == nil then
createStatsFolder(DataTemplate, playerFolder, Player.UserId)
-- Add UserId stat
local userIdStat = Instance.new("NumberValue")
userIdStat.Name = "UserId"
userIdStat.Value = Player.UserId
userIdStat.Parent = playerFolder
return
end
local function mergeData(template, data)
for key, value in pairs(template) do
if type(value) == "table" then
if not data[key] then
data[key] = {}
end
mergeData(value, data[key])
elseif data[key] == nil then
data[key] = value
end
end
end
mergeData(DataTemplate, Data)
createStatsFolder(Data, playerFolder, Player.UserId)
end
local function saveStats(Player, Leaving)
local playerFolder = ReplicatedStorage:FindFirstChild("PlayerDatas"):FindFirstChild(tostring(Player.UserId))
if not playerFolder then return end
local function collectData(folder)
local data = {}
for _, child in pairs(folder:GetChildren()) do
if child:IsA("Folder") then
data[child.Name] = collectData(child)
elseif child:IsA("ValueBase") then -- Ensure only values are collected
data[child.Name] = child.Value
end
end
return data
end
local dataToSave = collectData(playerFolder)
local success, errorMessage = pcall(function()
DataStore:SetAsync("Player_"..Player.UserId, dataToSave)
end)
local tries = 1
while not success and tries < 3 do
success, errorMessage = pcall(function()
DataStore:SetAsync("Player_"..Player.UserId, dataToSave)
end)
tries += 1
end
if not success then warn("Data Saving Error: "..errorMessage) return end
if Leaving then
playerFolder:Destroy()
end
end
game.Players.PlayerAdded:Connect(function(Player)
loadStats(Player)
local data = PlayerDataUtil.get(Player)
if data.JoinDate.Value == "nil" then data.JoinDate.Value = DateTime.now():FormatLocalTime("LL", "en-us") end
end)
game.Players.PlayerRemoving:Connect(function(Player)
saveStats(Player, true)
end)
task.spawn(function()
while task.wait(15) do
for _, Player in pairs(game.Players:GetPlayers()) do
saveStats(Player, false)
end
end
end)
game:BindToClose(function()
for _, Player in pairs(game.Players:GetPlayers()) do
saveStats(Player, true)
end
end)
Alongside a data template module, here: (using OmegaNum so bigger numbers can be stored)
--DATA TEMPLATE MODULE
local en = require(game.ReplicatedStorage.Packages.OmegaNum)
local template = {
Coins = en.toString(en.fromNumber(100000000)),
Pets = {},
Equipped = en.toString(en.fromNumber(0)),
MaxEquipped = en.toString(en.fromNumber(4)),
JoinDate = "",
}
return template
We now have our data loading/saving! (If you decide to use this code, you will need to add a folder named “PlayerDatas” inside of the ReplicatedStorage, aswell as use this module inside of the ReplicatedStorage to access the data later on):
--PLAYERDATA MODULE
local data = {}
function data.get(player)
return game:GetService("ReplicatedStorage").PlayerDatas:WaitForChild(tostring(player.UserId), 10)
end
function data.getAll()
local playerData = {}
for _, v in pairs(game.Players:GetPlayers()) do
playerData[data.get(v).Name] = data.get(v)
end
return playerData
end
return data
Moving on, now that we have our loading/saving which creates a way to access a table for our pets, we can now create the way for players to obtain pets, at this point, I’d suggest having at least SOME knowledge with Remotes (Events/Functions) as this is key for Server/Client communication in this case.
We now will create a few Remote Functions, as seen below:
Why remote functions you may ask?
Well, remote functions allow you to return information back to the invoker, in this case we are going to use it so the client knows if the invoke was a success and a pet was hatched/equipped/unequipped by returning a bool corresponding with the state, but, if you would like to display what pet was hatched, you could return the pets name, rarity, and more allowing you to create a client UI visually representing what the player has obtained.
Also, a quick note, for my system I store the “name” of the pet inside of the pet folder as a GUID so I can easily reference the specific pet and not get it mixed with pets with the same name.
Ex:
(no GUID) -> Pets = {"Dog", "Dog", "Cat"}
(GUID) -> Pets = {"123e4567-e89b-12d3-a456-426655440000", "53a4567-a73b-12g3-a456-426655573000"}
You might be thinking “wow, that looks extra”, and sure, it does look like, but now when you directly reference the pet using the GUID as its name, you will get the specific pet, not the first child found inside of the pet’s folder. Yes, of course, you could do more extra work to save another value which is just the GUID, but then you would have to do extra checks later on to access the specific pet, this also becomes extremely annoying when you are trying to equip/unequip the pet.
So how are we going to hatch the pet you may ask?
In this instance, we need to access the Remote Function corresponding to Hatching, so in this case:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Packages = ReplicatedStorage.Packages
local Assets = ReplicatedStorage.Assets
local Remotes = Assets.Remotes
local HatchRemote = Remotes.Hatch
(the extra variables are just how my system has been organized, feel free to replicate it using this image):
Now that we have the function to hatch, lets actually implement some functionality to it. A quick step by step here:
First, the function is invoked by the client.
Second, we need to check if the player has enough of a specified currency to continue with the hatching.
Third, if the player has enough of the currency, we need to remove the currency amount required.
Finally, we need to give the player their pet.
To do this; this is the code I have created:
--REMOTE HANDLER SCRIPT
local function GetSingleHatch(EggName)
local T_W = 0
for PetName, W in pairs(EggData[EggName].Pets) do
T_W += W
end
local R_N = math.random()*T_W
local C_W = 0
for PetName, W in pairs(EggData[EggName].Pets) do
C_W+=W
if R_N <= C_W then
return PetName
end
end
end
HatchRemote.OnServerInvoke = function(Player, EggName)
local PlayersData = PlayerData.get(Player)
local EggPrice = EggData[EggName].Price
local PlayerCoins = En.toNumber(PlayersData.Coins.Value)
if PlayerCoins >= EggPrice then
PlayerData.get(Player).Coins.Value = En.toString(En.toNumber(PlayerData.get(Player).Coins.Value)-EggPrice)
local HatchedPet = GetSingleHatch(EggName)
if HatchedPet then
AddToInventory(Player, HatchedPet)
return {HatchedPet}
end
else
return nil
end
end
As you may see, I am accessing some information using an EggData module, this is how I am storing that information:
--EGG DATA MODULE
return {
["Starter Egg"] = {
["Pets"] = {
Dog = 40,
Cat = 25,
Pig = 20,
Bunny = 10,
Panda = 5,
},
Price = 100,
}
}
Some advice, don’t try to overcomplicate things just because you learned a “cooler” more “advanced” way to do something
Now, when the Hatch remote function is invoked, if the player has enough money, the pet is added to their inventory and the required cost is removed from their money, as well as returning the pet if the hatch was successful, and nil if it was not (again, this can be used to display to the client what pet they have received, or tell them the hatch was not successful and prompt an error message)
You may have also seen that I had a separate function named “GetSingleHatch”, the reasoning behind this is so that if you were to ever implement a “Triple Hatch” or anything similar, you could simply loop through it x amount of times, as seen here:
TripleHatchRemote.OnServerInvoke = function(Player, EggName)
local PlayersData = PlayerData.get(Player)
local EggPrice = EggData[EggName].Price
local PlayerCoins = En.toNumber(PlayersData.Coins.Value)
if PlayerCoins >= EggPrice*3 then
PlayerData.get(Player).Coins.Value = En.toString(En.toNumber(PlayerData.get(Player).Coins.Value)-EggPrice*3)
local HatchedPets = {GetSingleHatch(EggName), GetSingleHatch(EggName), GetSingleHatch(EggName)}
if HatchedPets then
for _, Pet in pairs(HatchedPets) do
AddToInventory(Player, Pet)
end
return HatchedPets
end
else
return nil
end
end
See how easy that was? Remember to make your code efficient by making it easy to reuse!
Now, assuming you have managed to create an inventory which generated frames for each pet in the players inventory, we can move on to equipping and unequipping!
For equipping and unequipping, since we named the pet using a GUID, we can easily make some functions to get us rolling! As seen here:
--REMOTE HANDLER SCRIPT
local function GetPetFromInventory(Player, PetId)
local PlayersData = PlayerData.get(Player)
local Pets = PlayersData.Pets:GetChildren()
for _, Pet in pairs(Pets) do
if Pet.Name == PetId then
return Pet
end
end
end
EquipRemote.OnServerInvoke = function(Player, PetId)
print("Equip Attempt")
local PlayersData = PlayerData.get(Player)
local Equipped = En.toNumber(PlayersData.Equipped.Value)
local MaxEquip = En.toNumber(PlayersData.MaxEquipped.Value)
if Equipped<MaxEquip then
local PetInInventory = GetPetFromInventory(Player, PetId)
if PetInInventory.Equipped.Value == false then
PlayersData.Equipped.Value = En.toString(En.toNumber(PlayersData.Equipped.Value)+1)
PetInInventory.Equipped.Value = true
return true
end
end
return false
end
UnequipRemote.OnServerInvoke = function(Player, PetId)
print("Unequip Attempt")
local PlayersData = PlayerData.get(Player)
local PetInInventory = GetPetFromInventory(Player, PetId)
if PetInInventory.Equipped.Value == true then
PlayersData.Equipped.Value = En.toString(En.toNumber(PlayersData.Equipped.Value)-1)
PetInInventory.Equipped.Value = false
return true
end
return false
end
See how easy that is? Now for ease of implementation, inside of your inventory im assuming you have an equip/unequip button! All you need to do is invoke the function using the Pets “name” (GUID)! As seen here:
--DISPLAYING LOCAL SCRIPT
local function GenerateInventory()
local PlayersData = PlayerData.get(game.Players.LocalPlayer)
for _, Pet in PlayersData.Pets:GetChildren() do
local PetDisplay = Templates.PetInventoryTemplate:Clone()
PetDisplay.Name = Pet.Name
PetDisplay.Text = Pet.PetName.Value
PetDisplay.Parent = game.Players.LocalPlayer.PlayerGui.Frames.Inventory.Container
PetDisplay.Equipped.Value = Pet.Equipped.Value
PetDisplay.MouseButton1Click:Connect(function()
if ActionCD == false then
ActionCD = true
if PetDisplay.Equipped.Value == true then
UnequipRemote:InvokeServer(Pet.Name)
elseif PetDisplay.Equipped.Value == false then
EquipRemote:InvokeServer(Pet.Name)
end
task.wait()
ActionCD = false
end
end)
end
end
GenerateInventory()
local function GetPetDisplay(PetId)
for _, Display in game.Players.LocalPlayer.PlayerGui.Frames.Inventory.Container:GetChildren() do
if Display.Name == PetId then
return Display
end
end
return nil
end
local function UpdateInventory()
local PlayersData = PlayerData.get(game.Players.LocalPlayer)
for _, Pet in PlayersData.Pets:GetChildren() do
local PetDisplay = GetPetDisplay(Pet.Name)
if PetDisplay == nil then
PetDisplay = Templates.PetInventoryTemplate:Clone()
PetDisplay.Name = Pet.Name
PetDisplay.Text = Pet.PetName.Value
PetDisplay.Parent = game.Players.LocalPlayer.PlayerGui.Frames.Inventory.Container
PetDisplay.Equipped.Value = Pet.Equipped.Value
if Pet.Equipped.Value == true then
PetDisplay.TextColor3 = Color3.new(0.00392157, 1, 0.121569)
else
PetDisplay.TextColor3 = Color3.new(1, 0, 0.0156863)
end
PetDisplay.MouseButton1Click:Connect(function()
if ActionCD == false then
ActionCD = true
if PetDisplay.Equipped.Value == true then
local Unequipped = UnequipRemote:InvokeServer(Pet.Name)
if Unequipped then
PetDisplay.Equipped.Value = false
end
elseif PetDisplay.Equipped.Value == false then
local Equipped = EquipRemote:InvokeServer(Pet.Name)
if Equipped then
PetDisplay.Equipped.Value = true
end
end
task.wait()
ActionCD = false
end
end)
elseif PetDisplay ~= nil then
PetDisplay.Equipped.Value = Pet.Equipped.Value
if Pet.Equipped.Value == true then
PetDisplay.TextColor3 = Color3.new(0.00392157, 1, 0.121569)
else
PetDisplay.TextColor3 = Color3.new(1, 0, 0.0156863)
end
PetDisplay.MouseButton1Click:Connect(function()
if ActionCD == false then
ActionCD = true
if PetDisplay.Equipped.Value == true then
local Unequipped = UnequipRemote:InvokeServer(Pet.Name)
if Unequipped then
PetDisplay.Equipped.Value = false
end
elseif PetDisplay.Equipped.Value == false then
local Equipped = EquipRemote:InvokeServer(Pet.Name)
if Equipped then
PetDisplay.Equipped.Value = true
end
end
task.wait()
ActionCD = false
end
end)
end
end
end
All I did was just make it so when the client clicks to equip/unequip the pet, it invokes the corresponding function, and returns if it was successful or not.
The End!
As this tutorial was more of a “Back end” tutorial, there are no effects or animations, but please do check out the download below to see how I used remote functions and to see more “in depth” into my code! Best of luck to all of you out there!
AsyncablePetSystem.rbxl (169.8 KB)