Customizable KeyBinds. Better UIS or Advanced CAS

Update is delayed. Can you show how you use KeyHandler?
Attempt to compare number and nil often happens at InputEnded code. If it is in InputBegin code then something is not right with your usage. I can help you if you provide your KeyHandler usage code

1 Like

Sure, this is a simplified version of my input code because I can’t release the main one, but the important stuff is there.

local Projectiles = KeyHandler.Create({
	Keys = {
		{"E,ButtonR1"},
	},
	Func = function(state, ht)
		print("Working")
		if debounce == false and canUse and state == Enum.UserInputState.Begin then
			--do something
		elseif state == Enum.UserInputState.Cancel then
			return
		end
	end,
	
} :: KeyHandler.KeyValues)

My code not uses Enum.UserInputState.Cancel. Its Enum.UserInputState.End

Oh okay, I’ll switch it out and see how that works

Please show the line that causing error(in module)

if AllKeys[key] then
		local highest = 0
		for _, Key in ipairs(AllKeys[key]) do
			print(Key.Priority, highest)
			if Key.Priority > highest then --> this one
				ActiveKeys[key] = {Key}
				highest = Key.Priority
				Key.ActiveKeysIndex = 1
			elseif Key.Priority == highest then
				local function findKeyWithSameParent(parent)
					for i,v in ipairs(ActiveKeys[key]) do
						if v.Parent == parent then
							return i
						end
					end
				end
				local index = findKeyWithSameParent(Key.Parent)
				if index then 
					if Key.Index < ActiveKeys[key][index].Index then
						ActiveKeys[key][index] = Key
						Key.ActiveKeysIndex = index
					end
				else
					table.insert(ActiveKeys[key], Key)
					Key.ActiveKeysIndex = #ActiveKeys[key]
				end
			end
		end
		table.insert(AllKeys[key], Key)
		Key.AllKeysIndex = #AllKeys[key]
	else
		AllKeys[key] = {LocalKey}
		ActiveKeys[key] = {LocalKey}

		Key.ActiveKeysIndex = #ActiveKeys[key]
		Key.AllKeysIndex = #AllKeys[key]
	end
	setmetatable(LocalKey, Key)
end
1 Like

I think, I fixed this bug. If it will happen again contact me
Edit: Use updated script

Hey I think the way keycodes are assigned should be done via Enum.Keycode instead of strings.

Edit: I ended up changing this myself here is the code:
Edit2: Fixed the code because only the last keycode you set would activate the function

--Version 3.2
--Source:
--https://devforum.roblox.com/t/customizable-keybinds-better-uis-or-advanced-cas/2910676
export type KeyValues = {
	Keys: {{
		key : {KeyCode : Enum.KeyCode},
		func : (state: Enum.UserInputType, ht : number) -> nil,
		GUI : ImageButton | TextButton
	}},
	
	Function: (state: Enum.UserInputType, ht: number) -> nil,

	Active : boolean,
	ToggledKey : boolean, -- If true, the function will be activated after the key is pressed and deactivated if the key is pressed again. If false, the function will be activated when the key is held down and deactivated when the key is no longer held.

	Ordered: boolean,
	Priority: number,

	MaxHoldTime: number,
	AlterFunc: (state: Enum.UserInputType, ht: number) -> nil,

	ResetFuncs: boolean,

	MaxTime: number,
	LateFunc: (state: Enum.UserInputType, ht: number) -> nil
}

local UIS = game:GetService("UserInputService")
local RunS = game:GetService("RunService")

local ActiveKeys = {} --Enabled Keys with Highest Priority
local AllKeys = {} --All asigned Keys

local KeyHandler = {}

KeyHandler.Default = {
	ToggledKey = false,
	
	Ordered = false,
	ResetFuncs = false,

	Priority = 1,

	MaxHoldTime = 2,
	MaxTime = 3
}

KeyHandler.WasActivated = {}

local Key = {}
Key.__index = Key

function KeyHandler.Create(values : KeyValues)
	local KeytableProxy = values
	KeytableProxy.Active = true
	KeytableProxy.LastHT = 0
	KeytableProxy.KeysActive = {}
	KeytableProxy.TimeRunningUp = nil

	for i,v in pairs(KeyHandler.Default) do
		if KeytableProxy[i] == nil then
			KeytableProxy[i] = v
		end
	end

	for i, tablekey in ipairs(KeytableProxy.Keys) do
		local keycodetable = {}
		KeytableProxy.KeysActive[i] = false
		
		for _, keycode in ipairs(tablekey[1]) do
			table.insert(keycodetable, keycode.Name)
		end

		for _, key in ipairs(keycodetable) do
			Key.Create(i, key, tablekey[2], tablekey[3], KeytableProxy)
		end
	end

	local KeytableMeta = {}

	KeytableMeta.__index = KeytableProxy
	KeytableMeta.__newindex = function(t,i,v)
		local value = KeytableProxy[i]
		if value ~= v then
			if i == "Keys" then 
				if value ~= nil then 
					--//DeleteKey
					for i, keyindex in ipairs(KeytableProxy[i]) do
						local keys = string.split(keyindex, ",")

						for _, Key in ipairs(keys) do
							Key:Destroy()
						end
					end
				end
				--//Add Key
				for i, tablekey in ipairs(v) do
					local keys = string.split(tablekey[1], ",")
					KeytableProxy.KeysActive[i] = false

					for _, key in ipairs(keys) do
						Key.Create(i, key, tablekey[2], tablekey[3], KeytableProxy)
					end
				end
			end
			KeytableProxy[i] = v
		end
	end

	local Keytable = {}

	return setmetatable(Keytable, KeytableMeta)
end

function Key.Create(i, key : string, func, GUI : ImageButton|TextButton, Parent)
	local LocalKey = {
		key = key,
		Priority = Parent.Priority,
		Func = func,
		AlterFunc = Parent.AlterFuncs ~= nil and Parent.AlterFuncs[key] or Parent.AlterFuncs ~= nil and Parent.AlterFuncs[i] or nil,
		MaxHoldTime = Parent.MaxHoldTime or KeyHandler.MaxHoldTime,
		LastHT = 0,
		Keytable = Parent,
		Index = i
	}
	if game.UserInputService.TouchEnabled == true then
		if GUI ~= nil then
			GUI.Visible = true

			GUI.MouseButton1Down:Connect(function()
				LocalKey:Fire(Enum.UserInputState.Begin)
			end)
			GUI.MouseButton1Up:Connect(function()
				LocalKey:Fire(Enum.UserInputState.End)
			end)
		end
	end

	if AllKeys[key] then
		local highest = 0
		for _, Key in ipairs(AllKeys[key]) do
			if Key.Priority > highest then
				ActiveKeys[key] = {Key}
				highest = Key.Priority
				Key.ActiveKeysIndex = 1
			elseif Key.Priority == highest then
				local function findKeyWithSameParent(parent)
					for i,v in ipairs(ActiveKeys[key]) do
						if v.Parent == parent then
							return i
						end
					end
				end
				local index = findKeyWithSameParent(Key.Parent)
				if index then 
					if Key.Index < ActiveKeys[key][index].Index then
						ActiveKeys[key][index] = Key
						Key.ActiveKeysIndex = index
					end
				else
					table.insert(ActiveKeys[key], Key)
					Key.ActiveKeysIndex = #ActiveKeys[key]
				end
			end
		end
		table.insert(AllKeys[key], Key)
		Key.AllKeysIndex = #AllKeys[key]
	else
		AllKeys[key] = {LocalKey}
		ActiveKeys[key] = {LocalKey}

		Key.ActiveKeysIndex = #ActiveKeys[key]
		Key.AllKeysIndex = #AllKeys[key]
	end
	setmetatable(LocalKey, Key)
end

function Key:Fire(state : Enum.UserInputState)
	if self.Keytable.Active == false then return false end
	if state == Enum.UserInputState.Begin then
		if state ~= nil and self.Keytable.Ordered == true and self.Keytable.KeysActive[self.Index-1] ~= nil and self.Keytable.KeysActive[self.Index-1] == false then
			return false
		end

		if self.Keytable.TimeRunningUp == nil then
			task.spawn(function()
				local MaxTime = 0
				self.Keytable.TimeRunningUp = RunS.Heartbeat:Connect(function(dt)
					MaxTime += dt

					if MaxTime > self.Keytable.MaxTime then
						table.clear(KeyHandler.WasActivated)

						if self.Keytable.LateFunc ~= nil then
							self.Keytable.LateFunc()
						end
						self.Keytable.TimeRunningUp:Disconnect()
					end
				end)
			end)
		end

		self.Keytable.KeysActive[self.Index] = true

		self.LastHT = tick()
		if self.Func then
			self.Func(Enum.UserInputState.Begin)
		end

		if not table.find(self.Keytable.KeysActive, false) then
			self.Keytable.LastHT = tick()
			self.Keytable.Function(Enum.UserInputState.Begin)
		end
	else
		if tick() - self.LastHT < self.MaxHoldTime then
			if not table.find(self.Keytable.KeysActive, false) then
				self.Keytable.LastHT = tick() - self.Keytable.LastHT
				self.Keytable.Function(Enum.UserInputState.End, self.Keytable.LastHT)
			end
			if self.Func then
				if self.Keytable.ResetFuncs ~= true or self.Keytable.LastHT == 0 then
					self.LastHT = tick() - self.LastHT
					self.Func(Enum.UserInputState.End, self.LastHT)
				end
			end
		elseif self.AlterFunc then
			if self.Keytable.ResetFuncs ~= true or self.Keytable.LastHT == 0 then
				self.LastHT = tick() - self.LastHT
				self.AlterFunc(Enum.UserInputState.End, self.LastHT)
			end
		end
		self.Keytable.KeysActive[self.Index] = false
	end
	if state ~= nil then return true end
end

function Key:Destroy()
	table.remove(ActiveKeys[self.key], self.ActiveKeysIndex)
	table.remove(AllKeys[self.key], self.AllKeysIndex)
end

local KeyConnections = nil

function KeyHandler:Start()
	KeyConnections = {
		Began = UIS.InputBegan:Connect(function(input, gPE)
			local key = input.KeyCode.Name
			--if gPE then return end
			if ActiveKeys[key] then
				for _, Key in ipairs(ActiveKeys[key]) do
					local Active = Key:Fire(Enum.UserInputState.Begin)
					if Active then table.insert(KeyHandler.WasActivated, Key) end
				end
			end
		end),
		Ended = UIS.InputEnded:Connect(function(input, gPE)
			local key = input.KeyCode.Name
			--if gPE then return end
			for _, localKey in ipairs(KeyHandler.WasActivated) do
				localKey:Fire(Enum.UserInputState.End)
			end
			table.clear(KeyHandler.WasActivated)
		end)
	}
end

function KeyHandler:GetKeys()
	print("All Keys: ", AllKeys)
	print("Active Keys: ", ActiveKeys)
end

function KeyHandler:Stop()
	if KeyConnections ~= nil then
		KeyConnections.Begin:Disconnect()
		KeyConnections.Ended:Disconnect()
		KeyConnections = nil
	else
		warn("KeyHandler is not active")
	end
end

return KeyHandler
1 Like

Is this still being worked on? If not please let me know because I wanted to use this for my game but may have to change it back.

Still. I had work as Game Designer. Learned script architectures. I was making my own version of Knit, that has KeyHandler and more. The new KeyHandler is a lot faster and has additional funcs.

Maybe release it in 4 days.

I can’t decide to make this module:

  • ServerSided - module gets commands to add keybind from server script, binds this key to player and activates func when key pressed.
  • ClientSided - all previous versions. Requiring module from localscript, and call func.

Both have pros and cons. I think I will make 3 versions: ClientSided, ServerSided, Hybrid

1 Like

When you release all 3 versions, can you list the pros and cons of each version?

1 Like

Alright, thanks for the update

1 Like

Any update on these new versions being released? Also I think it should be client sided.

Users may need different cases. My pc is broken. I wrote code samples on my phone, but can’t test it. I will use library’s pc to test and publish it. 2-3 days

1 Like