Game genres

How to make a horror game in Roblox Studio

A horror game is a machine for building tension and then releasing it. The monster matters less than the minutes before it appears: the dark corridor, the sound behind a door, the flashlight at 12 percent. This guide covers what makes Roblox horror work, the systems you need, a build plan, a monster that patrols, hears, sees and loses players, a flashlight with a battery the server controls, and how to monetize a scary game without breaking the fear.

Updated 26 October 202611 min readGenre guide

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
  1. What makes it work
  2. Systems
  3. Build plan
  4. Monster script
  5. Flashlight script
  6. Maturity label
  7. Monetization
  8. Retention
  9. With RoCode
  10. Questions
  11. Sources

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

Systems, the Roblox pieces they use, and where to learn each one
SystemWhat it doesRoblox piecesGuide
Monster AIPatrols, notices players, chases, gives upPathfindingService, Humanoid:MoveTo, workspace:RaycastNPC pathfinding and this page
Flashlight and batteryLight the player controls but cannot keep on foreverSpotLight, RemoteEvent, attributesThis page
Lighting and moodDarkness, fog, time of nightLighting, Atmosphere, ClockTimeDay and night cycle
3D soundFootsteps, breathing, distant noises with directionSound in a part, RollOffMaxDistanceMusic and sounds
Doors and keysLocked rooms that gate progressProximityPrompt, attributesDoor script
Sprint and staminaRunning is fast but loud and limitedHumanoid.WalkSpeedSprint and stamina
Chapters or roundsStart, objectives, ending, back to lobbyServer state, teleportsRound system
Scripted scenesA camera shot for a reveal or an endingCamera, tweensCutscenes
SavingChapter progress, cosmetics, endings foundDataStoreServiceData 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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 in the monster model named MonsterAI
-- 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 Humanoid and an unanchored HumanoidRootPart (the pathfinding guide shows how to make one), and a Workspace folder PatrolPoints with 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 Sprinting attribute 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. MoveTo gives 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
-- 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
-- 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 Battery to 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.CameraMode to LockFirstPerson so 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:

Fear content levels in the questionnaire, as Roblox describes them
LevelExamples in Roblox's docsLabel it leads toWho can play by default
MildHeavy breathing, a pounding heart, screaming, creepy-looking NPCs, jump scares, ominous music, gameplay that builds suspenseMildEligible for Roblox Kids (5 to 8) and Roblox Select (9 to 15), plus their extra publishing requirements
ModerateDisfigured mouths with realistic blood, visible organs or connective tissue, realistic open wounds or bleeding eyesModerateRoblox 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.

What fits a horror game, and the trap in each
ItemRoblox featureAccepted?Watch out for
Revive after being caughtDeveloper productYes, in co-opKeep 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 productYesNone: they change nothing in the dark.
Better flashlight or bigger batteryPassMixedLight is your difficulty. A permanent upgrade makes every level easier for buyers.
Monster tracker, see through wallsPassNoIt deletes the tension. Even buyers lose the reason to play.
Private serverPrivate serversYesFriend groups like to play horror alone together. Note you cannot enable paid access and private servers at the same time.
Extra chapters or modesPassYes, if the base game is completePlayers 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.

You type

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.

RoCode does
  • 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

Build this in Studio

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

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.

Try RoCode on your own place.

Free every day, no card required. Install the plugin, describe the feature, and review what lands in Studio.