Skip to content
ENSign in

How do I make a spin wheel in Roblox?

Pick the prize first and animate second. The server rolls a weighted number, decides which slice won, and sends that index to the client; the client spins the wheel to stop on it. Animating first and reading the angle afterwards is how a wheel ends up payable from the client.

In short

  • Weights, not equal slices: a 1-in-200 prize is a weight of 1 against 199.
  • The server picks. The spin is an animation of a decision already made.
  • Always land the wheel on the slice the server chose, never the reverse.

Every spin wheel that gets exploited was built in the wrong order: spin the wheel, see where it stopped, pay that out. Build it the other way and there is nothing to exploit — the client is playing back a result it cannot change.

Steps

1. Write the prizes as weights

A weight is "how many tickets this prize has in the hat". It reads better than percentages because adding a prize does not force you to recompute every other number.

local PRIZES = {
	{ name = "50 Coins",    weight = 400, coins = 50 },
	{ name = "150 Coins",   weight = 250, coins = 150 },
	{ name = "500 Coins",   weight = 100, coins = 500 },
	{ name = "2,000 Coins", weight = 40,  coins = 2000 },
	{ name = "Rare Pet",    weight = 9,   pet = "Rare" },
	{ name = "Mythic Pet",  weight = 1,   pet = "Mythic" },
}

local TOTAL = 0
for _, p in PRIZES do
	TOTAL += p.weight
end

2. Roll on the server

One Random object for the whole server, not a new one per spin: a fresh Random.new() seeded from the clock inside a loop can hand two players in the same tick the same prize.

local rng = Random.new()

local function roll(): number
	local ticket = rng:NextInteger(1, TOTAL)
	local running = 0
	for i, p in PRIZES do
		running += p.weight
		if ticket <= running then
			return i
		end
	end
	return #PRIZES
end

3. Pay out, then tell the client where to stop

The index travels, not the prize: the client already has the list, and sending the payout invites somebody to change it on the way back.

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local spin = Instance.new("RemoteEvent")
spin.Name = "SpinWheel"
spin.Parent = ReplicatedStorage

local cooldown: { [number]: number } = {}

spin.OnServerEvent:Connect(function(player: Player)
	-- The one rate limit that matters: the client can fire a RemoteEvent as
	-- fast as it likes, so the spin has to be cheap to refuse.
	local now = os.clock()
	if cooldown[player.UserId] and now - cooldown[player.UserId] < 3 then return end
	cooldown[player.UserId] = now

	local index = roll()
	local prize = PRIZES[index]
	if prize.coins then
		local stats = player:FindFirstChild("leaderstats")
		local coins = stats and stats:FindFirstChild("Coins")
		if coins then
			coins.Value = coins.Value + prize.coins
		end
	end
	spin:FireClient(player, index)
end)

Players.PlayerRemoving:Connect(function(player)
	cooldown[player.UserId] = nil
end)

4. Spin the wheel onto that slice

The angle is arithmetic: full turns for the show, plus the offset of the winning slice. TweenService with a long ease-out does the rest.

local TweenService = game:GetService("TweenService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local wheel = script.Parent -- an ImageLabel with the slices drawn on it
local SLICES = 6
local spin = ReplicatedStorage:WaitForChild("SpinWheel")

spin.OnClientEvent:Connect(function(index: number)
	local per = 360 / SLICES
	-- Four full turns, then stop with the winning slice at the top.
	local target = 360 * 4 + (360 - (index - 1) * per)
	local tween = TweenService:Create(
		wheel,
		TweenInfo.new(4, Enum.EasingStyle.Quint, Enum.EasingDirection.Out),
		{ Rotation = target }
	)
	tween:Play()
	tween.Completed:Wait()
	wheel.Rotation = target % 360 -- keep the number small for the next spin
end)

Where to decide the prize

Where the roll happensCan a player change itFeels instantVerdict
Server, before the animationnoyes, the spin covers the waitWhat to build
Client, server just paysyes — entirelyyesNever: the payout is a client variable
Server, read from the final angleno, but fragilenoAvoid: one rounding error pays the wrong slice
Server with a per-player cooldownnoyesThe same as the first row, plus a floor on spam

FAQ

How do I make a prize exactly 0.5%?

Give it a weight of 1 and make the others add up to 199. Percentages are the thing you print on the UI; weights are the thing the code rolls. Keeping them separate means a designer can add a prize without re-deriving six numbers.

Does the wheel have to have equal slices?

Visually, no, and it is better if it does not — a slice drawn as a sliver reads as rare. What matters is that the slice at the top when the tween stops is the one the server chose, so the drawn geometry and the index order have to match.

Can two players get the mythic at the same moment?

Yes, and that is correct: each spin is its own roll. If you want a prize to exist once in the whole experience, that is not a wheel problem — it is a stock counter in a DataStore, decremented with UpdateAsync before the prize is announced.

Why does my wheel jump before it spins?

Because Rotation was left at a number like 1,448 from the last spin and the new tween starts from there. Normalise it with % 360 when the tween completes, as above.

Updated: Tested in Studio: Roblox Studio, October 2026Author: GustavoLicense: Code in this guide may be used in any Roblox experience, including a paid one

Related pack

Ultimate Script Pack

Ultimate Script Pack

$40See the pack

Get notified

Coming soon

Drop your email and we'll ping you the moment this pack goes live.