Tutorial · Rewards

How to make a fishing system in Roblox Studio

Fishing games are a calm loop players can repeat for hours: cast, wait, catch, sell, upgrade. This guide builds the core of one: a fishing rod Tool, a server that checks the player is standing by the water, a random wait for a bite, and a weighted roll that makes a Golden Koi rare. The client only asks to cast; the fish and the coins come from the server.

Updated 23 October 20266 min readIntermediateLuau

In short

Put a Tool named FishingRod in StarterPack with a LocalScript that fires a CastLine RemoteEvent on Activated. A Script in ServerScriptService checks the player holds the rod, is within a few studs of a part tagged FishingWater and is not already fishing, waits a random 2 to 6 seconds, then picks a fish with a weighted roll and adds its coins to leaderstats.

  • Roll the fish on the server. If the client picked, a cheater could catch a Golden Koi every cast.
  • A weighted roll: add up the weights, pick a random number below the total, and walk the list subtracting each weight.
  • Check again after the wait that the player still holds the rod.

Get the script

Fishing system server script

A Script in ServerScriptService: cast near water tagged FishingWater, wait 2 to 6 seconds for a bite, catch a weighted random fish and earn coins.

Script in ServerScriptService named FishingService
-- Script in ServerScriptService named FishingService.
-- The rod asks to cast; the server checks the player is by the water, waits for a
-- bite, rolls a fish and pays out. The client never chooses the fish.
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local MAX_DISTANCE = 12 -- studs from the edge of a part tagged "FishingWater"
local MIN_WAIT, MAX_WAIT = 2, 6 -- seconds before a bite

type Fish = { name: string, weight: number, coins: number }
-- weight is the chance: higher is more common. These add up to 100, so they read as percent.
local FISH: { Fish } = {
	{ name = "Sardine", weight = 50, coins = 5 },
	{ name = "Salmon", weight = 30, coins = 15 },
	{ name = "Pufferfish", weight = 15, coins = 40 },
	{ name = "Golden Koi", weight = 5, coins = 150 },
}

local castEvent = Instance.new("RemoteEvent")
castEvent.Name = "CastLine"
castEvent.Parent = ReplicatedStorage
local resultEvent = Instance.new("RemoteEvent")
resultEvent.Name = "FishingResult"
resultEvent.Parent = ReplicatedStorage

local fishing: { [Player]: boolean } = {}
local random = Random.new()

local function rollFish(): Fish
	local total = 0
	for _, fish in FISH do
		total += fish.weight
	end
	local roll = random:NextNumber(0, total)
	for _, fish in FISH do
		roll -= fish.weight
		if roll <= 0 then
			return fish
		end
	end
	return FISH[#FISH]
end

local function nearWater(position: Vector3): boolean
	for _, water in CollectionService:GetTagged("FishingWater") do
		if water:IsA("BasePart") then
			-- Distance from the position to the water's box, in the water's own space
			local offset = water.CFrame:PointToObjectSpace(position)
			local half = water.Size / 2
			local outside = Vector3.new(
				math.max(math.abs(offset.X) - half.X, 0),
				math.max(math.abs(offset.Y) - half.Y, 0),
				math.max(math.abs(offset.Z) - half.Z, 0)
			)
			if outside.Magnitude <= MAX_DISTANCE then
				return true
			end
		end
	end
	return false
end

local function addStat(player: Player, statName: string, amount: number)
	local leaderstats = player:FindFirstChild("leaderstats")
	local stat = leaderstats and leaderstats:FindFirstChild(statName)
	if stat and stat:IsA("IntValue") then
		stat.Value += amount
	end
end

castEvent.OnServerEvent:Connect(function(player: Player)
	local character = player.Character
	local rod = character and character:FindFirstChild("FishingRod")
	local humanoid = character and character:FindFirstChildOfClass("Humanoid")
	local root = humanoid and humanoid.RootPart
	-- Must be alive, holding the rod, by the water and not already fishing
	if not (rod and rod:IsA("Tool") and humanoid and root) or humanoid.Health <= 0 or fishing[player] then
		return
	end
	if not nearWater(root.Position) then
		resultEvent:FireClient(player, "far")
		return
	end
	fishing[player] = true
	resultEvent:FireClient(player, "cast")
	task.wait(random:NextNumber(MIN_WAIT, MAX_WAIT))
	-- Still holding the rod and still there after the wait?
	if player.Parent and rod.Parent == player.Character and fishing[player] then
		local fish = rollFish()
		addStat(player, "Fish", 1)
		addStat(player, "Coins", fish.coins)
		resultEvent:FireClient(player, "caught", fish.name, fish.coins)
	end
	fishing[player] = nil
end)

Players.PlayerAdded:Connect(function(player: Player)
	local leaderstats = Instance.new("Folder")
	leaderstats.Name = "leaderstats"
	for _, statName in { "Coins", "Fish" } do
		local stat = Instance.new("IntValue")
		stat.Name = statName
		stat.Parent = leaderstats
	end
	leaderstats.Parent = player
end)

Players.PlayerRemoving:Connect(function(player: Player)
	fishing[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 server script, the rod and the water
ServerScriptService
  FishingService Script new
StarterPack
  FishingRod Tool new
    Handle Part
    RodClient LocalScript new
Workspace
  Lake Part (tag: FishingWater)

Settings you can change

MAX_DISTANCEIn the script
How close to the edge of the water, in studs, a player must stand to cast. Default: 12.
FISHIn the script
The fish list: name, weight (chance) and coins. Add rows for more fish.

The rod needs the RodClient LocalScript from Step 2. Mark your water by tagging its part FishingWater.

What a cheater can and cannot doThe server picks the fish, checks the rod, the distance to water and that only one cast runs at a time. A cheater can cast as often as the bite wait allows, like any player, but cannot choose what they catch.

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 fishing to my game: a FishingRod tool in StarterPack whose LocalScript fires a CastLine RemoteEvent on click, and a server Script that checks the player holds the rod and stands within 12 studs of a part tagged FishingWater, waits 2 to 6 seconds, rolls a weighted random fish from a list, and adds a Fish count and the fish's coins to leaderstats.

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

How the fishing loop works

  1. The player clicks with the rod equipped. Tool.Activated fires in the rod's LocalScript, which fires CastLine to the server.
  2. The server checks the rod is in the player's character, they are alive, near water and not already fishing. It tells the client "cast".
  3. The server waits a random 2 to 6 seconds, checks the rod is still held, rolls a fish and adds to Fish and Coins.
  4. The server tells the client what was caught, and the rod's LocalScript shows it on screen.
The fish list and its chances
FishWeightChanceCoins
Sardine5050%5
Salmon3030%15
Pufferfish1515%40
Golden Koi55%150

A fish's chance is its weight divided by the total of all weights. These add up to 100, so they read as percentages, but they do not have to: add a fish with weight 1 and every chance shifts a little.

Step 1: The server script

  1. Make your water: a large blue part, Anchored, CanCollide off if players should wade in, tagged FishingWater. Terrain water cannot be tagged, so place an invisible tagged part over a terrain lake.
  2. Insert a Script in ServerScriptService named FishingService:
Script in ServerScriptService named FishingService
-- Script in ServerScriptService named FishingService.
-- The rod asks to cast; the server checks the player is by the water, waits for a
-- bite, rolls a fish and pays out. The client never chooses the fish.
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local MAX_DISTANCE = 12 -- studs from the edge of a part tagged "FishingWater"
local MIN_WAIT, MAX_WAIT = 2, 6 -- seconds before a bite

type Fish = { name: string, weight: number, coins: number }
-- weight is the chance: higher is more common. These add up to 100, so they read as percent.
local FISH: { Fish } = {
	{ name = "Sardine", weight = 50, coins = 5 },
	{ name = "Salmon", weight = 30, coins = 15 },
	{ name = "Pufferfish", weight = 15, coins = 40 },
	{ name = "Golden Koi", weight = 5, coins = 150 },
}

local castEvent = Instance.new("RemoteEvent")
castEvent.Name = "CastLine"
castEvent.Parent = ReplicatedStorage
local resultEvent = Instance.new("RemoteEvent")
resultEvent.Name = "FishingResult"
resultEvent.Parent = ReplicatedStorage

local fishing: { [Player]: boolean } = {}
local random = Random.new()

local function rollFish(): Fish
	local total = 0
	for _, fish in FISH do
		total += fish.weight
	end
	local roll = random:NextNumber(0, total)
	for _, fish in FISH do
		roll -= fish.weight
		if roll <= 0 then
			return fish
		end
	end
	return FISH[#FISH]
end

local function nearWater(position: Vector3): boolean
	for _, water in CollectionService:GetTagged("FishingWater") do
		if water:IsA("BasePart") then
			-- Distance from the position to the water's box, in the water's own space
			local offset = water.CFrame:PointToObjectSpace(position)
			local half = water.Size / 2
			local outside = Vector3.new(
				math.max(math.abs(offset.X) - half.X, 0),
				math.max(math.abs(offset.Y) - half.Y, 0),
				math.max(math.abs(offset.Z) - half.Z, 0)
			)
			if outside.Magnitude <= MAX_DISTANCE then
				return true
			end
		end
	end
	return false
end

local function addStat(player: Player, statName: string, amount: number)
	local leaderstats = player:FindFirstChild("leaderstats")
	local stat = leaderstats and leaderstats:FindFirstChild(statName)
	if stat and stat:IsA("IntValue") then
		stat.Value += amount
	end
end

castEvent.OnServerEvent:Connect(function(player: Player)
	local character = player.Character
	local rod = character and character:FindFirstChild("FishingRod")
	local humanoid = character and character:FindFirstChildOfClass("Humanoid")
	local root = humanoid and humanoid.RootPart
	-- Must be alive, holding the rod, by the water and not already fishing
	if not (rod and rod:IsA("Tool") and humanoid and root) or humanoid.Health <= 0 or fishing[player] then
		return
	end
	if not nearWater(root.Position) then
		resultEvent:FireClient(player, "far")
		return
	end
	fishing[player] = true
	resultEvent:FireClient(player, "cast")
	task.wait(random:NextNumber(MIN_WAIT, MAX_WAIT))
	-- Still holding the rod and still there after the wait?
	if player.Parent and rod.Parent == player.Character and fishing[player] then
		local fish = rollFish()
		addStat(player, "Fish", 1)
		addStat(player, "Coins", fish.coins)
		resultEvent:FireClient(player, "caught", fish.name, fish.coins)
	end
	fishing[player] = nil
end)

Players.PlayerAdded:Connect(function(player: Player)
	local leaderstats = Instance.new("Folder")
	leaderstats.Name = "leaderstats"
	for _, statName in { "Coins", "Fish" } do
		local stat = Instance.new("IntValue")
		stat.Name = statName
		stat.Parent = leaderstats
	end
	leaderstats.Parent = player
end)

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

nearWater measures how far the player is from the water part's box, not its centre, so a long river works from any bank. rollFish is the weighted roll. The fishing table holds one cast per player, so clicking again while waiting does nothing.

Step 2: The fishing rod

  1. Insert a Tool into StarterPack and name it FishingRod. The name matters: the server looks for it.
  2. Inside it, insert a long thin Part named Handle, brown, about 0.3, 0.3, 5. The player holds the tool by its Handle.
  3. Insert a LocalScript in the Tool named RodClient:
LocalScript in the FishingRod Tool named RodClient
-- LocalScript inside the FishingRod Tool, named RodClient.
-- Click with the rod to cast. A label above the screen says what happened.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local tool = script.Parent :: Tool
local castEvent = ReplicatedStorage:WaitForChild("CastLine") :: RemoteEvent
local resultEvent = ReplicatedStorage:WaitForChild("FishingResult") :: RemoteEvent

local label = Instance.new("TextLabel")
label.AnchorPoint = Vector2.new(0.5, 0)
label.Position = UDim2.new(0.5, 0, 0, 70)
label.Size = UDim2.fromOffset(320, 40)
label.BackgroundColor3 = Color3.fromRGB(20, 40, 70)
label.BackgroundTransparency = 0.2
label.TextColor3 = Color3.new(1, 1, 1)
label.Font = Enum.Font.GothamBold
label.TextSize = 20
label.Visible = false
local gui = Instance.new("ScreenGui")
gui.Name = "FishingGui"
gui.ResetOnSpawn = false
label.Parent = gui
gui.Parent = Players.LocalPlayer:WaitForChild("PlayerGui")

local function show(text: string, seconds: number?)
	label.Text = text
	label.Visible = true
	if seconds then
		task.delay(seconds, function()
			if label.Text == text then
				label.Visible = false
			end
		end)
	end
end

tool.Activated:Connect(function()
	castEvent:FireServer()
end)

resultEvent.OnClientEvent:Connect(function(kind: string, fishName: string?, coins: number?)
	if kind == "far" then
		show("Get closer to the water", 2)
	elseif kind == "cast" then
		show("Waiting for a bite...")
	elseif kind == "caught" and fishName and coins then
		show(string.format("You caught a %s! +%d coins", fishName, coins), 3)
	end
end)

Roblox copies everything in StarterPack into each player's Backpack when they spawn, so every player gets their own rod and their own copy of this script.

Step 3: Grow it into a fishing game

  • Keep fish instead of selling them at once: add each catch to an Items folder, and sell at an NPC. The inventory guide holds and saves items.
  • Better rods: give each rod a Luck attribute and multiply the weights of rare fish by it on the server.
  • Zones: tag a second lake FishingWater with a Zone attribute and use a different fish list there.
  • Save Coins and Fish with the DataStore guide.

Test it

  1. Press Play, equip the rod (press 1) and click far from water: "Get closer to the water".
  2. Walk to the lake and click: "Waiting for a bite...", then a catch and coins a few seconds later.
  3. Click again while waiting: nothing changes. Unequip during the wait: no fish.
  4. Set the Golden Koi weight to 100 for a test and fish a few times to see its message, then put it back.

Common errors and fixes

Check the Output window on both views if casting does nothing.

Clicking with the rod does nothing

Check the Tool is named FishingRod, has a part named Handle, and the LocalScript is inside it. Without a Handle the player cannot hold the tool.

It always says get closer to the water

The water part needs the tag FishingWater. With terrain water, add an invisible tagged part over it.

Coins and Fish do not show

FishingService makes leaderstats with Coins and Fish. If another script also makes leaderstats, keep one and add the values there.

The rare fish never comes

At 5 in 100, a Golden Koi comes about once in 20 casts on average, and luck can make that longer. Raise its weight to test.

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 tool, the parts and the scripts in your Explorer.

You type

Make a fishing game: a lake, a fishing rod tool, a random wait for a bite, 6 fish from common to legendary with weighted chances, a sell NPC, and coins that save.

RoCode does
  • Builds the rod tool, the water and the scripts, and can build a themed island around the lake in one call
  • Writes the weighted fish table on the server, never on the client
  • 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, equips tools, clicks and walks, and reports PASS or FAIL. It checks only what it chose to test, so fish a few times yourself. How RoCode connects to Studio.

Questions

How do I make a fishing rod in Roblox Studio?

A Tool named FishingRod in StarterPack with a Handle part and a LocalScript that fires a RemoteEvent on Activated. The server does the fishing. Steps 1 and 2 have both scripts.

How do I make rare fish?

Give each fish a weight and pick with a weighted random roll on the server. A fish with weight 5 out of a total of 100 is caught 5% of the time.

Can I use terrain water for fishing?

Yes, with an invisible part tagged FishingWater placed over the lake, because terrain water cannot be tagged.

How do I add a bobber and a line?

On the client, create a small part where the cast lands and a Beam between two Attachments, one on the rod tip and one on the bobber. They are visual only, so the client can own them.

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.