This is a tutorial on how to make your game a better experience for players on Mobile, Console and PC! This tutorial won’t go over VR because it’s complicated, the controls have to be made very specific depending on your game genre and VR is the device type with the smallest playerbase out of all the other devices, I’m sure there are great tutorials for VR support in devforum.
The tutorial will go over on how to use UIS (UserInputService) and CAS (ContextActionService) for handling input, we won’t go into IAS (Input Action System) because it sucks (no one wants to manage input using instances)
Preparing to track input
If you aren’t using CAS, the best way to manage inputs in your game is by making a custom Input Manager for your game and assigning multiple input types to the same action, example:
{
Action = "Shoot",
Inputs =
{
KeyCode.Touch,
KeyCode.MouseLeftButton,
KeyCode.ButtonR2,
(ShootUIButton)
},
Function = (...)
}
Then your Input Manager would take all of the Inputs and track them, whenever one of them is triggered, the provided Function gets executed.
Detecting the input type
Before displaying button icons to the player or choosing which inputs to track, you need to know the Input Type, for that, we can use UIS.PreferredInput: Enum.PreferredInput, which can be ‘Touch’, ‘Gamepad’ or ‘KeyboardAndMouse’. For gamepad, we still need to know if it’s playstation or xbox to use the right icons, you can use the following piece of code:
local IsPlayStation = UIS:GetStringForKeyCode(Enum.KeyCode.ButtonA) == "ButtonCross"
PC: Tracking Keystrokes (Z, X, C, E...)
Using UIS:
UIS.InputBegan:Connect(function(Input,IsBusy)
if not IsBusy and Input.KeyCode == Enum.KeyCode.E then
print("Pressed E!")
end
end)
Using CAS:
CAS:BindAction("KeybindPress", function(ActionName, InputState: Enum.UserInputState, Input)
if ActionName == "KeybindPress" and InputState == Enum.UserInputState.Begin then
print("Pressed E!")
end
end, false, Enum.KeyCode.E)
PC: Tracking Mouse Clicks
Using LocalPlayer:GetMouse()
Mouse.Button1Down:Connect(function()
print("Left Clicked!")
end)
Mouse.Button2Down:Connect(function()
print("Right Clicked!")
end)
Using UIS:
UIS.InputBegan:Connect(function(Input,IsBusy)
if not IsBusy then
if Input.UserInputType == Enum.UserInputType.MouseButton1 then
print("Left Clicked!")
elseif Input.UserInputType == Enum.UserInputType.MouseButton2 then
print("Right Clicked!")
elseif Input.UserInputType == Enum.UserInputType.MouseButton3 then
print("Middle Clicked!")
end
end
end)
Using CAS:
CAS:BindAction("MouseClick", function(ActionName, InputState: Enum.UserInputState, Input: InputObject)
if ActionName == "MouseClick" and InputState == Enum.UserInputState.Begin then
if Input.UserInputType == Enum.UserInputType.MouseButton1 then
print("Left Clicked!")
elseif Input.UserInputType == Enum.UserInputType.MouseButton2 then
print("Right Clicked!")
elseif Input.UserInputType == Enum.UserInputType.MouseButton3 then
print("Middle Clicked!")
end
end
end, false, Enum.UserInputType.MouseButton1, Enum.UserInputType.MouseButton2, Enum.UserInputType.MouseButton3 )
PC: Tracking Mouse Movement & Wheel
Using LocalPlayer:GetMouse():
Mouse.Move:Connect(function()
print("Moved Mouse! Position:", Mouse.X, Mouse.Y)
end)
Mouse.WheelForward:Connect(function()
print("Moved 1 unit forward!")
end)
Mouse.WheelBackward:Connect(function()
print("Moved 1 unit backward!")
end)
Using UIS:
UIS.InputChanged:Connect(function(Input,IsBusy)
if not IsBusy then
if Input.UserInputType == Enum.UserInputType.MouseMovement then
print("Moved Mouse! Position:", Input.Position.X, Input.Position.Y)
elseif Input.UserInputType == Enum.UserInputType.MouseWheel then
print("Moved Wheel By", Input.Position.Z)
end
end
end)
Using CAS:
CAS:BindAction("MouseMove", function(ActionName, InputState: Enum.UserInputState, Input: InputObject)
if ActionName == "MouseMove" and InputState == Enum.UserInputState.Change then
if Input.UserInputType == Enum.UserInputType.MouseMovement then
print("Moved Mouse! Position:", Input.Position.X, Input.Position.Y)
elseif Input.UserInputType == Enum.UserInputType.MouseWheel then
print("Moved Wheel By", Input.Position.Z)
end
end
end, false, Enum.UserInputType.MouseMovement, Enum.UserInputType.MouseWheel)
PC: Customizing Mouse (Behavior, Sensitivity and Icon)
You can customize the Mouse during run time with in-game scripts, example code using UIS:
--Locks mouse at center of the screen, similar to shift lock
UIS.MouseBehavior = Enum.MouseBehavior.LockCenter
--Locks mouse at the current mouse position on the screen
UIS.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition
--Mouse can move freely
UIS.MouseBehavior = Enum.MouseBehavior.Default
--Sets the mouse icon to whatever you want
UIS.MouseIcon = "rbxassetid://636810791"
--Modify the sensitivity of the mouse from 0 to 10 (0 = no movement at all)
UIS.MouseDeltaSensitivity = 10 --SUPER FAST
PC: Window Focus Lost & Focused
Roblox has an event for when a player minimizes/goes off the game and also when they come back:
UIS.WindowFocusReleased:Connect(function()
print("Player is AFK!")
end)
UIS.WindowFocused:Connect(function()
print("Player is back!")
end)
CONSOLE: Detect if PlayStation or Xbox
Using UIS:
local IsPlayStation = UIS:GetStringForKeyCode(Enum.KeyCode.ButtonA) == "ButtonCross"
CONSOLE: Tracking Button Presses
Using UIS:
UIS.InputBegan:Connect(function(Input,IsBusy)
if not IsBusy then
if Input.KeyCode == Enum.KeyCode.ButtonA then
print("Pressed A/Cross!")
elseif Input.KeyCode == Enum.KeyCode.ButtonR2 then
print("Pressed R2!")
elseif Input.KeyCode == Enum.KeyCode.DPadDown then
print("Pressed Directional Pad Down!")
end
end
end)
Using CAS:
CAS:BindAction("ButtonPress", function(ActionName, InputState: Enum.UserInputState, Input)
if ActionName == "ButtonPress" and InputState == Enum.UserInputState.Begin then
if Input.KeyCode == Enum.KeyCode.ButtonA then
print("Pressed A/Cross!")
elseif Input.KeyCode == Enum.KeyCode.ButtonR2 then
print("Pressed R2!")
elseif Input.KeyCode == Enum.KeyCode.DPadDown then
print("Pressed Directional Pad Down!")
end
end
end, false, Enum.KeyCode.ButtonA, Enum.KeyCode.ButtonR2, Enum.KeyCode.DPadDown)
CONSOLE: Tracking Thumbsticks/Movement
Thumbsticks are very different than buttons, because their input isn’t just true or false, it’s a Vector2 with 2 numbers from -1 to 1 (0 = thumbstick in center), that isn’t the only complication, you have to account for dead-zones, which is when a controller starts to trigger movement even when the thumbstick is not moving, so you have to implement in your code a safe guard that ignores the movement if it’s too small, Roblox uses a dead zone of 20% (0.2) which is actually a good number, my old crappy xbox controller from 2018 has stick drift of around 15% (0.15) at most, so setting your dead zone to 20% by default is a good choice, obviously the best thing you can do is implement a setting so players can choose their dead zone but by default 20% should be good, also, you have to worry about roblox’s movement modules which overwrite movement, if you wish to overwrite roblox’s character movement just use CAS + BindActionAtPriority with high priority or disable the controls module in the PlayerModule. Here are the implementations:
Using UIS:
local THUMBSTICK_DEAD_ZONE = 0.2
UIS.InputChanged:Connect(function(Input,IsBusy)
if not IsBusy and Input.KeyCode == Enum.KeyCode.Thumbstick1 then
local Pos = Input.Position
local X = math.abs(Pos.X) > THUMBSTICK_DEAD_ZONE and Pos.X or 0
local Y = math.abs(Pos.Y) > THUMBSTICK_DEAD_ZONE and Pos.Y or 0
print("Thumbstick moved:",X,Y)
end
end)
Using CAS:
local THUMBSTICK_DEAD_ZONE = 0.2
CAS:BindAction("ThumbstickMoved", function(ActionName, InputState: Enum.UserInputState, Input)
if ActionName == "ThumbstickMoved" and InputState == Enum.UserInputState.Change then
local Pos = Input.Position
local X = math.abs(Pos.X) > THUMBSTICK_DEAD_ZONE and Pos.X or 0
local Y = math.abs(Pos.Y) > THUMBSTICK_DEAD_ZONE and Pos.Y or 0
print("Thumbstick moved:",X,Y)
end
end, false, Enum.KeyCode.Thumbstick1)
CONSOLE: Tracking Triggers (R2 & L2)
Triggers can be used as buttons as it was shown previously, however they can also be used as a high precision input like a lever that you can move to any value between 0% and 100%, Triggers are mainly used in racing games as pedals (L2 = backward, R2 = forward) because using a Trigger allows you to control how deep your car’s accelerator can go, press your trigger a little and your car accelerates a little, press your trigger to the max and the car will accelerate at full speed. Triggers can also be used for “power” shots/abilities, such as a medieval bow, by using a trigger you can determine the strength of your bow shot from 0% to 100%, or in a soccer game to determine with how much force you will kick the ball. However with triggers you get the same issue as thumbsticks with dead zones, it isn’t a problem with my controller as it seems to already have a built-in anti-dead zone, it could also be a Roblox built-in anti-dead-zone, further testing with different controllers using Triggers is required to determine if dead-zone is a worrying issue or not for Triggers, just for safety set the default dead zone for triggers at 5%. Here are the implementations:
Using UIS:
--Similar to a car's accelerator
local TRIGGER_DEAD_ZONE = 0.05
UIS.InputChanged:Connect(function(Input,IsBusy)
if not IsBusy and Input.KeyCode == Enum.KeyCode.ButtonR2 then
local Raw = Input.Position.Z
local Power = math.abs(Raw) > TRIGGER_DEAD_ZONE and Raw or 0
print("Trigger moved, power:",Power)
end
end)
--Bow/Power shot logic
--Charge and then either: unhold immediately to shoot or slowly uncharge back
local LastPressure = 0 --Pressure applied to the trigger in the last frame
local LastShot = 0 --Time that the last shot was taken
-- Variables:
local TRIGGER_DEAD_ZONE = 0.05
local ShotCooldown = 0.5 --Cooldown between each shot
local ShootSensitivity = 0.25 --Sudden amount of change required to shoot
local MinimumPower = 0.5 --Minimum power required to shoot
UIS.InputChanged:Connect(function(Input,IsBusy)
if not IsBusy and Input.KeyCode == Enum.KeyCode.ButtonR2 then
local Raw = Input.Position.Z
local Power = math.abs(Raw) > TRIGGER_DEAD_ZONE and Raw or 0
print("Trigger moved, power:",Power)
if tick()-LastShot > ShotCooldown and LastPressure - Power > ShootSensitivity and LastPressure >= MinimumPower then
warn("SHOOT!")
LastShot = tick()
end
LastPressure = Power
end
end)
Using CAS:
--Similar to a car's accelerator
local TRIGGER_DEAD_ZONE = 0.05
CAS:BindAction("Accelerator", function(ActionName, InputState: Enum.UserInputState, Input)
if ActionName == "Accelerator" and InputState == Enum.UserInputState.Change then
local Raw = Input.Position.Z
local Power = math.abs(Raw) > TRIGGER_DEAD_ZONE and Raw or 0
print("Trigger moved, power:",Power)
end
end, false, Enum.KeyCode.ButtonR2)
--Bow/Power shot logic
--Charge and then either: unhold immediately to shoot or slowly uncharge back
local LastPressure = 0 --Pressure applied to the trigger in the last frame
local LastShot = 0 --Time that the last shot was taken
-- Variables:
local TRIGGER_DEAD_ZONE = 0.05
local ShotCooldown = 0.5 --Cooldown between each shot
local ShootSensitivity = 0.25 --Sudden amount of change required to shoot
local MinimumPower = 0.5 --Minimum power required to shoot
CAS:BindAction("BowShot", function(ActionName, InputState: Enum.UserInputState, Input)
if ActionName == "BowShot" and InputState == Enum.UserInputState.Change then
local Raw = Input.Position.Z
local Power = math.abs(Raw) > TRIGGER_DEAD_ZONE and Raw or 0
print("Trigger moved, power:",Power)
if tick()-LastShot > ShotCooldown and LastPressure - Power > ShootSensitivity and LastPressure >= MinimumPower then
warn("SHOOT!")
LastShot = tick()
end
LastPressure = Power
end
end, false, Enum.KeyCode.ButtonR2)
CONSOLE: Controller Disconnected/Connected
Sometimes the player is doing a certain action but suddenly their controller disconnects! But due to faulty code, the action keeps going and the player fails the mission. To fix this, your code should listen for the controller events:
- UIS.GamepadDisconnected: (GamepadNum: Enum.UserInputType)
- UIS.GamepadConnected: (GamepadNum: Enum.UserInputType)
Example code:
local ActiveController = Enum.UserInputType.Gamepad1
UIS.GamepadDisconnected:Connect(function(gamepadNum)
if gamepadNum == ActiveController then
warn("Oh no! Controller disconnected!")
end
end)
CONSOLE: Button Selection
For lots of games button selection sucks, it’s either a navigator that tries to guess what buttons to go to when you move your thumbstick which sometimes leads to unreachable spots in UI, or a virtual mouse that is absolute pain to control, there’s 2 ways you can fix this:
-
Focus only on navigator based selection, but to do this, you have to make your UI console friendly. Almost every UI object has properties related to selection such as Selectable (UI element can be selected or not), NextSelectionUp,Down,Right, … (Guides the navigator telling what button to go to if the player presses the directional-arrow button in the controller) and some others.
-
(Harder but best experience) Support both navigator and mouse! Roblox doesn’t allow you to switch between them at runtime, so instead for the navigator you have to implement your own navigator system, make a function called “ForceSelect” that sets GuiService.SelectedObject. Everytime the player leaves a menu use ForceSelect(nil). Everytime the player goes into the menu or goes to a different UI page use ForceSelect(UIObject) on the most relevant starting point for your UI, this way the player can press “Select” to turn on the mouse whenever necessary for precision, but the player can also easily navigate menus and UI because of the navigator. Example code (make sure to have StarterGui.VirtualCursorMode as enabled):
local GuiService = game:GetService("GuiService")
local LastStartingObject = nil
function ForceSelect(guiObject)
if not guiObject then GuiService.SelectedObject = nil return end
if not guiObject:IsA("GuiObject") then return end
if not guiObject.Visible then return end
if not guiObject.Selectable then
guiObject.Selectable = true
end
LastStartingObject = guiObject
GuiService.SelectedObject = guiObject
end
function EnableUINavigator()
local Info = CAS:GetBoundActionInfo("NavigatorSelection")
if not Info or not next(Info) then
ForceSelect(LastStartingObject)
GuiService.AutoSelectGuiEnabled = false
CAS:BindActionAtPriority("NavigatorSelection",function(ActionName,InputState,Input)
if ActionName == "NavigatorSelection" and InputState == Enum.UserInputState.Begin then
if GuiService.SelectedObject then
ForceSelect(nil)
else
ForceSelect(LastStartingObject)
end
end
end,false,9999,Enum.KeyCode.ButtonSelect)
end
end
function DisableUINavigator()
local Info = CAS:GetBoundActionInfo("NavigatorSelection")
if Info and next(Info) then
ForceSelect(nil)
GuiService.AutoSelectGuiEnabled = true
CAS:UnbindAction("NavigatorSelection")
end
end
local MenuButton = (...) --A button inside your menu that will be selected at the start
local Menu = (...)
ForceSelect(MenuButton)
EnableUINavigator()
MenuButton.MouseButton1Clicked:Connect(function()
Menu.Visible = false
DisableUINavigator()
end)
CONSOLE: Multi-controller support (Up to 8 controllers!)
I haven’t ever seen a single roblox game implement this, but multi controller support is technically possible, it’s difficult because you can’t render 2+ cameras at once in a world for split screen (except with ViewportFrames but they waste performance and suck) but for certain game genres it’s possible (“Together”, and other games that share 1 camera for all players). Here’s example code on how to add multi-controller support:
local Controllers = {}
UIS.GamepadConnected:Connect(function(gamepadNum)
local num = tonumber(gamepadNum.Name:sub(8,8))
Controllers[num] = gamepadNum
print("Controller "..num.." connected!")
end)
UIS.GamepadDisconnected:Connect(function(gamepadNum)
local num = tonumber(gamepadNum.Name:sub(8,8))
Controllers[num] = nil
print("Controller "..num.." disconnected!")
end)
local function HandleInput(ControllerNum, Input)
--something
end
UIS.InputBegan:Connect(function(Input, IsBusy)
if not IsBusy and Input.UserInputType.Name:match("Gamepad") then
local ControllerNum = tonumber(Input.UserInputType.Name:sub(8,8))
HandleInput(ControllerNum, Input)
end
end)
Lots of people don’t know this, but the gamepad number is present in Input.UserInputType
CONSOLE: Vibration (Force Feedback)
Roblox has a feature called HapticEffect which allows you to use vibrations which make the game more realistic for people playing on a controller, example of use cases:
- UI Button Hover/Press: Tiny vibration
- Explosion: Big vibration
- Shaking: Constant vibration (example: rocket taking off)
- Hard collision: Medium vibration
- Force Feedback: Advanced vibration for a car in a driving game
Example code:
local Effect = Instance.new("HapticEffect",workspace)
Effect.Type = Enum.HapticEffectType.GameplayExplosion
Effect:Play() --Vibrates your controller
HapticEffects are way more capable than just this, if you wish to implement custom force feedback or customize HapticEffects to your liking, check out the documentation: HapticEffect Reference, HapticEffect Showcase/Announcement
MOBILE: Tracking & Setting Screen Orientation (Portrait or Landscape)
By default, most games have the screen orientation set as Landscape, which is a good thing (some games forget to do this and the screen gets stuck in portrait), if you want to set the default screen orientation to Landscape, find StarterGui and set “ScreenOrientation” to LandscapeSensor.
However, if you want to dynamically change the ScreenOrientation in your game depending on the situation, you can do it in a script in the example code:
local PlayerGui = game.Players.LocalPlayer.PlayerGui
-- Set Orientation
PlayerGui.ScreenOrientation = Enum.ScreenOrientation.LandscapeSensor
-- Get Orientation
local Orientation = PlayerGui.CurrentScreenOrientation
MOBILE: Tracking Touches/Screen Presses
Using UIS:
--Avoid using this because the player might have touched the screen to move the camera, rotate, zoom, or anything else
UIS.TouchStarted:Connect(function(Input,IsBusy)
if not IsBusy then
print("Touch started!")
end
end)
--This detects a regular tap on the screen
UIS.TouchTap:Connect(function(Positions: {Vector2}, IsBusy)
if not IsBusy then
print("Tapped screen!")
end
end)
--This detects the end of ANY type of touch, this could be the end of a swipe, rotation, etc
UIS.TouchEnded:Connect(function(Input, IsBusy)
if not IsBusy then
print("Stopped touching!")
end
end)
MOBILE: Tracking Swipes, Pans, Pinchs, and more
Using UIS:
--Fires more than once for the same touch, once for Begin and once for End and possibly Cancel
UIS.TouchLongPress:Connect(function(Positions: {Vector2}, State: Enum.UserInputState, IsBusy)
if not IsBusy and State == Enum.UserInputState.End then
print("Touched for a long time!")
end
end)
--Fires more than once for the same touch, every frame that the finger has moved
UIS.TouchMoved:Connect(function(Input, IsBusy)
if not IsBusy then
print("Finger moved:", Input.Position.X, Input.Position.Y)
end
end)
--Fires more than once for the same touch, once for Begin, every time the finger moves for Change, and once for End, and possibly Cancel
UIS.TouchPan:Connect(function(Positions: {Vector2}, Translation: Vector2, Velocity: Vector2, State: Enum.UserInputState, IsBusy)
if not IsBusy then
if State == Enum.UserInputState.Begin then
print("Started moving finger!")
elseif State == Enum.UserInputState.Change then
print("Moving finger at "..Velocity.Magnitude.." pixels/sec")
elseif State == Enum.UserInputState.End then
print("Finished moving finger, moved "..tostring(Translation).." pixels offset from initial touch position")
elseif State == Enum.UserInputState.Cancel then
print("Finger pan cancelled")
end
end
end)
--Fires more than once for the same touch, once for Begin, every time the finger zooms in/out for Change, and once for End, and possibly Cancel
UIS.TouchPinch:Connect(function(Positions: {Vector2}, Scale: number, Velocity: number, State: Enum.UserInputState, IsBusy)
if not IsBusy then
if State == Enum.UserInputState.Begin then
print("Started zooming!")
elseif State == Enum.UserInputState.Change then
print("Zoomed "..(Scale > 1 and "In" or "Out").." by "..math.abs(1 - Scale))
elseif State == Enum.UserInputState.End then
print("Finished zoom!")
elseif State == Enum.UserInputState.Cancel then
print("Zoom cancelled")
end
end
end)
--Fires more than once for the same touch, once for Begin, every time the fingers rotate for Change, and once for End, and possibly Cancel
UIS.TouchRotate:Connect(function(Positions: {Vector2}, Rotation: number, Velocity: number, State: Enum.UserInputState, IsBusy)
if not IsBusy then
if State == Enum.UserInputState.Begin then
print("Started rotating finger!")
elseif State == Enum.UserInputState.Change then
print("Rotated "..Velocity.." degrees! Now: "..Rotation.." degrees")
elseif State == Enum.UserInputState.End then
print("Finished rotating finger, rotation: "..Rotation.." degrees")
elseif State == Enum.UserInputState.Cancel then
print("Finger rotation cancelled")
end
end
end)
--Fires once per swipe no matter the amount of fingers used in the swipe, if you want a more advanced swipe event, use TouchPan
UIS.TouchSwipe:Connect(function(Direction: Enum.SwipeDirection, FingersUsed: number, IsBusy)
if not IsBusy then
print("Swiped the screen "..Direction.Name.." with "..FingersUsed.." fingers!")
end
end)
--Fires once per tap in the world
local Mouse = game.Players.LocalPlayer:GetMouse()
UIS.TouchTapInWorld:Connect(function(Position: Vector2, IsBusy)
if not IsBusy then
print("Tapped in the world at "..tostring(Mouse.Hit.Position).." in screen position "..tostring(Position))
end
end)
MOBILE: Tracking Device Acceleration, Gravity and Rotation
Using UIS:
--Acceleration (Velocity)
if UIS.AccelerometerEnabled then
local Acceleration = UIS:GetDeviceAcceleration().Position
UIS.DeviceAccelerationChanged:Connect(function(Input)
print("Device is moving in "..tostring(Input.Position))
end)
end
--Gravity
if UIS.AccelerometerEnabled then
local Gravity = UIS:GetDeviceGravity().Position
UIS.DeviceGravityChanged:Connect(function(Input)
print("Gravity is pulling at "..tostring(Input.Position))
end)
end
--Rotation
if UIS.GyroscopeEnabled then
local Rotation = UIS:GetDeviceRotation().Position
UIS.DeviceRotationChanged:Connect(function(Input,CF)
print("Device rotated "..tostring(Input.Delta)..", current rotation: "..tostring(Input.Position))
end)
end
MOBILE: GUI Buttons for each Keyboard Keybind
Creating a button on mobile for each keybind action is annoying, but it doesn’t have to be! By using CAS, you can use the same piece of code that supports MOBILE, CONSOLE AND PC all at once with no headache by setting the parameter “createTouchButton” as true, you can also customize the button if necessary, here’s an example:
CAS:BindAction("SuperJump", function(ActionName, InputState: Enum.UserInputState, Input)
if InputState == Enum.UserInputState.Begin then
local Character = game.Players.LocalPlayer.Character
local Humanoid = Character and Character:FindFirstChildOfClass("Humanoid")
if Humanoid then
Humanoid.Jump = true
Character.HumanoidRootPart.AssemblyLinearVelocity *= Vector3.new(1,0,1)
Character.HumanoidRootPart.AssemblyLinearVelocity += Vector3.new(0,300,0)
end
end
end, true, Enum.KeyCode.E, Enum.KeyCode.ButtonX)
CAS:SetTitle("SuperJump","SUPER JUMP")
CAS:SetDescription("SuperJump","Makes your character jump super high!")
You can also do other things such as CAS:SetImage(Action, Image) and CAS:SetPosition(Action, UDim2). You can modify the button further by using CAS:GetButton(Action) which gives you full control over the button.
Using CAS to the maximum potential
CAS (ContextActionService) is the best way to track input, it allows you to override roblox inputs or create an hiearchy of inputs. Here’s an example overriding the W keystroke:
--You can no longer move your character forward with W on your keyboard!
CAS:BindActionAtPriority("CharacterOverride", function(ActionName, State: Enum.UserInputState, Input: InputObject)
if State == Enum.UserInputState.Begin then
print("Pressed W!")
end
end, false, 9999, Enum.KeyCode.W)
However, in certain cases, you want to allow character movement, so we can pass the action:
--Now you can move your character forward while this code can still track "W" key presses!
CAS:BindActionAtPriority("CharacterOverride", function(ActionName, State: Enum.UserInputState, Input: InputObject)
if State == Enum.UserInputState.Begin then
print("Pressed W!")
end
return Enum.ContextActionResult.Pass
end, false, 9999, Enum.KeyCode.W)
With this, you can make an advanced hiearchy input system, here’s an example:
CAS:BindActionAtPriority("ClaimReward", function(ActionName, State: Enum.UserInputState, Input: InputObject)
if State == Enum.UserInputState.Begin then
if workspace:FindFirstChild("TreasureChest") then
print("Claimed treasure chest!")
return Enum.ContextActionResult.Sink --Takes full control and all other "E" keybinds after this one don't work
else
return Enum.ContextActionResult.Pass
end
else
return Enum.ContextActionResult.Pass
end
end, false, 10, Enum.KeyCode.E)
--Only runs if there is no treasure chest
CAS:BindActionAtPriority("Keybind_E", function(ActionName, State: Enum.UserInputState, Input: InputObject)
if State == Enum.UserInputState.Begin then
print("You pressed E!")
end
end, false, 5, Enum.KeyCode.E)
In this example, the binding “ClaimReward” has higher priority than “Keybind_E”, so it runs first. If a treasure chest exists in the workspace, then “ClaimReward” prints “Claimed treasure chest” and sinks all other actions binded to E, which doesn’t allow “Keybind_E” to run, but if there’s no treasure chest then it passes and “Keybind_E” runs which prints “You pressed E!”. This way, you can make advanced systems such as custom gears, here’s another example of conflicting keybinds and how CAS handles the overlap:
CAS:BindActionAtPriority("ReloadGun", function(ActionName, State: Enum.UserInputState, Input: InputObject)
if State == Enum.UserInputState.Begin then
local Character = game.Players.LocalPlayer.Character
if Character and Character:FindFirstChild("Gun") then
print("Reloading gun")
return Enum.ContextActionResult.Sink
else
return Enum.ContextActionResult.Pass
end
else
return Enum.ContextActionResult.Pass
end
end, false, 10, Enum.KeyCode.R)
--Only runs if the player doesn't have a gun equipped
CAS:BindActionAtPriority("ResetCharacter", function(ActionName, State: Enum.UserInputState, Input: InputObject)
if State == Enum.UserInputState.Begin then
local Character = game.Players.LocalPlayer.Character
if Character and Character:FindFirstChildOfClass("Humanoid") then
Character:FindFirstChildOfClass("Humanoid").Health = 0
end
end
end, false, 5, Enum.KeyCode.R)
Button Icons
Wikimedia has a few good pages on button icons I recommend you use:
- PC¹: Category:Keyboard key icons - Wikimedia Commons
- Xbox: Category:Xbox Series Controller icons - Wikimedia Commons
- PlayStation²: Category:PlayStation controller buttons - Wikimedia Commons
¹: The icons are low quality, while they might look fine in a big monitor in most cases, if you wish to display it at high resolution, try finding something else
²: The Playstation button icons suck compared to xbox, so I made my own pack that looks very similar, it’s probably missing some buttons but it covers most of them:
PlayStation Icons.zip (73.8 KB)
But if you want to go the LAZIEST route and use text as button icons, here they are:
- PS Buttons: ▲△◯○●⬤□:white_medium_square:■:black_medium_square:✕
×X - PS Other Buttons: L1, R1, L2, R2, L3, R3, ⧉, (≡, ☰)
- Xbox Buttons: A B X Y Ⓐ Ⓑ Ⓧ Ⓨ
- Xbox Other Buttons: LB, RB, LT, RT, LS, RS, ⧉, (≡, ☰)
- DPad:



⇦⇨⇧⇩▲▼:reverse_button:
▴▾◂▸△▽◁▷▵▿◃▹
You can also use UIS:GetImageForKeyCode(KeyCode) but the icons provided from it suck.