(For those of you who don’t care for the tutorial and just want the Pet System (WHICH IS OKAY!), or want to download it to work alongside, here is the file!)
PetSystem_Asyncable2025.rbxl (70.4 KB)
(Remember to turn on API Services in Game → Security settings!)
Howdy y’all!
I made my last “How to make a pet system” post back in August of 2024, which has now gotten around 8,000 views! I firstly just want to say thank you for checking out my post, I am grateful for your time and hope that I was able to help at least someone with their development journey! ![]()
But now onto what you are all here for (assuming so atleast) the UPDATED pet system tutorial! This one is vastly more advanced than the previous! Buckle up and get ready to learn and create! (Or simply download the file and check it out yourself!)
PLEASE READ:
ADDITIONALLY! This is NOT at 100% optimization, nor to the highest quality of my professional work. This does NOT represent me as a developer entirely and should NOT be used against me in any way, this source is PURELY informational and used as a resource for others to learn!
ALSO! There is NO physical EGGS or a HATCHING ANIMATION, this is simply targeted to be a backend source
To start off, there are a few things we need to setup for this system to work, mainly, the general modularized hierarchy (structure of module scripts contained in various locations such as ServerScriptService, as well as StarterGui), in addition with some folders that we will be using throughout this tutorial!
Lets get ya’ started with setting your workspace up the same as mine! (I’ve attached a photo below)
Now that we have our workspace setup, allow me to explain what everything does in our structure!
ReplicatedStorage
Shared [FOLDER]
Shared logic and definitions used by both client and server.
EggInfo Module
Stores
egg configuration and pet chances:
▸
Defines pet names, rarities, and odds
▸
Used in hatching logic by both client/server
PetModels [FOLDER]
Contains
3D pet models used for display and following:
▸
Models cloned and placed into Workspace
▸
Used during hatching and pet rendering
(FYI, PET MODELS SHOULD BE MODELS AND HAVE A PRIMARYPART SET WITH ALL ADDITIONAL PARTS WELDED, AS SEEN BELOW) {body is the primary part and one weld for each cosmetic feature of a pet}
(FYI PT2, FOR ADVANCED/FANCIER PET MODELS THAT ARE MESHES, JUST ADD A PART TO BE THE PRIMARY PART, A (1,1,1) CUBE WORKS)
PlayerDatas [FOLDER]
Holds
synced player data instances for in-game access:
▸
Mirrors what’s stored in Datastore
▸
Lets GUI display or reference player data live
Remotes [FOLDER]
Holds
RemoteEvents/RemoteFunctions for client-server communication:
SingleHatch [RemoteEvent]
▸
Triggered to hatch one egg
▸
Sends request to server for 1x hatch logic
TripleHatch [RemoteEvent]
▸
Triggers hatching of 3 eggs
▸
Used for triple hatch animations & rewards
ServerScriptService
Datastore Module
Manages all
player save/load operations.
On player join/leave
Uses Template for new players
Version-based data key system
Template Module
Holds
default data structure for new players.
Version Module
Contains
current version used in datastore key:
▸
Allows clean version upgrades
▸
Prevents data corruption across updates
Pets Module
Handles
backend logic for pet spawning and syncing:
▸
Server creates, updates, and destroys player pets
▸
Positions pets in Workspace
StarterGui
Client Module
Central client-side initializer. Loads GUI & listens to remotes.
Hatching Module
Handles
animations and UI for hatching eggs:
▸
Interfaces with remotes (Single/Triple Hatch)
Pets Module
Controls
pet behavior and rendering on the client:
▸
Visual following
▸
Appearance updates
TestHatching (ScreenGui)
Testing GUI for hatching features.
Hatch (TextButton)
▸
Triggers a single egg hatch when clicked
TripleHatch (TextButton)
▸
Triggers a 3x hatch using the TripleHatch remote
Workspace
PlayerPets [FOLDER]
Holds
active pet models for each player in the world:
▸
Pets are placed/updated here based on player actions
▸
Managed by client-side Pets Module
Now that we have our workspace all set, we can begin with the scripting portion!
Remember that if you ever get lost, confused, or have questions, feel free to respond to this post as well as checking out the game file!
📜 Server Module
local Server = {}
local Start = tick()
local LoadedModules = {}
for _, Module in script:GetChildren() do
if Module:IsA("ModuleScript") then
if require(Module):: nil then
table.insert(LoadedModules, Module)
end
end
end
print("Client Loaded In: " .. tick()-Start .. " Seconds")
return Server
❔ Server Module Explanation
This script automatically loads all ModuleScript children of the script it’s placed in. For each module, it checks if it can be successfully required and adds it to a list. It also tracks and prints how long the loading process takes for debugging purposes.
[SPACER]
📜 Datastore Module
--!native
local Datastore = {}
local Version = require(script.Parent.Version)
local Template = require(script.Template)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local DataStoreService = game:GetService("DataStoreService")
local HttpService = game:GetService("HttpService")
local DataStore = DataStoreService:GetDataStore("PetSystemExample" .. Version)
local PlayerDatas = ReplicatedStorage:WaitForChild("PlayerDatas")
Datastore.LoadedUsers = {}
do
local function Player_StartSequence(player: Player)
print(player.Name .. " loading")
Datastore:RegisterPlayer(player)
end
local function Player_ExitSequence(player: Player)
print(player.Name .. " exiting")
Datastore:CleanupPlayer(player)
end
do
game.Players.PlayerAdded:Connect(function(player: Player)
Player_StartSequence(player)
end)
game.Players.PlayerRemoving:Connect(function(player: Player)
Player_ExitSequence(player)
end)
end
end
local function JsonE(t) return HttpService:JSONEncode(t) end
local function JsonD(t) return HttpService:JSONDecode(t) end
local function deepcopy(orig)
if typeof(orig) ~= "table" then return orig end
local copy = {}
for k, v in pairs(orig) do
copy[deepcopy(k)] = deepcopy(v)
end
return copy
end
local function FixData(data)
for key, default in pairs(Template) do
if data[key] == nil then
data[key] = deepcopy(default)
elseif typeof(default) == "table" and typeof(data[key]) == "table" then
for subKey, subValue in pairs(default) do
if data[key][subKey] == nil then
data[key][subKey] = subValue
end
end
end
end
for key in pairs(data) do
if not Template[key] then
data[key] = nil
end
end
return data
end
local function PathLink(player: Player, data: table)
if not Datastore.LoadedUsers[player.UserId] then
Datastore.LoadedUsers[player.UserId] = FixData(data)
end
end
local function HasDataPath(player: Player, jsonData: string)
local parsedData = JsonD(jsonData)
PathLink(player, parsedData)
end
local function NoDataPath(player: Player)
PathLink(player, deepcopy(Template))
end
local function RouteData(player: Player, raw)
if raw ~= nil then
HasDataPath(player, raw)
else
NoDataPath(player)
end
end
function Datastore:RegisterPlayer(player: Player)
local success, response
local attempts = 0
repeat
if attempts > 0 then
warn("Retrying, Attempt: " .. attempts)
task.wait(7)
end
success, response = pcall(DataStore.GetAsync, DataStore, tostring(player.UserId))
attempts += 1
until success
local dataFolder = Instance.new("Folder")
dataFolder.Name = tostring(player.UserId)
dataFolder.Parent = PlayerDatas
RouteData(player, response)
end
function Datastore:CleanupPlayer(player: Player)
local userId = player.UserId
local data = Datastore.LoadedUsers[userId]
if not data then return end
local attempts, success, response = 0, false, nil
repeat
if attempts > 0 then
warn("Retrying Cleanup, Attempt: " .. attempts)
task.wait()
end
local folder = PlayerDatas:FindFirstChild(tostring(userId))
if folder then folder:Destroy() end
success, response = pcall(DataStore.SetAsync, DataStore, tostring(userId), JsonE(data))
attempts += 1
until success
if success then
print("Successfully saved data for " .. player.Name)
else
warn("Failed to save data for " .. player.Name)
end
end
local function createOrUpdateValue(value, name, parent)
local class
if typeof(value) == "number" then
class = "NumberValue"
elseif typeof(value) == "string" then
class = "StringValue"
elseif typeof(value) == "boolean" then
class = "BoolValue"
end
if not class then return end
local existing = parent:FindFirstChild(name)
if not existing or not existing:IsA(class) then
if existing then existing:Destroy() end
existing = Instance.new(class)
existing.Name = name
existing.Parent = parent
end
if existing.Value ~= value then
existing.Value = value
end
end
local function ensureFolder(name, parent)
local folder = parent:FindFirstChild(name)
if not folder or not folder:IsA("Folder") then
if folder then folder:Destroy() end
folder = Instance.new("Folder")
folder.Name = name
folder.Parent = parent
end
return folder
end
local function syncDataToInstances(data, parent)
for key, value in pairs(data) do
if typeof(value) == "table" then
local subFolder = ensureFolder(key, parent)
syncDataToInstances(value, subFolder)
else
createOrUpdateValue(value, key, parent)
end
end
end
local function filterAndSpawn(data, userId, parent)
local playerFolder = PlayerDatas:FindFirstChild(tostring(userId))
if not playerFolder then return end
syncDataToInstances(data, parent or playerFolder)
end
-- Syncer
task.spawn(function()
while task.wait() do
for userId, userData in pairs(Datastore.LoadedUsers) do
if userData then
filterAndSpawn(userData, userId)
end
end
end
end)
return Datastore
❔ Datastore Module Explanation
This module handles loading, saving, and syncing player data using Roblox’s DataStoreService. It creates default data from a template if none exists, stores it in ReplicatedStorage.PlayerDatas, and keeps it synced with memory in real time.
Functions:
FixData(data)
Ensures the data follows the structure of Template:
- Fills in missing keys using defaults.
- Cleans up extra/unexpected keys.
- Handles nested tables recursively.
deepcopy(orig)
Recursively clones a table so that changes to one copy don’t affect the original. Used to duplicate the template safely.
JsonE(t) / JsonD(t)
Shortcuts for HttpService:JSONEncode() and JSONDecode(), used for saving and loading data as strings in Roblox’s DataStore.
PathLink(player, data)
Links cleaned and fixed player data into Datastore.LoadedUsers, ensuring each player has a valid in-memory data object.
HasDataPath(player, jsonData)
- Called when a player’s data exists in the datastore.
- Decodes the data and links it using
PathLink.
NoDataPath(player)
- Called when a player has no saved data.
- Links a fresh copy of the
Template.
RouteData(player, raw)
Determines whether to use HasDataPath or NoDataPath based on whether raw (the datastore result) is nil.
Datastore:RegisterPlayer(player)
- Called when a player joins.
- Tries to load their data from the datastore with retries on failure.
- Creates a folder in
ReplicatedStorage.PlayerDatasfor the player. - Passes data to
RouteData()to handle it.
Datastore:CleanupPlayer(player)
- Called when a player leaves.
- Destroys their folder in
PlayerDatas. - Saves their in-memory data (
LoadedUsers) back to the datastore. - Retries on failure.
createOrUpdateValue(value, name, parent)
- Creates or updates a
NumberValueorStringValueinstance in a folder. - Ensures the value reflects the current in-memory data.
ensureFolder(name, parent)
- Ensures a
Folderexists under the given parent. - Destroys it and remakes it if it’s the wrong type.
syncDataToInstances(data, parent)
- Recursively mirrors the nested table structure from in-memory data (
LoadedUsers) to Roblox instances insidePlayerDatas.
filterAndSpawn(data, userId, parent)
- Finds the player’s folder in
PlayerDatas. - Syncs the player’s data into it using
syncDataToInstances.
[SPACER]
📜 Template Module
local Template_Data = {
Cash = 1000,
Diamonds = 100,
Pets = {},
}
return Template_Data
❔ Template Module Explanation
Simply put, the Template Module stores the default values that a player will be granted in either of the following scenarios: The player does not have any data OR there was an issue loading the data and the default template data was granted instead.
[SPACER]
📜 Pets Module
--!native
local Pets = {}
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local HttpService = game:GetService("HttpService")
local EggInfo = require(ReplicatedStorage.Shared:WaitForChild("EggInfo"))
local Datastore = require(script.Parent.Datastore)
local Remotes = ReplicatedStorage:WaitForChild("Remotes")
local SingleHatch = Remotes:WaitForChild("SingleHatch")
local TripleHatch = Remotes:WaitForChild("TripleHatch")
local function rollPet(petTable)
local totalWeight = 0
for _, petData in pairs(petTable) do
totalWeight += petData.Chance
end
local roll = math.random(1, totalWeight)
local cumulative = 0
for petName, petData in pairs(petTable) do
cumulative += petData.Chance
if roll <= cumulative then
return petName, petData
end
end
end
local function hatchPets(player, eggName, count)
local userData = Datastore.LoadedUsers[player.UserId]
if not userData then return end
local egg = EggInfo[eggName]
if not egg then return end
local totalPrice = egg.Price * count
if userData.Coins < totalPrice then return end
userData.Coins -= totalPrice
userData.Pets = userData.Pets or {}
for i = 1, count do
local petName, petData = rollPet(egg.Pets)
if petName then
local petId = HttpService:GenerateGUID(false)
userData.Pets[petId] = {
Name = petName,
CoinBoost = petData.CoinBoost,
Equipped = true,
}
end
end
end
SingleHatch.OnServerEvent:Connect(function(player, eggName)
hatchPets(player, eggName, 1)
end)
TripleHatch.OnServerEvent:Connect(function(player, eggName)
hatchPets(player, eggName, 3)
end)
return Pets
❔ Pets Module Explanation
The Pets module is used to communicate with the client for hatching pets and choosing which pet has been acquired based on the pets odds! This module also creates the pets information to be stored in the players data.
[SPACER]
| OPTIONAL |
📜 Version Module
return "b.0.0.01"
❔ Version Module Explanation
The version module is only one line, and the only use for this line is to easily switch version, intended to be implemented throughout your games structure, in this case, its used as the second half of the Key for the Datastore, seen in the Datastore module.
📜 Client Module
local Client = {}
local Start = tick()
local LoadedModules = {}
for _, Module in script:GetChildren() do
if Module:IsA("ModuleScript") then
if require(Module):: nil then
table.insert(LoadedModules, Module)
end
end
end
print("Client Loaded In: " .. tick()-Start .. " Seconds")
return Client
❔ Client Module Explanation
This script automatically loads all ModuleScript children of the script it’s placed in. For each module, it checks if it can be successfully required and adds it to a list. It also tracks and prints how long the loading process takes for debugging purposes.
[SPACER]
📜 Pets Module
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")
local PetModels = ReplicatedStorage:WaitForChild("PetModels")
local PetFolder = Workspace:WaitForChild("PlayerPets")
local PlayerDatas = ReplicatedStorage:WaitForChild("PlayerDatas")
local Spacing = 5
local PetSize = 3
local MaxClimbHeight = 6
local RayParams = RaycastParams.new()
local RayDirection = Vector3.new(0, -500, 0)
local function RearrangeTables(Pets, Rows, MaxRowCapacity)
table.clear(Rows)
local AmountOfRows = math.ceil(#Pets / MaxRowCapacity)
for i = 1, AmountOfRows do
table.insert(Rows, {})
end
for i, v in Pets do
local Row = Rows[math.ceil(i / MaxRowCapacity)]
table.insert(Row, v)
end
end
local function GetRowWidth(Row, Pet)
if Pet ~= nil then
local SpacingBetweenPets = Spacing - Pet.PrimaryPart.Size.X
local RowWidth = 0
if #Row == 1 then
return 0
end
for i, v in Row do
if i ~= #Row then
RowWidth += Pet.PrimaryPart.Size.X + SpacingBetweenPets
else
RowWidth += Pet.PrimaryPart.Size.X
end
end
return RowWidth
end
end
task.wait(1)
RunService.Heartbeat:Connect(function(deltaTime)
for _, playerDataFolder in PlayerDatas:GetChildren() do
local userId = tonumber(playerDataFolder.Name)
if not userId then continue end
local player = Players:GetPlayerByUserId(userId)
if not player then continue end
local character = player.Character
if not character then continue end
local hrp = character:FindFirstChild("HumanoidRootPart")
if not hrp then continue end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then continue end
local playerPetFolder = PetFolder:FindFirstChild(tostring(userId)) or Instance.new("Folder")
playerPetFolder.Name = tostring(userId)
playerPetFolder.Parent = PetFolder
local equippedPetGuids = {}
local petsFolder = playerDataFolder:FindFirstChild("Pets")
if petsFolder then
for _, petFolder in petsFolder:GetChildren() do
local equipped = petFolder:FindFirstChild("Equipped")
local nameValue = petFolder:FindFirstChild("Name")
if equipped and equipped:IsA("BoolValue") and equipped.Value == true then
if nameValue and PetModels:FindFirstChild(nameValue.Value) then
equippedPetGuids[petFolder.Name] = nameValue.Value
end
end
end
end
for _, petModel in playerPetFolder:GetChildren() do
if not equippedPetGuids[petModel.Name] then
petModel:Destroy()
end
end
for guid, petName in pairs(equippedPetGuids) do
if not playerPetFolder:FindFirstChild(guid) then
local template = PetModels:FindFirstChild(petName)
if template and template:IsA("Model") and template.PrimaryPart then
local clone = template:Clone()
clone.Name = guid
clone:PivotTo(hrp.CFrame)
clone.Parent = playerPetFolder
end
end
end
local pets = {}
local rows = {}
for _, v in playerPetFolder:GetChildren() do
table.insert(pets, v)
end
RayParams.FilterDescendantsInstances = {PetFolder, character}
local maxRowCapacity = math.ceil(math.sqrt(#pets))
RearrangeTables(pets, rows, maxRowCapacity)
for i, pet in pets do
local rowIndex = math.ceil(i / maxRowCapacity)
local row = rows[rowIndex]
local rowWidth = GetRowWidth(row, pet)
local xOffset = #row == 1 and 0 or rowWidth / 2 - pet.PrimaryPart.Size.X / 2
local x = (table.find(row, pet) - 1) * Spacing
local z = rowIndex * Spacing
local y = 0
local rayResult = Workspace:Blockcast(pet.PrimaryPart.CFrame + Vector3.new(0, MaxClimbHeight, 0), pet.PrimaryPart.Size, RayDirection, RayParams)
if rayResult then
y = rayResult.Position.Y + pet.PrimaryPart.Size.Y / 2
end
local targetCFrame = CFrame.new(hrp.Position.X, 0, hrp.Position.Z)
* hrp.CFrame.Rotation
* CFrame.new(x - xOffset, y, z)
pet.PrimaryPart.CFrame = pet.PrimaryPart.CFrame:Lerp(targetCFrame, 0.1)
end
end
end)
return true
❔ Pets Module Explanation
This modules entire job is to make the pets visually appear properly, following the proper users. If you have attempted to create a pet system previously and noticed “laggy” pets when tweening from the server, or only to the direct user, then you may notice the solution here, each client tweens EVERY players pets, still to the proper user.
[SPACER]
📜 Hatching Module
--!native
local Hatching = {}
local ReplicatedStorage =game:GetService("ReplicatedStorage")
local Player = game.Players.LocalPlayer
local PlayerGui = Player:WaitForChild("PlayerGui")
local TestHatching = PlayerGui:WaitForChild("TestHatching")
local HatchButton = TestHatching:WaitForChild("Hatch")
local TripleHatchButton = TestHatching:WaitForChild("TripleHatch")
local Remotes = ReplicatedStorage:WaitForChild("Remotes")
local SingleHatchRemote = Remotes:WaitForChild("SingleHatch")
local TripleHatchRemote = Remotes:WaitForChild("TripleHatch")
HatchButton.MouseButton1Click:Connect(function()
print("Attempt to single hatch!")
SingleHatchRemote:FireServer("Basic Egg")
end)
TripleHatchButton.MouseButton1Click:Connect(function()
print("Attempt to triple hatch!")
TripleHatchRemote:FireServer("Basic Egg")
end)
return Hatching
❔ Hatching Module Explanation
This module is a placeholder module to trigger the hatching remotes, please implement the remotes into your system properly!
I personally spent a LOT of time making this resource for all of you current/future developers! It would mean alot to me if you could leave your thoughts here!
Did you find this tutorial to be helpful?
- Yes!
- Sorta, but confusing at times.
- Not really.
Feedback is not required, but all feedback is appreciated and will get a response from me! Please also leave any additional questions you may have!
Thank you for your time <3
Asyncable








