In short
Store each setting as an attribute on the Player, such as Setting_Music. A server Script loads saved values from a DataStore into those attributes on join, accepts changes through a RemoteEvent (only known names and true or false), and saves on leave. A LocalScript draws the switches, applies each setting on the device, and re-applies whenever the attribute changes.
- Settings like music volume and shadows are applied by a LocalScript, so they only change that player's game.
- The server keeps a list of allowed settings and their defaults, and ignores anything else a client sends.
- If loading failed, do not save: an empty load must never overwrite a player's real settings.
Get the script
Settings menu with music and shadow switches
A LocalScript with a Settings button and a panel of switches for music, sound effects and shadows that apply at once on the player's device.
-- LocalScript in StarterPlayerScripts named SettingsMenu.
-- A gear button opens a panel of on/off switches. Each switch changes the game on
-- this device straight away, and the server saves it for next time.
local Lighting = game:GetService("Lighting")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SoundService = game:GetService("SoundService")
local changeEvent = ReplicatedStorage:WaitForChild("ChangeSetting") :: RemoteEvent
local player = Players.LocalPlayer
-- The switches in the order they appear, with the text shown
local SETTINGS = {
{ name = "Music", label = "Music" },
{ name = "SoundEffects", label = "Sound effects" },
{ name = "Shadows", label = "Shadows" },
}
-- What each setting does on this device. Changes made by a LocalScript stay on this device.
local function apply(name: string, on: boolean)
if name == "Music" then
local music = SoundService:FindFirstChild("Music")
if music then
for _, sound in music:GetChildren() do
if sound:IsA("Sound") then
sound.Volume = if on then 0.4 else 0
end
end
end
elseif name == "SoundEffects" then
local effects = SoundService:FindFirstChild("Effects")
if effects and effects:IsA("SoundGroup") then
effects.Volume = if on then 0.5 else 0
end
elseif name == "Shadows" then
Lighting.GlobalShadows = on
end
end
local function get(name: string): boolean
return player:GetAttribute("Setting_" .. name) ~= false
end
-- The gear button and the panel
local gui = Instance.new("ScreenGui")
gui.Name = "SettingsGui"
gui.ResetOnSpawn = false
local function corner(parent: Instance, radius: number)
local c = Instance.new("UICorner")
c.CornerRadius = UDim.new(0, radius)
c.Parent = parent
end
local gear = Instance.new("TextButton")
gear.Text = "Settings"
gear.Size = UDim2.fromOffset(100, 40)
gear.Position = UDim2.new(0, 12, 0.5, -52)
gear.BackgroundColor3 = Color3.fromRGB(70, 64, 90)
gear.TextColor3 = Color3.new(1, 1, 1)
gear.Font = Enum.Font.GothamBold
gear.TextSize = 16
corner(gear, 10)
gear.Parent = gui
local panel = Instance.new("Frame")
panel.AnchorPoint = Vector2.new(0.5, 0.5)
panel.Position = UDim2.fromScale(0.5, 0.5)
panel.Size = UDim2.fromOffset(340, 60 + #SETTINGS * 52)
panel.BackgroundColor3 = Color3.fromRGB(30, 26, 40)
panel.Visible = false
corner(panel, 14)
panel.Parent = gui
local heading = Instance.new("TextLabel")
heading.Text = "Settings"
heading.Size = UDim2.new(1, 0, 0, 48)
heading.BackgroundTransparency = 1
heading.TextColor3 = Color3.new(1, 1, 1)
heading.Font = Enum.Font.GothamBold
heading.TextSize = 20
heading.Parent = panel
local switches: { [string]: TextButton } = {}
local function drawSwitch(name: string)
local on = get(name)
local switch = switches[name]
switch.Text = if on then "ON" else "OFF"
switch.BackgroundColor3 = if on then Color3.fromRGB(70, 200, 110) else Color3.fromRGB(90, 84, 104)
end
for i, setting in SETTINGS do
local row = Instance.new("TextLabel")
row.Text = setting.label
row.Size = UDim2.new(1, -120, 0, 44)
row.Position = UDim2.fromOffset(20, 48 + (i - 1) * 52)
row.BackgroundTransparency = 1
row.TextColor3 = Color3.new(1, 1, 1)
row.Font = Enum.Font.Gotham
row.TextSize = 17
row.TextXAlignment = Enum.TextXAlignment.Left
row.Parent = panel
local switch = Instance.new("TextButton")
switch.Size = UDim2.fromOffset(80, 36)
switch.Position = UDim2.new(1, -100, 0, 52 + (i - 1) * 52)
switch.TextColor3 = Color3.new(1, 1, 1)
switch.Font = Enum.Font.GothamBold
switch.TextSize = 16
corner(switch, 18)
switch.Parent = panel
switches[setting.name] = switch
drawSwitch(setting.name)
switch.Activated:Connect(function()
local on = not get(setting.name)
-- Change it here at once; the server checks it, stores it and saves it
player:SetAttribute("Setting_" .. setting.name, on)
changeEvent:FireServer(setting.name, on)
end)
-- Redraw and apply whenever the value changes, including when the server loads it
player:GetAttributeChangedSignal("Setting_" .. setting.name):Connect(function()
drawSwitch(setting.name)
apply(setting.name, get(setting.name))
end)
apply(setting.name, get(setting.name))
end
gear.Activated:Connect(function()
panel.Visible = not panel.Visible
end)
gui.Parent = player:WaitForChild("PlayerGui")
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
ServerScriptService
SettingsSaver Script new
StarterPlayer
StarterPlayerScripts
SettingsMenu LocalScript new
Settings you can change
SETTINGSIn the script- The switches in the menu, in order, with their labels. Each name must also be in the server's DEFAULTS.
Saving needs the SettingsSaver Script from Step 1 and Enable Studio Access to API Services to test in Studio. The Music switch works with the folder from the music player guide; Sound effects needs a SoundGroup named Effects in SoundService.
Type-checked with luau-lsp strict against Roblox's API definitions on 23 October 2026. Not playtested in Studio yet: press Play and watch the Output window before you publish.
Prompt for RoCode
Add a settings menu: a Settings button that opens a panel with ON/OFF switches for Music, Sound effects and Shadows, applied on the player's device right away, stored as attributes on the player, and saved by a server Script in a DataStore that only accepts known settings with true or false values.
How the settings menu works
| Setting | Changes | Why it helps |
|---|---|---|
| Music | Volume of every Sound in SoundService.Music | Players who listen to their own music. |
| Sound effects | Volume of the Effects SoundGroup | Quiet play. |
| Shadows | Lighting.GlobalShadows | Faster frames on older phones. |
Attributes on the Player carry the values. When the server sets one, it reaches the player's device, and GetAttributeChangedSignal lets the menu redraw and apply it. When the player flips a switch, the menu sets the attribute on its own copy for an instant response and tells the server, which checks it and stores it.
Step 1: Load and save settings on the server
Turn on Enable Studio Access to API Services in Game Settings, Security (the place must be published), then insert a Script in ServerScriptService named SettingsSaver:
-- Script in ServerScriptService named SettingsSaver.
-- Loads each player's settings into attributes on the player, and saves the changes
-- the settings menu sends. Only known settings with the right type are kept.
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Every setting and its default. The menu shows these.
local DEFAULTS: { [string]: boolean } = {
Music = true,
SoundEffects = true,
Shadows = true,
}
local store = DataStoreService:GetDataStore("PlayerSettings_v1")
local changeEvent = Instance.new("RemoteEvent")
changeEvent.Name = "ChangeSetting"
changeEvent.Parent = ReplicatedStorage
local dirty: { [Player]: boolean } = {} -- changed since the last save
local loaded: { [Player]: boolean } = {}
local function current(player: Player): { [string]: boolean }
local values: { [string]: boolean } = {}
for name, default in DEFAULTS do
local value = player:GetAttribute("Setting_" .. name)
values[name] = if type(value) == "boolean" then value else default
end
return values
end
Players.PlayerAdded:Connect(function(player: Player)
local ok, data = pcall(function()
return store:GetAsync(tostring(player.UserId))
end)
local saved = if ok and type(data) == "table" then data else {}
for name, default in DEFAULTS do
local value = saved[name]
player:SetAttribute("Setting_" .. name, if type(value) == "boolean" then value else default)
end
-- Only save later if the load worked, so a failed load never overwrites good settings
loaded[player] = ok
end)
changeEvent.OnServerEvent:Connect(function(player: Player, name: unknown, value: unknown)
if typeof(name) ~= "string" or DEFAULTS[name] == nil or typeof(value) ~= "boolean" then
return
end
player:SetAttribute("Setting_" .. name, value)
dirty[player] = true
end)
local function save(player: Player)
if not (dirty[player] and loaded[player]) then
return
end
dirty[player] = nil
local ok, err = pcall(function()
store:SetAsync(tostring(player.UserId), current(player))
end)
if not ok then
warn("SettingsSaver: could not save", player.Name, err)
end
end
Players.PlayerRemoving:Connect(function(player: Player)
save(player)
dirty[player] = nil
loaded[player] = nil
end)
game:BindToClose(function()
for _, player in Players:GetPlayers() do
task.spawn(save, player)
end
task.wait(3)
end)
DEFAULTS is the list of allowed settings. A change for a name not in it, or a value that is not true or false, is ignored. Settings save only when they changed and only if the load worked. BindToClose saves everyone when a server shuts down.
Step 2: The menu
- For the Sound effects switch, insert a SoundGroup in SoundService named
Effects, and set the SoundGroup property of your effect Sounds to it. - Insert a LocalScript in StarterPlayer > StarterPlayerScripts named
SettingsMenu:
-- LocalScript in StarterPlayerScripts named SettingsMenu.
-- A gear button opens a panel of on/off switches. Each switch changes the game on
-- this device straight away, and the server saves it for next time.
local Lighting = game:GetService("Lighting")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SoundService = game:GetService("SoundService")
local changeEvent = ReplicatedStorage:WaitForChild("ChangeSetting") :: RemoteEvent
local player = Players.LocalPlayer
-- The switches in the order they appear, with the text shown
local SETTINGS = {
{ name = "Music", label = "Music" },
{ name = "SoundEffects", label = "Sound effects" },
{ name = "Shadows", label = "Shadows" },
}
-- What each setting does on this device. Changes made by a LocalScript stay on this device.
local function apply(name: string, on: boolean)
if name == "Music" then
local music = SoundService:FindFirstChild("Music")
if music then
for _, sound in music:GetChildren() do
if sound:IsA("Sound") then
sound.Volume = if on then 0.4 else 0
end
end
end
elseif name == "SoundEffects" then
local effects = SoundService:FindFirstChild("Effects")
if effects and effects:IsA("SoundGroup") then
effects.Volume = if on then 0.5 else 0
end
elseif name == "Shadows" then
Lighting.GlobalShadows = on
end
end
local function get(name: string): boolean
return player:GetAttribute("Setting_" .. name) ~= false
end
-- The gear button and the panel
local gui = Instance.new("ScreenGui")
gui.Name = "SettingsGui"
gui.ResetOnSpawn = false
local function corner(parent: Instance, radius: number)
local c = Instance.new("UICorner")
c.CornerRadius = UDim.new(0, radius)
c.Parent = parent
end
local gear = Instance.new("TextButton")
gear.Text = "Settings"
gear.Size = UDim2.fromOffset(100, 40)
gear.Position = UDim2.new(0, 12, 0.5, -52)
gear.BackgroundColor3 = Color3.fromRGB(70, 64, 90)
gear.TextColor3 = Color3.new(1, 1, 1)
gear.Font = Enum.Font.GothamBold
gear.TextSize = 16
corner(gear, 10)
gear.Parent = gui
local panel = Instance.new("Frame")
panel.AnchorPoint = Vector2.new(0.5, 0.5)
panel.Position = UDim2.fromScale(0.5, 0.5)
panel.Size = UDim2.fromOffset(340, 60 + #SETTINGS * 52)
panel.BackgroundColor3 = Color3.fromRGB(30, 26, 40)
panel.Visible = false
corner(panel, 14)
panel.Parent = gui
local heading = Instance.new("TextLabel")
heading.Text = "Settings"
heading.Size = UDim2.new(1, 0, 0, 48)
heading.BackgroundTransparency = 1
heading.TextColor3 = Color3.new(1, 1, 1)
heading.Font = Enum.Font.GothamBold
heading.TextSize = 20
heading.Parent = panel
local switches: { [string]: TextButton } = {}
local function drawSwitch(name: string)
local on = get(name)
local switch = switches[name]
switch.Text = if on then "ON" else "OFF"
switch.BackgroundColor3 = if on then Color3.fromRGB(70, 200, 110) else Color3.fromRGB(90, 84, 104)
end
for i, setting in SETTINGS do
local row = Instance.new("TextLabel")
row.Text = setting.label
row.Size = UDim2.new(1, -120, 0, 44)
row.Position = UDim2.fromOffset(20, 48 + (i - 1) * 52)
row.BackgroundTransparency = 1
row.TextColor3 = Color3.new(1, 1, 1)
row.Font = Enum.Font.Gotham
row.TextSize = 17
row.TextXAlignment = Enum.TextXAlignment.Left
row.Parent = panel
local switch = Instance.new("TextButton")
switch.Size = UDim2.fromOffset(80, 36)
switch.Position = UDim2.new(1, -100, 0, 52 + (i - 1) * 52)
switch.TextColor3 = Color3.new(1, 1, 1)
switch.Font = Enum.Font.GothamBold
switch.TextSize = 16
corner(switch, 18)
switch.Parent = panel
switches[setting.name] = switch
drawSwitch(setting.name)
switch.Activated:Connect(function()
local on = not get(setting.name)
-- Change it here at once; the server checks it, stores it and saves it
player:SetAttribute("Setting_" .. setting.name, on)
changeEvent:FireServer(setting.name, on)
end)
-- Redraw and apply whenever the value changes, including when the server loads it
player:GetAttributeChangedSignal("Setting_" .. setting.name):Connect(function()
drawSwitch(setting.name)
apply(setting.name, get(setting.name))
end)
apply(setting.name, get(setting.name))
end
gear.Activated:Connect(function()
panel.Visible = not panel.Visible
end)
gui.Parent = player:WaitForChild("PlayerGui")
apply is where each setting does its job. Changes a LocalScript makes to Lighting and sounds stay on that device, so turning shadows off for a slow phone does not change anyone else's game. To add a setting, add it to the server's DEFAULTS, to SETTINGS here, and a branch in apply.
Test it
- Press Play and press Settings. Three switches show ON.
- Turn Shadows off: shadows disappear at once. Turn Music off: the music goes quiet.
- Stop, press Play again and open Settings: your choices are still there.
Common errors and fixes
Check the Output window on the Server view for saving errors.
Settings reset every time I play
Saving needs Enable Studio Access to API Services in Studio. Check the Output for "could not save".
Infinite yield possible on 'ReplicatedStorage:WaitForChild("ChangeSetting")'
SettingsSaver is not running. It must be a Script in ServerScriptService.
The Sound effects switch does nothing
Your effects need to be in a SoundGroup named Effects in SoundService: set each Sound's SoundGroup property to it.
Music turns back on when a new song starts
Another music script sets the volume when it plays a song. Have it read Setting_Music before setting the volume.
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 GUI in your Explorer.
Add a settings menu with switches for music, sound effects, shadows and a low graphics mode, saved between sessions.
- Can insert a ready-made, animated settings screen from its premium UI kits in one step, in one of five themes (candy, neon, gold, blocky, pastel), with working server scripts
- Or writes the saver and menu from this guide and wires them to your music and sounds
- Is told to compile-check each script with Check Script, and sets undo points around each batch
After a build, RoCode can run its test robot: it plays your game in Studio, clicks the buttons and reports PASS or FAIL. It checks only what it chose to test. How RoCode connects to Studio.
Questions
How do I make a settings menu in Roblox?
Keep each setting as a Player attribute, apply it in a LocalScript, and let a server Script check and save it in a DataStore. Steps 1 and 2 have both scripts.
How do I let players turn off music?
Play music from a LocalScript, then a switch can set its volume to 0 on that player's device only.
Can a settings menu make my game run faster?
Turning off Lighting.GlobalShadows on a device can help on slower phones. Players can also lower graphics quality in Roblox's own menu.
Why save settings on the server?
So they follow the player to any device. A LocalScript cannot write to a DataStore; only the server can.
Sources and further reading
- Roblox Creator Docs: Instance attributes. SetAttribute and GetAttributeChangedSignal
- Roblox Creator Docs: Lighting. GlobalShadows
- Roblox Creator Docs: SoundGroup. one volume for a group of sounds
- Roblox Creator Docs: Data stores. GetAsync, SetAsync and Studio access
How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 23 October 2026). A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.