Any ways to optimize and reduce bulk on my InputManager script?

I created this inputManager module to quickly set up many input types (Holds, Taps, Positive/Negative, etc.) It has currently not given me any problems so far, but i feel like it is a bit bulking and wanted to know if there’s some ways to reduce it and maybe some optimizations too.

here’s the module script:

local cas = game:GetService("ContextActionService")
local uis = game:GetService("UserInputService")
local inputs = {}
local bools = {}

local UIManager = {}

UIManager.__index = UIManager

local function CreateAxis(actionName, actionState, positiveName, negativeName)
	if bools[positiveName] == nil then bools[positiveName] = false end
	if bools[negativeName] == nil then bools[negativeName] = false end

	if actionName == positiveName then
		bools[positiveName] = actionState == Enum.UserInputState.Begin
	end
	if actionName == negativeName then
		bools[negativeName] = actionState == Enum.UserInputState.Begin
	end

	if bools[positiveName] == bools[negativeName] then 
		return 0
	else
		if bools[positiveName] then
			return 1
		elseif bools[negativeName] then
			return -1
		end
	end
end

local function CreateToggle(actionName, actionState, name, initialBool)
	if bools[name] == nil then bools[name] = initialBool end

	if actionState == Enum.UserInputState.Begin then
		bools[name] =  not bools[name]
	end

	return bools[name]
end

local function CreateHold(actionName, actionState, name, invert)
	if actionState == Enum.UserInputState.Begin then
		bools[name] = not invert
	elseif actionState == Enum.UserInputState.End then
		bools[name] = invert
	end

	return bools[name]
end

local function CreateTap(actionName, actionState, name, tapType, changedEvent)
	if actionState == Enum.UserInputState.Begin then
		if tapType == "KeyDown" then
			changedEvent:Fire()
		end
	end
	if actionState == Enum.UserInputState.End then
		if tapType == "KeyUp" then
			changedEvent:Fire()
		end
	end
end

UIManager.GetInputs = function()
	return inputs
end

UIManager.CreateMouseAxis = function(baseName, mouseAxis, bind)
	local InputMouseAxis = setmetatable({},UIManager)
	
	local changedEvent = Instance.new("BindableEvent")
	local unbindEvent = Instance.new("BindableEvent")
	local bindEvent = Instance.new("BindableEvent")
	local lastInput = 0
	local bindNow = bind or false
	local mouseConnection = nil
	
	InputMouseAxis.Value = 0
	InputMouseAxis.Name = baseName
	InputMouseAxis.UnBinding = unbindEvent.Event
	InputMouseAxis.Binding = bindEvent.Event
	InputMouseAxis.Changed = changedEvent.Event
	InputMouseAxis.Binded = false
		
	local function GetDelta()
		local deltaVector = uis:GetMouseDelta().Unit
		
		if deltaVector.Magnitude > 0 then
			if mouseAxis == "Vertical" then
				InputMouseAxis.Value = deltaVector.Y
			elseif mouseAxis == "Horizontal" then
				InputMouseAxis.Value = deltaVector.X
			else
				InputMouseAxis.Value = deltaVector
			end
		else
			if mouseAxis == "Vertical" or mouseAxis == "Horizontal" then
				InputMouseAxis.Value = 0
			else
				InputMouseAxis.Value = Vector2.zero
			end
		end
	end
		
	local changedCheck = game:GetService("RunService").RenderStepped:Connect(function()
		if InputMouseAxis.Value == lastInput then return end

		changedEvent:Fire(InputMouseAxis.Value)
		lastInput = InputMouseAxis.Value
	end)
	
	function InputMouseAxis:UnBind(resetValue)
		mouseConnection:Disconnect()
		mouseConnection = nil
		
		unbindEvent:Fire()
		self.Binded = false

		if typeof(self.Value) == "number" then
			self.Value = 0
		else
			self.Value = Vector2.zero
		end
	end

	function InputMouseAxis:Bind()
		mouseConnection = game:GetService("RunService").RenderStepped:Connect(GetDelta)
		bindEvent:Fire()
		self.Binded = true
	end
	
	if bindNow then
		InputMouseAxis:Bind()
	end

	inputs[baseName] = InputMouseAxis
end

UIManager.CreateAxis = function(baseName, positiveInput, negativeInput, GUIButton, bind)
	local InputAxis = setmetatable({}, UIManager)

	local changedEvent = Instance.new("BindableEvent")
	local unbindEvent = Instance.new("BindableEvent")
	local bindEvent = Instance.new("BindableEvent")
	local deleteEvent = Instance.new("BindableEvent")
	local lastInput = 0
	local bindNow = bind or false

	InputAxis.Value = 0
	InputAxis.Name = baseName
	InputAxis.PositiveName = tostring(baseName .. "Pos")
	InputAxis.NegativeName = tostring(baseName .. "Neg")
	InputAxis.Changed = changedEvent.Event
	InputAxis.UnBinding = unbindEvent.Event
	InputAxis.Binding = bindEvent.Event
	InputAxis.Deleting = deleteEvent.Event
	InputAxis.Binded  = false

	local func = function(as, an)
		InputAxis.Value = CreateAxis(as, an, InputAxis.PositiveName, InputAxis.NegativeName)
	end

	local changedCheck = game:GetService("RunService").RenderStepped:Connect(function()
		if InputAxis.Value == lastInput then return end

		changedEvent:Fire(InputAxis.Value)
		lastInput = InputAxis.Value
	end)

	function InputAxis:UnBind(resetValue)
		local reset = resetValue or false

		cas:UnbindAction(self.PositiveName)
		cas:UnbindAction(self.NegativeName)
		unbindEvent:Fire()
		self.Binded = false

		if reset then
			self.Value = 0
		end
	end

	function InputAxis:Bind()
		cas:BindAction(self.PositiveName, func, GUIButton, positiveInput)
		cas:BindAction(self.NegativeName, func, GUIButton, negativeInput)
		bindEvent:Fire()
		self.Binded = true
	end

	function InputAxis:Delete()
		if self.Binded then
			cas:UnbindAction(self.PositiveName)
			cas:UnbindAction(self.NegativeName)
		end

		changedCheck:Disconnect()
		inputs[self.Name] = nil
		bools[self.PositiveName] = nil
		bools[self.NegativeName] = nil
		deleteEvent:Fire()

		self = nil
	end

	if bindNow then
		InputAxis:Bind()
	end

	inputs[baseName] = InputAxis
end

UIManager.CreateToggle = function(baseName, toggleInput, GUIButton, initialBool, bind)
	local InputToggle = setmetatable({}, UIManager)

	local changedEvent = Instance.new("BindableEvent")
	local unbindEvent = Instance.new("BindableEvent")
	local bindEvent = Instance.new("BindableEvent")
	local deleteEvent = Instance.new("BindableEvent")
	local lastInput = initialBool
	local bindNow = bind or false

	InputToggle.Value = initialBool
	InputToggle.Name = baseName
	InputToggle.Changed = changedEvent.Event
	InputToggle.UnBinding = unbindEvent.Event
	InputToggle.Binding = bindEvent.Event
	InputToggle.Deleting = deleteEvent.Event
	InputToggle.Binded  = false

	local func = function(as, an)
		InputToggle.Value = CreateToggle(as, an, baseName, initialBool)
	end

	local changedCheck = game:GetService("RunService").RenderStepped:Connect(function()
		if InputToggle.Value == lastInput then return end

		changedEvent:Fire(InputToggle.Value)
		lastInput = InputToggle.Value
	end)

	function InputToggle:UnBind(resetValue)
		local reset = resetValue or false

		cas:UnbindAction(self.Name .. "Toggle")
		unbindEvent:Fire()
		self.Binded = false

		if reset then
			self.Value = initialBool
		end
	end

	function InputToggle:Bind()
		cas:BindAction(self.Name .. "Toggle", func, GUIButton, toggleInput)
		bindEvent:Fire()
		self.Binded = true
	end

	function InputToggle:Delete()
		if self.Binded then
			cas:UnbindAction(self.Name .. "Toggle")
		end

		changedCheck:Disconnect()
		inputs[self.Name] = nil
		bools[self.Name .. "Toggle"] = nil
		deleteEvent:Fire()

		self = nil
	end

	if bindNow then
		InputToggle:Bind()
	end

	inputs[baseName] = InputToggle
end

UIManager.CreateHold = function(baseName, holdInput, GUIButton, invert, bind)
	local InputHold = setmetatable({}, UIManager)

	local changedEvent = Instance.new("BindableEvent")
	local unbindEvent = Instance.new("BindableEvent")
	local bindEvent = Instance.new("BindableEvent")
	local deleteEvent = Instance.new("BindableEvent")
	local lastInput = invert
	local bindNow = bind or false

	bools[baseName] = invert
	InputHold.Value = invert
	InputHold.Name = baseName
	InputHold.Changed = changedEvent.Event
	InputHold.UnBinding = unbindEvent.Event
	InputHold.Binding = bindEvent.Event
	InputHold.Deleting = deleteEvent.Event
	InputHold.Binded  = false

	local func = function(as, am)
		InputHold.Value = CreateHold(as, am, baseName, invert)
	end

	local changedCheck = game:GetService("RunService").RenderStepped:Connect(function()
		if InputHold.Value == lastInput then return end

		changedEvent:Fire(InputHold.Value)
		lastInput = InputHold.Value
	end)

	function InputHold:UnBind(resetValue)
		local reset = resetValue or false

		cas:UnbindAction(self.Name .. "Hold")
		unbindEvent:Fire()
		self.Binded = false

		if reset then
			self.Value = invert
		end
	end

	function InputHold:Bind()
		cas:BindAction(self.Name .. "Hold", func, GUIButton, holdInput)
		bindEvent:Fire()
		self.Binded = true
	end

	function InputHold:Delete()
		if self.Binded then
			cas:UnbindAction(self.Name .. "Hold")
		end

		changedCheck:Disconnect()
		inputs[self.Name] = nil
		bools[self.Name .. "Hold"] = nil
		deleteEvent:Fire()

		self = nil
	end

	if bindNow then
		InputHold:Bind()
	end

	inputs[baseName] = InputHold
end

UIManager.CreateTap = function(baseName, tapInput, tapType, GUIButton, bind)
	local InputTap = setmetatable({}, UIManager)

	local changedEvent = Instance.new("BindableEvent")
	local unbindEvent = Instance.new("BindableEvent")
	local bindEvent = Instance.new("BindableEvent")
	local deleteEvent = Instance.new("BindableEvent")
	local bindNow = bind or false

	InputTap.Name = baseName
	InputTap.Fired = changedEvent.Event
	InputTap.UnBinding = unbindEvent.Event
	InputTap.Binding = bindEvent.Event
	InputTap.Deleting = deleteEvent.Event
	InputTap.Binded  = bindNow

	local func = function(as, am)
		CreateTap(as, am, baseName, tapType, changedEvent)
	end

	function InputTap:UnBind(resetValue)
		cas:UnbindAction(self.Name .. "Tap")
		unbindEvent:Fire()
		self.Binded = false
	end

	function InputTap:Bind()
		cas:BindAction(self.Name .. "Tap", func, GUIButton, tapInput)
		bindEvent:Fire()
		self.Binded = true
	end

	function InputTap:Delete()
		if self.Binded then
			cas:UnbindAction(self.Name .. "Tap")
		end

		inputs[self.Name] = nil
		deleteEvent:Fire()

		self = nil
	end

	if bindNow then
		InputTap:Bind()
	end

	inputs[baseName] = InputTap
end

return UIManager

here’s an example of how it works on a local script in case you need to know:

local inputManager = require(game:GetService("ReplicatedStorage").InputManager)
--create inputs here
inputManager.CreateAxis("TestAxis", Enum.KeyCode.Up, Enum.KeyCode.Down, false, false)
local testAxis = inputManager.GetInputs().TestAxis

--binds input
testAxis:Bind()
--unbinds input with an optional reset value
testAxis:UnBind(0)
--deletes the input object
testAxis:Delete()

testAxis.Value -- current input value
testAxis.Binded -- boolean for if input is currently binded

testAxis.Binding:Connect(function()
	--fires whenever the input is binded
end)
testAxis.UnBinding:Connect(function()
	--fires whenever the input is unbinded
end)
testAxis.Deleting:Connect(function()
	--fires whenever the input object gets deleted
end)
testAxis.Changed:Connect(function(inputValue)
	--fires whenever the input changes
end)
1 Like

It’s like 2 am when I’m posting that, so sorry if there’s any errors in the code


Looking at these two functions side by side, bar the variable names, they are functionally identical except in the places marked in red.

In order to clean this up, you probably want to use function currying. Function currying is where you use a function to return a function. (See example below) also you use function currying already: local func = function(as, an)

Code Currying Example
-- a special print that always says a defined work in front of every message
local function newPrint(prefix)
    return function(...)
        print(prefix, ...)
    end
end

local print = newPrint("[SYSTEM]") -- don't ever do this btw, bad practice
local warn = newPrint("[WARNING]") -- remember this is an example

print("test") --> [SYSTEM] test
warn("test") --> [WARNING] test

-- That's the general gist of function currying

So applying that to your codebase, you would create a generic function and pass whatever toggle / hold function as well as the name of the bind action.

(also heads up I’m changing the bindable events to Lua Signal Class Comparison & Optimal `GoodSignal` Class since it’s more performant and easier to use)

(also I’m moving the Bind, Unbind, and Delete functions outside and indexing them. This means that they will only be defined once instead of being defined every time you create an axis / input object. this saves on ram)

local UIManager = {}

-- Functions moved outside for performance reasons and will be indexed for the actual functions
function UIManager:UnBind(resetValue)
	local reset = resetValue or false

	cas:UnbindAction(self.Name .. self.ActionName) -- ActionName is passed
	self.UnBinding:Fire()
	self.Binded = false

	if reset then
		self.Value = invert
	end
end

function UIManager:Bind()
	cas:BindAction(self.Name .. self.ActionName, self.func, GUIButton, holdInput)
	self.Binding:Fire()
	self.Binded = true
end

function UIManager:Delete()
	if self.Binded then
		cas:UnbindAction(self.Name .. self.ActionName)
	end

    if self.ChangedCheck then -- note on this if statement, it's handy later for CreateTap ;)
    	self.ChangedCheck:Disconnect()
    end
	inputs[self.Name] = nil
	bools[self.Name .. self.ActionName] = nil
	self.Deleting:Fire()

	self = nil
end

local function GetGenericFunction(createFunction, actionName)
	return function(baseName, holdInput, GUIButton, invert, bind)
		local self = setmetatable({}, UIManager) -- changed to self, which is a generic name
		
		bools[baseName] = invert
		self.Value = invert
		self.Name = baseName
		self.Changed = Signal.new() -- everyhting converted to signals, :Fire() and :Connect() directly
		self.UnBinding = Signal.new()
		self.Binding = Signal.new()
		self.Deleting = Signal.new()
		self.Binded  = false
		self.ActionName = actionName -- used in the functions above
		local lastInput = invert
		local bindNow = bind or false

		self.func = function(as, am)
			self.Value = createFunction(as, am, baseName, invert, self.Changed)
		end

		self.ChangedCheck = game:GetService("RunService").RenderStepped:Connect(function()
			if self.Value == lastInput then return end
			self.Changed:Fire(self.Value)
			lastInput = self.Value
		end)


		if bindNow then
			self:Bind()
		end

		inputs[baseName] = self
	end
end

UIManager.CreateHold = GetGenericFunction(CreateHold, "Hold")
UIManager.CreateToggle = GetGenericFunction(CreateToggle, "Toggle")

Just like that, 2 functions worth 133 lines are halved to 68 lines.
And using this general idea of function currying, some more arguments, and probably some function overriding, you can also pass through all the other functions as well.

For example with CreateTap, simply pass through a variable that makes it where the RunService.RenderStepped loop isn’t created

local function GetGenericFunction(createFunction, actionName, disableRunService)
	return function(baseName, holdInput, GUIButton, invert, bind)
        ...

		self.func = function(as, am)
            if actionName == "Tap" then
    			createFunction(as, am, baseName, GUIButton, self.Changed) -- different arguments
                -- also gui button, isn't really a gui button
                -- probably change to generic argument names to reduce confusion
            else
                createFunction(as, am, baseName, invert)
            end
		end

        if not disableRunService then
			self.ChangedCheck = game:GetService("RunService").RenderStepped:Connect(function() 
                -- since it doesn't always exist make sure to update self:Delete() to check if it exists first
				if self.Value == lastInput then return end
				self.Changed:Fire(self.Value)
				lastInput = self.Value
			end)
        end

        ...
    end
end

UIManager.CreateHold = GetGenericFunction(CreateHold, "Hold")
UIManager.CreateToggle = GetGenericFunction(CreateToggle, "Toggle")
UIManager.CreateTap = GetGenericFunction(CreateHold, "Tap", false) -- yippee

Finally for CreateAxis and CreateMouseAxis, I’d recommend sending in a Bind and UnBind override function, that way it directly uses those instead of indexing the normal Bind and UnBind functions. Those two are probably going to be a bit tricky, so it might also be just better to leave them as is instead.

In the end I made a full implementation (below), however it required changing up a decent amount of code to accommodate for the different argument structures of each function. This was not tested, and I’d probably recommend refactoring your code yourself, but if you are interested, feel free to take a look at how the arguments are passed and overrides are handled. Hope ya learned something and can better your coding.

Final Code (398 lines -> 256)
local cas = game:GetService("ContextActionService")
local uis = game:GetService("UserInputService")
local inputs = {}
local bools = {}

local UIManager = {}

UIManager.__index = UIManager

local function CreateAxis(actionName, actionState, positiveName, negativeName)
	if bools[positiveName] == nil then bools[positiveName] = false end
	if bools[negativeName] == nil then bools[negativeName] = false end

	if actionName == positiveName then
		bools[positiveName] = actionState == Enum.UserInputState.Begin
	end
	if actionName == negativeName then
		bools[negativeName] = actionState == Enum.UserInputState.Begin
	end

	if bools[positiveName] == bools[negativeName] then 
		return 0
	else
		if bools[positiveName] then
			return 1
		elseif bools[negativeName] then
			return -1
		end
	end
end

local function CreateToggle(actionName, actionState, name, initialBool)
	if bools[name] == nil then bools[name] = initialBool end

	if actionState == Enum.UserInputState.Begin then
		bools[name] =  not bools[name]
	end

	return bools[name]
end

local function CreateHold(actionName, actionState, name, invert)
	if actionState == Enum.UserInputState.Begin then
		bools[name] = not invert
	elseif actionState == Enum.UserInputState.End then
		bools[name] = invert
	end

	return bools[name]
end

local function CreateTap(actionName, actionState, name, tapType, changedEvent)
	if actionState == Enum.UserInputState.Begin then
		if tapType == "KeyDown" then
			changedEvent:Fire()
		end
	end
	if actionState == Enum.UserInputState.End then
		if tapType == "KeyUp" then
			changedEvent:Fire()
		end
	end
end

UIManager.GetInputs = function()
	return inputs
end

-- Functions moved outside for performance reasons and will be indexed for the actual functions
function UIManager:UnBind(resetValue)
	if #self.Inputs > 1 then
		cas:UnbindAction(self.PositiveName)
		cas:UnbindAction(self.NegativeName)
	else
		cas:UnbindAction(self.Name .. self.ActionName) -- ActionName is passed
	end
	self.UnBinding:Fire()
	self.Binded = false

	self.Value = resetValue and self.Default or self.Value
end

function UIManager:Bind()
	if #self.Inputs > 1 then
		cas:UnbindAction(self.PositiveName, self.func, self.GUIButton, self.Inputs[1])
		cas:UnbindAction(self.NegativeName, self.func, self.GUIButton, self.Inputs[2])
	else
		cas:BindAction(self.Name .. self.ActionName, self.func, self.GUIButton, self.Inputs[1])
	end
	self.Binding:Fire()
	self.Binded = true
end

function UIManager:Delete()
	if self.Binded then
		if #self.Inputs > 1 then
			cas:UnbindAction(self.PositiveName)
			cas:UnbindAction(self.NegativeName)
		else
			cas:UnbindAction(self.Name .. self.ActionName) -- ActionName is passed
		end
	end

	self.Changed:Disconnect()
	inputs[self.Name] = nil
	bools[self.Name .. self.ActionName] = nil
	bools[self.PositiveName] = nil
	bools[self.NegativeName] = nil
	self.Deleting:Fire()
end

local function GetGenericFunction(baseName, actionName, args, disableRunService, overrides)
	local self = setmetatable({}, UIManager) -- changed to self, which is a generic name

	bools[baseName] = args.Initial
	self.Value = args.Initial
	self.Default = args.Initial
	self.Name = baseName
	self.PositiveName = baseName .. Pos
	self.NegativeName = baseName .. Neg
	self.Changed = Signal.new() -- everyhting converted to signals, :Fire() and :Connect() directly
	self.UnBinding = Signal.new()
	self.Binding = Signal.new()
	self.Deleting = Signal.new()
	self.Binded  = false
	self.ActionName = actionName -- used in the functions above
	self.Inputs = args.Inputs
	local lastInput = args.Initial
	local bindNow = args.bind or false

	self.func = function(as, am)
		self.Value = args.func(as, am, table.unpack(args.funcArgs))
	end

	if not disableRunService then
		self.ChangedCheck = game:GetService("RunService").RenderStepped:Connect(function() 
			-- since it doesn't always exist make sure to update self:Delete() to check if it exists first
			if self.Value == lastInput then return end
			self.Changed:Fire(self.Value)
			lastInput = self.Value
		end)
	end

	if overrides then
		for i, v in overrides do
			self[i] = v
		end
	end

	if bindNow then
		self:Bind()
	end

	inputs[baseName] = self
end

UIManager.CreateMouseAxis = function(baseName, mouseAxis, bind)
	GetGenericFunction(
		baseName,
		"Mouse",
		{
			Initial = 0,
			bind = bind,
		},
		true,
		{
			Bind = function(self)
				local function GetDelta()
					local deltaVector = uis:GetMouseDelta().Unit
					if deltaVector.Magnitude > 0 then
						if mouseAxis == "Vertical" then
							self.Value = deltaVector.Y
						elseif mouseAxis == "Horizontal" then
							self.Value = deltaVector.X
						else
							self.Value = deltaVector
						end
					else
						if mouseAxis == "Vertical" or mouseAxis == "Horizontal" then
							self.Value = 0
						else
							self.Value = Vector2.zero
						end
					end
				end
				self.mouseConnection = game:GetService("RunService").RenderStepped:Connect(GetDelta)
				self.Binding:Fire()
				self.Binded = true
			end
			UnBind = function(self)
				self.mouseConnection:Disconnect()
				self.mouseConnection = nil
				self.Binding:Fire()
				self.Binded = true
			end
		}
	)
end
UIManager.CreateAxis = function(baseName, positiveInput, negativeInput, GUIButton, bind)
	GetGenericFunction(
		baseName,
		"Axis",
		{
			GUIButton = GUIButton,
			Initial = 0,
			Inputs = {positiveInput, negativeInput},
			func = CreateHold, 
			funcArgs = {baseName, invert},
			bind = bind,
		}, 
	)
end
UIManager.CreateHold = function(baseName, holdInput, GUIButton, invert, bind) 
	GetGenericFunction(
		baseName,
		"Hold",
		{
			GUIButton = GUIButton, 
			Initial = invert,
			Inputs = {holdInput},
			func = CreateHold,
			funcArgs = {baseName, invert},
			bind = bind,
		}, 
	)
end
UIManager.CreateToggle = function(baseName, toggleInput, GUIButton, initialBool, bind)
	GetGenericFunction(
		baseName,
		"Toggle",
		{
			GUIButton = GUIButton,
			Initial = initialBool,
			Inputs = {toggleInput},
			func = CreateToggle,
			funcArgs = {baseName, initialBool},
			bind = bind,
		}, 
	)
end
UIManager.CreateTap = function(baseName, tapInput, tapType, GUIButton, bind) 
	GetGenericFunction(
		baseName,
		"Tap", 
		{
			GUIButton = GUIButton, 
			func = CreateTap, 
			funcArgs = {baseName, tapType, changedEvent},
			Inputs = {tapInput},
			bind = bind,
		}, 
		false
	)
end

return UIManager

also last thing

please please please don’t abbreviate services, write them out in full “ContextActionService” / “UserInputService”. It won’t save you that much time abbreviating services, but it will just make reading and sharing code harder.

  • “ReplicatedStorage” and “RunService” have the same abbreviations (RS)
  • “UserInputService” (UIS) can get confused for a uis variable
  • “Players” (P) is a terrible variable, but usually gets abbreviated as (Plrs) which is inconsistent with the other names
  • “MarketplaceService” and “MessagingService” are (MS)
  • “ServerStorage” and “SoundService” (SS), there’s also ServerScriptService (SSS), spam of S’
  • etc (there’s probably a lot more reasons)

Code is a lot cleaner and clearer when services aren’t abbreviated