I am here to introduce Input Callback Manager (ICM).
ICM.rbxl (58.3 KB)
Input Callback Manager is a basic utility made for managing dynamic InputBegan, InputEnded and InputChanged events with only one thread. It does this by storing callbacks for each UserInputType in a table. This module is great for making guis that constantly change input format, such as when a different window is “Focused” and requires different keybinds. When, for example, InputBegan is fired, it will check if any binds and/or return callbacks are registered for the UserInputType. This module is very short, but don’t let its length fool you, as it is still very useful.
Source:
--[[
Input Callback Manager
Made by Humanagon in 2025.
Public domain.
This module returns a function in order to allow mutiple module instances if desired.
To require and initialize, type:
require(game.Path.To.Module)()
--]]
local uis = game:GetService("UserInputService")
return function()
local ICM = {}
local input_began_callbacks = {}
local input_changed_callbacks = {}
local input_ended_callbacks = {}
local input_began_deconstructors = {}
local input_changed_deconstructors = {}
local input_ended_deconstructors = {}
local input_began_binds = {}
local input_changed_binds = {}
local input_ended_binds = {}
local input_began_connection = nil
local input_changed_connection = nil
local input_ended_connection = nil
local items = Enum.UserInputType:GetEnumItems()
for i = 1, #items do
input_began_binds[items[i]] = {}
input_changed_binds[items[i]] = {}
input_ended_binds[items[i]] = {}
end
items = nil
function ICM.ReplaceInputBeganCallback(enum_item, callback, deconstructor)
if input_began_deconstructors[enum_item] then
input_began_deconstructors[enum_item]()
end
input_began_callbacks[enum_item] = callback
input_began_deconstructors[enum_item] = deconstructor
end
function ICM.ReplaceInputChangedCallback(enum_item, callback, deconstructor)
if input_changed_deconstructors[enum_item] then
input_changed_deconstructors[enum_item]()
end
input_changed_callbacks[enum_item] = callback
input_changed_deconstructors[enum_item] = deconstructor
end
function ICM.ReplaceInputEndedCallback(enum_item, callback, deconstructor)
if input_ended_deconstructors[enum_item] then
input_ended_deconstructors[enum_item]()
end
input_ended_callbacks[enum_item] = callback
input_ended_deconstructors[enum_item] = deconstructor
end
function ICM.ClearInputBeganCallbacks()
for k, v in pairs(input_began_deconstructors) do
v()
end
table.clear(input_began_callbacks)
table.clear(input_began_deconstructors)
end
function ICM.ClearInputChangedCallbacks()
for k, v in pairs(input_changed_deconstructors) do
v()
end
table.clear(input_changed_callbacks)
table.clear(input_changed_deconstructors)
end
function ICM.ClearInputEndedCallbacks()
for k, v in pairs(input_ended_deconstructors) do
v()
end
table.clear(input_ended_callbacks)
table.clear(input_ended_deconstructors)
end
function ICM.ClearAllCallbacks()
ICM.ClearInputBeganCallbacks()
ICM.ClearInputChangedCallbacks()
ICM.ClearInputEndedCallbacks()
end
function ICM.BindFunctionToInputBegan(enum_item, func)
if input_began_binds[enum_item] then
input_began_binds[enum_item][func] = true
end
end
function ICM.UnbindFunctionFromInputBegan(enum_item, func)
if input_began_binds[enum_item] then
input_began_binds[enum_item][func] = nil
end
end
function ICM.BindFunctionToInputChanged(enum_item, func)
if input_changed_binds[enum_item] then
input_changed_binds[enum_item][func] = true
end
end
function ICM.UnbindFunctionFromInputChanged(enum_item, func)
if input_changed_binds[enum_item] then
input_changed_binds[enum_item][func] = nil
end
end
function ICM.BindFunctionToInputEnded(enum_item, func)
if input_ended_binds[enum_item] then
input_ended_binds[enum_item][func] = true
end
end
function ICM.UnbindFunctionFromInputEnded(enum_item, func)
if input_ended_binds[enum_item] then
input_ended_binds[enum_item][func] = nil
end
end
local function InputBegan(input, gameProcessedEvent)
for func in pairs(input_began_binds[input.UserInputType]) do
func(input, gameProcessedEvent, func)
end
if input_began_callbacks[input.UserInputType] then
return input_began_callbacks[input.UserInputType](input, gameProcessedEvent)
end
end
local function InputChanged(input, gameProcessedEvent)
for func in pairs(input_changed_binds[input.UserInputType]) do
func(input, gameProcessedEvent, func)
end
if input_changed_callbacks[input.UserInputType] then
return input_changed_callbacks[input.UserInputType](input, gameProcessedEvent)
end
end
local function InputEnded(input, gameProcessedEvent)
for func in pairs(input_ended_binds[input.UserInputType]) do
func(input, gameProcessedEvent, func)
end
if input_ended_callbacks[input.UserInputType] then
return input_ended_callbacks[input.UserInputType](input, gameProcessedEvent)
end
end
--For cleaning up if the script comes to an end.
function ICM.Disconnect()
if input_began_connection then
input_began_connection:Disconnect()
end
if input_changed_connection then
input_changed_connection:Disconnect()
end
if input_ended_connection then
input_ended_connection:Disconnect()
end
input_began_connection = nil
input_changed_connection = nil
input_ended_connection = nil
end
function ICM.Connect()
ICM.Disconnect()
input_began_connection = uis.InputBegan:Connect(InputBegan)
input_changed_connection = uis.InputChanged:Connect(InputChanged)
input_ended_connection = uis.InputEnded:Connect(InputEnded)
end
ICM.Connect()
return table.freeze(ICM)
end
Let’s say that we have a button that we want to change the background color of when it is clicked, and then changed back when it is released. Most people would simply add a MouseButton1Down for when it is clicked, and a MouseButton1Up for when it is released, but this has a downside, it will only change back if the button is being hovered over when the mouse is released. You could also use the UserInputService.InputEnded event, but that will use a new thread for every button you want to handle.
Instead, we can use the module:
local ICM = require(script.ICM)()
local button = script.Parent
local hover = false
function Callback()
print("Clicked!")
end
button.MouseButton1Down:connect(function()
button.BackgroundColor3 = Color3.new(0, 1, 0) --Green
ICM.ReplaceInputEndedCallback(Enum.UserInputType.MouseButton1, function()
ICM.ReplaceInputEndedCallback(Enum.UserInputType.MouseButton1) --Remove the callback.
if hover then
Callback()
end
end, function()
button.BackgroundColor3 = Color3.new(1, 0, 0) --Red
end)
end)
button.MouseEnter:connect(function()
hover = true
end)
button.MouseLeave:connect(function()
hover = false
end)
Let’s explain the code.
The first parameter of ICM.ReplaceInputEndedCallback is the UserInputType that the callback is for, the second is the callback function that is called if the UserInputType is fired with an InputEnded event, and the third is a deconstructor that will be called if the return callback is changed or removed. We obviously always want the button color to go back to red when it is no longer being clicked, so we put that in the deconstructor in case something else registers a MouseButton1 callback for InputEnded. We will put the button clicked logic inside the callback function parameter. This way, the callback itself will only be called if the button is released.
If you want to bind multiple functions to a UserInputType, you can use one of the binding functions.
ICM.BindFunctionToInputBegan(Enum.UserInputType.Keyboard, function(input, gameProcessedEvent, func)
print("A keyboard input has begun.")
ICM.UnbindFunctionFromInputBegan(Enum.UserInputType.Keyboard, func)
--func is simply the same binded function passed as an argument for the sake of anonymous functions.
end)
So, weather you need a simple quick clean-up of your code, or a cog in a full-on system, Input Callback Manager should be a reasonable choice. I hope you all enjoy the module!