Use this file to discover all available pages before exploring further.
Tycoons are one of Roblox’s most enduring genres because the loop is deeply satisfying at a visual level — players literally watch their base grow one building at a time. Currency flows in from droppers, gets converted at collectors, and gets spent on the next structure in a chain. Every purchase is a tangible, visible change to the world. This template provides the full server-authoritative infrastructure: plot claiming, a dropper spawning loop, collector touch detection, a button pad purchase system with prerequisite gating, and passive income tracking — all wired together with a clean init/start lifecycle to avoid circular dependencies.
Auto-assigns a tycoon plot to each player on join. Walk-on claiming via ClaimPad is also supported. Plots are released and cleaned up on player leave.
Dropper / Collector Pipeline
Dropper loops spawn physics parts on configurable intervals. Parts travel via conveyors to collectors, which award currency on touch. Per-plot part cap prevents overload.
Button Pad Purchasing
Server-side Touched detection on ButtonPads. Validates ownership, prerequisites, and currency before spawning the building model and enabling downstream pads.
Income Manager
Tracks per-player currency, multipliers, and passive income sources. Syncs to client HUD via CurrencyUpdate remote on every change.
Upgrade Tree
Linear-chain unlock dependencies enforce the intended build order. Each building lists a prerequisite field — pads remain invisible until prerequisites are met.
Prestige System
Optional reset-for-multiplier system. Destroys all buildings, zeros currency, applies a permanent +25% income multiplier per prestige level, and shows a counter on the plot.
The PlotSystem handles plot ownership, building anchor positions, and cleanup. The autoAssign function is called on PlayerAdded to immediately give every player a plot.
-- ServerScriptService/Services/PlotSystem.luafunction PlotSystem.claimPlot(player: Player, plotIndex: number): boolean -- Already owns a plot if playerPlots[player] then return false end local plot = plots[plotIndex] if not plot then return false end -- Plot already claimed if plot.owner then return false end -- Claim it plot.owner = player plot.ownerId = player.UserId playerPlots[player] = plotIndex -- Visual feedback: change claim pad color or hide it local claimPad = plot.model:FindFirstChild("ClaimPad") if claimPad then claimPad.Transparency = 1 claimPad.CanCollide = false end -- Set ownership label local billboardGui = plot.model:FindFirstChild("OwnerLabel") if billboardGui and billboardGui:IsA("BillboardGui") then local label = billboardGui:FindFirstChildOfClass("TextLabel") if label then label.Text = player.DisplayName .. "'s Tycoon" end end -- Teleport player to their plot spawn local character = player.Character if character and character.PrimaryPart then character:PivotTo(plot.spawnCFrame) end print("[PlotSystem] " .. player.Name .. " claimed Plot" .. plotIndex) return trueend
When a player steps on a pad, the server validates ownership, prerequisites, and balance in a single debounced handler before spawning the building and unlocking downstream pads.
-- ServerScriptService/Services/ButtonPadManager.luafunction ButtonPadManager._onPadTouched( plotOwner: Player, pad: BasePart, buildingId: string, hit: BasePart) -- Debounce check if purchaseDebounce[plotOwner] then return end -- Identify who touched the pad local character = hit.Parent if not character then return end local humanoid = character:FindFirstChildOfClass("Humanoid") if not humanoid or humanoid.Health <= 0 then return end local touchPlayer = Players:GetPlayerFromCharacter(character) if not touchPlayer then return end -- Only the plot owner can purchase on their plot if touchPlayer ~= plotOwner then return end -- Already purchased this building if playerBuildings[plotOwner] and playerBuildings[plotOwner][buildingId] then return end local buildingData = TycoonConfig.Buildings[buildingId] if not buildingData then return end -- Check prerequisite if buildingData.prerequisite then if not playerBuildings[plotOwner][buildingData.prerequisite] then return -- prerequisite not met end end -- Check currency local playerCurrency = IncomeManager.getCurrency(plotOwner) if playerCurrency < buildingData.cost then return end -- Set debounce purchaseDebounce[plotOwner] = true -- Deduct currency and mark purchased IncomeManager.addCurrency(plotOwner, -buildingData.cost) playerBuildings[plotOwner][buildingId] = true -- Spawn the building model ButtonPadManager._spawnBuilding(plotOwner, buildingId, buildingData, pad) -- Disable the pad pad.Transparency = 1 pad.CanCollide = false -- If this building is a dropper, register it if buildingData.incomePerDrop > 0 and buildingData.dropInterval > 0 then DropperSystem.registerDropper(plotOwner, buildingId, buildingData) end -- Reveal any pads whose prerequisite is this building ButtonPadManager._refreshPadVisibility(plotOwner) -- Release debounce after short delay task.delay(0.5, function() purchaseDebounce[plotOwner] = nil end)end
Each registered dropper runs its own task.spawn loop, spawning parts at the configured interval and storing the income value as a part attribute so the collector can award the right amount.
-- ServerScriptService/Services/DropperSystem.luafunction DropperSystem._dropperLoop( player: Player, buildingId: string, dropperInfo: any) local buildingData = dropperInfo.buildingData while dropperInfo.active do task.wait(buildingData.dropInterval) if not dropperInfo.active then break end if not player.Parent then break end -- player left -- Performance cap: don't spawn more parts if at limit local currentCount = playerPartCount[player] or 0 if currentCount >= TycoonConfig.Dropper.maxPartsPerPlot then continue end -- Find the dropper model to get spawn position local plotModel = PlotSystem.getPlotModel(player) if not plotModel then break end local buildingsFolder = plotModel:FindFirstChild("Buildings") if not buildingsFolder then continue end local dropperModel = buildingsFolder:FindFirstChild(buildingId) if not dropperModel then continue end local dropPoint = dropperModel:FindFirstChild("DropPoint", true) if not dropPoint then continue end local spawnPosition: Vector3 if dropPoint:IsA("Attachment") then spawnPosition = dropPoint.WorldPosition elseif dropPoint:IsA("BasePart") then spawnPosition = dropPoint.Position else continue end -- Spawn the dropper part local part = Instance.new("Part") part.Name = "DropperPart" part.Size = TycoonConfig.Dropper.partSize part.Color = TycoonConfig.Dropper.partColor part.Material = Enum.Material.Neon part.Position = spawnPosition part.Anchored = false part.CanCollide = true part.Parent = buildingsFolder part:SetAttribute("IncomeValue", buildingData.incomePerDrop) part:SetAttribute("OwnerUserId", player.UserId) -- Auto-destroy after lifetime Debris:AddItem(part, TycoonConfig.Dropper.partLifetime) -- Track part count playerPartCount[player] = (playerPartCount[player] or 0) + 1 part.Destroying:Connect(function() local count = playerPartCount[player] if count then playerPartCount[player] = math.max(0, count - 1) end end) endend
The dropper part problem: With 8 plots each running 3 droppers at 1-second intervals, that is 24 new physics parts per second. Without mitigation, this will tank frame rates on mobile. The template enforces a per-plot part cap (maxPartsPerPlot = 50), auto-destroys parts via Debris:AddItem after 15 seconds, and the Auto-Collect GamePass bypasses part physics entirely by ticking currency directly.
To scaffold a new tycoon game, describe your concept and the skill applies this template:
Build me a pizza restaurant tycoon. Players buy pizza ovens that drop pizza boxes,conveyors carry boxes to the register, and customers pay automatically.Add a delivery upgrade that doubles income at Tier 2.