Sound Region

Learn how to make an advanced sound region system

by Tom_minecraft

Author Avatar

Introduction:

This tutorial will demonstrate how to create a sound region with parts. Upon entering a part (zone), the sound will be played, and upon leaving the part (zone), the sound will cease.


Setup:

image|100x100

image|100x100

image|100x100

image|100x100


Now, let's code!

Getting services:

Let's start by getting the services to use them later.

local Players = game:GetService("Players") -- Get player service
local SoundService = game:GetService("SoundService") -- Get sound service
local RunService = game:GetService("RunService") -- get run service for heartbeat, ...
local player = Players.LocalPlayer -- get local player (client)

Zone Function

Now, let's make a function which will handle the zone system, for every zone parts, this function will bee called

local function zone_added(part:BasePart)
	
end

First, we will check if the part is a basepart and check if there is a sound in there. If no sound is found, then the zone wont work.

local function zone_added(part:BasePart)
	if part:IsA("BasePart") then -- Check if the instance is a basepart, to avoid errors
		local sound = part:FindFirstChildOfClass("Sound")
		if sound ~= nil then -- check if there is a sound instance which will be played when walking in the part, if there is no sound instance then it won't work
			
		end
	end
end

Let's configure the sound, by looping it and cloning it into the SoundService to hear it. We will also set the volume to 0 and create an old volume variable which will store the max volume.

local function zone_added(part:BasePart)
	if part:IsA("BasePart") then -- Check if the instance is a basepart, to avoid errors
		local sound = part:FindFirstChildOfClass("Sound")
		if sound ~= nil then -- check if there is a sound instance which will be played when walking in the part, if there is no sound instance then it won't work
			sound = sound:Clone()
			sound.Parent = SoundService 
			local oldVolume = tonumber(sound.Volume) -- Volume that will be set when entering the zone
			sound.Volume = 0 -- Set the volume to 0
			sound.Looped = true -- loop the sound
		end
	end
end

After doing that, we can now start by making a variable to check if the player is in the part, let's name it isInPart and it'll be a boolean. We'll also create a variable which will store a coroutine to make coroutines for our system. They will be useful for cancelling loops when the player will exit a zone or something else.

local function zone_added(part:BasePart)
	if part:IsA("BasePart") then -- Check if the instance is a basepart, to avoid errors
		local sound = part:FindFirstChildOfClass("Sound")
		if sound ~= nil then -- check if there is a sound instance which will be played when walking in the part, if there is no sound instance then it won't work
			sound = sound:Clone()
			sound.Parent = SoundService 
			local oldVolume = tonumber(sound.Volume) -- Volume that will be set when entering the zone
			sound.Volume = 0 -- Set the volume to 0
			sound.Looped = true -- loop the sound
			
			local isInPart = false -- check if the player is in the zone or not
			local actualCoroutine = coroutine.create(function() -- Create a coroutine

			end)
		end
	end
end

Now, everything is ready to be connected. Let's connect the touch event and the touch ended event. We will detect when the player enters the zone with the Touched event and when the player leaves the zone with the TouchEnded event.

local function zone_added(part:BasePart)
	if part:IsA("BasePart") then -- Check if the instance is a basepart, to avoid errors
		local sound = part:FindFirstChildOfClass("Sound")
		if sound ~= nil then -- check if there is a sound instance which will be played when walking in the part, if there is no sound instance then it won't work
			sound = sound:Clone()
			sound.Parent = SoundService 
			local oldVolume = tonumber(sound.Volume) -- Volume that will be set when entering the zone
			sound.Volume = 0 -- Set the volume to 0
			sound.Looped = true -- loop the sound

			local isInPart = false -- check if the player is in the zone or not
			local actualCoroutine = coroutine.create(function() -- Create a coroutine

			end)

			part.Touched:Connect(function(hit:BasePart)

			end)

			part.TouchEnded:Connect(function(hit:BasePart)

			end)
		end
	end
end

Now, we connected the events. Let's add some script in, the hardest part for the script. Let's start by detecting if the hit part is the player and if the player is in or is not in the zone (depends for each event)

local function zone_added(part:BasePart)
	if part:IsA("BasePart") then -- Check if the instance is a basepart, to avoid errors
		local sound = part:FindFirstChildOfClass("Sound")
		if sound ~= nil then -- check if there is a sound instance which will be played when walking in the part, if there is no sound instance then it won't work
			sound = sound:Clone()
			sound.Parent = SoundService 
			local oldVolume = tonumber(sound.Volume) -- Volume that will be set when entering the zone
			sound.Volume = 0 -- Set the volume to 0
			sound.Looped = true -- loop the sound

			local isInPart = false -- check if the player is in the zone or not
			local actualCoroutine = coroutine.create(function() -- Create a coroutine

			end)

			part.Touched:Connect(function(hit:BasePart)
				if hit.Parent == player.Character and isInPart == false then
					
				end
			end)

			part.TouchEnded:Connect(function(hit:BasePart)
				if hit.Parent == player.Character and isInPart == true then
					
				end
			end)
		end
	end
end

We can start the hardest part. First, we will set the variable isInPart to true for the touched event and to false for the touchended event when the player will get in.

local function zone_added(part:BasePart)
	if part:IsA("BasePart") then -- Check if the instance is a basepart, to avoid errors
		local sound = part:FindFirstChildOfClass("Sound")
		if sound ~= nil then -- check if there is a sound instance which will be played when walking in the part, if there is no sound instance then it won't work
			sound = sound:Clone()
			sound.Parent = SoundService 
			local oldVolume = tonumber(sound.Volume) -- Volume that will be set when entering the zone
			sound.Volume = 0 -- Set the volume to 0
			sound.Looped = true -- loop the sound

			local isInPart = false -- check if the player is in the zone or not
			local actualCoroutine = coroutine.create(function() -- Create a coroutine

			end)

			part.Touched:Connect(function(hit:BasePart)
				if hit.Parent == player.Character and isInPart == false then
					isInPart = true
				end
			end)

			part.TouchEnded:Connect(function(hit:BasePart)
				if hit.Parent == player.Character and isInPart == true then
					isInPart = false
				end
			end)
		end
	end
end

Let's check the status of the coroutine that we created, named actualCoroutine. If this coroutine is running, we will cancel it to break the loop.

local function zone_added(part:BasePart)
	if part:IsA("BasePart") then -- Check if the instance is a basepart, to avoid errors
		local sound = part:FindFirstChildOfClass("Sound")
		if sound ~= nil then -- check if there is a sound instance which will be played when walking in the part, if there is no sound instance then it won't work
			sound = sound:Clone()
			sound.Parent = SoundService 
			local oldVolume = tonumber(sound.Volume) -- Volume that will be set when entering the zone
			sound.Volume = 0 -- Set the volume to 0
			sound.Looped = true -- loop the sound

			local isInPart = false -- check if the player is in the zone or not
			local actualCoroutine = coroutine.create(function() -- Create a coroutine

			end)

			part.Touched:Connect(function(hit:BasePart)
				if hit.Parent == player.Character and isInPart == false then
					isInPart = true
					if coroutine.status(actualCoroutine) == "running" then -- check if actualCoroutine is running, if it is, then it will stop it and stop the loops, ... which are inside the coroutine
						coroutine.close(actualCoroutine)
					end
				end
			end)

			part.TouchEnded:Connect(function(hit:BasePart)
				if hit.Parent == player.Character and isInPart == true then
					isInPart = false
					if coroutine.status(actualCoroutine) == "running" then -- check if actualCoroutine is running, if it is, then it will stop it and stop the loops, ... which are inside the coroutine
						coroutine.close(actualCoroutine)
					end
				end
			end)
		end
	end
end

Now, we will define the coroutine and what will be inside. Inside of the coroutine, we will set:

  1. Playing the sound
  2. A smooth volume transition (- to +)
local function zone_added(part:BasePart)
	if part:IsA("BasePart") then -- Check if the instance is a basepart, to avoid errors
		local sound = part:FindFirstChildOfClass("Sound")
		if sound ~= nil then -- check if there is a sound instance which will be played when walking in the part, if there is no sound instance then it won't work
			sound = sound:Clone()
			sound.Parent = SoundService 
			local oldVolume = tonumber(sound.Volume) -- Volume that will be set when entering the zone
			sound.Volume = 0 -- Set the volume to 0
			sound.Looped = true -- loop the sound

			local isInPart = false -- check if the player is in the zone or not
			local actualCoroutine = coroutine.create(function() -- Create a coroutine

			end)

			part.Touched:Connect(function(hit:BasePart)
				if hit.Parent == player.Character and isInPart == false then
					isInPart = true
					if coroutine.status(actualCoroutine) == "running" then -- check if actualCoroutine is running, if it is, then it will stop it and stop the loops, ... which are inside the coroutine
						coroutine.close(actualCoroutine)
					end
					
					actualCoroutine = coroutine.create(function() -- create a new coroutine
						sound:Play() -- play the target sound
						for i = 0, oldVolume, 0.01 do -- turn up the sound
							if isInPart == false then
								break
							end

							sound.Volume = i
							RunService.Heartbeat:Wait()
						end					
					end)

					coroutine.resume(actualCoroutine) -- start the new coroutine
				end
			end)

			part.TouchEnded:Connect(function(hit:BasePart)
				if hit.Parent == player.Character and isInPart == true then
					isInPart = false
					if coroutine.status(actualCoroutine) == "running" then -- check if actualCoroutine is running, if it is, then it will stop it and stop the loops, ... which are inside the coroutine
						coroutine.close(actualCoroutine)
					end
				end
			end)
		end
	end
end

  1. A smooth volume transition (+ to -)
  2. Stopping the sound (but before, we must check if the coroutine is still running and is not dead / suspended)
local function zone_added(part:BasePart)
	if part:IsA("BasePart") then -- Check if the instance is a basepart, to avoid errors
		local sound = part:FindFirstChildOfClass("Sound")
		if sound ~= nil then -- check if there is a sound instance which will be played when walking in the part, if there is no sound instance then it won't work
			sound = sound:Clone()
			sound.Parent = SoundService 
			local oldVolume = tonumber(sound.Volume) -- Volume that will be set when entering the zone
			sound.Volume = 0 -- Set the volume to 0
			sound.Looped = true -- loop the sound

			local isInPart = false -- check if the player is in the zone or not
			local actualCoroutine = coroutine.create(function() -- Create a coroutine

			end)

			part.Touched:Connect(function(hit:BasePart)
				if hit.Parent == player.Character and isInPart == false then
					isInPart = true
					if coroutine.status(actualCoroutine) == "running" then -- check if actualCoroutine is running, if it is, then it will stop it and stop the loops, ... which are inside the coroutine
						coroutine.close(actualCoroutine)
					end
					
					actualCoroutine = coroutine.create(function() -- create a new coroutine
						sound:Play() -- play the target sound
						for i = 0, oldVolume, 0.01 do -- turn up the sound
							if isInPart == false then
								break
							end

							sound.Volume = i
							RunService.Heartbeat:Wait()
						end					
					end)

					coroutine.resume(actualCoroutine) -- start the new coroutine
				end
			end)

			part.TouchEnded:Connect(function(hit:BasePart)
				if hit.Parent == player.Character and isInPart == true then
					isInPart = false
					if coroutine.status(actualCoroutine) == "running" then -- check if actualCoroutine is running, if it is, then it will stop it and stop the loops, ... which are inside the coroutine
						coroutine.close(actualCoroutine)
					end
					
					actualCoroutine = coroutine.create(function() -- create a new coroutine
						for i = sound.Volume, 0, -.01 do -- turn off the sound
							if isInPart == true then
								break
							end

							sound.Volume = i -- set the volume
							RunService.Heartbeat:Wait() -- wait for next heartbeat event fires
						end
						if coroutine.status(actualCoroutine) == "running" then -- check if the coroutine hasn't be closed, and if it isn't, the sound will stop
							sound:Stop()
						end
					end)

					coroutine.resume(actualCoroutine) -- start the new coroutine
				end
			end)
		end
	end
end

Connections

We finished to make the zone function. Now, let's connect it!

  1. We will make a loop by taking all zones folder children to connect them to the function:
for _, v in pairs(workspace:WaitForChild("Zones"):GetChildren()) do
	zone_added(v) 
end
  1. Let's make a Roblox Signal Connection to connect every new children to the main function:
workspace:WaitForChild("Zones").ChildAdded:Connect(zone_added) -- if a zone is added while in game it will be fired to the function and the system will work with this part

How to use the code?

It could be hard for you to take every code parts from here and to insert them into your LocalScript but I have no choice. I can't send the full script because there is a text limit but you can contact me if you have any problem (message roblox).

View in-game to comment, award, and more!