Tutorial · Menus, HUD and sound

How to make music zones in Roblox

Calm music in the lobby, drums in the arena, wind and birds in the forest: area music makes a map feel like several places. This guide builds it with invisible boxes you place in Studio. One LocalScript works out which box the player is standing in and fades from the old track to the new one, so every player hears the music of where they are, not where someone else is.

Updated 5 November 20266 min readBeginnerLuauWritten by

In short

Place invisible, non-colliding, anchored Parts over each area, tag them MusicZone and give each a string attribute SoundId. In a LocalScript in StarterPlayerScripts, twice a second, test whether the character's root part is inside each zone with zone.CFrame:PointToObjectSpace(position) against half the zone's size. When the zone changes, tween the old Sound's Volume to 0 and the new one's up. Outside every zone, play a default track.

  • Run it on the client: each player hears their own area, and the server sends nothing.
  • Use your own audio or audio from the Creator Store that your experience is allowed to use, or it will not play.
  • A zone check by position is simpler and steadier than Touched and TouchEnded, which fire many times as players move.

Get the script

Area music script: music zones with cross-fade

One LocalScript that plays the music of the zone the player stands in, fades between tracks and falls back to a default track outside every zone.

LocalScript in StarterPlayerScripts named MusicZones
-- LocalScript in StarterPlayerScripts named MusicZones.
-- Plays the music of the zone the player stands in, with a cross-fade.
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")
local SoundService = game:GetService("SoundService")
local TweenService = game:GetService("TweenService")

local ZONE_TAG = "MusicZone"
local FADE_SECONDS = 1.5
local CHECK_EVERY = 0.5 -- seconds between zone checks
local DEFAULT_SOUND_ID = "" -- music outside every zone, or "" for silence
local DEFAULT_VOLUME = 0.5

local player = Players.LocalPlayer

-- one Sound per SoundId, made the first time it is needed
local sounds: { [string]: Sound } = {}
local currentId = ""

local function soundFor(soundId: string): Sound
	local existing = sounds[soundId]
	if existing then
		return existing
	end
	local sound = Instance.new("Sound")
	sound.SoundId = soundId
	sound.Looped = true
	sound.Volume = 0
	sound.Parent = SoundService
	sounds[soundId] = sound
	return sound
end

local function numberAttribute(instance: Instance, name: string, default: number): number
	local value = instance:GetAttribute(name)
	return if typeof(value) == "number" then value else default
end

local function isInside(zone: BasePart, position: Vector3): boolean
	local localPoint = zone.CFrame:PointToObjectSpace(position)
	local half = zone.Size / 2
	return math.abs(localPoint.X) <= half.X and math.abs(localPoint.Y) <= half.Y and math.abs(localPoint.Z) <= half.Z
end

-- the zone the position is in; the highest Priority wins where zones overlap
local function zoneAt(position: Vector3): BasePart?
	local best: BasePart? = nil
	local bestPriority = -math.huge
	for _, zone in CollectionService:GetTagged(ZONE_TAG) do
		if zone:IsA("BasePart") and isInside(zone, position) then
			local priority = numberAttribute(zone, "Priority", 0)
			if priority > bestPriority then
				best, bestPriority = zone, priority
			end
		end
	end
	return best
end

local function playTrack(soundId: string, volume: number)
	if soundId == currentId then
		return
	end
	local fade = TweenInfo.new(FADE_SECONDS, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)
	if currentId ~= "" then
		local old = soundFor(currentId)
		local fadeOut = TweenService:Create(old, fade, { Volume = 0 })
		fadeOut.Completed:Connect(function(state: Enum.PlaybackState)
			if state == Enum.PlaybackState.Completed and old.SoundId ~= currentId then
				old:Pause() -- keep its place in case the player walks back
			end
		end)
		fadeOut:Play()
	end
	currentId = soundId
	if soundId ~= "" then
		local new = soundFor(soundId)
		if not new.IsPlaying then
			new:Resume()
		end
		TweenService:Create(new, fade, { Volume = volume }):Play()
	end
end

while true do
	local character = player.Character
	local root = character and character:FindFirstChild("HumanoidRootPart")
	if root and root:IsA("BasePart") then
		local zone = zoneAt(root.Position)
		if zone then
			local id = zone:GetAttribute("SoundId")
			playTrack(if typeof(id) == "string" then id else "", numberAttribute(zone, "Volume", DEFAULT_VOLUME))
		else
			playTrack(DEFAULT_SOUND_ID, DEFAULT_VOLUME)
		end
	end
	task.wait(CHECK_EVERY)
end

RoCode builds it in the place you have open in Roblox Studio, on Windows or macOS with a desktop browser; on a phone the button sends the prompt and the link to your computer. Free to start, with free credits every day. More Roblox Studio scripts

Where to put it

Explorer: one LocalScript and tagged zone parts
StarterPlayer
  StarterPlayerScripts
    MusicZones LocalScript new
Workspace
  Zones Folder new
    Lobby Part (tag: MusicZone)
    Arena Part (tag: MusicZone)

Settings you can change

FADE_SECONDSIn the script
How long one track fades into the next. Default: 1.5.
DEFAULT_SOUND_IDIn the script
Music outside every zone, such as "rbxassetid://1234567890". Empty plays nothing. Default: "".

Each zone part needs a string attribute SoundId, and optionally a number Volume and a number Priority for overlapping zones.

Type-checked with luau-lsp strict against Roblox's API definitions on 5 November 2026. Not playtested in Studio yet: press Play and watch the Output window before you publish.

Prompt for RoCode

Add area music: invisible parts tagged MusicZone with a SoundId attribute, and one LocalScript that plays the music of the zone the player is in, fades between tracks over 1.5 seconds and plays a default track outside every zone.

On this page
  1. Get the script
  2. 1. Place zones
  3. 2. Zone script
  4. 3. Overlaps
  5. 4. Mute and playlist
  6. Common errors
  7. With RoCode
  8. Questions
  9. Sources

Step 1: Place the zones

  1. In Workspace, insert a Folder named Zones.
  2. Insert a Part into it and scale it to cover the first area, floor to well above head height. Name it after the area, such as Lobby.
  3. Set Transparency to 1, untick CanCollide, CanTouch and CanQuery, and tick Anchored. The box is now invisible and nothing interacts with it.
  4. Add the tag MusicZone (Properties window, Tags, +).
  5. Add a string attribute named SoundId and paste an audio ID, such as rbxassetid://1234567890. Optionally add a number attribute Volume (0.5 is a good start).
  6. Repeat for each area. Zones can be rotated; the script measures inside the box whatever its angle.

To see the zones while you build, set their Transparency to 0.8 for a moment, then back to 1.

Step 2: The music zone script

Insert a LocalScript in StarterPlayer, StarterPlayerScripts named MusicZones:

LocalScript in StarterPlayerScripts named MusicZones
-- LocalScript in StarterPlayerScripts named MusicZones.
-- Plays the music of the zone the player stands in, with a cross-fade.
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")
local SoundService = game:GetService("SoundService")
local TweenService = game:GetService("TweenService")

local ZONE_TAG = "MusicZone"
local FADE_SECONDS = 1.5
local CHECK_EVERY = 0.5 -- seconds between zone checks
local DEFAULT_SOUND_ID = "" -- music outside every zone, or "" for silence
local DEFAULT_VOLUME = 0.5

local player = Players.LocalPlayer

-- one Sound per SoundId, made the first time it is needed
local sounds: { [string]: Sound } = {}
local currentId = ""

local function soundFor(soundId: string): Sound
	local existing = sounds[soundId]
	if existing then
		return existing
	end
	local sound = Instance.new("Sound")
	sound.SoundId = soundId
	sound.Looped = true
	sound.Volume = 0
	sound.Parent = SoundService
	sounds[soundId] = sound
	return sound
end

local function numberAttribute(instance: Instance, name: string, default: number): number
	local value = instance:GetAttribute(name)
	return if typeof(value) == "number" then value else default
end

local function isInside(zone: BasePart, position: Vector3): boolean
	local localPoint = zone.CFrame:PointToObjectSpace(position)
	local half = zone.Size / 2
	return math.abs(localPoint.X) <= half.X and math.abs(localPoint.Y) <= half.Y and math.abs(localPoint.Z) <= half.Z
end

-- the zone the position is in; the highest Priority wins where zones overlap
local function zoneAt(position: Vector3): BasePart?
	local best: BasePart? = nil
	local bestPriority = -math.huge
	for _, zone in CollectionService:GetTagged(ZONE_TAG) do
		if zone:IsA("BasePart") and isInside(zone, position) then
			local priority = numberAttribute(zone, "Priority", 0)
			if priority > bestPriority then
				best, bestPriority = zone, priority
			end
		end
	end
	return best
end

local function playTrack(soundId: string, volume: number)
	if soundId == currentId then
		return
	end
	local fade = TweenInfo.new(FADE_SECONDS, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)
	if currentId ~= "" then
		local old = soundFor(currentId)
		local fadeOut = TweenService:Create(old, fade, { Volume = 0 })
		fadeOut.Completed:Connect(function(state: Enum.PlaybackState)
			if state == Enum.PlaybackState.Completed and old.SoundId ~= currentId then
				old:Pause() -- keep its place in case the player walks back
			end
		end)
		fadeOut:Play()
	end
	currentId = soundId
	if soundId ~= "" then
		local new = soundFor(soundId)
		if not new.IsPlaying then
			new:Resume()
		end
		TweenService:Create(new, fade, { Volume = volume }):Play()
	end
end

while true do
	local character = player.Character
	local root = character and character:FindFirstChild("HumanoidRootPart")
	if root and root:IsA("BasePart") then
		local zone = zoneAt(root.Position)
		if zone then
			local id = zone:GetAttribute("SoundId")
			playTrack(if typeof(id) == "string" then id else "", numberAttribute(zone, "Volume", DEFAULT_VOLUME))
		else
			playTrack(DEFAULT_SOUND_ID, DEFAULT_VOLUME)
		end
	end
	task.wait(CHECK_EVERY)
end

Sounds parented to SoundService play everywhere at the same volume, which is what background music wants. Each track keeps its place when it fades out (Pause, then Resume), so walking out of the arena and back does not restart its song. A new tween on the same Sound's Volume cancels the old one, so quickly stepping in and out of a zone fades smoothly instead of jumping.

Step 3: Overlapping zones and ambience

A building inside a forest is a small zone inside a big one. Give the inner zone a number attribute Priority of 1 (the outer one has 0 by default) and the script picks the inner one while the player is in both.

Zone attributes
AttributeTypeDefaultWhat it does
SoundIdstringnoneThe track for this zone. Empty means silence here.
Volumenumber0.5How loud this zone's track plays, 0 to 10 (above 1 is louder than the file).
Prioritynumber0Which zone wins where zones overlap. Higher wins.

For ambience such as wind or a river under the music, the simplest way is a Sound inside a part in the world: Roblox plays a Sound in a Part from that part's position, louder as you get close, using its RollOffMinDistance and RollOffMaxDistance. Put a looping river sound in a part by the river and it fades with distance with no script at all.

Step 4: A mute button and the music player

Players appreciate a way to turn music off. Put every zone Sound in a SoundGroup and set the group's Volume to 0 to mute them all at once. In the script, create a SoundGroup in SoundService once, and add sound.SoundGroup = musicGroup in soundFor. A settings toggle then only changes musicGroup.Volume. The settings menu guide builds toggles that save.

If you want a playlist that skips between songs rather than area music, the music player guide does that. The two can share a SoundGroup and a mute button.

Common errors and fixes

Open the Output window (Window menu, then Output) before you press Play.

No music plays anywhere

Check that the script is a LocalScript in StarterPlayerScripts, the zones have the tag MusicZone and a SoundId string attribute, and that the ID is audio your experience may use. A "Failed to load sound" line in Output means the audio is not allowed or does not exist.

The music does not change when I walk into a zone

The zone box may not reach your character's middle: make it taller. During Play, set the zone's Transparency to 0.5 to see where it really is. Also check its tag is spelled exactly.

Two zones fight and the music flips back and forth

They overlap with the same priority. Give the one that should win a higher Priority attribute.

Everyone hears the same music

The sounds are being played by a server Script. Area music should be in a LocalScript, so each player hears their own zone.

Build it with RoCode

If you would rather not set this up by hand, RoCode can build it in the place you have open. RoCode is an independent AI agent for Roblox Studio, not made by Roblox: you describe the change in a web chat, and its Studio plugin creates the scripts and instances in your Explorer, so there is no code to paste.

You type

Add area music to my map: invisible zone parts over the lobby, the arena and the forest, each tagged MusicZone with a SoundId attribute I will fill in, and a LocalScript that cross-fades to the zone's track and plays the lobby track outside every zone.

RoCode does
  • Can look at your map first with Search Game, so it sizes the zone parts to the areas you name
  • Creates the zone parts with Create Instance and writes the LocalScript with Create Script
  • Is told to compile-check each script it writes with Check Script
  • Sets an undo point before and after each batch of changes, so Ctrl+Z steps back one batch at a time

You choose the audio: use IDs your experience is allowed to play. Then walk between the areas yourself to hear the fades. How RoCode connects to Studio.

Questions

How do I make music play in a certain area in Roblox?

Cover the area with an invisible part tagged MusicZone with a SoundId attribute, and use a LocalScript that checks which zone the player is in and plays its sound, as in Step 2.

Should area music be in a Script or a LocalScript?

A LocalScript, so each player hears the music of their own area. A server Script would play the same music for everyone.

Why not use Touched and TouchEnded for zones?

They fire repeatedly as body parts move, and TouchEnded can fire while the player is still inside. Checking the position against the box twice a second is steadier.

Why does my music not play?

Usually the audio is not one your experience is allowed to use. Use audio you uploaded, or audio its owner made available to all experiences.

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, 5 November 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.