InputManager — Modular Input Binding with Context and Debounce
A clean, scalable input system for Roblox that supports keyboard, gamepad, and touch inputs with context filtering, debounce logic, and centralized registration.
Why Use This?
Roblox’s default input handling is device-specific and scattered across scripts. This module solves that by:
- Centralizing input logic in one place
- Supporting multiple input types (keyboard, gamepad, touch)
- Preventing accidental double triggers with debounce
- Enabling context-based input filtering (e.g., gameplay vs menu)
- Making input registration declarative and readable
- Preparing for future expansion, like rebinding, priority sorting, and input stacks
Whether you’re building a mission system, a UI-heavy experience, or a sandbox toolset, this module gives you clean control over player input.
Features
Register()inputs with context, debounce, and priority- Supports
Enum.KeyCodeandEnum.UserInputType - Context switching (
SetContext()) - Debounce with custom timing:
Debounce = {true, 0.05} - Touch and gamepad support
- Unregister bindings
- Debug printout
Installation
- Create a ModuleScript named
InputManagerinReplicatedStorage - Paste the full module code below
- Create a LocalScript in
StarterPlayerScriptsto use it
Usage Example
local Input = require(game.ReplicatedStorage.InputManager)
Input.Register("OpenMenu", {
Key = Enum.KeyCode.M,
Callback = function()
print("Menu opened")
end,
Context = "gameplay",
Debounce = {true, 0.05}
})
Input.SetContext("gameplay")
API Reference
InputManager.Register(name: string, config: table)
Registers an input binding.
Config fields:
Key:Enum.KeyCodeorEnum.UserInputTypeCallback: function to runContext: optional string (default"global")Type: optional string ("Tap","Hold","DoubleTap")Priority: optional number (for future sorting)Debounce: optional{boolean, number}— e.g.{true, 0.05}for 50ms cooldown
InputManager.Unregister(name: string)
Removes a registered input binding by name.
InputManager.SetContext(context: string)
Sets the active input context. Only bindings matching this context (or "global") will respond.
InputManager.Debug()
Prints all registered bindings to the console for inspection.
Context System
Use contexts to isolate input logic by game state:
| Context | Example Inputs |
|---|---|
"gameplay" |
Move, jump, interact |
"menu" |
Navigate, confirm, cancel |
"cutscene" |
Skip, pause, toggle subtitles |
"buildMode" |
Place, rotate, delete, snap |
Switch contexts with:
InputManager.SetContext("menu")
Expansion Roadmap
- Context stack (push/pop)
- Hold + double-tap detection
- Priority sorting
- Rebinding UI
- Input logging
Full Module Code
local UserInputService = game:GetService("UserInputService")
local InputManager = {}
local bindings = {}
local activeContext = "global"
local lastInputTime = {}
local InputType = {
Tap = "Tap",
Hold = "Hold",
DoubleTap = "DoubleTap"
} Function on InputManager.Register(name: string, config: {
Key: Enum.KeyCode | Enum.UserInputType,
Callback: (...any) -> (),
Context: string?,
Type: string?,
Priority: number?,
Debounce: {boolean, number}?
})
assert(config.Key, "Input must have a Key")
assert(config.Callback, "Input must have a Callback")
bindings[name] = {
Key = config.Key,
Callback = config.Callback,
Context = config.Context or "global",
Type = config.Type or InputType.Tap,
Priority = config.Priority or 0,
Debounce = config.Debounce or {false, 0.2}
}
end
function InputManager.Unregister(name: string)
bindings[name] = nil
end
function InputManager.SetContext(context: string)
activeContext = context
end
local function isDebounced(name: string)
local bind = bindings[name]
if not bind or not bind.Debounce[1] then return false end
local now = tick()
local last = lastInputTime[name] or 0
if now - last < bind.Debounce[2] then return true end
lastInputTime[name] = now
return false
end
local function handleInput(input, gameProcessed)
if gameProcessed then return end
for name, bind in pairs(bindings) do
if bind.Context ~= activeContext and bind.Context ~= "global" then continue end
if input.KeyCode == bind.Key or input.UserInputType == bind.Key then
if isDebounced(name) then continue end
bind.Callback(input)
end
end
end
UserInputService.InputBegan:Connect(handleInput)
UserInputService.TouchTap:Connect(function(touchPositions, processed)
handleInput({UserInputType = Enum.UserInputType.Touch}, processed)
end)
UserInputService.InputChanged:Connect(function(input)
if input.UserInputType == Enum.UserInputType.Gamepad1 then
handleInput(input, false)
end
end)
function InputManager.Debug()
for name, bind in pairs(bindings) do
print(`[Input] {name} → {bind.Key} ({bind.Context})`)
end
end
return InputManager
I created this because I was tired of writing more code to cancel overlapping keybinds.