InputController is a module designed to make it easier to manage client input. It primarily uses Keymaps
, Actions
and Binds
.
A Keymap
is a group of Actions
and Binds
. They can be turned on/off using at any time and make it much easier to manage input during different game states. An example setup would be having one Keymap
for character movement, one for camera movement, one for each tool, and one for each UI panel. Keymaps
have a priority value that allows Binds
to optionally sink input for other Keymaps
below the priority of the Bind
.
An Action
is a function linked to a Bind
, and Binds
determine when the Action
should be triggered.
An example keymap:
local Keymap = {
Priority = 1, --Higher priorities are triggered first
Actions = {
--A dictionary of actions, read down for more info
["ExampleAction"] = function(Bind, InputObject)
print("ExampleAction triggered")
end,
},
Binds = {
--An array of binds, read down for more info
{Action = "ExampleAction", Trigger = Enum.KeyCode.E, TriggerState = "Press"},
},
}
InputController.RegisterKeymap(Name, Keymap, SetActive)
Registers a Keymap
with the InputController. Name should be unique, for example “Tool_Pistol”, “Tool_Rifle”, “UI_Store”, “UI_RobuxStore”, “Game_CharacterMovement”, “Game_CameraMovement”. Third parameter is optional, defaults to false, and if set to true then the Keymap
will be instantly active
InputController.SetKeymapActive(Name, IsActive)
if IsActive is true, the keymap is being listened to. If it’s false, the keymap will be ignored.
Demo
local InputController = require(game.ReplicatedStorage.InputController)
local ExampleKeymap = InputController.RegisterKeymap("ExmapleKeymap", {
Priority = 1, --Higher priority goes first
Actions = {
["ExampleAction1"] = function(Bind, Input)
print("ExampleAction1 was triggered by " .. tostring(Bind.Trigger))
end,
["PressAnyButtonToContinue"] = function(Bind, Input)
print("Pressed some button on keyboard", Input.KeyCode)
end,
["MoveCharacter"] = function(Bind, Input)
Bind.Active = (Input.UserInputState.Name == "Begin" and true or false)
local MoveDirection = Vector3.new()
for _, Bind in pairs(Bind.Keymap.Binds) do
if (Bind.Action == "MoveCharacter" and Bind.Active) then
MoveDirection = MoveDirection + Bind.Direction
end
end
print("Moving in", MoveDirection.Magnitude > 0 and MoveDirection.Unit or MoveDirection)
end,
},
Binds = {
{
--Required
Action = "ExampleAction1", --The name of the action. Must be in the same keymap
Trigger = Enum.UserInputType.MouseButton1, --What key will trigger the action. Must be `Enum.KeyCode.` or `Enum.UserInputType.`
TriggerState = "Press", --When should it be triggered. Supports "Down", "Press", "Up" and "Any"
--Optional
SinkInput = true, --If set to true, keymaps with a lower priority will not be triggered if this bind gets triggered. Defaults to `false`
IgnoreGameProcessed = true, --If set to true, the action will be triggered regardless if `gameProcessedEvent` is true, see https://developer.roblox.com/en-us/api-reference/event/UserInputService/InputBegan. Defaults to `false`
--Any custom bind-specific data can be added after here. Example use case would be for a character controller where there is a single `MoveCharacter` action, and inside each bind there could be a `Direction` value
Direction = Vector3.new(1, 0, 0),
},
{Action = "PressAnyButtonToContinue", Trigger = Enum.UserInputType.Keyboard, TriggerState = "Down"},
{Action = "MoveCharacter", Trigger = Enum.KeyCode.W, TriggerState = "Any", Direction = Vector3.new(0, 0, -1)},
{Action = "MoveCharacter", Trigger = Enum.KeyCode.A, TriggerState = "Any", Direction = Vector3.new(-1, 0, 0)},
{Action = "MoveCharacter", Trigger = Enum.KeyCode.S, TriggerState = "Any", Direction = Vector3.new(0, 0, 1)},
{Action = "MoveCharacter", Trigger = Enum.KeyCode.D, TriggerState = "Any", Direction = Vector3.new(1, 0, 0)},
},
}, true) --If third parameter of `InputController.RegisterKeymap` is true, then they keymap is activated instantly. Otherwise call `InputController.SetKeymapActive("Name", true)`
InputController.rbxmx (5.1 KB)
Lua source
local UserInputService = game:GetService("UserInputService")
local Controller = {
Keymaps = {},
Cache = {
Triggers = {},
},
}
local function mpairs(...)
--mpairs is used to loop through multiple tables at once
local Tables = {...}
local TableIndex, Table = next(Tables)
local Key, Value = nil, nil
return function()
if (not Table) then return end
repeat
Key, Value = next(Table, Key)
if (Key == nil) then
TableIndex, Table = next(Tables, TableIndex)
end
until (Key ~= nil or not Table)
return Key, Value
end
end
function Controller.Main()
UserInputService.InputBegan:Connect(Controller.HandleInput)
UserInputService.InputChanged:Connect(Controller.HandleInput)
UserInputService.InputEnded:Connect(Controller.HandleInput)
return Controller
end
function Controller.HandleInput(Input, GameProcessed)
local InputStateValue = Input.UserInputState.Value
local Container = (Controller.Cache.Triggers[Input.KeyCode] or Controller.Cache.Triggers[Input.UserInputType])
local SinkInputAtPriority
if (InputStateValue == 0) then
for _, Bind in mpairs(Controller.Cache.Triggers[Input.KeyCode], Controller.Cache.Triggers[Input.UserInputType]) do
if (SinkInputAtPriority and (Bind.Priority or Bind.Keymap.Priority) < SinkInputAtPriority) then break end
if (not Bind.Keymap.Active) then continue end
if (GameProcessed and not Bind.IgnoreGameProcessed) then continue end
if (Bind.TriggerState == "Press") then
Bind._TriggerDown = tick()
elseif (Bind.TriggerState == "Down" or Bind.TriggerState == "Any") then
if (Bind.Keymap.Actions[Bind.Action]) then
coroutine.wrap(Bind.Keymap.Actions[Bind.Action])(Bind, Input)
end
if (Bind.SinkInput) then
SinkInputAtPriority = (Bind.Priority or Bind.Keymap.Priority)
end
end
end
elseif (InputStateValue == 2) then
for _, Bind in mpairs(Controller.Cache.Triggers[Input.KeyCode], Controller.Cache.Triggers[Input.UserInputType]) do
if (SinkInputAtPriority and (Bind.Priority or Bind.Keymap.Priority) < SinkInputAtPriority) then break end
if (not Bind.Keymap.Active) then continue end
if (GameProcessed and not Bind.IgnoreGameProcessed) then continue end
if (Bind.TriggerState == "Press") then
if (tick() - (Bind._TriggerDown or 0) <= 0.25) then
if (Bind.Keymap.Actions[Bind.Action]) then
coroutine.wrap(Bind.Keymap.Actions[Bind.Action])(Bind, Input)
end
if (Bind.SinkInput) then
SinkInputAtPriority = (Bind.Priority or Bind.Keymap.Priority)
end
end
elseif (Bind.TriggerState == "Up" or Bind.TriggerState == "Any") then
if (Bind.Keymap.Actions[Bind.Action]) then
coroutine.wrap(Bind.Keymap.Actions[Bind.Action])(Bind, Input)
end
if (Bind.SinkInput) then
SinkInputAtPriority = (Bind.Priority or Bind.Keymap.Priority)
end
end
end
end
end
function Controller.RegisterKeymap(Name, Keymap, SetActive)
if (not Name or not Keymap) then return end
Keymap.Active = (SetActive and true or false)
Keymap.Priority = (tonumber(Keymap.Priority) or 0)
if (Controller.Keymaps[Name]) then
warn("InputController: Overwriting keymap '" .. tostring(Name) .. "'")
if (Controller.Keymaps[Name].Binds) then
for _, Bind in pairs(Controller.Keymaps[Name].Binds) do
local Index = table.find(Controller.Cache.Triggers[Bind.Trigger], Bind)
if (Index) then
table.remove(Controller.Cache.Triggers[Bind.Trigger], Index)
if (#Controller.Cache.Triggers[Bind.Trigger] <= 0) then
Controller.Cache.Triggers[Bind.Trigger] = nil
end
end
end
end
end
Controller.Keymaps[Name] = Keymap
if (Keymap.Binds) then
for _, Bind in pairs(Keymap.Binds) do
Bind.Keymap = Keymap
if (not Controller.Cache.Triggers[Bind.Trigger]) then
Controller.Cache.Triggers[Bind.Trigger] = {}
end
table.insert(Controller.Cache.Triggers[Bind.Trigger], Bind)
table.sort(Controller.Cache.Triggers[Bind.Trigger], function(Bind0, Bind1)
return (Bind0.Priority or Bind0.Keymap.Priority) > (Bind1.Priority or Bind1.Keymap.Priority)
end)
end
end
return Keymap
end
function Controller.SetKeymapActive(Name, IsActive)
if (not Controller.Keymaps[Name]) then
return warn("InputController: " .. tostring(Name) .. " is not a valid keymap")
end
Controller.Keymaps[Name].Active = (IsActive and true or false)
end
return Controller.Main()