In short
A Roblox horror game needs four things: atmosphere (dark lighting and 3D sound), a threat the player can read and outsmart (a monster that patrols, notices, chases and gives up), objectives that force the player into danger (keys, fuses, notes), and scarce resources such as flashlight battery. Build one greybox level with the monster and objectives first, test it in the dark with friends, then add chapters, co-op and cosmetics.
- Pace it in waves: quiet exploration, a warning sign, the threat, escape, then quiet again.
- The monster must be fair: players should learn its rules (it hears running, it loses you behind closed doors) and use them.
- Fill out the Maturity & Compliance questionnaire honestly. Jump scares count as mild fear content; realistic gore moves you to Moderate.
On this page
What makes a horror game work
The loop in one line: explore a dark place, find what you need, survive the thing that hunts you, reach the exit. Unlike most Roblox genres, horror does not run on a number going up. It runs on how the player feels, which makes it harder to design and easier to ruin.
- Every 10 seconds: uncertainty. A sound, a shadow, a door that was closed before. Most of a horror game is the player expecting something that has not happened yet.
- Every minute or two: a decision under pressure. Run or hide. Spend battery now or save it. Open the door with the noise behind it, or go the long way round.
- Every 5 to 10 minutes: a peak. A chase, a reveal, a scare. Then a safe room, a save point or a quiet corridor, so the next peak lands.
- Every session: a story beat. A chapter end, a new area, a clue about what the monster is. Players come back to find out what happens.
- Friends. Horror works well in small groups. Shared fear, and splitting up to do objectives, give a group something to talk about.
Abandoned horror games usually fail in one of three ways: the scares are constant, so nothing is scary; the monster is unfair (it sees through walls, it is faster than the player forever); or the level is so dark that players get lost instead of scared. Good ones give the player rules and then test them.
Roblox's genre list has no Horror genre. Pick the one that matches your core gameplay: Survival (its subgenres include Escape, and 1 vs All for one player hunting the rest) or Adventure with the Story subgenre. If one player is the monster, see the killer vs survivors guide, and for puzzle-heavy horror, the escape room guide.
The systems a horror game needs
| System | What it does | Roblox pieces | Guide |
|---|---|---|---|
| Monster AI | Patrols, notices players, chases, gives up | PathfindingService, Humanoid:MoveTo, workspace:Raycast | NPC pathfinding and this page |
| Flashlight and battery | Light the player controls but cannot keep on forever | SpotLight, RemoteEvent, attributes | This page |
| Lighting and mood | Darkness, fog, time of night | Lighting, Atmosphere, ClockTime | Day and night cycle |
| 3D sound | Footsteps, breathing, distant noises with direction | Sound in a part, RollOffMaxDistance | Music and sounds |
| Doors and keys | Locked rooms that gate progress | ProximityPrompt, attributes | Door script |
| Sprint and stamina | Running is fast but loud and limited | Humanoid.WalkSpeed | Sprint and stamina |
| Chapters or rounds | Start, objectives, ending, back to lobby | Server state, teleports | Round system |
| Scripted scenes | A camera shot for a reveal or an ending | Camera, tweens | Cutscenes |
| Saving | Chapter progress, cosmetics, endings found | DataStoreService | Data stores |
Two Roblox facts matter for atmosphere. A Sound parented to a part or attachment in Workspace is a 3D sound: players hear where it comes from, and RollOffMaxDistance sets how far away it can still be heard. Put the monster's footsteps and breathing in its body, not in the player's GUI, and the sound itself tells players where it is. And an Atmosphere object in Lighting changes how light scatters, which is how you get fog that hides the end of a corridor without making the whole screen black.
Build plan: from a baseplate to launch
- Greybox one level. Corridors and rooms in plain parts, about five minutes to walk through. Mark three objectives (a key, a fuse, an exit) and a few hiding spots. Leave the lights on while you build.
- Turn the lights off. Lower the brightness and ambient light in Lighting, add fog with an Atmosphere, set the time to night. Play it with the flashlight script below. If you get lost, add landmarks: a lit window, a red emergency lamp, a distinct room. Darkness should hide threats, not the route.
- Add the monster. Build a rig, add the monster script and numbered patrol points. Tune speed and hearing until you can escape when you play well and get caught when you are careless.
- Objectives and a win. Keys open doors (the door guide has key locks), the exit opens when all objectives are done, and reaching it ends the round.
- Sound pass. Footsteps for the monster, an ambient loop per area, one stinger sound for when it spots a player. Sound is the cheapest scare you can add.
- First public test. Play it with two or three friends who have not seen it, in the dark, and stay quiet. Watch where they get lost, where they laugh instead of scream, and whether they understand how to escape.
- Launch features. A lobby with a main menu, a loading screen, a round system, saving of chapters reached, and the Maturity & Compliance questionnaire filled out before you make the game public.
A monster that patrols, hears, sees and gives up
The pathfinding guide builds an NPC that chases the nearest player. A horror monster needs more: it should patrol when it knows nothing, react to what it senses, chase to where it last noticed you (not to where you really are), and give up after a while. That gap between "where it thinks you are" and "where you are" is what makes hiding work.
-- Script inside the monster model named MonsterAI.
-- Patrols numbered points, chases a player it sees or hears, gives up when it loses them.
local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local monster = script.Parent :: Model
local humanoid = monster:WaitForChild("Humanoid") :: Humanoid
local root = monster:WaitForChild("HumanoidRootPart") :: BasePart
local patrol = workspace:WaitForChild("PatrolPoints") :: Folder -- parts named 1, 2, 3...
local SIGHT_RANGE = 60 -- studs, needs a clear line
local HEAR_RANGE = 25 -- studs, through walls, only players with Sprinting = true
local PATROL_SPEED, CHASE_SPEED = 8, 17 -- walking is 16: running escapes it, but it hears running
local GIVE_UP_AFTER = 5 -- seconds without seeing or hearing anyone
root:SetNetworkOwner(nil) -- the server moves it, so it does not stutter
local path = PathfindingService:CreatePath({ AgentRadius = 2, AgentHeight = 5, AgentCanJump = false })
local rayParams = RaycastParams.new()
rayParams.FilterType = Enum.RaycastFilterType.Exclude
rayParams.FilterDescendantsInstances = { monster }
-- Where the closest player it can see (or hear running) is, if any
local function findTarget(): Vector3?
local best: Vector3?, bestDistance = nil, math.huge
for _, player in Players:GetPlayers() do
local character = player.Character
local hrp = character and character:FindFirstChild("HumanoidRootPart")
local hum = character and character:FindFirstChildOfClass("Humanoid")
if character and hrp and hrp:IsA("BasePart") and hum and hum.Health > 0 then
local offset = hrp.Position - root.Position
local heard = offset.Magnitude < HEAR_RANGE and player:GetAttribute("Sprinting") == true
local hit = if offset.Magnitude <= SIGHT_RANGE then workspace:Raycast(root.Position, offset, rayParams) else nil
local seen = hit and hit.Instance:IsDescendantOf(character)
if (heard or seen) and offset.Magnitude < bestDistance then
best, bestDistance = hrp.Position, offset.Magnitude
end
end
end
return best
end
local function stepToward(goal: Vector3)
local ok = pcall(function()
path:ComputeAsync(root.Position, goal)
end)
local waypoints = if ok and path.Status == Enum.PathStatus.Success then path:GetWaypoints() else {}
local nextPoint = waypoints[2] or waypoints[1]
humanoid:MoveTo(if nextPoint then nextPoint.Position else goal) -- repathed 4 times a second
end
local lastKnown: Vector3?, lastSensed, patrolIndex = nil, 0, 1
while monster.Parent and humanoid.Health > 0 do
local seen = findTarget()
if seen then
lastKnown, lastSensed = seen, os.clock()
end
if lastKnown and os.clock() - lastSensed < GIVE_UP_AFTER then
humanoid.WalkSpeed = CHASE_SPEED
stepToward(lastKnown) -- goes to where it last sensed you, not where you are now
else
lastKnown = nil
humanoid.WalkSpeed = PATROL_SPEED
local point = patrol:FindFirstChild(tostring(patrolIndex))
if point and point:IsA("BasePart") then
if (point.Position - root.Position).Magnitude < 4 then
patrolIndex = patrolIndex % #patrol:GetChildren() + 1
else
stepToward(point.Position)
end
end
end
task.wait(0.25)
end
- Setup. A rig with a
Humanoidand an unanchoredHumanoidRootPart(the pathfinding guide shows how to make one), and a Workspace folderPatrolPointswith parts named 1, 2, 3 and so on. Make the patrol parts invisible and non-colliding. - Sight is a raycast from the monster to the player. If a wall is in the way, the ray hits the wall and the monster sees nothing. Closed doors and lockers hide players for free.
- Hearing reads a
Sprintingattribute on the player. Set it from a server script, for example when the character's root part moves faster than walking speed, because an attribute a LocalScript sets never reaches the server. Then running (see the sprint guide) becomes a risk players must weigh. - Speed is a rule too. The chase speed of 17 is just above the default walking speed of 16 studs per second. A walking player is slowly caught; a sprinting player escapes but can be heard. That is a choice, not a dice roll.
- Giving up. After 5 seconds without sensing anyone, it returns to its patrol. Long enough to be scary, short enough that hiding feels like it worked.
- Server owned.
SetNetworkOwner(nil)makes the server move the monster, so it does not stutter as it passes near players. The loop asks for a new path four times a second and walks to the next waypoint only, so it reacts quickly.MoveTogives up after 8 seconds on its own, which this loop never reaches. - Catching players is a touch or distance check that deals damage, as in Step 4 of the pathfinding guide. A short scare screen before the respawn is where a jump scare belongs.
A flashlight with a battery
A flashlight turns darkness into a resource. The server owns the battery, so a player cannot give themselves infinite light, and the light is a SpotLight in the character's head, so other players see each other's beams.
-- Script in ServerScriptService named FlashlightServer.
-- Every player has a flashlight with a battery the server drains and refills.
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local DRAIN_PER_SECOND = 2 -- a full battery lasts 50 seconds of light
local BATTERY_PICKUP = 40 -- charge a Battery part gives back
local toggle = Instance.new("RemoteEvent")
toggle.Name = "ToggleFlashlight"
toggle.Parent = ReplicatedStorage
local function getLight(player: Player): SpotLight?
local head = player.Character and player.Character:FindFirstChild("Head")
local light = head and head:FindFirstChild("Flashlight")
return if light and light:IsA("SpotLight") then light else nil
end
Players.PlayerAdded:Connect(function(player)
player:SetAttribute("Battery", 100)
player.CharacterAdded:Connect(function(character)
local head = character:WaitForChild("Head")
local light = Instance.new("SpotLight")
light.Name = "Flashlight"
light.Range, light.Angle, light.Brightness = 40, 45, 3
light.Enabled = false
light.Parent = head
end)
end)
-- The client only asks; the server decides if there is charge left
toggle.OnServerEvent:Connect(function(player)
local light = getLight(player)
local battery = player:GetAttribute("Battery")
if light and typeof(battery) == "number" then
light.Enabled = not light.Enabled and battery > 0
end
end)
for _, part in CollectionService:GetTagged("Battery") do
if part:IsA("BasePart") then
part.Touched:Connect(function(hit)
local player = hit.Parent and Players:GetPlayerFromCharacter(hit.Parent :: Model)
local battery = player and player:GetAttribute("Battery")
if player and typeof(battery) == "number" and part.Parent then
player:SetAttribute("Battery", math.min(100, battery + BATTERY_PICKUP))
part:Destroy() -- one use; respawn them per round if you like
end
end)
end
end
while true do
task.wait(1)
for _, player in Players:GetPlayers() do
local light = getLight(player)
local battery = player:GetAttribute("Battery")
if light and light.Enabled and typeof(battery) == "number" then
local left = math.max(0, battery - DRAIN_PER_SECOND)
player:SetAttribute("Battery", left)
light.Enabled = left > 0
end
end
end
-- LocalScript in StarterPlayerScripts named FlashlightInput.
-- Press F to ask the server to switch the flashlight.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService = game:GetService("UserInputService")
local toggle = ReplicatedStorage:WaitForChild("ToggleFlashlight") :: RemoteEvent
UserInputService.InputBegan:Connect(function(input, typing)
if not typing and input.KeyCode == Enum.KeyCode.F then
toggle:FireServer()
end
end)
- The battery is a number attribute on the player from 0 to 100, so a GUI LocalScript can show it with
GetAttributeChangedSignal("Battery")without another remote. - Tag pickup parts
Batteryto scatter spare batteries through the level. Where you put them is level design: a battery behind the monster's patrol route is a decision. - The SpotLight points the way the head faces. For a first-person game, set
StarterPlayer.CameraModetoLockFirstPersonso the beam follows the view. - Phones have no F key. Add a screen button that fires the same remote, or bind the action with ContextActionService, which can create a touch button for you.
Content maturity: what your scares mean for your audience
Every Roblox game should fill out the Maturity & Compliance questionnaire in Creator Hub (Publish settings, then Content rating). Without content maturity information, Roblox restricts who can play the game. Horror developers need to read the Fear question closely:
| Level | Examples in Roblox's docs | Label it leads to | Who can play by default |
|---|---|---|---|
| Mild | Heavy breathing, a pounding heart, screaming, creepy-looking NPCs, jump scares, ominous music, gameplay that builds suspense | Mild | Eligible for Roblox Kids (5 to 8) and Roblox Select (9 to 15), plus their extra publishing requirements |
| Moderate | Disfigured mouths with realistic blood, visible organs or connective tissue, realistic open wounds or bleeding eyes | Moderate | Roblox Select (9 to 15) and standard Roblox (16 and over) |
So most of the classic horror toolkit (darkness, sound, a creepy monster, a jump scare) counts as mild fear content. Realistic gore pushes the game to Moderate and narrows the default audience. Violence and blood have their own questions, and the questionnaire asks you to answer for the most extreme content a player can meet. If an update adds scarier content, update your answers and submit the questionnaire again: Roblox's docs say you must.
Monetization that fits horror
Horror players pay for more of the experience and to look good while scared. Anything that removes the danger removes the game, so sell carefully.
| Item | Roblox feature | Accepted? | Watch out for |
|---|---|---|---|
| Revive after being caught | Developer product | Yes, in co-op | Keep it limited per round. Unlimited revives remove the fear, and in co-op the team may come to expect someone to pay. |
| Cosmetics (flashlight colours, outfits, emotes in the lobby) | Pass or developer product | Yes | None: they change nothing in the dark. |
| Better flashlight or bigger battery | Pass | Mixed | Light is your difficulty. A permanent upgrade makes every level easier for buyers. |
| Monster tracker, see through walls | Pass | No | It deletes the tension. Even buyers lose the reason to play. |
| Private server | Private servers | Yes | Friend groups like to play horror alone together. Note you cannot enable paid access and private servers at the same time. |
| Extra chapters or modes | Pass | Yes, if the base game is complete | Players resent a story that stops mid-chapter behind a purchase. |
A pass is bought once and a developer product can be bought again, so revives are developer products and permanent cosmetics are passes. The game pass guide shows how to check ownership on the server.
What brings horror players back
- Chapters. Release the story in parts and end each one on a question. Saved chapter progress lets players come back to the next one.
- Variation. Randomise where keys spawn and which doors are locked, so a second run is not a memory test. A known level is not scary.
- Endings and secrets. Hidden notes, alternative endings and a collection screen of what you have found reward players who explore the dark instead of running through it.
- Difficulty modes. A harder mode with a faster monster and less battery gives players who finished the game a reason to return.
- Co-op. Objectives that need two people (one holds a door while the other runs) make a group session the natural way to play, and a group that had fun has a reason to come back together.
Build it with RoCode
RoCode is an independent AI agent for Roblox Studio, not made by Roblox. You describe the game in a web chat and its Studio plugin writes the Luau and builds parts, scripts and UI in the place you have open. For horror it can set up the monster AI, the flashlight, doors, lighting values and a greybox level, and take edit-mode screenshots to check how dark a room looks. It cannot make monster meshes or animations, and it cannot tell you whether something is scary: you need players in the dark for that.
Make the first playable version of a horror game in my place: a dark greybox house with fog and night lighting, a monster rig that patrols numbered points, chases players it sees or hears sprinting and gives up after 5 seconds, a flashlight on F with a battery the server drains, 3 keys to find and an exit door that opens when all keys are found.
- Writes the monster, flashlight, key and door scripts, and builds the greybox level and patrol points
- Sets lighting and atmosphere values, then checks the look with edit-mode screenshots
- Builds the game in rounds: the level and monster first, then objectives, then menus
- Can add UI kit screens such as a main menu, a loading screen, settings and notifications
RoCode does not publish the game, fill out the maturity questionnaire or spend Robux. The free plan gives free credits every day, no card needed. How RoCode builds a game.
Questions
How do I make a horror game on Roblox without scripting?
You can build a dark, creepy level with parts and lighting alone, but a monster that hunts, a flashlight and locked doors all need scripts. Paste the scripts on this page and change the numbers at the top, or describe the game to RoCode and let it add them.
How do I make my Roblox horror game actually scary?
Make the player expect something before it happens. Use 3D sound for the monster so players hear it coming, keep long quiet stretches between scares, and give the monster rules players learn and then have to use under pressure.
Are jump scares allowed on Roblox?
Yes. Roblox's questionnaire lists jump scares as mild fear content, which you disclose in the Maturity & Compliance questionnaire. Realistic gore is a different, higher level.
Why does my monster get stuck or stutter?
Stutter usually comes from network ownership switching between players: call SetNetworkOwner(nil) on its root part. Getting stuck usually means the path agent is bigger than your doorways; lower AgentRadius or widen the doors. The pathfinding guide covers both.
Should my horror game be single player or multiplayer?
Roblox servers are multiplayer by default, and many horror games lean into small groups. If you want a lonely, single-player feel, set the server size small in your game settings or design objectives that split the group up.
Sources and further reading
- Roblox Creator Docs: Content maturity and compliance. fear content levels, labels and audiences
- Roblox Engine API: PathfindingService. CreatePath agent parameters
- Roblox Engine API: Humanoid. MoveTo and its 8 second timeout
- Roblox Engine API: BasePart. SetNetworkOwner
- Roblox Engine API: Sound. 3D sounds and RollOffMaxDistance
- Roblox Engine API: Atmosphere. fog and light scattering
- Roblox Creator Docs: Genres. Survival and Adventure subgenres
- Roblox Creator Docs: Private servers. not combined with paid access
- RoCode guide: NPC pathfinding. rig setup, chase and touch damage
How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 26 October 2026). A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.