How to make a keybind editor with datasaving

This one handles datastores and a bit more complicated code, so make sure you have some experience first!

Warning to less experienced programmers: Contains someone intermediate programming and some complicated functions/API’s

-This also probably looks more complicate than it is
-this WILL INCLUDE: How to make it, and an example of how to use it with a sprinting keybind
-this took me a good few hours to make and bug fix, any support in the replies is greatly thanked!
-edits will by typo fixes, bug fixes, and optimizations
-remember to save/publish often!

First things we need to set up:

  • GUI to edit keybinds with
  • Folder to store default keybinds values & copy able instances for data loading
  • DataStore code to save, load, and edit data

API Links:

Read up on the pages to solve any basic confusion and if the problem still persists leave a reply!

–API Links might look a bit overkill, but I want to help everyone get knowledge on programming!

How I will name things:

  • DataStores = DS
  • UserInputService = UIS
  • ReplicatedStorage = repStorage
  • Anything in [ ] on GUI’s will be edited by the code and loaded when the player joins

Explorer Setup:

  • Folder in ServerStorage named “SettingsTemplate”
  • StringValue in SettingsTamplte named “SprintKeybind”, Value set to “LeftShift” for the default key
  • RemoteEvent in ReplicatedStorage named “UpdateKeybinds”
  • Script in ServerScriptService named “SettingsHandler” PS: Name doesn’t matter on this script
  • ScreenGui in StarterGui, named “KeybindsGUI”
  • Frame in KeybindsGUI, named “MainFrame”
  • TextButton in MainFrame, named “Sprintkeybind” (should be the same as the value in SettingsTemplate but it doesnt matter to much)
  • A TextButton in KeybindsGUI, named “Activate”

You should end up with something like this (Sizes, Colors, Fonts, etc on GUI’s are all up to you)

2021-12-25 19_37_56-Main - Roblox Studio

we will be programming the client side then the server side because thats how I feel like doing it :wink:

Now onto the GUI setup:

It can get a little messy here having LocalScripts under multiple buttons and such so I will use 1 LocalScript that controls it all called “Main”, but first we’ll do the Close/Open button first

Inside Main we will need variables to get the objects and such that we will need for the programming, the code comments should explain what they do, any questions should be asked in replies!

local repStorage = game:GetService("ReplicatedStorage") -- Anything placed here can be seen by the Client and the Server so we will use it for events
local updateBindsEvent = repStorage:WaitForChild("UpdateKeybinds") -- The RemoteEvent we will use to update the keybinds in the DS's

local Player = game:GetService("Players").LocalPlayer -- The client object, not the physical character, just the client object

local GUI = script.Parent -- The ScreenGui
local MainFrame = GUI.MainFrame -- The Frame
local ActivateBtn = GUI.Activate -- The TextButton used the open said Frame

local SprintKeybindBtn = MainFrame.SprintKeybind -- The TextButton used to update the players keybind

local opened = false -- Checking if the GUI is already opened so if we close it or open it 
local sprintSelected -- will be used to find out what the player selected to update the data stores!

Now we have that you should take a moment to understand what those do and means and then lets continue!

Now we have to add the actual open and closing function to the button, like so, as always, coding comments should explain it!

MainFrame.Visible = false -- sets the frame to un-seeable by default 

ActivateBtn.Activated:Connect(function() -- .Activate means it was clicked by a mouse, or tapped on a mobile device's screen, and then creates the function to run what happens
	if opened == true then -- checking if the GUI is already opened, double equal signs means we are checking the value not setting it "=="
		opened = false -- if they click it and its opened it closes
		
		MainFrame.Visible = false -- sets the visibilty on the frame so the player can or cant see it
	else 
		opened = true -- if they click it and its closed it opens
		
		MainFrame.Visible = true -- sets the visibilty on the frame so the player can or cant see it
	end
end)

If you wish you can test to make sure you got it all right, otherwise lets move on!
We now have to add the GUI function to our keybind button, but no actual data saving stuff yet! We have to add a UIS detection, and 2 new variables that I forgot at the start to find out of its listening or not

local listeningForSprintInput = false -- used to detect if UIS should be detecting inputs or not
local UIS = game:GetService("UserInputService") -- UserInputSerivce, detects when the player inputs anything, a key click, mouse move, xbox controller button click, etc

Now we add the UIS detection, now for the 3rd time, code comments! :sweat_smile:

SprintKeybindBtn.Activated:Connect(function()
	listeningForSprintInput = true -- tells UIS that we are listening for a new keybind input
	SprintKeybindBtn.Text = "Sprint keybind: listening for new key input" -- changes the text to tell the player we got it and are listening for a new keycode input
end)

UIS.InputBegan:Connect(function(input) -- .InputBegan is when you give any input, "input" is all the info on what you pressed
	if input.UserInputType == Enum.UserInputType.Keyboard then -- Checking if the detection was frame a keyboard (this check if un-needed if your game is VR/Xbox based or anything of the like)
		if listeningForSprintInput == true then -- if we are listening for detection of a new keybinds
			sprintSelected = UIS:GetStringForKeyCode(input.KeyCode) -- this UIS function I recently found, it turns they KeyCode from an Enum to the letter like "Enum.KeyCode.E" to "E"
			SprintKeybindBtn.Text = "Sprint keybind: ".. tostring(sprintSelected) -- resets the GUI to the new keybind
			listeningForSprintInput = false -- sets it to false so we know that we dont want to continue listening for inputs on the keybind
		end
	end
end)

The entire LocalScript so far:

local repStorage = game:GetService("ReplicatedStorage") -- Anything placed here can be seen by the Client and the Server so we will use it for events
local updateBindsEvent = repStorage:WaitForChild("UpdateKeybinds") -- The RemoteEvent we will use to update the keybinds in the DS's

local Player = game:GetService("Players").LocalPlayer -- The client object, not the physical character, just the client object

local UIS = game:GetService("UserInputService") -- UserInputSerivce, detects when the player inputs anything, a key click, mouse move, xbox controller button click, etc

local GUI = script.Parent -- The ScreenGui
local MainFrame = GUI.MainFrame -- The Frame
local ActivateBtn = GUI.Activate -- The TextButton used the open said Frame

local SprintKeybindBtn = MainFrame.SprintKeybind -- The TextButton used to update the players keybind

local opened = false -- Checking if the GUI is already opened so if we close it or open it 
local sprintSelected -- will be used to find out what the player selected to update the data stores!
local listeningForSprintInput = false -- used to detect if UIS should be detecting inputs or not

MainFrame.Visible = false

ActivateBtn.Activated:Connect(function() -- .Activate means it was clicked by a mouse, or tapped on a mobile device's screen, and then creates the function to run what happens
	if opened == true then -- checking if the GUI is already opened, double equal signs means we are checking the value not setting it "=="
		opened = false -- if they click it and its opened it closes
		
		MainFrame.Visible = false -- sets the visibilty on the frame so the player can or cant see it
	else 
		opened = true -- if they click it and its closed it opens
		
		MainFrame.Visible = true -- sets the visibilty on the frame so the player can or cant see it
	end
end)

SprintKeybindBtn.Activated:Connect(function()
	listeningForSprintInput = true -- tells UIS that we are listening for a new keybind input
	SprintKeybindBtn.Text = "Sprint keybind: listening for new key input" -- changes the text to tell the player we got it and are listening for a new keycode input
end)

UIS.InputBegan:Connect(function(input) -- .InputBegan is when you give any input, "input" is all the info on what you pressed
	if input.UserInputType == Enum.UserInputType.Keyboard then -- Checking if the detection was frame a keyboard (this check if un-needed if your game is VR/Xbox based or anything of the like)
		if listeningForSprintInput == true then -- if we are listening for detection of a new keybinds
			sprintSelected = UIS:GetStringForKeyCode(input.KeyCode) -- this UIS function I recently found, it turns they KeyCode from an Enum to the letter like "Enum.KeyCode.E" to "E"
			SprintKeybindBtn.Text = "Sprint keybind: ".. tostring(sprintSelected) -- resets the GUI to the new keybind
			listeningForSprintInput = false -- sets it to false so we know that we dont want to continue listening for inputs on the keybind
		end
	end
end)

Now im remembering, we have to add 2 more TextButtons to MainFrame, “SaveSettings” and “DiscardSettings”
2021-12-26 08_12_15-Window


And we have to add functionality to these, discard settings will make the settings go back to previous values, and save settings will save them and lock them in place
New variables for the buttons:

local saveSettingsBtn = MainFrame.SaveSettings
local discardSettingsBtn = MainFrame.DiscardSettings

And now the functions for what they need to do!

MAKE SURE TO GO TO GAME SETTINGS AND ENABLE HTTPS AND API SERVICES IN SECURITY IF YOU HAVENT ALREADY

And the new functions (as always, code comments)

saveSettingsBtn.Activated:Connect(function()
	updateBindsEvent:FireServer("SprintKeybind", sprintSelected)
	
	--setting, changeValue
	--settings = the case specific setting name we are changing
	--changeValue = the value we need to change it to, either a key or a bool value, etc
	
	MainFrame.Visible = false -- they finished up with settings so lets close the UI
	opened = false -- reset the opened value so they can open it again later
	
	--!!!! If there is ever an issue where your game is sending to many data requests and it rate limits you, saving data when they close the GUI is not required as long as it saves when they leave, I just include it as an extra precaution
end)

discardSettingsBtn.Activated:Connect(function()
	SprintKeybindBtn.Text = "Sprint keybind: ".. tostring(Player:WaitForChild("Settings"):WaitForChild("SprintKeybind").Value) -- reset the GUI to the previous settings because they want to discard these new ones
	MainFrame.Visible = false -- they finished up with settings so lets close the UI
	opened = false -- reset the opened value so they can open it again later
end)

Now I will show you the server side code, and this will allow your settings to work!

code comments as always and this part gets a little messy so just ask any questions in replies!

And as im writing and testing this, I realize we dont set any of the GUI text thats in [ ] on load, lets do that quickly

SprintKeybindBtn.Text = "Sprint keybind: ".. tostring(Player:WaitForChild("Settings"):WaitForChild("SprintKeybind").Value) -- just sets the text on load

And for anyone lost here is the LocalScript “Main” so far:

local repStorage = game:GetService("ReplicatedStorage") -- Anything placed here can be seen by the Client and the Server so we will use it for events
local updateBindsEvent = repStorage:WaitForChild("UpdateKeybinds") -- The RemoteEvent we will use to update the keybinds in the DS's

local Player = game:GetService("Players").LocalPlayer -- The client object, not the physical character, just the client object

local UIS = game:GetService("UserInputService") -- UserInputSerivce, detects when the player inputs anything, a key click, mouse move, xbox controller button click, etc

local GUI = script.Parent -- The ScreenGui
local MainFrame = GUI.MainFrame -- The Frame
local ActivateBtn = GUI.Activate -- The TextButton used the open said Frame

local saveSettingsBtn = MainFrame.SaveSettings
local discardSettingsBtn = MainFrame.DiscardSettings

local SprintKeybindBtn = MainFrame.SprintKeybind -- The TextButton used to update the players keybind

local opened = false -- Checking if the GUI is already opened so if we close it or open it 
local sprintSelected -- will be used to find out what the player selected to update the data stores!
local listeningForSprintInput = false -- used to detect if UIS should be detecting inputs or not

MainFrame.Visible = false

SprintKeybindBtn.Text = "Sprint keybind: ".. tostring(Player:WaitForChild("Settings"):WaitForChild("SprintKeybind").Value) -- just sets the text on load

ActivateBtn.Activated:Connect(function() -- .Activate means it was clicked by a mouse, or tapped on a mobile device's screen, and then creates the function to run what happens
	if opened == true then -- checking if the GUI is already opened, double equal signs means we are checking the value not setting it "=="
		opened = false -- if they click it and its opened it closes
		
		MainFrame.Visible = false -- sets the visibilty on the frame so the player can or cant see it
	else 
		opened = true -- if they click it and its closed it opens
		
		MainFrame.Visible = true -- sets the visibilty on the frame so the player can or cant see it
	end
end)

SprintKeybindBtn.Activated:Connect(function()
	listeningForSprintInput = true -- tells UIS that we are listening for a new keybind input
	SprintKeybindBtn.Text = "Sprint keybind: listening for new key input" -- changes the text to tell the player we got it and are listening for a new keycode input
end)

UIS.InputBegan:Connect(function(input) -- .InputBegan is when you give any input, "input" is all the info on what you pressed
	if input.UserInputType == Enum.UserInputType.Keyboard then -- Checking if the detection was frame a keyboard (this check if un-needed if your game is VR/Xbox based or anything of the like)
		if listeningForSprintInput == true then -- if we are listening for detection of a new keybinds
			sprintSelected = UIS:GetStringForKeyCode(input.KeyCode) -- this UIS function I recently found, it turns they KeyCode from an Enum to the letter like "Enum.KeyCode.E" to "E"
			SprintKeybindBtn.Text = "Sprint keybind: ".. tostring(sprintSelected) -- resets the GUI to the new keybind
			listeningForSprintInput = false -- sets it to false so we know that we dont want to continue listening for inputs on the keybind
		end
	end
end)

saveSettingsBtn.Activated:Connect(function()
	updateBindsEvent:FireServer("SprintKeybind", sprintSelected)
	
	--setting, changeValue
	--settings = the case specific setting name we are changing
	--changeValue = the value we need to change it to, either a key or a bool value, etc
	
	MainFrame.Visible = false -- they finished up with settings so lets close the UI
	opened = false -- reset the opened value so they can open it again later
	
	--!!!! If there is ever an issue where your game is sending to many data requests and it rate limits you, saving data when they close the GUI is not required as long as it saves when they leave, I just include it as an extra precaution
end)

discardSettingsBtn.Activated:Connect(function()
	SprintKeybindBtn.Text = "Sprint keybind: ".. tostring(Player:WaitForChild("Settings"):WaitForChild("SprintKeybind").Value) -- reset the GUI to the previous settings because they want to discard these new ones
	MainFrame.Visible = false -- they finished up with settings so lets close the UI
	opened = false -- reset the opened value so they can open it again later
end)

And here is the server code and explained in our favorite code comments

local serverStorage = game:GetService("ServerStorage")
local SettingsTemplate = serverStorage:WaitForChild("SettingsTemplate")

local DTS = game:GetService("DataStoreService")
local Http = game:GetService("HttpService")
local MainDataStore = DTS:GetDataStore("MyDataStore") -- Change "MyDataStore" to your datastore name and keep private

local repStorage = game:GetService("ReplicatedStorage")

local updateEvent = repStorage:WaitForChild("UpdateKeybinds")

function saveData(plr)
	local plrUserId = "Player_"..plr.UserId
	
	--the key would end up being Player_[their UserID]
	
	local data = {}

	for i, v in pairs(plr:WaitForChild("Settings"):GetChildren()) do
		data[v.Name] = v.Value
	end

	local Encoded_Data = Http:JSONEncode(data)

	local Success, Error_Message = pcall(function()
		MainDataStore:SetAsync(plrUserId, Encoded_Data)
	end)
	
	--all the stuff above does some fancy HTTPS work and makes all the data into a nice table for us to look at
	
	if Success == true then
		print("saved data")
	else
		print("Error: "..Error_Message) -- never got this, no clue how to fix if you got this, would recommend just rewriting this
	end
end

function loadData(player, gameData)
	--loads the players data!
	--"gameData" is the term used for the folder under the player that stores the settings value
	
	local plrUserId = "Player_".. player.UserId
	local User_Data = nil

	local Success, Error_Message = pcall(function()
		User_Data = MainDataStore:GetAsync(plrUserId)
	end)

	if Success == true then --if it worked and we didnt have any error finding their dataa
		if User_Data ~= nil then
			local Decoded_User_Data = Http:JSONDecode(User_Data)

			for Index, Value in pairs(Decoded_User_Data) do
				if Index then
					if Value ~= nil then
						if gameData then
							if gameData:FindFirstChild(Index) then
								--above are just checks for 
								gameData:WaitForChild(Index).Value = Value
							end
						end
					end
				end
			end
		else
			print("players data is nil setting default values")

			for Index, Value in pairs(gameData:GetChildren()) do
				Value.Value = game.ServerStorage.SettingsTemplate:FindFirstChild(Value).Value
			end
		end
	else
		error("failed to load data for "..player.Name) --something went wrong, idk, never got this so no clue how to fix
	end

	--while wait(600) do
	--	saveData(player) --Uncomment if you want badly optimized auto saves or something, would recommend leaving commented
	--end
end

game.Players.PlayerAdded:Connect(function(player)
	--sets up the players gameData (settings) and loads it when they join the game
	local gameData = SettingsTemplate:Clone()
	gameData.Name = "Settings"
	gameData.Parent = player

	loadData(player, gameData)
end)

game.Players.PlayerRemoving:Connect(function(plr)
	saveData(plr, plr:WaitForChild("Settings"):GetChildren())
	
	--saves the players data when they leave the game
end)

updateEvent.OnServerEvent:Connect(function(plr, setting, changeValue)
	--when we fire from the LocalScript to change the value this will change it
	local selectedSetting = plr:WaitForChild("Settings"):WaitForChild(setting)

	selectedSetting.Value = changeValue
	saveData(plr)
end)

Now I can show you how to actually use the selected keybinds for stuff in your game!

We will be using Enum.KeyCode[MySetting.Value] for the UIS inputs here, so just take a look

  • This is a LocalScript in StartCharacterScripts named “SprintHandler” just for an example on how the keybinds would be used
local plr = game.Players.LocalPlayer
local char = plr.Character or plr.CharacterAdded:Wait()
local humanoid = char:WaitForChild("Humanoid")
local cam = game.Workspace.Camera

local UIS = game:GetService("UserInputService")

local sprinting = false

local function checkSprint()
	if sprinting == true then
		humanoid.WalkSpeed = humanoid.WalkSpeed + 10
		wait()
	else
		humanoid.WalkSpeed = humanoid.WalkSpeed - 10
		wait()
	end
	
	--a very nifty way of doing sprinting I used for my game because of biomes that have different speeds, for example snow
	--instead of setting it to a value like "20" you can just add 10 to whatever it is and get a sprint boost!
end

-- just the functions to activate or de-activate sprinting

UIS.InputBegan:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.Keyboard then
		if input.KeyCode == Enum.KeyCode[plr:WaitForChild("Settings"):WaitForChild("SprintKeybind").Value] then
			-- the line above me (27) is how we detect the sprint setting, and we use [] to attach a value to the enum detection and
			-- boom, it works!
			sprinting = true
			checkSprint()
		end
	end
end)

UIS.InputEnded:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.Keyboard then
		if input.KeyCode == Enum.KeyCode[plr:WaitForChild("Settings"):WaitForChild("SprintKeybind").Value] then
			sprinting = false
			checkSprint()
		end
	end
end)

Yes this way of doing it with remote events for every keybind is sloppy and not the best, so if anyone has any better ideas they will be tried and cred
(This took me a bit to make and it was really late & early while writing this, so if I missed anything please let me know!)

Ask any questions in the replies, and let me know if anyone uses this, I wish more games allowed keybind editting

I use this datastore framework from a few forum posts, if this is anyones original design link the post and credit will be given!

With that I hope everyone has a happy holiday, and here is the downloadable place

6 Likes

My use of the term “API” in this post may be extremely wrong if someone knows the correct term I should be using please tell me!
Here was my most recent tutorial: How to make a "Press any key to continue" starting menu

This tutorial looks amazing!

API is one of the correct terms, however for some, it would be a service or instance.

1 Like

Thank you! The support is much appreciated and thanks for the info on the term!

1 Like

This Tutorial is Great, Nice Work man.

1 Like