§ How to · with Picoo

How to make a wave spawner in Roblox

By Sametcan Tasgiran, Founder & Developer·Published ·Updated

Wave systems fail in two ways: the curve outruns the player by wave 6, or a single stuck enemy freezes the round forever.

Both are solved by treating the wave as state the server owns — count, health, damage and clear condition all derived, never accumulated by hand.

Scaling waves

Enemy count, health and damage each grow on their own multiplier, so difficulty ramps without one number exploding.

Clear detection

The wave ends when the live count reaches zero — tracked by death signals, not by a timer that hopes.

Rewards per wave

Coins into leaderstats on clear, server-side.

Wave HUD

Current wave, enemies left, and an intermission countdown.

Ring spawning

Enemies appear around the arena rather than on top of the player.

Tunable in one block

Waves, growth rates and rewards are constants at the top — not scattered through the spawn loop.

Files Picoo ships for this prompt

2 files · 282 lines · ~35s · 1 credit

WaveSpawner

Wave loop, scaling, spawning, clear detection, rewards.

216 lines

WaveHUD

Wave number, enemies remaining, intermission countdown.

66 lines

Sample output: ServerScriptService.WaveSpawner

-- Growth is per-axis. One combined "difficulty" multiplier is what makes
-- wave 8 unbeatable: count x health x damage compounds three times over.
local ENEMIES_BASE, ENEMIES_GROWTH = 3, 1.3
local HEALTH_BASE,  HEALTH_GROWTH  = 50, 1.15
local DAMAGE_BASE,  DAMAGE_GROWTH  = 10, 1.10

local function statsFor(wave: number)
	return {
		count  = math.floor(ENEMIES_BASE * ENEMIES_GROWTH ^ (wave - 1)),
		health = math.floor(HEALTH_BASE  * HEALTH_GROWTH  ^ (wave - 1)),
		damage = math.floor(DAMAGE_BASE  * DAMAGE_GROWTH  ^ (wave - 1)),
	}
end

-- Clear detection by SIGNAL, not by timer. A timer either ends the wave while
-- enemies are alive, or waits forever on one that fell through the map.
local alive = 0
local function spawnEnemy(stats)
	local enemy = buildEnemy(stats)
	alive += 1
	local hum = enemy:WaitForChild("Humanoid") :: Humanoid
	hum.Died:Once(function()
		alive -= 1
		if alive <= 0 then waveCleared:Fire() end
	end)
	-- Safety net: an enemy that leaves the world never fires Died.
	enemy.AncestryChanged:Connect(function(_, parent)
		if not parent and hum.Health > 0 then
			alive -= 1
			if alive <= 0 then waveCleared:Fire() end
		end
	end)
end

Building a wave spawner in Roblox

A wave system is a difficulty curve with a spawner attached, and the curve is where it lives or dies.

The standard mistake is one multiplier. Difficulty goes up, so count, health and damage all scale by the same growing number — and because they multiply against each other, wave 8 arrives with three times the enemies, each with triple health, hitting three times harder. It feels fine while you test the first three waves and impossible after that. Growing each axis on its own modest multiplier (1.3 for count, 1.15 for health, 1.10 for damage) gives a ramp you can actually tune.

The second failure is subtler: the wave that never ends. If clearing depends on counting deaths, any enemy that leaves without firing Died — falling through the map, destroyed by another script, spawned without a Humanoid — decrements nothing and the round waits forever. The fix is a second signal: watch AncestryChanged too, so an enemy that stops existing still counts as gone.

Spawn placement matters more than it looks. Enemies appearing next to the player read as cheating even when the math is fair, because there is no reaction window. A ring around the arena gives players the half-second that makes a wave feel survivable.

Everything that decides outcomes — when the wave cleared, what it paid, how much health an enemy had — stays on the server. The HUD is a display of that state, not the source of it.

See more on the Luau generator, the game builder, or browse the full blog.

Frequently asked

Why does my wave never end?+

An enemy died in a way that did not fire Died — it fell out of the world, was destroyed by another script, or was never given a Humanoid. If your clear check counts deaths, one lost enemy stalls the round permanently. Watch AncestryChanged as well, or recount live enemies instead of decrementing a counter.

How fast should difficulty scale?+

Grow count, health and damage on separate multipliers, and keep each modest — 1.3 / 1.15 / 1.10 compounds fast enough. A single "difficulty" number multiplying all three at once produces a wave 8 nobody beats, which is the most common tuning mistake in wave games.

Should enemies spawn near the player?+

No — spawn on a ring around the arena. Enemies materialising next to the player reads as unfair even when the numbers are fair, and it removes the reaction window that makes waves fun.

Where do rewards belong?+

On the server, on clear. Awarding coins from the client at the end of a wave means the client decides when the wave ended and how much it paid.

Waves or rounds — what is the difference?+

A wave spawner escalates continuously against the same players. A round system resets: intermission, match, winner, repeat. If you want a lobby and a winner, that is a mini-game round loop, not a wave spawner.

Related Picoo prompts