Code for Hunty Zombie: a practical Roblox Luau survival setup

Writing the code for Hunty Zombie in Roblox is a useful exercise because it forces you to combine several systems at once: a spawning director, a wave timer, player damage, and enemy damage against players. Each piece is small, but they have to agree on who has authority, when a round ends, and what counts as a kill. Once those decisions are explicit, the rest of the script becomes much easier to reason about and much easier to debug when something feels off in a playtest. A common beginner mistake is to write the spawner first, then bolt on a wave counter that does not actually know how many zombies are still alive, and end up with rounds that end on the first frame. The fix is almost always to decide the data model before the visual layer.

Roblox games run on the Luau scripting language inside Roblox Studio, with the server controlling authority and clients rendering state. Any code that affects game balance, currency, or hit detection belongs on the server, while clients can read state and play effects. If you have not set up a project before, install Roblox Studio, create a new Baseplate place, and add a Script under ServerScriptService for server logic and a LocalScript under StarterPlayerScripts for client feedback. The patterns below assume that structure, and they were tested on Studio version 0.640 and the production client as of 2026, so the service calls and event names should still match what you see in the explorer.

Designing the game loop before writing scripts

A survival shooter lives or dies by the pacing of its rounds. Players need a short calm window to loot or position, a clear wave start signal, escalating pressure, and a clean transition between waves. If you start scripting before deciding the loop, you usually end up rewriting the same timer in three different places, and each copy drifts a little further from the original. Pin the loop down on paper first, then translate it into a state machine that the rest of the code can poll.

A workable structure for the code for Hunty Zombie looks like this:

  • Intermission phase of about 15 to 20 seconds where the spawn area is safe and players can prepare.
  • Wave phase with a fixed number of zombies, ending only when every active zombie is dead or the wave timer expires.
  • Brief rest period between waves, with a difficulty curve that increases zombie count, health, or speed over time.
  • Game over state when all players have died, with a clear UI and a path back to the lobby.
  • Optional shop phase during intermission so players can spend currency earned from kills on better weapons or perks.

Encoding these phases in a single state variable, such as Intermission, Wave, or GameOver, prevents race conditions where the spawner keeps creating zombies after the round has technically ended. Every other system checks that variable before running, including the damage handler, the shop, and the spectator camera. A single source of truth for phase is one of the cheapest bugs you can prevent in a Roblox survival project.

Setting up the folder layout in Roblox Studio

Before you write a line of code for Hunty Zombie, lay out the hierarchy so the scripts can find each other. A clean structure makes the project easier to refactor when you add new enemy types or a shop system later. It also makes it obvious where a new contributor should drop a new module, which matters the moment a friend offers to help you finish the boss wave.

  • ServerScriptService holds the main round script, a zombie spawner module, and a damage handler.
  • ReplicatedStorage contains a Events folder with RemoteEvent objects that the server uses to tell clients when a wave starts, ends, or when a player takes damage.
  • StarterPlayerScripts holds the LocalScript that listens to those events and updates the wave UI and damage flashes.
  • Workspace contains the arena, spawn points for players, and a folder called ZombieStorage that stores the unspawned zombie models.
  • ServerStorage keeps the zombie template, boss template, and any reference models that should never reach the client by accident.
  • StarterGui holds the wave label, health bar, and score widgets that the LocalScript can reparent into the player’s PlayerGui on spawn.

Keeping unspawned zombies in Workspace but in a separate folder lets the server move them in and out of the game world using simple parent changes, which is faster than cloning models on every spawn during a stress test. Putting the reference model in ServerStorage instead also prevents exploiters from grabbing a copy of the zombie mesh and studying it in their inventory.

Core server script for round management

The following server script is a working baseline for the code for Hunty Zombie. It manages phases, spawns zombies, listens for kills, and ends the round. It is intentionally simple so you can extend it with your own enemy types, weapons, or scoring system. Copy it into a Script under ServerScriptService named RoundManager, then create the matching events under ReplicatedStorage.Events and the zombie template under ServerStorage for it to run without errors.

-- ServerScriptService/RoundManager (Script)
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")

local Events = ReplicatedStorage:WaitForChild("Events")
local WaveStarted = Events:WaitForChild("WaveStarted")
local WaveEnded = Events:WaitForChild("WaveEnded")
local GameOverEvent = Events:WaitForChild("GameOver")

local ZombieTemplate = ServerStorage:WaitForChild("Zombie")

local INTERMISSION_TIME = 15
local WAVE_TIME = 60
local ZOMBIES_PER_WAVE = 8
local DIFFICULTY_GROWTH = 2

local state = "Intermission"
local currentWave = 0
local aliveZombies = {}

local function broadcastWave(waveNumber, zombieCount)
 WaveStarted:FireAllClients(waveNumber, zombieCount)
end

local function endWave()
 state = "Intermission"
 WaveEnded:FireAllClients(currentWave)
 currentWave += 1
 task.wait(INTERMISSION_TIME)
end

local function spawnWave()
 local total = ZOMBIES_PER_WAVE + (currentWave - 1) * DIFFICULTY_GROWTH
 broadcastWave(currentWave, total)

 for i = 1, total do
 local zombie = ZombieTemplate:Clone()
 zombie.Parent = workspace
 local spawnPoint = workspace.Spawns:GetChildren()[math.random(#workspace.Spawns:GetChildren())]
 zombie:MoveTo(spawnPoint.Position)
 table.insert(aliveZombies, zombie)
 task.wait(0.4)
 end

 local elapsed = 0
 while elapsed < WAVE_TIME and #aliveZombies > 0 do
 task.wait(1)
 elapsed += 1
 end

 for _, z in ipairs(aliveZombies) do
 if z and z.Parent then
 z:Destroy()
 end
 end
 table.clear(aliveZombies)
end

Players.PlayerAdded:Connect(function(player)
 player.CharacterAdded:Connect(function(character)
 local humanoid = character:WaitForChild("Humanoid")
 humanoid.Died:Connect(function()
 local survivors = 0
 for _, p in Players:GetPlayers() do
 if p.Character and p.Character:FindFirstChild("Humanoid") and p.Character.Humanoid.Health > 0 then
 survivors += 1
 end
 end
 if survivors == 0 then
 state = "GameOver"
 GameOverEvent:FireAllClients()
 end
 end)
 end)
end)

while true do
 state = "Intermission"
 task.wait(INTERMISSION_TIME)
 if state == "GameOver" then
 break
 end
 spawnWave()
 endWave()
end

Read this as a pattern rather than a finished product. It demonstrates how the round manager stays in charge of timing, how zombies are tracked in a simple list, and how the script reacts when no players remain alive. In a real project you would split this into modules: a wave controller, a spawner, a state machine, and a player tracker that all live in their own scripts and communicate through a shared module. Keeping everything on one page helps when you are still deciding how the systems should talk to each other, and once the contract is stable you can move each block into its own ModuleScript without changing the public behavior.

Damage and hit detection on the server

Damage belongs on the server. Putting it on the client leads to teleport hacks, infinite health, and one-shot weapons. The most reliable approach in a Roblox survival game is to use a Tool with a RemoteEvent that asks the server to validate a hit. The server checks distance, rate limit, and the target’s health before applying damage. This pattern is the same one used by most successful round-based shooters on the platform, including community clones of survival classics, and it is the cheapest way to keep exploiters from turning your game into a public testing ground.

A minimal server-side hit handler for the code for Hunty Zombie looks like this:

-- ServerScriptService/HitHandler (Script)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local HitEvent = ReplicatedStorage.Events:WaitForChild("HitEvent")

local COOLDOWN = 0.5
local lastHit = {}

HitEvent.OnServerEvent:Connect(function(player, target)
 if not target or not target:IsA("Model") or not target:FindFirstChild("Humanoid") then
 return
 end

 local now = tick()
 if lastHit[player.UserId] and now - lastHit[player.UserId] < COOLDOWN then
 return
 end
 lastHit[player.UserId] = now

 local character = player.Character
 if not character or not character:FindFirstChild("HumanoidRootPart") then
 return
 end

 local distance = (character.HumanoidRootPart.Position - target.HumanoidRootPart.Position).Magnitude
 if distance > 15 then
 return
 end

 target.Humanoid:TakeDamage(20)

 if target.Humanoid.Health <= 0 then
 local roundManager = game.ServerScriptService:FindFirstChild("RoundManager")
 if roundManager and roundManager.aliveZombies then
 for i, z in ipairs(roundManager.aliveZombies) do
 if z == target then
 table.remove(roundManager.aliveZombies, i)
 break
 end
 end
 end
 end
end)

The distance check prevents players from sniping zombies across the map by exploiting network latency. The cooldown prevents a single click from turning into ten damage events because Roblox can fire the same input multiple times under packet loss. The kill removal keeps the round manager’s count accurate so the wave ends when it should. You will also want to bind the HitEvent inside the weapon Tool rather than firing it from a generic input script, because that lets the server check that the player is actually holding the tool they claim to be holding before validating a hit.

Zombie AI: chasing, attacking, and dying

Zombies do not need complex behavior to feel threatening. A simple state machine that switches between idle, chase, and attack covers the vast majority of a Roblox survival game. PathfindingService handles the navigation, and a short attack script handles the damage to nearby players. Trying to imitate a full stealth AI or a learning agent on top of Roblox’s pathfinding is a great way to burn a week of development on something players will not notice, so start simple and only add complexity when playtests demand it.

For the code for Hunty Zombie, a useful AI structure is:

  • An Idle state when the zombie first spawns, lasting about one second, so all zombies do not move as a single herd on the first frame.
  • A Chase state where the zombie computes a path to the nearest living player every 0.5 seconds and moves along it.
  • An Attack state triggered when the zombie is within 5 studs of a player, applying damage on a 1 second cooldown.
  • A Dead state that cleans up the model, plays a short death sound, and removes the zombie from the alive list.
  • An optional Stun state triggered by headshot damage or a flashbang, which freezes the zombie for two seconds and adds tactical depth.

PathfindingService can be expensive if you call it for every zombie every frame, so the 0.5 second recompute is a reasonable compromise. For higher enemy counts, pool the path computations or replace PathfindingService with a handcrafted navigation grid if your arena is mostly rectangular. A navigation grid is a simple 2D array of walkable and blocked cells that the zombie reads to step toward the player, and for a small Roblox arena it is usually faster than calling the built-in service on every tick.

Communicating with clients without breaking authority

Clients need to know when a wave starts, when it ends, and when their own health changes, but the server is the only source of truth. The safe pattern is to have the server fire RemoteEvent objects that carry small amounts of data, and let the client LocalScript react to them. Never trust a client to tell the server what the wave number is, how much health they have, or how many zombies are alive. Even something as innocent as a “current wave” label can be spoofed to confuse other players in the lobby, so keep that information flowing in one direction only.

A short client script for the code for Hunty Zombie could look like this:

-- StarterPlayerScripts/WaveUI (LocalScript)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Events = ReplicatedStorage:WaitForChild("Events")

local WaveStarted = Events:WaitForChild("WaveStarted")
local WaveEnded = Events:WaitForChild("WaveEnded")
local GameOverEvent = Events:WaitForChild("GameOver")

local waveLabel = script.Parent:WaitForChild("WaveLabel")

WaveStarted.OnClientEvent:Connect(function(waveNumber, zombieCount)
 waveLabel.Text = "Wave " .. waveNumber .. " - " .. zombieCount .. " zombies"
 waveLabel.TextColor3 = Color3.fromRGB(220, 70, 70)
end)

WaveEnded.OnClientEvent:Connect(function(waveNumber)
 waveLabel.Text = "Wave " .. waveNumber .. " cleared"
 waveLabel.TextColor3 = Color3.fromRGB(80, 200, 120)
end)

GameOverEvent.OnClientEvent:Connect(function()
 waveLabel.Text = "Game over - return to lobby"
 waveLabel.TextColor3 = Color3.fromRGB(200, 200, 200)
end)

This keeps the UI responsive without letting the client decide the wave state. If a client tampers with the event, the server still enforces the rules and the worst that happens is a slightly inaccurate label on the cheater’s screen. For richer feedback, add a separate DamageFlash LocalScript that listens to a player-only RemoteEvent fired when that specific player takes damage, and tints the screen red for a tenth of a second so the hit feels real.

Common pitfalls when writing the code for Hunty Zombie

Most Roblox survival prototypes run into the same handful of bugs. Recognizing them early saves a lot of time during playtesting, and a short list of known issues is also a useful onboarding doc if you ever hand the project to a collaborator.

  • Zombies spawn inside the map and clip through walls. Use a spawn-point folder and only choose points that are not obstructed by raycast checks.
  • Wave ends instantly because the alive list is never updated when zombies die. The hit handler must remove the dead model from aliveZombies so the round manager can move on.
  • Damage works once, then never again. A missing cooldown reset inside the weapon Tool means subsequent hits are rejected as rate limited.
  • Players respawn mid-wave and immediately die because the spawn point is inside the zombie horde. Mark a safe zone around the spawn area and gate zombies from entering it.
  • The server script stops because of a nil reference. Wrap spawn loops in pcall for production builds so a single bad spawn does not kill the whole round manager.
  • The wave timer uses a single while loop with task.wait instead of a heartbeat loop, which makes it impossible to pause the round when a player joins late.

If you are building something more complex than a wave shooter, the survival genre shares structural DNA with several other popular Roblox formats. Roblox itself hosts a wide variety of community-driven experiences that vary from round-based shooters to tycoons, and a quick look at the platform overview on Wikipedia’s Roblox page can help you understand how different genres share the same engine and the same player base.

Tuning the experience with concrete numbers

Hard numbers matter more than clever code in a survival game. A weapon that deals too much damage removes tension, while a zombie that takes too many hits to kill turns the round into a slog. The table below shows a reasonable starting point for a solo developer playtesting against two or three friends. Adjust by feel, then write down what you changed so the next iteration is repeatable, because a survival design that you cannot retune is a survival design that will stagnate.

Parameter Starting value When to increase When to decrease
Zombie health 100 Players clear wave 1 in under 20 seconds Players run out of ammo before the timer ends
Zombie damage per hit 15 Players ignore the threat and farm kills Two zombies delete a full health player
Zombies per wave 8 Players can stand in one spot and survive Map becomes a traffic jam of enemies
Difficulty growth per wave 2 extra zombies Players reach wave 5 with no challenge Wave 4 already feels overwhelming
Player respawn time 5 seconds Players feel punished for one mistake Respawns pile up and break the round
Zombie walk speed 12 studs per second Players outrun the horde without thinking Zombies feel sluggish and ignore players
Headshot multiplier 2x damage Players ignore headshots entirely Every encounter becomes a sniper duel

These numbers are not rules. They are a calibration baseline. Survival design is a conversation between the player, the enemy, and the map, and the only way to find the right values is to play, write down the moment where the experience felt off, and adjust one variable at a time. Keep a small spreadsheet with the wave number, the change you made, and the playtester feedback so you can see trends over several sessions rather than relying on memory.

Performance considerations for higher zombie counts

The code for Hunty Zombie will start to stutter once you push past 40 or 50 active zombies on a low-end client, because each one runs its own pathfinding computation and humanoid update. Three strategies help without rewriting the entire system, and a fourth helps once you start measuring instead of guessing.

  • Cap the number of active path computations and stagger them. Recompute a path for a given zombie only every 1 to 2 seconds instead of every frame.
  • Use Humanoid.WalkSpeed and HumanoidRootPart.CFrame updates sparingly. MoveTo handles navigation, but constant orientation changes cost more than you would expect.
  • Pool the zombie models. Keep a queue of 20 preloaded zombies, reuse them across waves, and reset their health instead of destroying and recreating them.
  • Profile the game with the built-in microprofiler. The developer console exposes per-frame cost per script, which is the fastest way to see whether your bottleneck is pathfinding, network replication, or rendering.

You can also lean on Roblox’s built-in streaming and the use of anchored parts in Workspace to limit how many zombies are considered active at any given moment. A good rule of thumb is to keep the number of physics-driven models under 60 per server tick, and use simple visual stand-ins for distant enemies if you ever decide to scale beyond a small arena. On mobile clients in particular, the difference between 30 active zombies and 60 is the difference between a smooth round and a slideshow.

Validating the build with a real playtest

Scripts that look correct in Roblox Studio can still misbehave under real network conditions. Before you publish anything that uses the code for Hunty Zombie, run a structured playtest with at least two players on the same server. A useful test script is short and specific, and it should be the same script you run after every meaningful change so you can compare sessions fairly.

  • Start the round. Confirm that the intermission lasts 15 seconds and that the wave label updates.
  • During the wave, check that every player who takes damage also sees a health change on the server. Watch the Humanoid.Health property in the explorer while the client takes hits.
  • Kill every zombie. Confirm that the round manager transitions to the next intermission without leaving any model behind in Workspace.
  • Let a player die and remain dead. Confirm that the game over event fires and that new players joining late are placed in a clean lobby state.
  • Reload the place and run the test again. The state machine must be reset to Intermission on every server boot, not just the first one.
  • Open the developer console on at least one client and skim the network and script error sections for silent warnings that did not show up in Studio.

If you can run this checklist twice without a failure, your survival core is stable enough to expand. If something fails, write down the exact reproduction steps, because a bug that cannot be reproduced reliably cannot be fixed reliably. The cheapest playtest setup is a free private server with two browser tabs and a friend, and it will catch roughly 80 percent of the bugs you would otherwise discover only after publishing the game to a wider audience.

Where this fits in a larger GameDev project

A round-based zombie survival game is one of the most efficient ways to test a small team or a solo developer’s ability to ship a polished experience. The system count is small enough to finish in a few weeks, but large enough to surface real production problems: replication, state management, asset budgeting, and the discipline of writing server-authoritative code. Those four skills transfer cleanly to almost any other Roblox genre and to most other multiplayer engines, so a project like this is rarely wasted effort even if the final game never reaches a wide audience.

If you are working on a wider portfolio of game development work, a project like this also gives you something tangible to show. A useful companion topic is a breakdown of how to think about mobile game UI decisions for fast touch feedback in survival games, since wave shooters are often played on tablets and phones and the on-screen layout of reload, fire, and dodge buttons can make or break a session. Another related read is a closer look at visual style choices in low poly games and their production advantages, which is a natural fit for a Roblox project where simple geometry keeps frame rates stable on shared servers and lets a single artist cover the entire enemy roster without ballooning the place’s memory budget.

Extending the project without breaking it

Once the core loop works, the temptation is to add ten systems at once. A safer approach is to add one system at a time, validate that the round still works, and only then move on. The table below lists common extensions, the order in which they tend to pay off, and the risk that each one introduces if added too early. Treat the ordering as a guideline rather than a law, because the right next feature is always the one your playtesters keep asking for.

Extension What it adds Recommended order Main risk if rushed
Weapon variety Player choice and replayability 1 Balance becomes hard to track without a stats system
Currency and shop Long-term progression 2 Server validation gaps can let players duplicate coins
Boss zombie Climactic moments 3 Boss pathing can break the arena and trap players
Perks or class system Build diversity 4 Interactions between perks become exponential to test
Leaderboards Competition 5 Cheating becomes a real concern without server-side data stores
Daily challenges Return visits 6 Mission logic fragments the round into special cases
Custom maps Longevity 7 Map rotation breaks spawn points and safe zones

Each row above adds real value, but each one also expands the test surface. The pattern of small validated increments is what keeps a Roblox survival project fun to develop instead of frustrating, and it is also the pattern most likely to survive the moment you step away from the project for a month and come back to find a codebase you can still read. A short README inside ServerScriptService that lists the order of operations, the state variable names, and the names of the RemoteEvents is usually enough to get back up to speed in a single afternoon.

Frequently asked questions

What language is the code for Hunty Zombie written in?

The code for Hunty Zombie on Roblox is written in Luau, the scripting language used inside Roblox Studio. Luau is a typed, lightweight dialect of Lua that runs on the Roblox engine, with server scripts placed under ServerScriptService and client scripts placed under StarterPlayerScripts or StarterGui. You can use type annotations and strict mode to catch a lot of the small mistakes that beginners usually hit, and the Studio editor will underline the rest as you type.

Do I need an external server to run a Hunty Zombie-style game?

No. Roblox Studio publishes to the Roblox platform, which provides the server infrastructure. Your scripts run inside the Roblox server environment, and players connect through the official Roblox client. You do not need to host a separate backend, although you can use Roblox’s data stores and messaging services if you want to add cross-server features like global leaderboards, daily missions, or friend invites that survive a server shutdown.

How do I make sure zombie damage cannot be exploited?

Always run damage logic on the server. The client can request a hit through a RemoteEvent, but the server must check distance, cooldown, weapon ownership, and the target’s health before applying damage. Never store the player’s health on the client as the source of truth, and never trust a client-sent value for currency, kills, or wave progress. The single most common exploit on round-based Roblox games is a client that tells the server “I killed 200 zombies” without ever firing a single hit event, and the only defense is server-side validation of every reward.

Why does my wave end instantly after the first zombie dies?

The most common reason is that the aliveZombies table is not being updated when a zombie dies. The hit handler needs to remove the destroyed zombie from the table, otherwise the round manager sees the list as empty and transitions to the next phase. Add a removal step in the humanoid death callback to keep the count correct, and consider also listening to the Humanoid.Died event on each zombie at spawn time so the table is updated even if the kill came from a different source such as a trap or a fall.

Can I use this pattern for non-zombie survival games?

Yes. The structure described in this code for Hunty Zombie guide is a generic round-based survival framework. Replace the zombie model with any enemy type, change the spawn points and the difficulty curve, and you have a basis for arena shooters, tower defense prototypes, and cooperative boss fights. The phases, state machine, and damage pipeline are reusable as long as you keep the server in charge of authority, and the same folder layout in Studio will accept almost any enemy without modification.

How many zombies can Roblox handle before performance drops?

Performance depends on the client hardware, the complexity of the zombie model, and how often pathfinding recomputes. A practical ceiling for a basic Robloxian-style zombie on modern hardware is around 40 to 60 active enemies per server. Beyond that, pool the models, reduce pathfinding frequency, and consider replacing complex animations with simpler locomotion to keep the frame time stable. On tablets and older laptops, halve those numbers and use a stress test that simulates three players in a small room before shipping a wave size update.

Should the wave timer continue after all zombies are dead?

No. A clean survival experience ends the wave the moment the last enemy falls, gives players a short rest, and starts the next wave on a predictable cadence. Letting the timer run out punishes skilled players and removes the satisfying moment of clearing a wave. End the wave when the alive list is empty, and use a short intermission to keep the rhythm steady. If you want to reward fast clears, give a small currency bonus based on remaining wave time, but never extend the round just to fill the clock.

How do I reset the round when a new player joins late?

Keep the round state in a single module under ServerScriptService and make sure the script reads the state on every server boot. Late joiners should be placed into the current wave if one is active, or queued for the next intermission if the round is in a transition. Use the PlayerAdded event to teleport them to a safe spawn and to fire a RemoteEvent that resyncs their wave UI to the current state. A small “joining wave 3” message above the health bar is usually enough to make late arrivals feel included rather than lost.

Can I monetize the code for Hunty Zombie without ruining the balance?

Yes, but keep the paid content cosmetic or progression-only. Selling straight damage boosts for Robux tends to push paying players into a stale meta and pushes free players out of the game, which shrinks your daily active count over time. A safer approach is to sell weapon skins, lobby emotes, extra loadout slots, and a battle pass that rewards playtime rather than purchases. Validate every purchase on the server using MarketplaceService callbacks and a server-side receipt log so exploiters cannot grant themselves items by spoofing the prompt.

What is the fastest way to debug a wave that never ends?

Insert a print statement at three points: the start of the wave, every time a zombie is removed from aliveZombies, and the loop condition that decides whether the wave continues. Run a one-player test, kill a zombie, and watch which prints fire. If the removal print never fires, the hit handler is not updating the table. If the removal print fires but the wave still continues, the round manager is reading a different table than the one you are editing, which is usually a sign that you put the state in a ModuleScript but the hit handler is reading a Script, or vice versa.