Tutorial · Rewards

How to make an egg hatching system in Roblox Studio

Eggs are how most simulators hand out pets: walk up, pay coins, watch the egg shake, see what hatched. The part players see is an animation. The part that matters is a weighted random roll that must run on the server, with the price taken in the same step. This guide builds both, with every egg's price and odds in one module you can tune.

Updated 23 October 20266 min readIntermediateLuau

In short

Keep each egg's price and its pets with their weights in a ModuleScript named EggConfig in ReplicatedStorage. Tag each egg model Egg and give it an EggName attribute. A Script adds a ProximityPrompt to every egg; on Triggered it checks the player's coins, subtracts the price, rolls a pet with weighted random, adds it to the player's Pets folder and fires a RemoteEvent so the client plays the hatch animation.

  • The server rolls. If the client chose the pet, a cheater would hatch legendaries every time.
  • Take the coins and give the pet in the same step, with no wait in between.
  • The client may read EggConfig to show the odds; changing its copy changes nothing on the server.

Get the script

Egg hatching server script with weighted odds

A Script that adds a hatch prompt to every egg, takes the price in coins, rolls a pet by weight on the server and adds it to the player's Pets folder.

Script in ServerScriptService named EggHatching
-- Script in ServerScriptService named EggHatching.
-- Every Model tagged "Egg" gets a ProximityPrompt. The server checks the price,
-- takes the coins, rolls the pet and adds it to the player's Pets folder.
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

type PetChance = { pet: string, weight: number, rarity: string }
type Egg = { price: number, pets: { PetChance } }
local EggConfig = require(ReplicatedStorage:WaitForChild("EggConfig")) :: { [string]: Egg }

local HATCH_COOLDOWN = 3.5 -- seconds between hatches, the length of the animation

local hatchedEvent = Instance.new("RemoteEvent")
hatchedEvent.Name = "EggHatched"
hatchedEvent.Parent = ReplicatedStorage

local random = Random.new()
local lastHatch: { [Player]: number } = {}

local function roll(egg: Egg): PetChance
	local total = 0
	for _, chance in egg.pets do
		total += chance.weight
	end
	local pick = random:NextNumber(0, total)
	for _, chance in egg.pets do
		pick -= chance.weight
		if pick <= 0 then
			return chance
		end
	end
	return egg.pets[#egg.pets]
end

local function hatch(player: Player, eggName: string)
	local egg = EggConfig[eggName]
	local leaderstats = player:FindFirstChild("leaderstats")
	local coins = leaderstats and leaderstats:FindFirstChild("Coins")
	local pets = player:FindFirstChild("Pets")
	if not (egg and coins and coins:IsA("IntValue") and pets) then
		return
	end
	local now = os.clock()
	if now - (lastHatch[player] or -math.huge) < HATCH_COOLDOWN then
		return
	end
	if coins.Value < egg.price then
		hatchedEvent:FireClient(player, eggName, nil)
		return
	end
	lastHatch[player] = now
	coins.Value -= egg.price -- take the price before rolling, in the same step

	local result = roll(egg)
	local owned = pets:FindFirstChild(result.pet)
	if owned and owned:IsA("IntValue") then
		owned.Value += 1
	else
		local newPet = Instance.new("IntValue")
		newPet.Name = result.pet
		newPet.Value = 1
		newPet.Parent = pets
	end
	hatchedEvent:FireClient(player, eggName, result.pet, result.rarity)
end

local function setUpEgg(eggModel: Instance)
	local eggName = eggModel:GetAttribute("EggName")
	if typeof(eggName) ~= "string" or not EggConfig[eggName] then
		warn("EggHatching: give", eggModel:GetFullName(), "an EggName attribute that matches EggConfig")
		return
	end
	local prompt = Instance.new("ProximityPrompt")
	prompt.ActionText = string.format("Hatch (%d coins)", EggConfig[eggName].price)
	prompt.ObjectText = eggName
	prompt.HoldDuration = 0.3
	prompt.MaxActivationDistance = 10
	prompt.Parent = if eggModel:IsA("Model") then eggModel.PrimaryPart or eggModel else eggModel
	prompt.Triggered:Connect(function(player: Player)
		hatch(player, eggName)
	end)
end

for _, egg in CollectionService:GetTagged("Egg") do
	setUpEgg(egg)
end
CollectionService:GetInstanceAddedSignal("Egg"):Connect(setUpEgg)

Players.PlayerAdded:Connect(function(player: Player)
	local pets = Instance.new("Folder")
	pets.Name = "Pets"
	pets.Parent = player
end)

Players.PlayerRemoving:Connect(function(player: Player)
	lastHatch[player] = nil
end)

RoCode builds it in the place you have open in Roblox Studio, on Windows or macOS with a desktop browser; on a phone the button sends the prompt and the link to your computer. Free to start, with free credits every day. More Roblox Studio scripts

Where to put it

Explorer: the odds, the server, the animation and an egg
ReplicatedStorage
  EggConfig ModuleScript new
ServerScriptService
  EggHatching Script new
StarterPlayer
  StarterPlayerScripts
    HatchAnimation LocalScript new
Workspace
  BasicEgg Model (tag: Egg, EggName = BasicEgg)

Settings you can change

HATCH_COOLDOWNIn the script
Seconds between hatches for one player, about the length of the animation. Default: 3.5.
EggNameAttribute
On each egg model: which egg in EggConfig it is, such as BasicEgg. Default: none.

Players need a Coins value in leaderstats, as in the coin collecting guide. Prices and odds live in the EggConfig module from Step 1.

What a cheater can and cannot doThe server checks the egg exists, the player can pay and the cooldown, then takes the coins and rolls the pet itself. A cheater can trigger the prompt from anywhere their character can reach, but cannot hatch for free or pick the pet.

Type-checked with luau-lsp strict against Roblox's API definitions on 23 October 2026. Not playtested in Studio yet: press Play and watch the Output window before you publish.

Prompt for RoCode

Add egg hatching: an EggConfig ModuleScript in ReplicatedStorage with egg prices and pets with weights and rarities, a server Script that puts a ProximityPrompt on every model tagged Egg, takes the price from the Coins leaderstat, rolls a pet by weight and adds it to a Pets folder on the player, and a LocalScript that shakes an egg on screen and reveals the pet in its rarity colour.

On this page
  1. Get the script
  2. How it works
  3. 1. Egg config
  4. 2. Server
  5. 3. Animation
  6. Common errors
  7. With RoCode
  8. Questions
  9. Sources

How egg hatching works

  1. The player walks up to an egg and holds E (or taps the prompt on a phone).
  2. The ProximityPrompt's Triggered event fires on the server with the player.
  3. The server checks the player has enough Coins and is off cooldown, takes the price, rolls a pet and adds it to their Pets folder.
  4. The server fires EggHatched to that player with the pet's name and rarity, and their screen plays the animation.

A weighted roll adds up all the weights, picks a random number between 0 and that total, then walks down the list subtracting each pet's weight until it drops to 0 or below. With weights 50, 35, 12 and 3, the Dragon comes up 3 times in 100 on average.

Step 1: Prices and odds in one module

Insert a ModuleScript in ReplicatedStorage named EggConfig:

ModuleScript in ReplicatedStorage named EggConfig
-- ModuleScript in ReplicatedStorage named EggConfig.
-- Prices and odds for every egg. The server rolls with these numbers; the client
-- only reads them to show the odds.
export type PetChance = { pet: string, weight: number, rarity: string }
export type Egg = { price: number, pets: { PetChance } }

local EggConfig: { [string]: Egg } = {
	BasicEgg = {
		price = 100,
		pets = {
			{ pet = "Dog", weight = 50, rarity = "Common" },
			{ pet = "Cat", weight = 35, rarity = "Common" },
			{ pet = "Bunny", weight = 12, rarity = "Rare" },
			{ pet = "Dragon", weight = 3, rarity = "Legendary" },
		},
	},
	GoldEgg = {
		price = 1000,
		pets = {
			{ pet = "Fox", weight = 60, rarity = "Rare" },
			{ pet = "Unicorn", weight = 35, rarity = "Epic" },
			{ pet = "Phoenix", weight = 5, rarity = "Legendary" },
		},
	},
}

return EggConfig

Each egg has a price and a list of pets with a weight and a rarity. The weights in one egg do not have to add to 100. If you show the odds in a menu, divide each weight by the egg's total.

Step 2: The hatching server

  1. Build an egg: a Model with an egg-shaped part as its PrimaryPart. Tag the model Egg and add a String attribute EggName set to BasicEgg.
  2. Insert a Script in ServerScriptService named EggHatching:
Script in ServerScriptService named EggHatching
-- Script in ServerScriptService named EggHatching.
-- Every Model tagged "Egg" gets a ProximityPrompt. The server checks the price,
-- takes the coins, rolls the pet and adds it to the player's Pets folder.
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

type PetChance = { pet: string, weight: number, rarity: string }
type Egg = { price: number, pets: { PetChance } }
local EggConfig = require(ReplicatedStorage:WaitForChild("EggConfig")) :: { [string]: Egg }

local HATCH_COOLDOWN = 3.5 -- seconds between hatches, the length of the animation

local hatchedEvent = Instance.new("RemoteEvent")
hatchedEvent.Name = "EggHatched"
hatchedEvent.Parent = ReplicatedStorage

local random = Random.new()
local lastHatch: { [Player]: number } = {}

local function roll(egg: Egg): PetChance
	local total = 0
	for _, chance in egg.pets do
		total += chance.weight
	end
	local pick = random:NextNumber(0, total)
	for _, chance in egg.pets do
		pick -= chance.weight
		if pick <= 0 then
			return chance
		end
	end
	return egg.pets[#egg.pets]
end

local function hatch(player: Player, eggName: string)
	local egg = EggConfig[eggName]
	local leaderstats = player:FindFirstChild("leaderstats")
	local coins = leaderstats and leaderstats:FindFirstChild("Coins")
	local pets = player:FindFirstChild("Pets")
	if not (egg and coins and coins:IsA("IntValue") and pets) then
		return
	end
	local now = os.clock()
	if now - (lastHatch[player] or -math.huge) < HATCH_COOLDOWN then
		return
	end
	if coins.Value < egg.price then
		hatchedEvent:FireClient(player, eggName, nil)
		return
	end
	lastHatch[player] = now
	coins.Value -= egg.price -- take the price before rolling, in the same step

	local result = roll(egg)
	local owned = pets:FindFirstChild(result.pet)
	if owned and owned:IsA("IntValue") then
		owned.Value += 1
	else
		local newPet = Instance.new("IntValue")
		newPet.Name = result.pet
		newPet.Value = 1
		newPet.Parent = pets
	end
	hatchedEvent:FireClient(player, eggName, result.pet, result.rarity)
end

local function setUpEgg(eggModel: Instance)
	local eggName = eggModel:GetAttribute("EggName")
	if typeof(eggName) ~= "string" or not EggConfig[eggName] then
		warn("EggHatching: give", eggModel:GetFullName(), "an EggName attribute that matches EggConfig")
		return
	end
	local prompt = Instance.new("ProximityPrompt")
	prompt.ActionText = string.format("Hatch (%d coins)", EggConfig[eggName].price)
	prompt.ObjectText = eggName
	prompt.HoldDuration = 0.3
	prompt.MaxActivationDistance = 10
	prompt.Parent = if eggModel:IsA("Model") then eggModel.PrimaryPart or eggModel else eggModel
	prompt.Triggered:Connect(function(player: Player)
		hatch(player, eggName)
	end)
end

for _, egg in CollectionService:GetTagged("Egg") do
	setUpEgg(egg)
end
CollectionService:GetInstanceAddedSignal("Egg"):Connect(setUpEgg)

Players.PlayerAdded:Connect(function(player: Player)
	local pets = Instance.new("Folder")
	pets.Name = "Pets"
	pets.Parent = player
end)

Players.PlayerRemoving:Connect(function(player: Player)
	lastHatch[player] = nil
end)

The prompt shows the egg's name and price, and needs a short 0.3 second hold so players do not buy by accident. hatch checks everything first, then takes the price, rolls and adds the pet, all without yielding. Owned pets are IntValues in a Pets folder, one per kind, whose value counts how many the player has.

Step 3: The hatch animation

Insert a LocalScript in StarterPlayer > StarterPlayerScripts named HatchAnimation:

LocalScript in StarterPlayerScripts named HatchAnimation
-- LocalScript in StarterPlayerScripts named HatchAnimation.
-- Shows the egg shaking, then the pet's name in its rarity colour.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")

local hatchedEvent = ReplicatedStorage:WaitForChild("EggHatched") :: RemoteEvent

local RARITY_COLORS: { [string]: Color3 } = {
	Common = Color3.fromRGB(220, 220, 220),
	Rare = Color3.fromRGB(80, 160, 255),
	Epic = Color3.fromRGB(180, 90, 255),
	Legendary = Color3.fromRGB(255, 190, 40),
}

local gui = Instance.new("ScreenGui")
gui.Name = "HatchGui"
gui.ResetOnSpawn = false
gui.Enabled = false
gui.Parent = Players.LocalPlayer:WaitForChild("PlayerGui")

local egg = Instance.new("TextLabel")
egg.AnchorPoint = Vector2.new(0.5, 0.5)
egg.Position = UDim2.fromScale(0.5, 0.45)
egg.Size = UDim2.fromOffset(160, 200)
egg.BackgroundColor3 = Color3.fromRGB(250, 240, 220)
egg.Text = ""
egg.Parent = gui
local eggCorner = Instance.new("UICorner")
eggCorner.CornerRadius = UDim.new(0.5, 0)
eggCorner.Parent = egg

local result = Instance.new("TextLabel")
result.AnchorPoint = Vector2.new(0.5, 0)
result.Position = UDim2.fromScale(0.5, 0.65)
result.Size = UDim2.fromOffset(400, 60)
result.BackgroundTransparency = 1
result.Font = Enum.Font.GothamBlack
result.TextSize = 34
result.TextStrokeTransparency = 0.4
result.Parent = gui

local busy = false

hatchedEvent.OnClientEvent:Connect(function(eggName: string, petName: string?, rarity: string?)
	if not petName then
		-- The server said no: not enough coins
		result.Text = "Not enough coins for " .. eggName
		result.TextColor3 = Color3.fromRGB(255, 110, 110)
		gui.Enabled = true
		egg.Visible = false
		task.wait(1.5)
		gui.Enabled = false
		egg.Visible = true
		return
	end
	if busy then
		return
	end
	busy = true
	gui.Enabled = true
	egg.Visible = true
	egg.Size = UDim2.fromOffset(160, 200)
	result.Text = ""

	-- Shake: rotate left and right a few times, faster each time
	for i = 1, 6 do
		local angle = if i % 2 == 0 then 12 else -12
		local shake = TweenService:Create(egg, TweenInfo.new(0.25 - i * 0.025), { Rotation = angle })
		shake:Play()
		shake.Completed:Wait()
	end
	-- Pop: the egg grows and disappears, the pet's name appears
	local pop = TweenService:Create(egg, TweenInfo.new(0.2), { Size = UDim2.fromOffset(240, 300), Rotation = 0 })
	pop:Play()
	pop.Completed:Wait()
	egg.Visible = false
	result.Text = string.format("%s! (%s)", petName, rarity or "")
	result.TextColor3 = RARITY_COLORS[rarity or ""] or Color3.new(1, 1, 1)
	task.wait(2)
	gui.Enabled = false
	busy = false
end)

The egg is a rounded TextLabel that tweens its Rotation left and right six times, each shake shorter, then grows and disappears as the pet's name appears in its rarity colour. If the server refused the hatch, the same screen says the player cannot afford it.

Test it

  1. Give yourself coins for the test: in the server view during Play, set your Coins in leaderstats to 1000.
  2. Walk to the egg and hold E: 100 coins are taken, the egg shakes and a pet appears.
  3. Look in your Player in the Explorer: the Pets folder has the new pet with a count.
  4. Spend down to below 100 and try again: "Not enough coins".

Common errors and fixes

Watch the Output window for warnings from EggHatching.

EggHatching: give ... an EggName attribute that matches EggConfig

The egg model needs a String attribute EggName spelled exactly like a key in EggConfig, such as BasicEgg.

No prompt appears on the egg

Check the model is tagged Egg and has a PrimaryPart. Prompts show when you are within 10 studs.

It always says not enough coins

The player needs a Coins IntValue inside a leaderstats folder, set by the server. Coins you change in a LocalScript only exist on your screen.

Hatching twice quickly shows only one animation

That is the cooldown and the animation guard. Raise or lower HATCH_COOLDOWN to match your animation.

Build it with RoCode

If you would rather not set this up by hand, RoCode can build it in the place you have open. RoCode is an independent AI agent for Roblox Studio, not made by Roblox: you describe the change in a web chat, and its Studio plugin creates the eggs and scripts in your Explorer.

You type

Add three eggs (100, 1000 and 10000 coins) with 4 pets each from common to legendary, hatched on the server, with an egg shake animation, and make the pets follow the player.

RoCode does
  • Builds the egg models, the odds module and the server script, and wires them to your existing Coins
  • Can insert a ready-made egg hatching screen from its premium UI kits in one step, animated, in one of five themes (candy, neon, gold, blocky, pastel), with working server scripts
  • Is told to compile-check each script with Check Script, and sets undo points around each batch

After a build, RoCode can run its test robot: it plays your game in Studio, walks to the egg, presses keys and clicks buttons, and reports PASS or FAIL. It checks only what it chose to test. RoCode cannot create developer products for Robux eggs: make those in Creator Hub. How RoCode connects to Studio.

Questions

How do I make an egg hatching system in Roblox?

Put egg prices and pet weights in a module, roll the pet on the server when a ProximityPrompt is triggered, take the price in the same step, and tell the client to play an animation. Steps 1 to 3 have the scripts.

How do egg odds work?

Each pet has a weight. Its chance is its weight divided by the total of all weights in that egg.

Why must the hatch roll be on the server?

Anything on the client can be changed by a cheater. If the client rolled or named the pet, they could hatch whatever they wanted.

How do I make the hatched pet follow me?

Clone a pet model with the same name from ServerStorage and attach it to the player with the pet follow guide's script.

Sources and further reading

How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 23 October 2026). A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.

Try RoCode on your own place.

Free every day, no card required. Install the plugin, describe the feature, and review what lands in Studio.