Pet System [Open Source + How To Make Your Own]

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:

image

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):
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 :slight_smile:

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)

5 Likes