§ 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 players a few waves in, 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 the clear condition all derived, never accumulated by hand. Picoo's wave_spawner recipe does it in one server Script and one HUD script.
Scaling waves
Enemy count, health and damage each grow on their own multiplier (×1.3, ×1.15 and ×1.1 per wave by default), and walk speed rises 0.4 per wave. Wave 1 is 3 enemies with 50 HP hitting for 10; wave 10 is 31 enemies with 175 HP hitting for 23.
Enemies that chase and hit
Each enemy is a glowing Neon block (red by default; green, blue or purple on request) with a health readout overhead. Every half second it walks toward the nearest player, it deals contact damage through Humanoid:TakeDamage at most once per 0.5 seconds per player, and it bursts into sparks when it dies.
Clear detection by recount
Every half second the server counts the wave's Humanoids that are still in the world with health above zero. The wave clears at zero, so an enemy another script destroys counts as gone.
Rewards per wave
On clear, every player in the server gets 25 coins × the wave number in leaderstats Coins, added on the server.
Wave HUD
Wave number out of 20, the incoming count, enemies left, the coin reward on clear, and VICTORY after the last wave.
Ring spawning, tunable
Enemies spawn evenly spaced on a 30-stud ring around the arena centre (0, 5, 0), 0.15 seconds apart. Waves, growth rates, reward, the 5-second break, ring radius and centre are constants at the top of WaveSpawner and recipe parameters.
Files Picoo ships for this prompt
3 files · 282 lines · one recipe call
ServerScriptService/WaveSpawner.lua
Server Script: wave loop, per-axis scaling, enemy builder, ring spawning, live recount and coin rewards.
216 lines
StarterPlayer/StarterPlayerScripts/WaveHUD.lua
LocalScript: wave number, incoming and remaining enemies, the reward and the victory banner.
66 lines
ReplicatedStorage/Remotes/WaveStateChanged
RemoteEvent the server fires with each wave state; the HUD only displays it.
instance
Sample output: ServerScriptService/WaveSpawner.lua
-- ─── Tunable config ───
local TOTAL_WAVES = 20
local BETWEEN_WAVE_SECONDS = 5
local ENEMIES_PER_WAVE_BASE = 3
local ENEMIES_PER_WAVE_GROWTH = 1.3
local ENEMY_HEALTH_BASE = 50
local ENEMY_HEALTH_GROWTH = 1.15
local ENEMY_DAMAGE_BASE = 10
local ENEMY_DAMAGE_GROWTH = 1.1
local COIN_REWARD_PER_WAVE = 25
local SPAWN_RING_RADIUS = 30
local ARENA_CENTER = Vector3.new(0, 5, 0)
-- (excerpt) spawnEnemy() and the leaderstats helpers sit here; then:
local function runWaveLoop()
for wave = 1, TOTAL_WAVES do
local count = math.floor(ENEMIES_PER_WAVE_BASE * (ENEMIES_PER_WAVE_GROWTH ^ (wave - 1)))
WaveStateChanged:FireAllClients({
state = "starting", wave = wave, totalWaves = TOTAL_WAVES, enemyCount = count,
})
local liveHumanoids: { Humanoid } = {}
for i = 1, count do
local angle = (i / count) * math.pi * 2
local pos = ARENA_CENTER + Vector3.new(
math.cos(angle) * SPAWN_RING_RADIUS, 0, math.sin(angle) * SPAWN_RING_RADIUS
)
local hum = spawnEnemy(wave, pos)
if hum then table.insert(liveHumanoids, hum) end
task.wait(0.15)
end
while true do
local aliveCount = 0
for _, hum in ipairs(liveHumanoids) do
if hum.Parent and hum.Health > 0 then aliveCount = aliveCount + 1 end
end
WaveStateChanged:FireAllClients({
state = "in_progress", wave = wave, totalWaves = TOTAL_WAVES, enemiesLeft = aliveCount,
})
if aliveCount == 0 then break end
task.wait(0.5)
end
awardCoins(COIN_REWARD_PER_WAVE * wave)
WaveStateChanged:FireAllClients({
state = "cleared", wave = wave, totalWaves = TOTAL_WAVES, reward = COIN_REWARD_PER_WAVE * wave,
})
if wave < TOTAL_WAVES then task.wait(BETWEEN_WAVE_SECONDS) end
end
WaveStateChanged:FireAllClients({ state = "victory", wave = TOTAL_WAVES, totalWaves = TOTAL_WAVES })
endBuilding 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 the wave compounds three times over. It feels fine while you test the first three waves and impossible after that. Growing each axis on its own modest multiplier gives a ramp you can actually tune. Picoo's wave_spawner recipe uses 1.3 for count, 1.15 for health and 1.1 for damage, so wave 10 sends 31 enemies with 175 HP each, hitting for 23.
The second failure is subtler: the wave that never ends. If clearing depends on counting deaths, any enemy that leaves without firing Died decrements nothing and the round waits forever. The recipe avoids the counter. Every half second the server walks the list of Humanoids it spawned for the wave and counts the ones still parented with health above zero; when that reaches zero the wave is cleared, it pays out, and after a five-second break the next one starts.
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. The recipe spaces each wave around a 30-stud ring at the arena centre, 0.15 seconds apart, and each enemy then walks toward whoever is nearest.
Everything that decides outcomes stays on the server: when the wave cleared, what it paid (25 coins × the wave number to every player), how much health an enemy had. The HUD is a LocalScript that displays the WaveStateChanged events it receives, not the source of any of it.
See more on the Luau generator, the AI Luau script writer, the 3D model generator and the game builder, read how Picoo compares with ChatGPT, or browse the full blog.
Frequently asked
Why does my wave never end?+
An enemy left without your clear check noticing: it fell out of the world, was destroyed by another script, or never fired Died. If the check decrements a counter on Died, one lost enemy stalls the round permanently. Picoo's WaveSpawner recounts instead: every half second it counts the wave's Humanoids that are still parented and above zero health, so an enemy that was destroyed counts as gone. An enemy stuck alive somewhere players cannot reach still holds the wave; a despawn timer for that is a follow-up prompt.
How fast should difficulty scale?+
Grow count, health and damage on separate multipliers and keep each modest. The recipe's defaults are 1.3 for count, 1.15 for health and 1.1 for damage, which still compounds: wave 10 has 31 enemies and wave 20 has 438. For a long game, ask for a lower enemies_growth or fewer total_waves. A single "difficulty" number multiplying all three at once compounds three times over, which is the most common tuning mistake in wave games.
Should enemies spawn near the player?+
No, spawn them on a ring around the arena. Enemies materialising next to the player read as unfair even when the numbers are fair, and it removes the reaction window that makes waves fun. The recipe spaces each wave evenly around a 30-stud ring at the arena centre; ask for a different spawn_ring_radius or arena_center to fit your map.
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. WaveSpawner adds 25 × the wave number to every player's leaderstats Coins when the wave clears. Saving that balance between sessions is a follow-up prompt.
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
Roblox boss fight
1 file · 199 lines · one recipe call. Five color themes and a scale setting for titan-sized bosses.
Roblox tower defense game
Example build: 11 files · 390 lines. Per-team economy, wave-based progression.
Roblox matchmaking system
Example build: 7 files · 280 lines. Picoo writes this code for each prompt, so file names and line counts vary from run to run. Queue state is shared across servers with MessagingService.