Sooo, i really want my game to have some really cool settings!
like… field of view, retro ADS, custom UI colors and so on
but the thing is, i’m not really sure how to accomplish that
i had the idea of using a modulescript inside starter player scripts to store those settings,
but then i got the revelation that i’d have no clue how to store those settings in a datastore
You can use a basic module, like you suggested, to store these settings, and apply them globally.
For example:
--i know settings is Studio settings global but ill just overwrite it here sorry!
local settings = { --example settings
["FOV"] = 10,
["UIColour"] = Color3.fromRGB(85, 170, 0),
["SettingChanged"] = nil :: ((string, any) -> (nil))? --this will be a callback
}
--create methods as modifying externally won't always work
function settings:GetSetting(name: string)
return settings[name]
end
function settings:UpdateSetting(name: string, val: any)
settings[name] = val
settings.SettingChanged(name, val)
end
return settings
Now, we can have a LocalScript to apply the changes:
settings.SettingChanged = function(setting: string, value: any)
--example where UIColour
if setting == "UIColour" then
for _, ui in next, playerGui:GetDescendants(), nil do
--apply changes
end
end :: (string, any) -> (nil)
As for saving it, sadly you can’t use PlayerRemoving because the module will be destroyed by then but you can save on a regular interval or when settings are changed (not write to the store then, but store the new settings ready for when the player leaves and write to the store then). You can store them in a table and retrieve them when the player joins, pass them along a remote and have the client apply them.
I hope this helps, let me know if you need me to explain any of it!
I was just making user settings like a couple days ago so here is the approach that I took:
First, store the player’s settings in a form of a table in their data via DataStoreService or ProfileService
On the client, have a RemoteFunction – In my case, It’s called ‘GetUserSettings’ – to retrieve the settings the player has saved in their data and invoke it. It should return the player’s settings
Loop through the player’s settings and set an attribute to the player with the name and value using the function :SetAttribute()
Connect a function to the :GetAttributeChangedSignal() event inside the loop
Every time the attribute value changes (e.x. from a true to a false and vice versa) you would want to fire a RemoteEvent to the server and change the player’s settings data on the server
And finally, to actually have functionality you would want to have a folder underneath your ModuleScript or LocalScript or whatever and have ModuleScripts like update_fov or update_uicolors or whatever. Afterwards, inside the ModuleScripts you would want to return a function. Inside of this function is going to be the main functionality you would want for that setting.
For example, for one of my settings I have a setting where you can turn on and off in game music and this is the function I have for that:
--!strict
local SoundService = game:GetService("SoundService")
local Players = game:GetService("Players")
return function(): ()
local local_player = Players.LocalPlayer
local music_enabled = local_player:GetAttribute("Music") :: boolean
if music_enabled == nil then return end
local sound_group = SoundService:FindFirstChild("Music")
if not sound_group then return end
sound_group.Volume = music_enabled and 0.5 or 0
end
And for my :GetAttributeChangedSignal() part, I am using this function:
local function on_update(): ()
local current_value = local_player:GetAttribute(settingName)
local module = utility_folder:FindFirstChild(
`update_{string.lower(settingName)}`) :: ModuleScript
if module then
require(module)()
end
update_button(settingName, current_value)
update_usersettings:FireServer(settingName, current_value)
end
on_update()
local_player:GetAttributeChangedSignal(settingName):Connect(on_update)
If you have any more questions or would like for me to send more code please ask! I would gladly do it for you.
Using attributes may not be the best system as it takes more memory (connections, etc.), but as for applying the settings, you can do it all in one script, you can also group it out into multiple functions:
local settingUpdaters = {
["Music"] = function(newVolume: number)
yourSoundInstance.Volume = newVolume / 10
end,
["UIColours"] = function(colour: Color3)
for _, ui in next, playerGui:GetDescendants(), nil do
--update
end
end
}
settings.SettingChanged = function(setting: string, ...: values) --... if you need multiple values
if settingUpdaters[setting] then
settingUpdaters[setting](table.unpack({...}))
end
end
But for saving and retrieving, @Free_Br1cks explained a good system for that. Just make sure:
DoS/DDoS protection
Don’t access the DataStore each time, rather have a table on the server of player settings. Only access the data store during saving and loading (i.e. don’t save or load when player settings update / player requests settings)
Have a local(?) script that handles changing settings →
Send changed information to the server everytime something is changed + fire a BindableEvent(?) and receive it from the module script that changes said settings →
Store the changed data in a datastore server-side →
No, if you use my module structure you can require it from every script and call settings:UpdateSetting()
When receiving the remote, store it on the server but don’t save it until data would be saved anyway (otherwise exploiters could really mess with your data store system)
Yes.
Your necessary questions
all that does is explicitely tell next to start at the beginning of the table when iterating. It’s not needed, I just do it anyway.
table.unpack here because it was an example if you passed multiple different amounts of values when a setting is changed. You likely won’t need it.
Exploiters can spam your remote events and cause lag/script crash or even server crashing. You need to have a cooldown system where they get kicked/server refuses to process their requests if they spam it.
DoS means Denial of Service - it’s where clients flood the server with useless information to stop legitimate requests from getting through and cause the server to crash. It’s kind of the same here, except it’s just a ton of requests.
DDoS means Distributed Denial of Service - the same thing but from multiple computers.
Whenever you would normally save data for the second part. It’s a good idea to have an autosave save data about every ~10 minutes.
For the top bit, that’s just a type annotation, I was trying to indicate there is a callback.
((string, any) -> (nil))?
--(string, any) means it's a callback that takes a string type as first parameter and anything as second parameter
-- -> means returns
-- (nil) means it won't return anything
-- ? means it might be that type or it might be nil
-- so basically we are saying that section might be a callback that takes a string and anything and returns nothing OR it might be nil
-- we later use :: ((string, any) -> (nil)) with no ? to indicate that the callback has been set
--doing it this way just works out with the type solver
--again, it's not necessary I was just trying to show it's a callback
--sorry for not using multiline commenting to explain this :(
No it is not needed really. It’s an aesthetic choice, I think it looks less condensed and more cleaner because you don’t have a big table of settings and functions
You don’t need to know about the double colon and colon things. Like I said, they’re optional.
table.unpack just returns all the values from a table. It’s the same as unpack.
local myTable = {1, 3, "hi", "foo", "bar"}
print(unpack(myTable)) --> 1 3 hi foo bar
print(table.unpack(myTable)) --> 1 3 hi foo bar
local a, b, c, d, e = unpack(myTable)
print(a) --> 1
print(b) --> 3
print(e) --> bar
fire a remote event with the changed setting in the module script, then fire another remote event on the server that tells the client something has changed and passes that value
kind of a good idea, verify the information on the server first before applying it