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.
-- 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
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
How the fishing loop works
- The player clicks with the rod equipped.
Tool.Activatedfires in the rod's LocalScript, which firesCastLineto the server. - 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".
- The server waits a random 2 to 6 seconds, checks the rod is still held, rolls a fish and adds to Fish and Coins.
- The server tells the client what was caught, and the rod's LocalScript shows it on screen.
| Fish | Weight | Chance | Coins |
|---|---|---|---|
| Sardine | 50 | 50% | 5 |
| Salmon | 30 | 30% | 15 |
| Pufferfish | 15 | 15% | 40 |
| Golden Koi | 5 | 5% | 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
- 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. - Insert a 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
- Insert a Tool into StarterPack and name it
FishingRod. The name matters: the server looks for it. - Inside it, insert a long thin Part named
Handle, brown, about0.3, 0.3, 5. The player holds the tool by its Handle. - Insert a LocalScript in the 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
Luckattribute and multiply the weights of rare fish by it on the server. - Zones: tag a second lake
FishingWaterwith aZoneattribute and use a different fish list there. - Save Coins and Fish with the DataStore guide.
Test it
- Press Play, equip the rod (press 1) and click far from water: "Get closer to the water".
- Walk to the lake and click: "Waiting for a bite...", then a catch and coins a few seconds later.
- Click again while waiting: nothing changes. Unequip during the wait: no fish.
- 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.
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.
- 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
- Roblox Creator Docs: In-experience tools. Handles, StarterPack and the LocalScript plus Script pattern
- Roblox Creator Docs: Tool. Activated
- Roblox Creator Docs: Random. NextNumber
- Roblox Creator Docs: Remote events and functions. FireServer and FireClient
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.