Cross-device controls
A game mechanic should have a name before it has a key. “Show help” is an action; H, a controller button, and a touchscreen button are ways to request it. You will make a help panel that works through all three inputs, while leaving Roblox's familiar movement and camera controls alone.
Prerequisites: Understand a LocalScript and PlayerGui from lesson 08. Use a new Baseplate for this lab, or a saved copy of a previous project. No server code, RemoteEvent, paid asset, or plugin is needed.
Outcome: Each player can independently open and close instructions, including after respawning. You can explain which parts are local presentation and which would require server validation.
Choose the input layer deliberately
Roblox's current input documentation presents the Input Action System as its cross-platform approach. Its InputContext, InputAction, and InputBinding instances organize actions and hardware bindings. This lesson uses the still-supported ContextActionService API because one self-contained script makes the first experiment easy to inspect. It is a compact alternative, not a claim that it is Roblox's newest recommended architecture. For a larger project, continue with the official Input Action System guide.
The same design principles transfer: name the action, support each target input, handle press/release deliberately, and avoid collisions with platform controls. Input can change during a session when a controller is connected. Do not decide that a player must always be using touch merely because they have a phone. Roblox exposes PreferredInput for adaptive prompts. See input overview.
Build the help action
- Stop the simulation before editing.
- In Explorer, open StarterPlayer, then StarterPlayerScripts.
- Insert a LocalScript, rename it HelpControls, and replace its contents with the complete code below.
- Run Test, click the game viewport, and press H. Press it again to close the panel.
- Stop and save the place. Changes made to the runtime PlayerScripts copy during a test are not your editable source.
Exact source path: StarterPlayer > StarterPlayerScripts > HelpControls (LocalScript).
Runnable file: 13-help-controls.client.luau.
-- Placement: StarterPlayer > StarterPlayerScripts > HelpControls (LocalScript)
-- Standalone client-only exercise. No RemoteEvent or server script is required.
local Players = game:GetService("Players")
local ContextActionService = game:GetService("ContextActionService")
local UserInputService = game:GetService("UserInputService")
local playerGui = Players.LocalPlayer:WaitForChild("PlayerGui")
local actionName = "TutorialToggleHelp"
local gui = Instance.new("ScreenGui")
gui.Name = "TutorialHelpGui"
gui.ResetOnSpawn = false
gui.Parent = playerGui
local hint = Instance.new("TextLabel")
hint.Name = "Hint"
hint.AnchorPoint = Vector2.new(0.5, 0)
hint.Position = UDim2.fromScale(0.5, 0.02)
hint.Size = UDim2.new(0.8, 0, 0, 48)
hint.BackgroundColor3 = Color3.fromRGB(24, 35, 55)
hint.TextColor3 = Color3.new(1, 1, 1)
hint.Font = Enum.Font.Gotham
hint.TextSize = 16
hint.TextWrapped = true
hint.Text = "Help: H on keyboard / Y on gamepad / Help on touch"
hint.Parent = gui
local panel = Instance.new("TextLabel")
panel.Name = "HelpPanel"
panel.AnchorPoint = Vector2.new(0.5, 0.5)
panel.Position = UDim2.fromScale(0.5, 0.55)
panel.Size = UDim2.fromScale(0.8, 0.6)
panel.BackgroundColor3 = Color3.fromRGB(24, 35, 55)
panel.TextColor3 = Color3.new(1, 1, 1)
panel.Font = Enum.Font.Gotham
panel.TextSize = 20
panel.TextWrapped = true
panel.Text = "Take your time. Explore together.\n\nUse Roblox's usual movement and jump controls.\n\nPress the Help action again to close this panel."
panel.Visible = false
panel.Parent = gui
local padding = Instance.new("UIPadding")
padding.PaddingTop = UDim.new(0, 16)
padding.PaddingBottom = UDim.new(0, 16)
padding.PaddingLeft = UDim.new(0, 16)
padding.PaddingRight = UDim.new(0, 16)
padding.Parent = panel
local function toggleHelp(_action, inputState, _inputObject)
if UserInputService:GetFocusedTextBox() then
return Enum.ContextActionResult.Pass
end
if inputState == Enum.UserInputState.Begin then
panel.Visible = not panel.Visible
end
return Enum.ContextActionResult.Sink
end
ContextActionService:BindAction(
actionName, toggleHelp, true, Enum.KeyCode.H, Enum.KeyCode.ButtonY
)
ContextActionService:SetTitle(actionName, "Help")
ContextActionService:SetPosition(actionName, UDim2.fromScale(0.15, 0.15))
script.Destroying:Connect(function()
ContextActionService:UnbindAction(actionName)
gui:Destroy()
end)
The third BindAction argument asks Roblox to make a touch button. SetTitle labels that button; SetPosition positions it within the touch-control layout. The function acts only on Begin, so releasing a key does not toggle the panel a second time. Returning Sink consumes the bound input; Pass lets it continue. These API behaviors are documented in ContextActionService.
Check the three ways in
- Keyboard: H opens help, H closes it, and ordinary movement still works. Click the viewport if Studio's script editor has focus.
- Controller: Use a connected controller or Studio's Controller Emulator, select a gamepad, and trigger ButtonY. The logical ButtonY name does not imply every controller prints the letter Y on its physical button. See Controller Emulator.
- Touch: Use Studio's Device Simulator with a phone profile. Look for the Help touch button, tap it twice, and check both orientations. Inspect the panel alongside Roblox's movement and jump controls.
The expected result is a local panel. In a two-client test, opening it in one window should not open it in the other. Reset your character and try again: ResetOnSpawn is false, so this ScreenGui survives the respawn.
This is a help overlay, not a modal menu. Movement continues while it is open. A future inventory menu would need a deliberate input context, focus/navigation behavior, and a clear close route. That is a larger design decision than making a Frame visible.
Troubleshoot and improve
Nothing happens: Confirm the class is LocalScript, the path is StarterPlayerScripts, and the viewport has keyboard focus. Read the client Output. Do not move this code into ServerScriptService.
No touch button on desktop: That is expected without touch simulation. The createTouchButton flag is not a request for a permanent desktop button.
The panel changes when typing: The handler already yields to a focused TextBox. Test that behavior with chat or a temporary text-entry field; do not remove the guard to make your shortcut “more reliable.”
Controls overlap: Move the touch button and test actual screen shapes. The initial position is a teaching starting point, not a universal layout guarantee.
A teammate cannot see my panel: Correct. A request that changes a shared door, score, or round is different. Send a bounded request and let the server decide, as in lesson 10.
Exercises and completion check
Change the help text to explain your co-op round objective. Ask a tester to use it without coaching. Note which sentence or button label they misunderstood before adjusting the layout.
As a stretch exercise, rebuild this one action using InputContext, InputAction, and keyboard/gamepad/touch InputBindings from the official guide. Remove the old binding first so one button does not trigger two systems. Compare the number of decisions you can make in Explorer with those hidden inside code.
You are done when all three input routes work, typing is unaffected, two clients stay independent, and a respawn does not duplicate the interface.
Verification: APIs and documentation checked 2026-10-03. The sample was statically reviewed; it has not been executed in Roblox Studio for this manual.
Previous: 12 Co-op rounds · Next: 14 Testing multiplayer
Related: 08 UI and client scripts · 09 Client–server boundary · Glossary