This is a small lightweight system that I use in my game for playing randomized one-off sounds in UI and in 3D space. It allows you to choose from a series of sounds without accidentally repeating the same sound.
You can use this system to:
- Play single-shot sounds in
- 3D space
- GUI
- Replicate a sound in 3D space to all clients (or all clients but one).
I’m posting it here since I open-sourced it in my Discord server today. It might be a quick drop-in for your game. Audio design is a tremendous source of juice/satisfaction in video games, and is a piece of polish often neglected.
Code and Examples:
SoundProvider.rbxl (52,9 KB)
The sounds configured in the above example are for the purpose of demonstrating how to use the module. You can add or remove them and organize them within the sounds table as you see fit.
Usage
Sound Selection
SoundData is defined within SoundProvider.Sounds. This data contains a number for the previous sound played (so we can avoid playing duplicates, and accidentally re-playing sounds that are currently playing), and a list of sound instances to choose from.
You must pass SoundData to GetNext and GetOneOf to obtain a sound to play with Play, PlayAt, or PlayForOthers.
The Sound instances specified in SoundData support an attribute called TimePositionOffset, which allows you to specify an alternative starting time for audio that has been trimmed improperly, such as some audio already uploaded on Roblox.
type SoundData = {
current:number, -- The previously played sound from the list of sounds.
sounds:{Sound} -- A list of sound instances to pick from for this type of sound.
}
An example SoundData entry:
hit = {
current = 0,
sounds = {
script:WaitForChild("Hit1"),
script:WaitForChild("Hit2"),
script:WaitForChild("Hit3"),
script:WaitForChild("Hit4"),
script:WaitForChild("Hit5"),
script:WaitForChild("Hit6"),
}
},
GetNext(SoundData, number)
Choose the next sound from the sound data.
GetOneOf(SoundData)
Choose a random sound from the sound data that was not the previously played sound from this data.
Sound Playback
Play(Sound)
Play a sound (such as for UI).
PlayAt(Sound, Vector3)
Play a sound at a location.
PlayForOthers(playerToExclude:Player, Sound, Vector3)
Play a sound for everyone in the server at a location, except for the specified player. (e.g. you played the sound for them already locally, and the server needs to play it for everyone else).
Note that in most cases, you want to detect when you want to play a sound locally on all clients and play it there instead, rather than using this approach. Detecting when to play the sound locally instead can reduce latency for clients and reduce network usage.
PlayForOthers is intended for niche situations where design does not easily allow this, or when there are no concrete client-side events for clients to use to know when to play the sound.
Known Issues
Playing a sound from SoundData with a very short list of sounds too frequently can result in the previous play of that sound being cut off. To avoid this, you should avoid rapidly playing sounds, or provide sufficient sound variants (or the same sound multiple times) in the SoundData sounds array.
Code
SoundProvider ModuleScript
--!strict
local RunService = game:GetService("RunService")
local sounds = require(script:WaitForChild("Sounds"))
local soundRemote:RemoteEvent = script:WaitForChild("ReplicateSound")
local random:Random = Random.new()
----------------------------
local module = {}
module.sounds = sounds
module.replicateSoundEvent = soundRemote
----------------------------
-- Choose the next sound from the sound data.
function module.GetNext(soundInstanceData:sounds.SoundData, currentIndex:number)
local sounds:{Sound} = soundInstanceData.sounds
soundInstanceData.current = (soundInstanceData.current+1) % #sounds
return sounds[soundInstanceData.current+1]
end
-- Choose a random sound from the sound data that was not the previously played sound from this data.
function module.GetOneOf(soundInstanceData:sounds.SoundData)
local sounds:{Sound} = soundInstanceData.sounds
soundInstanceData.current = (soundInstanceData.current + random:NextInteger(1, #sounds-1)) % #sounds
return sounds[soundInstanceData.current+1]
end
-- Play a sound (such as for UI).
function module.Play(sound:Sound)
local offset:number = sound:GetAttribute("TimePositionOffset")
sound.TimePosition = if offset ~= nil then offset else 0
sound:Play()
end
-- Play a sound at a location.
function module.PlayAt(sound:Sound, position:Vector3)
local sound:Sound = sound:Clone()
local att:Attachment = Instance.new("Attachment")
sound.Parent = att
att.WorldPosition = position
att.Parent = workspace.Terrain
local offset:number = sound:GetAttribute("TimePositionOffset")
sound.TimePosition = if offset ~= nil then offset else 0
sound.Ended:Connect(function()
sound:Destroy()
att:Destroy()
end)
sound:Play()
end
-- Play a sound for everyone in the server at a location, except for the specified player
-- (e.g. you played the sound for them already locally, and the server needs to play it for everyone else).
---- Note that in most cases, you want to detect when you want to play a sound locally on all clients and play it there instead, rather than using
---- this approach. Detecting when to play the sound locally instead can reduce latency for other clients and reduce network activity.
---- PlayForOthers is intended for niche situations where design does not easily allow this, or when there are no concrete client-side events for
---- clients to use to know when to play the sound.
if RunService:IsServer() then
function module.PlayForOthers(playerToExclude:Player, sound:Sound, position:Vector3)
soundRemote:FireAllClients(playerToExclude, sound, position)
end
end
----------------------------
return module
SoundProvider.Sound ModuleScript
--!strict
export type SoundData = {
current:number, -- The previously played sound from the list of sounds.
sounds:{Sound} -- A list of sound instances to pick from for this type of sound.
}
----------------------------
local sounds = {
hit = {
current = 0,
sounds = {
script:WaitForChild("Hit1"),
script:WaitForChild("Hit2"),
script:WaitForChild("Hit3"),
script:WaitForChild("Hit4"),
script:WaitForChild("Hit5"),
script:WaitForChild("Hit6"),
}
},
attack = {
current = 0,
sounds = {
script:WaitForChild("AttackSwoosh1"),
script:WaitForChild("AttackSwoosh2"),
script:WaitForChild("AttackSwoosh3"),
}
},
buttonHover = {
current = 0,
sounds = {
script:WaitForChild("ButtonHover1"),
script:WaitForChild("ButtonHover2"),
script:WaitForChild("ButtonHover3"),
}
},
buttonPress = {
current = 0,
sounds = {
script:WaitForChild("ButtonPress1"),
script:WaitForChild("ButtonPress2"),
script:WaitForChild("ButtonPress3"),
}
},
buttonPressSpecial = {
current = 0,
sounds = {
script:WaitForChild("ButtonPressSpecial1"),
script:WaitForChild("ButtonPressSpecial2"),
script:WaitForChild("ButtonPressSpecial3"),
}
},
}
return sounds
Examples
PlayGUILocal LocalScript
local SoundProvider = require(game:GetService("ReplicatedStorage"):WaitForChild("SoundProvider"))
--------------------------
-- Set up hover sounds
for _,button in pairs(script.Parent:GetChildren()) do
if button:IsA("GuiButton") then
button.MouseEnter:Connect(function()
SoundProvider.Play(SoundProvider.GetNext(SoundProvider.sounds.buttonHover))
end)
end
end
--------------------------
-- Set up specific sounds
script.Parent:WaitForChild("Hit").Activated:Connect(function()
SoundProvider.Play(SoundProvider.GetOneOf(SoundProvider.sounds.hit))
end)
script.Parent:WaitForChild("Attack").Activated:Connect(function()
SoundProvider.Play(SoundProvider.GetOneOf(SoundProvider.sounds.attack))
end)
script.Parent:WaitForChild("Button").Activated:Connect(function()
SoundProvider.Play(SoundProvider.GetNext(SoundProvider.sounds.buttonPress))
end)
script.Parent:WaitForChild("SpecialButton").Activated:Connect(function()
SoundProvider.Play(SoundProvider.GetNext(SoundProvider.sounds.buttonPressSpecial))
end)
PlayContinuousServer Script
local Players = game:GetService("Players")
local SoundProvider = require(game:GetService("ReplicatedStorage"):WaitForChild("SoundProvider"))
local color:Color3 = script.Parent.Color
-------------------------------
while true do
task.wait(0.5)
SoundProvider.PlayForOthers(nil, SoundProvider.GetNext(SoundProvider.sounds.buttonPress), script.Parent.Position) -- script.Parent also works.
script.Parent.Color = Color3.new(1,0,0)
task.wait(0.5)
script.Parent.Color = color
end
PlayPositionalServer Script
local Players = game:GetService("Players")
local SoundProvider = require(game:GetService("ReplicatedStorage"):WaitForChild("SoundProvider"))
-------------------------------
local debounce:boolean = false
script.Parent.Touched:Connect(function(hit)
if debounce then return end
debounce = true
-- We exclude the player who touched the button because they are playing the sound locally immediately when they touch it (see: PlayAtLocal).
-- If we only played the sound on the server, there would be a delay between the player pressing the button, and them hearing the sound.
local character:Model = hit:FindFirstAncestorOfClass("Model")
local player:Player? = Players:GetPlayerFromCharacter(character)
if player ~= nil then
SoundProvider.PlayForOthers(player, SoundProvider.GetNext(SoundProvider.sounds.buttonPress), script.Parent.Position)
end
wait(1)
debounce = false
end)
PlayPositionalLocal LocalScript
local Players = game:GetService("Players")
local SoundProvider = require(game:GetService("ReplicatedStorage"):WaitForChild("SoundProvider"))
-------------------------------
local debounce:boolean = false
script.Parent.Touched:Connect(function(hit)
if debounce then return end
debounce = true
-- Play the sound locally, no other player except for this client can hear this sound.
-- We use PlayPositionalServer instead to send the sound to everyone else.
SoundProvider.PlayAt(SoundProvider.GetNext(SoundProvider.sounds.buttonPress), script.Parent.Position)
wait(1)
debounce = false
end)
Further Reading
You can find free sound effects already uploaded on Roblox: https://create.roblox.com/marketplace/soundeffects
Or on various CC0 free sound websites like MixKit, FreeSound, etc.