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.
-- 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
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
Step 1: Place the zones
- In Workspace, insert a Folder named
Zones. - 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. - Set
Transparencyto 1, untickCanCollide,CanTouchandCanQuery, and tickAnchored. The box is now invisible and nothing interacts with it. - Add the tag
MusicZone(Properties window, Tags, +). - Add a string attribute named
SoundIdand paste an audio ID, such asrbxassetid://1234567890. Optionally add a number attributeVolume(0.5 is a good start). - 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.
-- 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.
| Attribute | Type | Default | What it does |
|---|---|---|---|
SoundId | string | none | The track for this zone. Empty means silence here. |
Volume | number | 0.5 | How loud this zone's track plays, 0 to 10 (above 1 is louder than the file). |
Priority | number | 0 | Which 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.
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.
- 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
- Roblox Creator Docs: Sound. SoundId, Looped, Volume, Resume and roll-off
- Roblox Creator Docs: Audio assets. which audio an experience may use
- Roblox Creator Docs: SoundGroup
- Roblox Creator Docs: CFrame:PointToObjectSpace
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.