title | description |
---|---|
Cross-Platform Design |
Explains best practices on designing UI that adapts across different screens and devices. |
Roblox is inherently cross-platform as users can discover and join experiences on a PC, then later pick up their phone and continue where they left off. You should design your Roblox experiences to be accessible and enjoyable on all platforms that you choose to support, instead of optimizing for one platform and neglecting others.
Just because a UI fits perfectly on a PC screen doesn't mean it's as functional on smaller mobile screens. For example, the color customization tiles in this UI become small and cramped on a phone:
Color customization tiles positioned in two rows on bottom of UI modal Tiles small and difficult to tap on phones and mini tabletsIn contrast, a slight redesign of the UI menu offers a better user experience on both PC and phone:
Color customization tiles positioned on right side of UI modal Tiles larger and easier to tap on phones and mini tabletsOn mobile devices, the default controls occupy a portion of the bottom-left and bottom-right corners of the screen. When you design an experience's UI, avoid placing important info or virtual buttons in these zones.
If your experience uses the default control setting of `Enum.DevTouchMovementMode|DevTouchMovementMode.UserChoice`, users on mobile devices will be able to choose their input from **Dynamic Thumbstick**, **Classic Thumbstick**, or **Tap to Move**, causing the on-screen controls and reserved zones to vary slightly. Remember to design your UI around this possibility.Most mobile users use two thumbs — one on the virtual thumbstick and one on the jump button. Depending on the physical size of the device and the user's hands, "reaching" too far from the bottom corners becomes uncomfortable or impossible, so you should avoid placing frequently-used buttons outside of easy-to-reach zones.
Button comfortably within reach of user's right thumb Button difficult to reach unless user stretches hand or thumbRemember that comfortable thumb zones differ between phones and tablets because tablets have a larger screen. A button placed 30% below the screen's top edge is reachable on a phone but almost unreachable on a tablet, so you should consider relational position over percentage-based.
Button 30% from top edge of phone, within reach of user's thumb Button 30% from top edge of tablet, difficult to reach without stretchingScreen space is limited on mobile devices, so you should show only the most vital information during active gameplay. For example, if your experience includes a special input action to open doors and treasure chests, it doesn't make sense to constantly show an "Open" button on the screen. Instead, use a proximity prompt or similar method to accept input only when the character approaches a door or chest.
`Class.ProximityPrompt` that automatically appears when character is near chest Custom button that you display only when character is near chestA cross-platform UI layout should be adaptable across all devices, not just between PC, mobile, and console but also between phones and tablets. You can achieve this through relational positioning and input type detection.
A reliable approach for adaptable UI on both phones and tablets is to position custom buttons near frequently-used controls like the default jump button, placing them within easy reach of the user's right thumb.
The following code, placed in a Class.LocalScript
within Class.StarterPlayerScripts
, fetches the position of the jump button and creates a placeholder Class.ImageButton
above it.
local Players = game:GetService("Players")
-- Get reference to player's jump button
local player = Players.LocalPlayer
local PlayerGui = player:WaitForChild("PlayerGui")
local ScreenGui = PlayerGui:WaitForChild("ScreenGui")
local TouchGui = PlayerGui:WaitForChild("TouchGui")
local TouchControlFrame = TouchGui:WaitForChild("TouchControlFrame")
local JumpButton = TouchControlFrame:WaitForChild("JumpButton")
-- Get absolute size and position of button
local absSizeX, absSizeY = JumpButton.AbsoluteSize.X, JumpButton.AbsoluteSize.Y
local absPositionX, absPositionY = JumpButton.AbsolutePosition.X, JumpButton.AbsolutePosition.Y
-- Create new button above jump button
local customButton = Instance.new("ImageButton")
customButton.Parent = ScreenGui
customButton.AnchorPoint = Vector2.new(0.5, 1)
customButton.Size = UDim2.new(0, absSizeX * 0.8, 0, absSizeY * 0.8)
customButton.Position = UDim2.new(0, absPositionX + (absSizeX / 2), 0, absPositionY - 20)
Another approach for adaptable UI is to adjust your layout based on which input type the user is using, for example keyboard/mouse, touch, or gamepad.
The following Class.ModuleScript
determines the user's input type on join and detects changes to input type during gameplay. From a Class.LocalScript
that requires the module, you can detect the user's input type at any time and/or connect to the module's Class.BindableEvent
to detect input type changes. Upon detection, you can reposition UI elements to better accommodate the current input.
local UserInputService = game:GetService("UserInputService")
local PlayerInput = {}
PlayerInput.InputTypes = {
KEYBOARD_MOUSE = "keyboard/mouse",
TOUCH = "touch",
GAMEPAD = "gamepad"
}
PlayerInput.CurrentInput = nil
PlayerInput.InputTypeChanged = Instance.new("BindableEvent")
local InputTypes = {
[Enum.UserInputType.None] = nil,
[Enum.UserInputType.TextInput] = nil,
[Enum.UserInputType.InputMethod] = nil,
[Enum.UserInputType.MouseButton1] = PlayerInput.InputTypes.KEYBOARD_MOUSE,
[Enum.UserInputType.MouseButton2] = PlayerInput.InputTypes.KEYBOARD_MOUSE,
[Enum.UserInputType.MouseButton3] = PlayerInput.InputTypes.KEYBOARD_MOUSE,
[Enum.UserInputType.MouseWheel] = PlayerInput.InputTypes.KEYBOARD_MOUSE,
[Enum.UserInputType.MouseMovement] = PlayerInput.InputTypes.KEYBOARD_MOUSE,
[Enum.UserInputType.Keyboard] = PlayerInput.InputTypes.KEYBOARD_MOUSE,
[Enum.UserInputType.Touch] = PlayerInput.InputTypes.TOUCH,
[Enum.UserInputType.Accelerometer] = PlayerInput.InputTypes.TOUCH,
[Enum.UserInputType.Gyro] = PlayerInput.InputTypes.TOUCH,
[Enum.UserInputType.Gamepad1] = PlayerInput.InputTypes.GAMEPAD,
[Enum.UserInputType.Gamepad2] = PlayerInput.InputTypes.GAMEPAD,
[Enum.UserInputType.Gamepad3] = PlayerInput.InputTypes.GAMEPAD,
[Enum.UserInputType.Gamepad4] = PlayerInput.InputTypes.GAMEPAD,
[Enum.UserInputType.Gamepad5] = PlayerInput.InputTypes.GAMEPAD,
[Enum.UserInputType.Gamepad6] = PlayerInput.InputTypes.GAMEPAD,
[Enum.UserInputType.Gamepad7] = PlayerInput.InputTypes.GAMEPAD,
[Enum.UserInputType.Gamepad8] = PlayerInput.InputTypes.GAMEPAD,
}
local function setPlayerInputType(userInputType)
local playerInputType = InputTypes[userInputType]
if playerInputType and playerInputType ~= PlayerInput.CurrentInput then
PlayerInput.CurrentInput = playerInputType
PlayerInput.InputTypeChanged:Fire(playerInputType)
end
end
-- Initially set the player's input type
setPlayerInputType(UserInputService:GetLastInputType())
-- Update current input type based on last input type received
UserInputService.LastInputTypeChanged:Connect(setPlayerInputType)
return PlayerInput
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PlayerInput = require(ReplicatedStorage:WaitForChild("PlayerInput"))
-- Check the player's input type at any time by reading PlayerInput.CurrentInput
print("Player is using", PlayerInput.CurrentInput)
-- Listen for input type changes with PlayerInput.InputTypeChanged event
PlayerInput.InputTypeChanged.Event:Connect(function(newInputType)
print("Input changed to", newInputType)
end)