Claude Skill

roblox-input

Use when handling Roblox keyboard, mouse, gamepad, touch, motion input, or cross-platform action binding.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download tabooharmony-roblox-brain-skills_gameplay_roblox-input-6051c35.zip · 11 KB
Part of tabooharmony/roblox-brain — 29 skills

Install

skills CLI npx skills add https://github.com/TabooHarmony/roblox-brain/tree/main/skills/gameplay/roblox-input
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tabooharmony-roblox-brain@llmmart
Git git clone https://github.com/TabooHarmony/roblox-brain.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole tabooharmony/roblox-brain collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Roblox Input

When to Load

Load for keyboard, mouse, gamepad, touch, motion, or cross-platform action binding. Client-side only. For simulation-affecting input in a Server Authority project, use the Input Action System rather than traditional input events.

Quick Reference

Core events (UserInputService): InputBegan, InputChanged, InputEnded fire as (input: InputObject, gameProcessedEvent: boolean). InputBegan does NOT fire for mouse wheel. Events only fire while the client window is focused.

Prefer ContextActionService over InputBegan for gameplay: free conflict resolution (chat won't steal H) and free mobile buttons:

local CAS = game:GetService("ContextActionService")

local function onAction(name, state, _input)
    if name == "Jump" and state == Enum.UserInputState.Begin then
        humanoid.Jump = true
    end
end

CAS:BindAction("Jump", onAction, true,
    Enum.KeyCode.Space, Enum.KeyCode.ButtonA)

BindAction(name, handler, createTouchButton, ...inputTypes). Handler returns ContextActionResult.Sink to consume, .Pass to fall through.

Server Authority: use InputAction/InputContext for inputs that affect the core simulation, store the input state where the synchronized simulation can read it, and process it through RunService:BindToSimulation() (requires Workspace.UseFixedSimulation enabled in Studio). ContextActionService remains appropriate for UI-only or classic-project actions.

Gamepad UI focus: separate gameplay bindings from menu selection. Set GuiService.SelectedObject, mark controls Selectable, and test nested/modal navigation.

Gamepad: use GetConnectedGamepads() and listen to connection changes.

Touch: use high-level gesture events or raw TouchStarted/TouchMoved/TouchEnded when tracking fingers.

Dragging: DragDetector (3D parts/models, physics-capable) and UIDragDetector (UI) make objects draggable with zero code; events DragStart/DragContinue/DragEnd.

Pitfalls:

  • gameProcessedEvent=true in InputBegan → UI consumed it. Filter for gameplay.
  • BindAction is stack-based: most-recent wins. Use BindActionAtPriority.
  • Client-only. Server scripts silently no-op.
  • JumpRequest fires multiple times per jump; debounce.
  • Mouse wheel only fires InputChanged.

Full event tables and polling methods: references/full.md.

Files (roblox-brain)
  • references
    • full.md 28.1 KB
      # Roblox Input: Full Reference
      
      
      > **Code in this reference is illustrative. Adapt to your game and verify in Studio before production use.**
      
      Both `UserInputService` (UIS) and `ContextActionService` (CAS) are client-only. They work in `LocalScript`, `ModuleScript` required by a `LocalScript`, or `Script` with `RunContext` set to `Client`. Server-side calls silently no-op.
      
      ## Input Action System and Server Authority
      
      For a Server Authority project, inputs that affect the core simulation should use Roblox's Input Action System (`InputAction` and `InputContext`) rather than traditional `UserInputService.InputBegan` or a continuous `RemoteEvent` stream. Store the current action state where the shared simulation can read it, then process synchronized movement or physics through `RunService:BindToSimulation()` (requires `Workspace.UseFixedSimulation` enabled in Studio) so client prediction and server rollback use the same input history.
      
      `ContextActionService` remains useful for UI-only actions and classic projects. Do not use the UI binding choice as a security boundary; the server still validates the resulting action and its game-specific permissions.
      
      ## UserInputService: Properties
      
      | Property | Type | Notes |
      |----------|------|-------|
      | `KeyboardEnabled` / `MouseEnabled` | bool | Physical keyboard/mouse present |
      | `TouchEnabled` | bool | Touchscreen present (mobile + some laptops) |
      | `GamepadEnabled` | bool | Any supported gamepad connected |
      | `AccelerometerEnabled` / `GyroscopeEnabled` | bool | Mobile sensors present |
      | `VREnabled` | bool | VR headset active |
      | `PreferredInput` | Enum.UserInputType | Device the player is **currently using most**. Better than per-device flags on hybrids. |
      | `MouseBehavior` | Enum.MouseBehavior | `Default`, `LockCenter`, `LockCurrentPosition` |
      | `MouseDeltaSensitivity` | number | 0–1, sensitivity multiplier |
      | `MouseIcon` / `MouseIconEnabled` / `MouseIconContent` | string/bool | Custom cursor (AssetId, ContentText) |
      | `OnScreenKeyboardPosition` / `OnScreenKeyboardSize` / `OnScreenKeyboardVisible` | Vector2/bool | Mobile/console on-screen keyboard state |
      | `ModalEnabled` | bool | Block all input while a modal is active |
      | `UserHeadCFrame` / `GetUserCFrame()` | CFrame | VR head pose |
      
      ## UserInputService: Methods (selection)
      
      | Method | Returns | Use for |
      |--------|---------|---------|
      | `IsKeyDown(KeyCode)` | bool | Held keyboard keys |
      | `IsMouseButtonPressed(UserInputType)` | bool | Held mouse buttons |
      | `IsGamepadButtonDown(UserInputType, KeyCode)` | bool | Held gamepad buttons |
      | `GetKeysPressed()` | InputObject[] | All held keyboard inputs |
      | `GetMouseButtonsPressed()` | InputObject[] | All held mouse buttons |
      | `GetMouseDelta()` | Vector2 | Per-frame mouse movement |
      | `GetMouseLocation()` | Vector2 | Mouse position in viewport |
      | `GetLastInputType()` | Enum.UserInputType | Last input across all devices |
      | `GetConnectedGamepads()` | UserInputType[] | Currently connected gamepads |
      | `GetGamepadState(UserInputType)` | InputObject[] | All active inputs on a gamepad |
      | `GetGamepadConnected(UserInputType)` | bool | Is a specific pad slot connected |
      | `GetDeviceAcceleration()` / `GetDeviceGravity()` / `GetDeviceRotation()` | Vector3 | Current mobile sensor readings |
      | `GetFocusedTextBox()` | TextBox? | Currently-focused text input (if any) |
      | `GetStringForKeyCode(KeyCode)` / `GetImageForKeyCode(KeyCode)` | string | Display labels for key bindings |
      | `RecenterUserHeadCFrame()` | () | Reset VR head to current look direction |
      
      ## UserInputService: Events
      
      ### Discrete (Began → Ended)
      - `InputBegan(input: InputObject, gameProcessedEvent: boolean)`: fires when input starts. Does NOT fire for mouse wheel.
      - `InputChanged(input, gameProcessedEvent)`: fires while input is changing (mouse move, thumbstick, wheel, drag).
      - `InputEnded(input, gameProcessedEvent)`: fires when input stops.
      
      All three only fire when the Roblox client window has focus.
      
      ### Touch (high-level gestures)
      - `TouchTap`, `TouchTapInWorld` (world-space position), `TouchPan`, `TouchPinch`, `TouchRotate`, `TouchSwipe`, `TouchLongPress`, `TouchDrag`.
      
      ### Touch (raw)
      - `TouchStarted`, `TouchMoved`, `TouchEnded`. Use raw events when you need per-touch tracking across multiple fingers.
      
      ### Gamepad
      - `GamepadConnected(UserInputType)`, `GamepadDisconnected(UserInputType)`.
      
      ### Mobile sensors
      - `DeviceGravityChanged(Vector3, rotation)`: fires when accelerometer present + `AccelerometerEnabled`.
      - `DeviceRotationChanged(CFrame, rotation, CFrame)`: fires when gyroscope present.
      - `DeviceAccelerationChanged(Vector3, acceleration)`: fires when accelerometer present.
      
      ### Player state
      - `JumpRequest()`: fires on jump key press. Fires multiple times per jump; debounce.
      - `LastInputTypeChanged(Enum.UserInputType)`: when the active input device changes.
      - `PointerAction(Enum.PointerAction, Vector2, number)`: middle-click navigation.
      
      ### UI focus
      - `TextBoxFocused(TextBox)`, `TextBoxFocusReleased(TextBox)`: track when text input gains/loses focus.
      
      ### Window
      - `WindowFocused()`, `WindowFocusReleased()`: fires when the Roblox window gains/loses OS focus.
      
      ### VR
      - `UserCFrameChanged(Enum.UserCFrame, CFrame)`: head/hand motion in VR.
      
      ## InputObject
      
      Properties you read off the input arg:
      - `UserInputType`: Keyboard, MouseButton1..3, MouseWheel, MouseMovement, Touch, Gamepad1..8, Accelerometer, Gyro, etc.
      - `KeyCode`: the specific key/button (e.g. `Enum.KeyCode.Space`, `Enum.KeyCode.ButtonA`).
      - `UserInputState`: `Begin`, `Change`, `End`, `Cancel`. `Cancel` fires when input was in progress and another action bound over it.
      - `Position`: Vector2 in viewport (mouse, touch).
      - `Delta`: Vector3 (mouse/gamepad movement this frame).
      
      Note: when `Cancel` fires, the `InputObject` is `UserInputType.None` / `KeyCode.Unknown`.
      
      ## ContextActionService
      
      ### BindAction
      
      Signature: `BindAction(actionName: string, handler: Function, createTouchButton: boolean, ...inputTypes)`.
      
      The handler receives `(actionName, inputState, inputObject)` and returns `Enum.ContextActionResult`:
      - `Sink`: consume the input. Stops propagation.
      - `Pass`: let lower-priority bindings also receive it.
      
      Bindings form a **stack**: most recent binding on the same input wins. When you unbind, the previous binding takes over. Use `BindActionAtPriority` to force ordering (higher priority first).
      
      **Touch button auto-creation**: set `createTouchButton=true` and an `ImageButton` is auto-added under `PlayerGui.ContextActionGui.ContextButtonFrame`. Max 7 buttons per screen. First binding creates the ScreenGui + Frame automatically.
      
      ### Full signature table
      
      | Method | Purpose |
      |--------|---------|
      | `BindAction(name, handler, createTouchButton, ...inputs)` | Standard binding |
      | `BindActionAtPriority(name, handler, createTouchButton, priority, ...inputs)` | Force order via priority |
      | `BindActionToInputTypes(name, handler, createTouchButton, inputTypesList, ...)` | Bind from an array of input types |
      | `BindActivate(...)` | Bind to the player's "Activate" button (used for Tools) |
      | `UnbindAction(name)` | Remove a binding |
      | `UnbindActivate()` | Remove activate binding |
      | `UnbindAllActions()` | Clear everything |
      | `GetButton(name)` | Get the auto-created `ImageButton` for customization |
      | `GetBoundActionInfo(name)` | Inspect what a name is currently bound to |
      | `GetAllBoundActionInfo()` | All currently-bound actions (for debugging) |
      | `SetTitle`/`SetImage`/`SetPosition`/`SetDescription` | Customize a mobile button's appearance |
      | `GetCurrentLocalToolIcon()` | Currently-equipped tool icon (for CAS-owned GUI) |
      
      ### Action handler return values
      
      ```luau
      local function handleAction(actionName: string, inputState: Enum.UserInputState, input: InputObject)
          if inputState == Enum.UserInputState.Begin then
              -- start something
          elseif inputState == Enum.UserInputState.End then
              -- stop it
          elseif inputState == Enum.UserInputState.Cancel then
              -- we were unbound mid-action; clean up
          end
      
          -- Return Sink to consume, Pass to fall through
          return Enum.ContextActionResult.Sink
      end
      ```
      
      ### Tool-equip pattern (the canonical CAS use case)
      
      ```luau
      -- Place this LocalScript inside a Tool
      local CAS = game:GetService("ContextActionService")
      local ACTION_RELOAD = "Reload"
      
      local function handleAction(actionName, inputState, _input)
          if actionName == ACTION_RELOAD and inputState == Enum.UserInputState.Begin then
              print("Reloading!")
          end
      end
      
      tool.Equipped:Connect(function()
          CAS:BindAction(ACTION_RELOAD, handleAction, true, Enum.KeyCode.R)
      end)
      
      tool.Unequipped:Connect(function()
          CAS:UnbindAction(ACTION_RELOAD)
      end)
      ```
      
      ### Bind vs InputBegan: when to use which
      
      Use `ContextActionService.BindAction` when:
      - The action only exists in a context (holding tool, sitting in seat, near door).
      - You want automatic conflict resolution with chat/text input.
      - You want automatic mobile touch buttons.
      
      Use `UserInputService.InputBegan` when:
      - The action is always available (movement, inventory toggle).
      - You need raw per-frame state (thumbstick position, mouse delta).
      - You're handling complex multi-input logic that doesn't map cleanly to "contexts."
      
      ## Cross-Platform Binding Pattern
      
      Bind one logical action to keyboard + gamepad + touch in one call:
      
      ```luau
      local moving = false
      
      local function handleMoveUp(_name, state, _input)
          -- Process stop states FIRST, before any key-code filtering:
          -- ContextActionService delivers End/Cancel with KeyCode.Unknown
          -- (e.g. unbound mid-press, synthesized touch-button release), and a
          -- key-code gate would skip the stop path and strand the action active.
          if state == Enum.UserInputState.End or state == Enum.UserInputState.Cancel then
              moving = false -- idempotent: safe even if Begin never ran
              return Enum.ContextActionResult.Sink
          end
          if state == Enum.UserInputState.Begin then
              moving = true
          end
          return Enum.ContextActionResult.Sink
      end
      
      -- W key + left thumbstick up on any gamepad + mobile button
      CAS:BindAction("MoveUp", handleMoveUp, true,
          Enum.KeyCode.W,
          Enum.PlayerActions.MoveUp   -- also binds default WASD / stick
      )
      ```
      
      Gate handlers on `state`, not on `input.KeyCode`: touch-button input also arrives with `KeyCode.Unknown`, so a key-code filter on the start path would drop mobile presses too. Call `UnbindAction` on context exit; the `Cancel` delivery then clears the running state.
      
      For movement bindings, prefer `Enum.PlayerActions` (e.g. `MoveForward`, `Jump`); they automatically bind to WASD, arrows, left stick, and d-pad across platforms.
      
      ## UI Focus and Directional Selection
      
      `ContextActionService` maps gameplay actions. It does not design the focus graph for menus. For native UI navigation:
      
      ```luau
      local GuiService = game:GetService("GuiService")
      
      firstButton.Selectable = true
      secondButton.Selectable = true
      GuiService.SelectedObject = firstButton
      ```
      
      Set `SelectedObject` when a menu opens or a modal takes control, and clear or restore it when that owner closes. For ambiguous layouts, use selection groups and explicit directional behavior where the current UI API supports them. Test gamepad, keyboard arrows, covered elements, nested containers, and dynamic list updates. The Roblox `focus-navigation` repository provides a richer optional focus model with React integration; it is not required for native selection.
      
      ## Dragging: DragDetector and UIDragDetector
      
      `DragDetector` (3D; parent under a `BasePart` or `Model`) and `UIDragDetector` (2D; parent under any `GuiObject`) make objects draggable via all input types (mouse, touch, gamepad, VR), often with zero code. Both work in Studio edit mode while the Select/Move/Scale/Rotate tools (and, for UI, UI-editor plugins) are not active. Sources: create.roblox.com/docs/ui/3D-drag-detectors, create.roblox.com/docs/ui/ui-drag-detectors.
      
      ### Choosing a detector
      
      | Need | Use |
      |------|-----|
      | Click/hover only, no motion | `ClickDetector` (`MouseClick`) |
      | Drag a 3D part/model, optionally with physics response | `DragDetector` |
      | Drag/rotate a UI element (sliders, spinners, inventory icons) | `UIDragDetector` |
      
      `DragDetector` is the general-purpose option: it inherits `ClickDetector` members (`MouseClick`, `RightMouseClick`, `MouseHoverEnter`/`MouseHoverLeave`, `CursorIcon`, `MaxActivationDistance`), supports 3D dragging of anchored parts (exact placement on release) and unanchored parts (constraint-force physics), and is highly scriptable (`Scriptable` drag style, custom constraint functions). `UIDragDetector` is the newer UI-only counterpart; prefer it over hand-rolled `GuiObject` input tracking for 2D drags. Both expose the same event trio, so knowledge transfers. (*practitioner*: UIDragDetector fully released Dec 2025, announced Aug 2024; source: devforum.roblox.com/t/introducing-uidragdetectors-released/3109263.)
      
      ### DragDetector essentials
      
      Default behavior: draggable in the ground plane. Key properties (defaults): `DragStyle` (`TranslatePlane`), `Axis` (world Y; changing it updates `Orientation` and vice versa), `ResponseStyle` (`Geometric`), `RunLocally` (false), `Enabled` (true).
      
      `DragStyle` (Enum.DragDetectorDragStyle): `TranslateLine` (1D along `Axis`), `TranslatePlane` (2D perpendicular to `Axis`), `TranslatePlaneOrLine`/`TranslateLineOrPlane` (2D or 1D, modifier toggles), `TranslateViewPlane` (always faces the camera, updates live), `RotateAxis`, `RotateTrackball`, `Scriptable` (custom function), `BestForDevice` (per-input default).
      
      `ResponseStyle` (Enum.DragDetectorResponseStyle):
      - `Geometric`: object is moved exactly; unanchored parts are temporarily anchored during the drag and restored on release.
      - `Physical`: unanchored parts are moved by constraint forces; tune `Responsiveness` (10), `MaxForce` (10000000), `MaxTorque` (10000), `ApplyAtCenterOfMass` (false = force at the clicked point).
      - `Custom`: object does not move; `DragFrame` still updates and events still fire, so drive movement yourself.
      
      Limits: `MinDragTranslation`/`MaxDragTranslation` (Vector3) and `MinDragAngle`/`MaxDragAngle` (`RotateAxis` only) impede motion but are not constraints. When using limits, set `ReferenceInstance` first; without a reference frame, limits re-anchor to the object's own pose on each drag.
      
      Direction/reference: `Axis`/`Orientation` set the direction of motion; `ReferenceInstance` defines the reference frame; `DragFrame` (CFrame) is expressed relative to it and readable via `GetPropertyChangedSignal("DragFrame")` or `GetReferenceFrame()`.
      
      Permissions: `PermissionPolicy` = `Nobody` | `Everybody` (default) | `Scriptable` plus `SetPermissionPolicyFunction(function(player, part) -> boolean)`. Missing function or invalid return blocks everyone.
      
      Replication: `RunLocally=false` (default) → the client interprets input and the SERVER performs the drag: event connections and registered functions belong in server `Script`s. `RunLocally=true` → client-local: use `LocalScript`s and RemoteEvents to propagate changes.
      
      Events: `DragStart(playerWhoDragged: Player, cursorRay: Ray, viewFrame: CFrame, hitFrame: CFrame, clickedPart: BasePart, ...)`, `DragContinue(playerWhoDragged, cursorRay, viewFrame, ...)`, `DragEnd(playerWhoDragged)`. Modifier keys for dual-mode styles: `KeyboardModeSwitchKeyCode`/`GamepadModeSwitchKeyCode`/`VRSwitchKeyCode` (default LeftControl / ButtonR1 / ButtonL2).
      
      Scripting hooks: `SetDragStyleFunction(fn)` with `DragStyle = Scriptable`: `fn(cursorRay: Ray) -> CFrame?` returning the desired pivot CFrame in world space (`nil` = don't move). `AddConstraintFunction(priority, fn): RBXScriptConnection`: `fn(proposedMotion: CFrame) -> CFrame`, chained by priority; `Disconnect()` to remove. `RestartDrag()` re-evaluates the drag after changing `DragStyle`/`Axis`/`SecondaryAxis`.
      
      ```luau
      -- Drawer: slide along its own axis, clamped (illustrative)
      local detector = Instance.new("DragDetector")
      detector.DragStyle = Enum.DragDetectorDragStyle.TranslateLine
      detector.ResponseStyle = Enum.DragDetectorResponseStyle.Geometric
      detector.ReferenceInstance = dresserBody -- stable frame for the limits
      detector.MinDragTranslation = Vector3.zero
      detector.MaxDragTranslation = Vector3.new(0, 0, -4) -- open distance
      detector.Parent = drawer
      
      detector.DragStart:Connect(function(player, ray, viewFrame, hitFrame, clickedPart)
          print(player.Name, "started dragging", clickedPart.Name)
      end)
      detector.DragEnd:Connect(function(player)
          -- e.g. RemoteEvent to the server to latch state / play a sound
      end)
      ```
      
      ### UIDragDetector essentials
      
      `DragStyle` (Enum.UIDragDetectorDragStyle): `TranslatePlane` (default, free 2D), `TranslateLine` (1D along `DragAxis: Vector2`), `Rotate`, `Scriptable`. `ResponseStyle` (Enum.UIDragDetectorResponseStyle): `Offset` (default; applies motion to the parent's `Position` Offset), `Scale`, `CustomOffset`/`CustomScale` (UI does not move; `DragUDim2` still updates and events still fire; read `DragUDim2`/`DragRotation` to drive logic yourself).
      
      Limits and bounds: `MinDragTranslation`/`MaxDragTranslation` (UDim2), `MinDragAngle`/`MaxDragAngle` (`Rotate`), `BoundingUI` (a `GuiBase2d`, e.g. a container Frame) with `BoundingBehavior` (`Automatic` default | `EntireObject` | `HitPoint`), and `ReferenceUIInstance` to re-anchor axes/origin. Speed: `SelectionModeDragSpeed` (UDim2), `SelectionModeRotateSpeed` (deg/sec), `UIDragSpeedAxisMapping`.
      
      Events: `DragStart(inputPosition: Vector2)`, `DragContinue(inputPosition: Vector2)`, `DragEnd(inputPosition: Vector2)`. For custom logic, connect `DragContinue` and read the parent's `Position` (or `DragUDim2` under Custom styles): the same callback pattern as a `.Activated` button handler.
      
      Scripting hooks: `SetDragStyleFunction(fn)` with `Scriptable`: `fn(inputPosition: Vector2) -> UDim2, float, [relativity, space]`; `DragSpace` (`Parent` | `LayerCollector`) and `DragRelativity` (`Absolute` | `Relative`) define return semantics. `AddConstraintFunction` chains like the 3D detector but passes UDim2 + float. `GetReferencePosition()`/`GetReferenceRotation()` read the reference origin. Event connections and registered functions run client-side (`LocalScript` or `RunContext = Client`), like all UI input.
      
      ```luau
      -- Volume slider: handle drags along X inside its container (illustrative)
      local handle = script.Parent            -- Frame inside a container Frame
      local detector = Instance.new("UIDragDetector")
      detector.DragStyle = Enum.UIDragDetectorDragStyle.TranslateLine
      detector.DragAxis = Vector2.new(1, 0)   -- X only
      detector.ResponseStyle = Enum.UIDragDetectorResponseStyle.Scale
      detector.BoundingUI = handle.Parent     -- confine to container bounds
      detector.Parent = handle
      
      detector.DragContinue:Connect(function(_inputPos)
          local value = math.clamp(handle.Position.X.Scale, 0, 1)
          -- apply `value` to game state (volume, fill bar, etc.)
      end)
      ```
      
      ## Gamepad Deep Dive
      
      ### Detection
      ```luau
      if UIS.GamepadEnabled then
          for _, pad in ipairs(UIS:GetConnectedGamepads()) do
              print("Connected pad:", pad)  -- Enum.UserInputType.Gamepad1..8
          end
      end
      
      UIS.GamepadConnected:Connect(function(pad)
          print("Connected:", pad)
      end)
      UIS.GamepadDisconnected:Connect(function(pad)
          print("Disconnected:", pad)
      end)
      ```
      
      ### Reading inputs (event-style)
      ```luau
      UIS.InputBegan:Connect(function(input, gpe)
          if input.UserInputType == Enum.UserInputType.Gamepad1 and input.KeyCode == Enum.KeyCode.ButtonA then
              if gpe then return end  -- UI consumed it
              print("A pressed on pad 1")
          end
      end)
      ```
      
      ### Reading inputs (poll-style for held-state)
      ```luau
      RunService.RenderStepped:Connect(function()
          if UIS:IsGamepadButtonDown(Enum.UserInputType.Gamepad1, Enum.KeyCode.ButtonR2) then
              -- accelerate
          end
      
          local state = UIS:GetGamepadState(Enum.UserInputType.Gamepad1)
          for _, input in ipairs(state) do
              if input.KeyCode == Enum.KeyCode.Thumbstick1 then
                  -- input.Position is the stick direction
              end
          end
      end)
      ```
      
      ### Thumbstick deadzone
      Sticks report a small non-zero value at rest. Apply a deadzone:
      ```luau
      local function applyDeadzone(stick: Vector2, dz: number): Vector2
          if stick.Magnitude < dz then return Vector2.zero end
          return (stick - stick.Unit * dz) / (1 - dz)
      end
      ```
      
      ### Common gamepad buttons (KeyCode enum)
      - Face: `ButtonA`, `ButtonB`, `ButtonX`, `ButtonY`
      - Shoulders: `ButtonL1`, `ButtonR1`, `ButtonL2`, `ButtonR2` (triggers)
      - Sticks: `ButtonL3`, `ButtonR3` (click), `Thumbstick1`, `Thumbstick2` (axes)
      - D-pad: `DPadUp`, `DPadDown`, `DPadLeft`, `DPadRight`
      - System: `ButtonStart`, `ButtonSelect`
      
      ## Touch Deep Dive
      
      ### High-level gestures
      - `TouchTap`: brief single-finger tap.
      - `TouchTapInWorld`: same, with the world-space hit position (via `Camera:ScreenPointToRay`).
      - `TouchPan`: drag with one finger. Use for camera rotation/zoom in mobile games.
      - `TouchPinch`: two-finger pinch. Use for zoom.
      - `TouchRotate`: two-finger rotate gesture.
      - `TouchSwipe`: quick directional swipe.
      - `TouchLongPress`: held touch.
      - `TouchDrag`: continuous drag (useful for inventory drag-and-drop).
      
      ### Multi-touch tracking
      Use raw `TouchStarted`/`TouchMoved`/`TouchEnded` and maintain your own per-touch state by `input` instance.
      
      ## Mobile Sensors
      
      ### Accelerometer (gravity direction)
      ```luau
      if UIS.AccelerometerEnabled then
          UIS.DeviceGravityChanged:Connect(function(gravity, _rot)
              -- gravity is a unit Vector3 pointing in the direction gravity appears
              -- to pull on the device. Z is out of screen.
              ball.BodyForce.Force = gravity * workspace.Gravity * ball:GetMass()
          end)
      end
      ```
      
      ### Gyroscope (device rotation)
      ```luau
      if UIS.GyroscopeEnabled then
          UIS.DeviceRotationChanged:Connect(function(cframe, _rot, _prev)
              -- cframe represents the device's orientation in world space
              camera.CFrame = CFrame.new(camera.CFrame.Position) * (cframe - Vector3.new(0,0,0))
          end)
      end
      ```
      
      ## VRService
      
      `VRService` handles Roblox's VR interaction. Check `VREnabled` first; every method below is LocalScript-only.
      
      Key members (verified against the class page):
      
      - **`VREnabled`** (read-only boolean): true when a VR session is active. The same flag exists on `UserInputService`.
      - **`GetUserCFrame(type: Enum.UserCFrame): CFrame`**: device pose as an offset from real-world origin. Multiply by `Camera.CFrame`, and scale the position by `Camera.HeadScale`, to place something at a headset/hand:
        ```luau
        local handOffset = VRService:GetUserCFrame(Enum.UserCFrame.LeftHand)
        handOffset = handOffset.Rotation + handOffset.Position * camera.HeadScale
        part.CFrame = camera.CFrame * handOffset
        ```
      - **`GetUserCFrameEnabled(type): boolean`**: whether that device (Head, LeftHand, RightHand) is connected.
      - **`UserCFrameChanged(type, cframe)`**: fires on device movement; re-mirror parts there.
      - **`RecenterUserHeadCFrame()`**: re-centers the head pose (same as `UserInputService:RecenterUserHeadCFrame()`).
      - **`RequestNavigation(cframe, inputUserCFrame)`**: shows a parabola path visualizer toward a destination; pairs with the `NavigationRequested` event.
      - **`GetTouchpadMode(pad)` / `SetTouchpadMode(pad, mode)`**: legacy touchpad interaction modes (`Enum.VRTouchpad`, `Enum.VRTouchpadMode`).
      - Behavior properties: `AutomaticScaling` (`VRScaling.World` makes `Camera.HeadScale` track avatar size), `AvatarGestures` (server-set boolean for controller-driven hand/head animation), `FadeOutViewOnCollision` (default true, fades the view when the head clips geometry; do not disable without a replacement), `GuiInputUserCFrame` (which device drives UI input), `LaserPointer`, `ThirdPersonFollowCamEnabled`.
      
      Do not invent member names: older community references carry stale VRService members. Check the class page when in doubt.
      
      ## Patterns
      
      ### Platform-adaptive mobile UI
      
      Don't gate mobile UI on `UIS.TouchEnabled` alone: a touchscreen laptop would show mobile controls even when the player is using mouse/keyboard. Roblox core scripts detect the **last input actually used** (`GetLastInputType` / `LastInputTypeChanged`), which adapts instantly when a player switches devices mid-session.
      
      ```luau
      local UIS = game:GetService("UserInputService")
      local frame = script.Parent -- mobile button container
      
      local function updateInput()
          local last = UIS:GetLastInputType()
          if last == Enum.UserInputType.Focus then return end -- app focus, not an input
          frame.Visible = (last == Enum.UserInputType.Touch)
      end
      
      updateInput()
      UIS.LastInputTypeChanged:Connect(updateInput)
      ```
      
      For placement, anchor to the safe area (a child `ScreenGui` with `ScreenInsets = DeviceSafeInsets` gives the safe `AbsolutePosition`/`AbsoluteSize`). Keep custom buttons out of the thumbstick zone (left edge) and don't hard-place them relative to the default jump button, which swaps size/position at a ~500px min-axis preset.
      
      ```luau
      -- Recompute position from the safe-area screen size each frame
      local RS = game:GetService("RunService")
      RS.RenderStepped:Connect(function()
          if not frame.Visible then return end
          local size = frame.Screen.AbsoluteSize
          local minAxis = math.min(size.X, size.Y)
          local buttonSize = (minAxis <= 500) and 70 or 120
          frame.Size = UDim2.fromOffset(buttonSize, buttonSize)
          frame.Position = UDim2.new(1, -(buttonSize * 1.5 - 10), 1, -buttonSize * 1.75)
      end)
      ```
      
      ### Switch UI on PreferredInput change
      ```luau
      UIS.LastInputTypeChanged:Connect(function(newType)
          if newType == Enum.UserInputType.Touch then
              showMobileButtons()
          elseif newType == Enum.UserInputType.Keyboard then
              hideMobileButtons()
          end
      end)
      ```
      
      ### Camera mouse-look (client-only)
      ```luau
      local camera = workspace.CurrentCamera
      local ROT_SPEED = 0.003
      local x, y = 0, 0
      
      UIS.InputChanged:Connect(function(input, gpe)
          if gpe then return end
          if input.UserInputType == Enum.UserInputType.MouseMovement then
              x = x - input.Delta.X * ROT_SPEED
              y = math.clamp(y - input.Delta.Y * ROT_SPEED, -1.4, 1.4)
              camera.CFrame = CFrame.new(camera.CFrame.Position) * CFrame.Angles(y, x, 0)
          end
      end)
      ```
      
      ### Touch camera drag
      ```luau
      -- One finger owns the drag: identify it by its InputObject instance, which
      -- stays the same across that touch's Started/Moved/Ended events.
      local activeTouch: InputObject? = nil
      local lastPos = Vector2.zero
      
      UIS.TouchStarted:Connect(function(input, gpe)
          if gpe then return end
          if activeTouch then return end -- a second finger never hijacks the drag
          activeTouch = input
          lastPos = input.Position
      end)
      
      UIS.TouchMoved:Connect(function(input, gpe)
          if input ~= activeTouch or gpe then return end
          local delta = input.Position - lastPos
          -- rotate camera by delta
          lastPos = input.Position
      end)
      
      UIS.TouchEnded:Connect(function(input)
          if input ~= activeTouch then return end -- a different finger lifting is ignored
          activeTouch = nil
      end)
      
      -- Clear ownership on teardown so a stale touch can't block future drags.
      script.Destroying:Connect(function()
          activeTouch = nil
      end)
      ```
      
      ### Disable default jump and handle custom
      ```luau
      humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, false)
      
      UIS.JumpRequest:Connect(function()
          if canJump() then
              humanoid.Jump = true
          end
      end)
      ```
      
      ## Common Mistakes
      
      - **Forgetting `gameProcessedEvent` filter.** If `gpe==true` in InputBegan, a UI element (button, text box, chat) already consumed it. Filter out for gameplay.
      - **Using `UserInputService` on the server.** Silently no-ops. Use `LocalScript`.
      - **Not un-binding on context exit.** Stale bindings fire even after the player leaves the context. Call `UnbindAction` in cleanup.
      - **Hard-coding platform assumptions.** Check `TouchEnabled` / `GamepadEnabled` at runtime; don't assume desktop-only.
      - **Reading `MouseWheel` from InputBegan.** Wheel events only fire `InputChanged`.
      - **Touching `IsKeyDown` in a tight loop without throttling.** It's cheap but RenderStepped is the right cadence.
      - **No debounce on JumpRequest.** Fires once per frame the jump key is held.
      - **Mixing `PlayerActions` with `KeyCode` in a single binding.** Use one or the other, not both. `PlayerActions` maps to the platform's natural input.
      - **Setting `MouseBehavior = LockCenter` and forgetting to reset it.** Reset on player leave or context exit.
      - **Bypassing `ContextActionService` because it "feels indirect."** Most gameplay bindings should use CAS: it correctly handles chat/text-box conflicts for free.
      - **Auto-creating touch buttons beyond the 7 limit.** BindAction silently refuses to create the 8th button.
      - **Using `gameProcessedEvent` to filter CAS handlers.** CAS doesn't pass `gpe` to its handlers; by design, CAS handles conflicts itself.
  • SKILL.md 2.8 KB
    ---
    name: roblox-input
    description: "Use when handling Roblox keyboard, mouse, gamepad, touch, motion input, or cross-platform action binding."
    last_reviewed: 2026-09-05
    sources:
      - https://devforum.roblox.com/t/the-correct-way-to-design-mobile-buttons/2494558
      - https://create.roblox.com/docs/reference/engine/classes/UserInputService
      - https://create.roblox.com/docs/projects/server-authority
      - https://create.roblox.com/docs/ui/3D-drag-detectors
      - https://create.roblox.com/docs/ui/ui-drag-detectors
    ---
    
    # Roblox Input
    
    ## When to Load
    
    Load for keyboard, mouse, gamepad, touch, motion, or cross-platform action binding. Client-side only. For simulation-affecting input in a Server Authority project, use the Input Action System rather than traditional input events.
    
    ## Quick Reference
    
    **Core events** (`UserInputService`): `InputBegan`, `InputChanged`, `InputEnded` fire as `(input: InputObject, gameProcessedEvent: boolean)`. `InputBegan` does NOT fire for mouse wheel. Events only fire while the client window is focused.
    
    **Prefer `ContextActionService` over `InputBegan`** for gameplay: free conflict resolution (chat won't steal H) and free mobile buttons:
    
    ```luau
    local CAS = game:GetService("ContextActionService")
    
    local function onAction(name, state, _input)
        if name == "Jump" and state == Enum.UserInputState.Begin then
            humanoid.Jump = true
        end
    end
    
    CAS:BindAction("Jump", onAction, true,
        Enum.KeyCode.Space, Enum.KeyCode.ButtonA)
    ```
    
    `BindAction(name, handler, createTouchButton, ...inputTypes)`. Handler returns `ContextActionResult.Sink` to consume, `.Pass` to fall through.
    
    **Server Authority:** use `InputAction`/`InputContext` for inputs that affect the core simulation, store the input state where the synchronized simulation can read it, and process it through `RunService:BindToSimulation()` (requires `Workspace.UseFixedSimulation` enabled in Studio). `ContextActionService` remains appropriate for UI-only or classic-project actions.
    
    **Gamepad UI focus:** separate gameplay bindings from menu selection. Set `GuiService.SelectedObject`, mark controls `Selectable`, and test nested/modal navigation.
    
    **Gamepad:** use `GetConnectedGamepads()` and listen to connection changes.
    
    **Touch:** use high-level gesture events or raw `TouchStarted`/`TouchMoved`/`TouchEnded` when tracking fingers.
    
    **Dragging:** `DragDetector` (3D parts/models, physics-capable) and `UIDragDetector` (UI) make objects draggable with zero code; events `DragStart`/`DragContinue`/`DragEnd`.
    
    **Pitfalls**:
    - `gameProcessedEvent=true` in InputBegan → UI consumed it. Filter for gameplay.
    - `BindAction` is stack-based: most-recent wins. Use `BindActionAtPriority`.
    - Client-only. Server scripts silently no-op.
    - `JumpRequest` fires multiple times per jump; debounce.
    - Mouse wheel only fires `InputChanged`.
    
    Full event tables and polling methods: `references/full.md`.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related