In short
Keep the prices and formulas in a ModuleScript in ReplicatedStorage. A Script in ServerScriptService owns each player's Coins, Click Power level and Rebirths: a Click RemoteEvent adds Click Power × (Rebirths + 1) coins, counting at most 15 clicks in each one-second window, and two RemoteFunctions sell the next Click Power level and the rebirth after checking the price. A LocalScript builds the Click, Upgrade and Rebirth buttons and only displays the numbers.
- A click sends nothing but the click. The server works out what it is worth, so an edited client cannot give itself coins.
- Rate limit clicks on the server. An exploiter can fire a remote as often as they like, and Roblox's security guide names awarding currency as something to limit.
- Save the whole table on a timer and when players leave, never on each click. Roblox allows each server 60 + 40 × players data store writes a minute.
On this page
How a simulator's core loop works
A simulator is a loop of three actions. The player does something simple over and over to earn a currency, spends it on upgrades that earn it faster, and when the upgrades get expensive, rebirths: they give up their coins and upgrades for a bonus that never goes away, then climb again faster. Other simulators replace the click with lifting, mining or collecting, but the loop underneath is the same.
| Action | What it costs | What it gives | Who decides |
|---|---|---|---|
| Click | Nothing | Coins equal to Click Power × (Rebirths + 1) | The server, counting at most 15 clicks in each one-second window |
| Upgrade | 10 coins, then 1.5 times the last price for each level | One more Click Power level, up to level 50 | The server, from its own copy of the prices |
| Rebirth | 1,000 coins, then 3 times the last price for each rebirth | Coins back to 0 and Click Power back to level 1, plus a permanent +1 to the multiplier | The server |
Every number lives on the server. The player's device draws the buttons and asks for things ("I clicked", "I want an upgrade"); the server decides what that is worth and whether it can happen. Clicker scripts that get exploited often add coins in a LocalScript, or let the client send how many coins to add.
| Piece | Where it lives | What it does |
|---|---|---|
SimulatorConfig ModuleScript | ReplicatedStorage | Prices, limits and the formulas for a click's value, the upgrade price and the rebirth price. Both sides read it. |
SimulatorService Script | ServerScriptService | Holds each player's numbers, handles clicks, upgrades and rebirths, and fills leaderstats. |
SimulatorRemotes Folder | ReplicatedStorage (SimulatorService creates it) | The Click RemoteEvent and the BuyUpgrade and Rebirth RemoteFunctions. |
SimulatorClient LocalScript | StarterPlayerScripts | Builds the buttons, sends clicks and requests, and shows the server's numbers. |
PlayerData ModuleScript (optional) | ServerScriptService | Saves progress between visits. It comes from the DataStore tutorial and is only needed for Step 4. |
ReplicatedStorage
SimulatorConfig ModuleScript new
SimulatorRemotes Folder new
Click RemoteEvent new
BuyUpgrade RemoteFunction new
Rebirth RemoteFunction new
ServerScriptService
SimulatorService Script new
PlayerData ModuleScript
StarterPlayer
StarterPlayerScripts
SimulatorClient LocalScript new
Step 1: Put the prices and formulas in a module
Every number you might want to tune goes in one ModuleScript: what a click is worth, what each upgrade and rebirth costs, and the click limit. The server uses it to decide, and the LocalScript uses it to show prices on the buttons, so a change is one edit in one place.
- In the Explorer, hover over ReplicatedStorage, click the + button and insert a ModuleScript.
- Rename it
SimulatorConfig(names are case-sensitive) and replace its contents with the code below.
-- Every number in the core loop, and the formulas both sides use. The client only
-- uses these to show prices; the server uses its own copy to decide.
local SimulatorConfig = {}
SimulatorConfig.UPGRADE_BASE_COST = 10 -- price of Click Power level 2
SimulatorConfig.UPGRADE_COST_GROWTH = 1.5 -- each level costs 1.5 times the last
SimulatorConfig.MAX_POWER_LEVEL = 50
SimulatorConfig.REBIRTH_BASE_COST = 1000 -- coins needed for the first rebirth
SimulatorConfig.REBIRTH_COST_GROWTH = 3 -- each rebirth costs 3 times the last
SimulatorConfig.MAX_CLICKS_PER_SECOND = 15 -- clicks past this in one second are ignored
-- Coins for one click: the Click Power level, multiplied by 1 + the number of rebirths
function SimulatorConfig.clickValue(powerLevel: number, rebirths: number): number
return powerLevel * (rebirths + 1)
end
-- Price of the next Click Power level
function SimulatorConfig.upgradeCost(powerLevel: number): number
return math.floor(SimulatorConfig.UPGRADE_BASE_COST * SimulatorConfig.UPGRADE_COST_GROWTH ^ (powerLevel - 1))
end
-- Coins needed for the next rebirth
function SimulatorConfig.rebirthCost(rebirths: number): number
return SimulatorConfig.REBIRTH_BASE_COST * SimulatorConfig.REBIRTH_COST_GROWTH ^ rebirths
end
return SimulatorConfig
Players can read a ModuleScript in ReplicatedStorage, so these prices are public. That is fine: they are on the buttons anyway. The client and the server each run their own copy of the module, so a player who edits the numbers on their device changes nothing on the server.
| Click Power level | Price of the next level | Coins a click, no rebirths | Coins a click, 1 rebirth |
|---|---|---|---|
| 1 | 10 | 1 | 2 |
| 2 | 15 | 2 | 4 |
| 3 | 22 | 3 | 6 |
| 4 | 33 | 4 | 8 |
| 5 | 50 | 5 | 10 |
Rebirths cost 1,000 coins, then 3,000, then 9,000, because rebirthCost multiplies the price by 3 each time. After one rebirth every click is worth twice as much, after two rebirths three times as much, and so on.
Tuning the growth
The two _GROWTH numbers set how fast prices climb. At 1.5, each upgrade costs half as much again as the one before, while each level adds only one coin a click (before the rebirth multiplier), so every new level takes longer to pay for itself. That slowdown is what makes a rebirth worth taking. Raise the growth for a longer climb, lower it for a faster one, and keep REBIRTH_BASE_COST high enough that players buy a few upgrades before their first rebirth.
Step 2: Add the server script
This Script owns every number. It creates the remotes, gives each player a leaderstats folder with Coins and Rebirths, and answers the three requests. Hover over ServerScriptService, insert a Script, rename it SimulatorService and replace its contents with this:
-- Owns every number: clicks earn coins here, upgrades and rebirths are bought here,
-- and leaderstats only display the result.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
-- false: everyone starts fresh each visit. true: load and save with the PlayerData module
-- from the DataStore tutorial (add PowerLevel = 1 and Rebirths = 0 to its DEFAULTS first).
local SAVE_PROGRESS = false
type Config = {
MAX_POWER_LEVEL: number,
MAX_CLICKS_PER_SECOND: number,
clickValue: (powerLevel: number, rebirths: number) -> number,
upgradeCost: (powerLevel: number) -> number,
rebirthCost: (rebirths: number) -> number,
}
local Config = require(ReplicatedStorage:WaitForChild("SimulatorConfig")) :: Config
type Stats = { Coins: number, PowerLevel: number, Rebirths: number }
local remotes = Instance.new("Folder")
remotes.Name = "SimulatorRemotes"
local clickEvent = Instance.new("RemoteEvent")
clickEvent.Name = "Click"
clickEvent.Parent = remotes
local buyUpgrade = Instance.new("RemoteFunction")
buyUpgrade.Name = "BuyUpgrade"
buyUpgrade.Parent = remotes
local rebirthRemote = Instance.new("RemoteFunction")
rebirthRemote.Name = "Rebirth"
rebirthRemote.Parent = remotes
remotes.Parent = ReplicatedStorage
local playerStats: { [Player]: Stats } = {}
local windowStart: { [Player]: number } = {} -- when this player's current one-second window began
local clickCount: { [Player]: number } = {} -- clicks counted in that window
-- Copies the server's numbers to what players see: leaderstats and one attribute
local function refresh(player: Player)
local data = playerStats[player]
local leaderstats = player:FindFirstChild("leaderstats")
if not (data and leaderstats) then
return
end
local coins = leaderstats:FindFirstChild("Coins")
local rebirths = leaderstats:FindFirstChild("Rebirths")
if coins and coins:IsA("IntValue") then
coins.Value = data.Coins
end
if rebirths and rebirths:IsA("IntValue") then
rebirths.Value = data.Rebirths
end
player:SetAttribute("PowerLevel", data.PowerLevel)
end
-- Starts a player with the data passed in (fresh numbers, or a saved table)
local function setup(player: Player, data: Stats)
playerStats[player] = data
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
for _, name in { "Coins", "Rebirths" } do
local value = Instance.new("IntValue")
value.Name = name
value.Parent = leaderstats
end
leaderstats.Parent = player
refresh(player)
end
-- A click carries no numbers: the server works out what it is worth
clickEvent.OnServerEvent:Connect(function(player: Player)
local data = playerStats[player]
if not data then
return
end
local now = os.clock()
local start = windowStart[player]
if not start or now - start >= 1 then
windowStart[player] = now
clickCount[player] = 0
end
local count = clickCount[player] or 0
if count >= Config.MAX_CLICKS_PER_SECOND then
return -- an auto-clicker or a script firing the remote in a loop
end
clickCount[player] = count + 1
data.Coins += Config.clickValue(data.PowerLevel, data.Rebirths)
refresh(player)
end)
buyUpgrade.OnServerInvoke = function(player: Player): (boolean, string)
local data = playerStats[player]
if not data then
return false, "Your stats have not loaded yet."
end
if data.PowerLevel >= Config.MAX_POWER_LEVEL then
return false, "Click Power is maxed out."
end
local cost = Config.upgradeCost(data.PowerLevel)
if data.Coins < cost then
return false, `You need {cost - data.Coins} more coins.`
end
data.Coins -= cost
data.PowerLevel += 1
refresh(player)
return true, `Click Power is now level {data.PowerLevel}.`
end
rebirthRemote.OnServerInvoke = function(player: Player): (boolean, string)
local data = playerStats[player]
if not data then
return false, "Your stats have not loaded yet."
end
local cost = Config.rebirthCost(data.Rebirths)
if data.Coins < cost then
return false, `You need {cost - data.Coins} more coins to rebirth.`
end
-- A rebirth trades coins and upgrades for a permanent multiplier
data.Coins = 0
data.PowerLevel = 1
data.Rebirths += 1
refresh(player)
return true, `Rebirth {data.Rebirths}! Every click is now worth x{data.Rebirths + 1}.`
end
-- Starting numbers: fresh ones, or the table the PlayerData module loads and saves
if SAVE_PROGRESS then
type PlayerDataModule = {
Loaded: RBXScriptSignal,
Get: (player: Player) -> any,
}
local PlayerData = require(ServerScriptService:WaitForChild("PlayerData")) :: PlayerDataModule
local function onLoaded(player: Player)
local data = PlayerData.Get(player)
if not data or playerStats[player] then
return
end
if type(data.Coins) ~= "number" or type(data.PowerLevel) ~= "number" or type(data.Rebirths) ~= "number" then
warn("SimulatorService: add PowerLevel = 1 and Rebirths = 0 to DEFAULTS in the PlayerData module")
return
end
setup(player, data) -- the same table PlayerData saves, so every change is saved
end
PlayerData.Loaded:Connect(onLoaded)
for _, player in Players:GetPlayers() do
onLoaded(player) -- anyone whose data loaded before this script started
end
else
local function onPlayerAdded(player: Player)
setup(player, { Coins = 0, PowerLevel = 1, Rebirths = 0 })
end
Players.PlayerAdded:Connect(onPlayerAdded)
for _, player in Players:GetPlayers() do
onPlayerAdded(player)
end
end
Players.PlayerRemoving:Connect(function(player: Player)
playerStats[player] = nil
windowStart[player] = nil
clickCount[player] = nil
end)
Click Power reaches the player as a PowerLevel attribute on their Player object, not as a third leaderstats value, so the player list shows only Coins and Rebirths. Attributes the server sets replicate to the player's device; a change a player makes on their own device stays there.
Clicks and the rate limit
The Click remote carries no arguments. On the server, the first parameter of an OnServerEvent handler is always the player who fired it, so the server knows who clicked, looks up their Click Power level and Rebirths, and adds clickValue to their Coins. There is nothing in the request for a cheater to change.
Roblox's security guide says never to rely only on a rate limit in the client, and it lists awarding currency among the actions to limit. The server counts each player's clicks in one-second windows: the first click starts a window, and once 15 clicks are counted, any more are ignored until a second has passed since the window began. A script that fires the remote in a loop gets no more than 15 clicks counted in each window. To allow faster or slower clicking, change MAX_CLICKS_PER_SECOND in SimulatorConfig.
Upgrades and rebirths
BuyUpgrade and Rebirth are RemoteFunctions, so the button can wait for the answer and show it. Each handler reads the price from the server's copy of SimulatorConfig, checks the player's Coins, and only then changes anything. It returns true or false and a message such as "You need 5 more coins.", which the LocalScript shows in a status line.
Nothing between the price check and the change waits or yields, so two requests from the same player cannot both pass the check before the first one takes the coins. A rebirth sets Coins to 0 and Click Power to level 1 and adds 1 to Rebirths, which the click formula turns into a multiplier of Rebirths + 1.
The SAVE_PROGRESS switch
With SAVE_PROGRESS = false, every player starts at 0 coins each visit, which is what you want while you build. Step 4 turns it on. The script then waits for the PlayerData module to load each player and works directly on the table that module saves, so every coin and rebirth is saved without any extra code.
Step 3: Build the buttons
This LocalScript builds the whole screen in code: the coin count at the top, a large Click button in the middle, Upgrade and Rebirth buttons at the bottom, and a status line for the server's answers. It never changes a number itself.
- In the Explorer, expand StarterPlayer, hover over StarterPlayerScripts and insert a LocalScript named
SimulatorClient. - Replace its contents with the code below and press Play.
-- Builds the simulator buttons and shows the server's numbers. It never changes them.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
type Config = {
MAX_POWER_LEVEL: number,
clickValue: (powerLevel: number, rebirths: number) -> number,
upgradeCost: (powerLevel: number) -> number,
rebirthCost: (rebirths: number) -> number,
}
local Config = require(ReplicatedStorage:WaitForChild("SimulatorConfig")) :: Config
local remotes = ReplicatedStorage:WaitForChild("SimulatorRemotes")
local clickEvent = remotes:WaitForChild("Click") :: RemoteEvent
local buyUpgrade = remotes:WaitForChild("BuyUpgrade") :: RemoteFunction
local rebirthRemote = remotes:WaitForChild("Rebirth") :: RemoteFunction
local player = Players.LocalPlayer
local leaderstats = player:WaitForChild("leaderstats")
local coins = leaderstats:WaitForChild("Coins") :: IntValue
local rebirths = leaderstats:WaitForChild("Rebirths") :: IntValue
local BOLD = Font.fromEnum(Enum.Font.BuilderSansBold)
-- 1234 -> "1.2K", 5600000 -> "5.6M"
local UNITS = {
{ size = 1e12, suffix = "T" },
{ size = 1e9, suffix = "B" },
{ size = 1e6, suffix = "M" },
{ size = 1e3, suffix = "K" },
}
local function short(n: number): string
for _, unit in UNITS do
if n >= unit.size then
return string.format("%.1f%s", math.floor(n / unit.size * 10) / 10, unit.suffix)
end
end
return tostring(n)
end
local function newButton(parent: Instance, text: string, position: UDim2, size: UDim2, color: Color3): TextButton
local button = Instance.new("TextButton")
button.AnchorPoint = Vector2.new(0.5, 0.5)
button.Position = position
button.Size = size
button.BackgroundColor3 = color
button.TextColor3 = Color3.new(1, 1, 1)
button.FontFace = BOLD
button.TextScaled = true
button.Text = text
button.Parent = parent
local corner = Instance.new("UICorner")
corner.CornerRadius = UDim.new(0, 12)
corner.Parent = button
return button
end
local gui = Instance.new("ScreenGui")
gui.Name = "SimulatorGui"
gui.ResetOnSpawn = false
local coinsLabel = Instance.new("TextLabel")
coinsLabel.AnchorPoint = Vector2.new(0.5, 0)
coinsLabel.Position = UDim2.new(0.5, 0, 0, 12)
coinsLabel.Size = UDim2.fromOffset(320, 48)
coinsLabel.BackgroundTransparency = 1
coinsLabel.TextColor3 = Color3.fromRGB(255, 214, 90)
coinsLabel.FontFace = BOLD
coinsLabel.TextScaled = true
coinsLabel.Parent = gui
local clickButton = newButton(gui, "Click!", UDim2.new(0.5, 0, 0.5, -30), UDim2.fromScale(0.38, 0.38), Color3.fromRGB(124, 77, 255))
-- Keep the Click button square, sized to the screen and never bigger than 200 by 200,
-- so on a phone it fits between the coin count and the buttons at the bottom
local square = Instance.new("UIAspectRatioConstraint")
square.Parent = clickButton
local maxSize = Instance.new("UISizeConstraint")
maxSize.MaxSize = Vector2.new(200, 200)
maxSize.Parent = clickButton
local upgradeButton = newButton(gui, "", UDim2.new(0.5, -110, 1, -60), UDim2.fromOffset(200, 56), Color3.fromRGB(40, 150, 90))
local rebirthButton = newButton(gui, "", UDim2.new(0.5, 110, 1, -60), UDim2.fromOffset(200, 56), Color3.fromRGB(200, 90, 40))
local status = Instance.new("TextLabel")
status.AnchorPoint = Vector2.new(0.5, 1)
status.Position = UDim2.new(0.5, 0, 1, -96)
status.Size = UDim2.fromOffset(480, 28)
status.BackgroundTransparency = 1
status.TextColor3 = Color3.new(1, 1, 1)
status.FontFace = BOLD
status.TextSize = 20
status.Text = ""
status.Parent = gui
gui.Parent = player:WaitForChild("PlayerGui")
local function powerLevel(): number
local level = player:GetAttribute("PowerLevel")
return if type(level) == "number" then level else 1
end
local function update()
coinsLabel.Text = `{short(coins.Value)} coins`
local level = powerLevel()
upgradeButton.Text = if level >= Config.MAX_POWER_LEVEL
then "Click Power maxed"
else `Upgrade: {short(Config.upgradeCost(level))}`
rebirthButton.Text = `Rebirth: {short(Config.rebirthCost(rebirths.Value))}`
end
coins.Changed:Connect(update)
rebirths.Changed:Connect(update)
player:GetAttributeChangedSignal("PowerLevel"):Connect(update)
update()
-- A "+N" that floats up from the button. The number is a guess for show only;
-- the server decides what the click is really worth.
local function popUp()
local pop = Instance.new("TextLabel")
pop.AnchorPoint = Vector2.new(0.5, 0.5)
pop.Position = UDim2.fromScale(0.5 + (math.random() - 0.5) * 0.1, 0.42)
pop.Size = UDim2.fromOffset(120, 40)
pop.BackgroundTransparency = 1
pop.TextColor3 = Color3.fromRGB(255, 214, 90)
pop.FontFace = BOLD
pop.TextScaled = true
pop.Text = `+{short(Config.clickValue(powerLevel(), rebirths.Value))}`
pop.Parent = gui
local tween = TweenService:Create(pop, TweenInfo.new(0.6), {
Position = pop.Position - UDim2.fromOffset(0, 60),
TextTransparency = 1,
})
tween.Completed:Once(function()
pop:Destroy()
end)
tween:Play()
end
clickButton.Activated:Connect(function()
clickEvent:FireServer()
popUp()
end)
local function ask(remote: RemoteFunction)
local ok, success, message = pcall(function()
return remote:InvokeServer()
end)
status.Text = if ok and type(message) == "string" then message else "No answer from the server. Try again."
status.TextColor3 = if ok and success == true then Color3.fromRGB(120, 220, 150) else Color3.new(1, 1, 1)
end
upgradeButton.Activated:Connect(function()
ask(buyUpgrade)
end)
rebirthButton.Activated:Connect(function()
ask(rebirthRemote)
end)
The buttons use Activated, which Roblox fires for a left click on desktop, a tap on mobile, and A (or cross) on a controller in UI navigation mode.
On a controller, A only presses a button that is selected. With GuiService.AutoSelectGuiEnabled on, the gamepad's Select button selects your on-screen buttons; players can then move between them and press A. If you would rather a controller button clicked straight away, bind one with ContextActionService and fire the same Click remote from it.
Four details in the script:
- The Click button scales with the screen. Its size is a fraction of the screen, a
UIAspectRatioConstraintkeeps it square and aUISizeConstraintcaps it at 200 by 200 pixels. On a monitor it is 200 pixels across; on a phone it shrinks to fit between the coin count and the buttons at the bottom. Keep it clear of them if you move things around: Rebirth has no confirmation, so a stray tap there can reset the player's coins and upgrades. - The "+N" pop is only for show. It uses the client's copy of the formula. If the server ignores a click because of the rate limit, the pop still appears but the coins do not change.
short()keeps big numbers readable. 1,234 shows as 1.2K and 5,600,000 as 5.6M. It rounds down, so the coin count never shows more than you have. Add entries toUNITSfor numbers past T (trillions).- The screen updates from events. It listens to the Coins and Rebirths values and the PowerLevel attribute, so the button prices are right however the numbers change.
The ScreenGui has ResetOnSpawn set to false, so it stays when the character respawns. That matters here: a LocalScript in StarterPlayerScripts is copied to the player once, when they join, so it would not build the screen again.
Step 4: Save progress with PlayerData
So far everything resets when a player leaves. Rather than add a second saving script, the simulator plugs into the PlayerData module from the DataStore tutorial. That module already retries failed requests, autosaves every 180 seconds, saves when a player leaves and when the server shuts down, and never saves a player whose data failed to load.
- Follow Step 1 of the DataStore tutorial to add the
PlayerDataModuleScript to ServerScriptService. To test saving in Studio, turn on Studio access to API services in a test copy of your game, as its first section explains. SimulatorService requires the module itself, so you do not need the tutorial'sMainscript for this. - In PlayerData, add
PowerLevel = 1,andRebirths = 0,inside theDEFAULTStable, next toCoins = 0,. - In the same module, add
PowerLevel: number,andRebirths: number,to theDatatype, so strict type checking knows about the new fields. - In SimulatorService, change
local SAVE_PROGRESS = falsetolocal SAVE_PROGRESS = true.
Players who saved before you added the fields get them automatically: PlayerData's migrate function fills in any DEFAULTS field a saved table is missing, so they start at Click Power level 1 and 0 rebirths and keep their Coins. If you skip step 2, SimulatorService prints a warning saying which fields to add and does not set the player up.
Why the simulator writes into PlayerData's table
With saving on, SimulatorService does not copy the numbers anywhere. PlayerData.Get(player) returns the live table the module saves, and the simulator changes Coins, PowerLevel and Rebirths on that same table. There is one copy of each number and one script that writes to the data store. Two scripts saving the same key overwrite each other, and one of them can save older numbers over the other's.
Do not also build leaderstats in a Loaded handler, as the DataStore tutorial suggests for other games: SimulatorService already makes Coins and Rebirths from the loaded table. If a leaderboard script already makes leaderstats, delete it.
Why not save on every click
Roblox allows each server 60 + 40 × players standard data store writes a minute (the DataStore tutorial's limits table has the rest). Saving on every click at 15 clicks a second would be 900 writes a minute for one player, far over the 100 a one-player server gets. Saving the whole table every 180 seconds, when the player leaves and when the server shuts down, costs only a few writes per player, and a crash loses at most the progress since the last autosave.
If a load fails, the module lets the player play on fresh numbers but never saves them, so a bad load cannot replace real progress. The tutorial explains how the module handles that.
Why the server decides every number
Exploiters control their own device. Roblox's security guide says they can change anything in their local copy of the game, and fire or invoke any RemoteEvent or RemoteFunction as often as they like with any arguments. A simulator's economy is only as safe as its server script.
| An exploiter tries to | If the client decided | What SimulatorService does |
|---|---|---|
| Fire Click in a loop as fast as possible | Unlimited coins | Counts at most 15 clicks in each one-second window and ignores the rest. |
| Send a coin amount with the click | They choose their own reward | Ignores any arguments. A click's value comes from the server's numbers. |
| Buy an upgrade or a rebirth without enough coins | Free upgrades | Checks the price against the server's Coins and replies "You need N more coins." |
| Edit Coins in leaderstats on their own screen | Spending coins they do not have | Never reads leaderstats. It only writes to it, from its own table. |
| Set the PowerLevel attribute on their Player | A bigger click | Never reads the attribute, and the change stays on their device. |
| Invoke Rebirth twice at the same moment | Two rebirths for the price of one | Checks the price again on each call, and nothing yields between the check and the change. |
| Change the prices in their copy of SimulatorConfig | Cheaper upgrades | Uses its own copy of the module. |
The rule carries over to every remote in a simulator: send what the player wants to do, never what it is worth. The RemoteEvents guide covers validating remote arguments in more detail.
Patterns to avoid in copied clicker scripts
| Pattern | The problem | This guide |
|---|---|---|
A LocalScript adds coins with leaderstats.Coins.Value += 1 | The change stays on that one device, and exploiters control their device | The server adds coins |
clickEvent:FireServer(amount) | The client picks its own reward | Click sends no arguments |
| No rate limit on the server | Roblox's docs say never to rely only on a limit in the client | At most 15 clicks in each one-second window |
SetAsync on every click or upgrade | Runs through the per-server write budget | PlayerData saves every 180 seconds, on leave and on shutdown. Save at once with PlayerData.SaveNow only after something that must not be lost, such as a Robux purchase. |
| A second data store script just for the simulator | Two scripts saving one key overwrite each other | Writes into PlayerData's table |
while wait(1) do loops for passive income | Roblox marks the global wait as deprecated, superseded by task.wait | task.wait (see Extending it) |
MouseButton1Click on buttons | Documented for mouse clicks | Activated, for mouse, touch and controller |
Step 5: Test it
Run steps 1 to 8 with SAVE_PROGRESS = false in SimulatorService, so every playtest starts from fresh numbers. If you already turned saving on in Step 4, set it to false for now and back to true for step 9.
- Press Play. The top of the screen reads 0 coins, the green button reads Upgrade: 10 and the orange one reads Rebirth: 1.0K.
- Click the purple Click! button ten times. Each click shows a gold +1, and Coins in the player list goes up by 1.
- Press Upgrade. Coins go back to 0, the status line turns green with "Click Power is now level 2." and the button now reads Upgrade: 15. From now on each click gives 2 coins.
- Before clicking again, press Rebirth. The status line says "You need 1000 more coins to rebirth." and nothing changes.
- To try a rebirth without 1,000 clicks, stop the test, change
Coins = 0toCoins = 1000in the line near the bottom of SimulatorService that sets up fresh players, and press Play. Press Rebirth: Coins go to 0, Rebirths in the player list goes to 1, the status line says "Rebirth 1! Every click is now worth x2." and the button now reads Rebirth: 3.0K. Each click now gives 2 coins at Click Power level 1. Change the line back to 0 afterwards. - To see the rate limit, set
MAX_CLICKS_PER_SECONDto 2 in SimulatorConfig and click quickly. The pops keep appearing, but Coins rise by at most 2 clicks' worth in each second. Set it back to 15. - Choose a phone in Studio's Device Simulator (a Studio beta; the older Device Emulator works too) and press Play. Check that the Click button overlaps neither the coin count nor the Upgrade and Rebirth buttons, and that the status line shows clear of it when you press Upgrade.
- Connect a controller, or open Studio's Controller Emulator (Controller, on the Emulation tab) during the playtest, press Select, and check that you can reach the three buttons and press them with A.
- With saving on (Step 4) and
SAVE_PROGRESS = trueagain, earn some coins, press Stop, then press Play again. Your Coins, Click Power level and Rebirths come back.
Extending it: pets, zones, auto-click and leaderboards
Pets as multipliers
A common design makes each pet a multiplier. Keep the list of owned pet ids in the player's saved table, and have the server multiply clickValue by the bonus of each equipped pet. Hatch eggs on the server: the client asks to open an egg, and the server checks and takes the price, then picks the pet with a weighted random roll. Never let the client roll the egg or name the pet it got. The inventory guide shows how to hold, save and display owned items, and pets work the same way.
Zones unlocked by rebirths
A zone is an area of the map with better rewards behind a barrier. Gate the reward, not only the door: whatever pays out inside the zone should check the player's Rebirths in the server's table before it pays. A door that a LocalScript opens for players who qualify is fine for looks, but exploiters can change parts and move their character on their own device.
Auto-click as a game pass
An auto-clicker sold as a game pass should run on the server. Move the body of the Click handler into a function such as addClick(player), connect the remote to it, and for pass owners call it from a server loop with task.wait(0.5), not the deprecated wait. Auto-clicks then count towards the same per-second limit as manual clicks. Check ownership on the server with MarketplaceService:UserOwnsGamePassAsync, as the game pass guide does for its perks. A 2x coins pass works the same way: multiply clickValue on the server for owners.
A global top 10 of rebirths
leaderstats only compares players in the same server. To rank every player's Rebirths, write them to an ordered data store and read the top entries back, as in Step 4 of the leaderboard guide. Ordered data stores allow fewer writes than standard ones, 30 + 5 × players a minute per server, so write when a player leaves and on a timer.
Very big numbers
Luau numbers are doubles, which keep about 15 significant digits. An IntValue holds whole numbers up to about 9.2 quintillion, but Roblox notes that values above 2^53 (about 9 quadrillion) lose precision in scripts. With these settings the rebirth price passes that point at the 29th rebirth. If your game goes further, switch the leaderstats values to NumberValue (change IntValue in both scripts), which holds far larger numbers at the same 15 or so digits of precision, and add suffixes to short().
Common errors and fixes
Open the Output window (Window menu, then Output) before you press Play. Most problems print a warning or an error there, with the script name and the line.
Infinite yield possible on 'ReplicatedStorage:WaitForChild("SimulatorRemotes")'
The LocalScript is waiting for the remotes, which SimulatorService creates when it starts. Check that SimulatorService is a Script, not a LocalScript, that it is inside ServerScriptService, and that Output shows no error from it. The same warning for SimulatorConfig means the module is missing from ReplicatedStorage or named differently.
The buttons never appear
The LocalScript waits for your leaderstats before it builds the screen. If Output shows Infinite yield possible on leaderstats, SimulatorService has not set you up yet. With SAVE_PROGRESS = true it waits for PlayerData, so check that PlayerData is in ServerScriptService and loads without errors.
Output says "SimulatorService: add PowerLevel = 1 and Rebirths = 0 to DEFAULTS in the PlayerData module"
Saving is on, but the PlayerData module does not have the new fields yet. Add them to its DEFAULTS as in Step 4 and press Play again.
The "+1" pops appear but my coins do not go up
The server is not counting the clicks. Either SimulatorService hit an error (look for red text from it in Output), or clicks arrive faster than MAX_CLICKS_PER_SECOND allows. If players reach the limit in normal play, raise it in SimulatorConfig.
The status line says "No answer from the server. Try again."
The call to BuyUpgrade or Rebirth failed instead of returning an answer. The usual cause is an error inside SimulatorService while it handled the request, so look for a red error from it in Output: it names the line.
The coin count on screen and the player list show different numbers
Another script is also making a leaderstats folder, so each player has two, and the LocalScript may be reading the other one. Delete the extra script: SimulatorService makes Coins and Rebirths itself.
My coins reset every time I rejoin
Saving is off. Follow Step 4. If it is on and coins still reset in Studio, check that Studio access to API services is turned on in your test copy, as the DataStore tutorial explains.
Build it with RoCode
If you would rather not paste this in by hand, RoCode can build it in the place you have open. It is an independent AI coding agent, not made by Roblox: you describe the system in its web chat, and its Roblox Studio plugin creates the scripts in your Explorer, so you are not copying code out of a chat window.
Make a clicker simulator: a big Click button that gives coins equal to my Click Power level times (rebirths + 1), an Upgrade button that costs 10 coins and grows 1.5 times each level, and a Rebirth button at 1,000 coins that resets coins and upgrades. Keep the numbers on the server and limit clicks to 15 a second. I already have a PlayerData module.
- Can read your PlayerData module and any leaderstats script first with Search Scripts and Read Script, so it adds fields to your existing data instead of writing a second save script
- Can scaffold the screen with Build UI's HUD layout (with plugin 3.9 or newer, in its simulator style), or build the buttons in a LocalScript, and writes the ModuleScript, the server Script and the LocalScript with Create Script
- Is told to compile-check each script it writes with Check Script, which compiles it in Studio without running it
- Sets an undo point before and after each batch of changes, so Ctrl+Z steps back one batch at a time
RoCode cannot create game passes or developer products, or upload pet meshes and icons: do those in Creator Hub and Studio. With plugin 3.9 or newer it can run a short playtest and read the errors, but it does not click buttons, so you test the loop yourself. A whole simulator is several prompts, one system at a time: how building one system per prompt works with RoCode. How RoCode connects to Studio.
Questions
How do I make a simulator game on Roblox without scripting?
A working simulator needs scripts: something has to add coins, check prices and save progress. You can paste the three scripts from this guide as they are and only change the numbers in SimulatorConfig.
How does a rebirth system work?
A rebirth trades your coins and upgrades for a permanent bonus. In this guide the first rebirth costs 1,000 coins; it sets Coins to 0 and Click Power to level 1 and adds 1 to Rebirths, and every click is then multiplied by Rebirths + 1. Each rebirth costs three times as much as the one before.
How do I stop auto clickers?
The server counts at most 15 clicks per player in each one-second window and ignores the rest. Below that limit a script looks the same as a fast player, so the cap is what protects your economy. You can also sell auto-click as a game pass that runs on the server.
Why do my coins reset when I rejoin?
Nothing saves them yet. Add the PlayerData module from the DataStore tutorial, add PowerLevel and Rebirths to its DEFAULTS, and set SAVE_PROGRESS = true in SimulatorService, as in Step 4.
How do I add pets to my simulator?
Make each pet a multiplier that the server applies to clicks. Hatch eggs on the server with a weighted random pick, save the owned pet ids with the rest of the player's data, and show them with an inventory GUI like the one in the inventory guide.
Should players click a part or a button?
Either works. A screen button suits phones, and a part with a ClickDetector suits a game set in the world. Roblox's docs say the server does no checks at all on ClickDetector events, so rate limit those clicks on the server the same way.
Sources and further reading
- Roblox Creator Docs: Securing the client-server boundary. rate limiting, the in-game shop example and ClickDetector events
- Roblox Creator Docs: Security and cheat mitigation tactics. what an exploiter controls on their own device
- Roblox Creator Docs: Remote events and callbacks. FireServer, the player parameter, InvokeServer and OnServerInvoke
- Roblox Creator Docs: Leaderboards. leaderstats and ordered data stores
- Roblox Creator Docs: Data store error codes and limits. write limits per server for standard and ordered data stores
- Roblox Engine API: GuiButton. Activated on desktop, mobile and console
- Roblox Engine API: GuiService. AutoSelectGuiEnabled and selecting GUI objects with a gamepad
- Roblox Engine API: IntValue. the 64-bit range and the loss of precision above 2^53
- Roblox Creator Docs: Size modifiers and constraints. UIAspectRatioConstraint and UISizeConstraint
- Roblox Creator Docs: Studio testing modes. Device Simulator and Controller Emulator
How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 13 October 2026). A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.