mobile-input

SKILL.md

Roblox Mobile Input Systems

When implementing touch controls, follow these patterns for responsive mobile gameplay.

Platform Detection

local UserInputService = game:GetService("UserInputService")
local GuiService = game:GetService("GuiService")

local function getPlatform()
    if UserInputService.TouchEnabled then
        if UserInputService.KeyboardEnabled then
            return "tablet"  -- Has both touch and keyboard (Surface, iPad with keyboard)
        else
            return "mobile"  -- Touch only (phone, tablet)
        end
    elseif UserInputService.GamepadEnabled then
        return "console"
    else
        return "desktop"
    end
end

local function isMobile()
    return UserInputService.TouchEnabled and not UserInputService.KeyboardEnabled
end

local function isTouch()
    return UserInputService.TouchEnabled
end

-- Detect phone vs tablet by screen size
local function isPhone()
    local viewport = workspace.CurrentCamera.ViewportSize
    return viewport.X < 800 or viewport.Y < 500
end

Virtual Joystick

Basic Joystick

local VirtualJoystick = {}
VirtualJoystick.__index = VirtualJoystick

function VirtualJoystick.new(config)
    local self = setmetatable({}, VirtualJoystick)

    self.size = config.size or 150
    self.deadzone = config.deadzone or 0.1
    self.position = config.position or UDim2.new(0.15, 0, 0.75, 0)

    -- State
    self.active = false
    self.direction = Vector2.new(0, 0)
    self.inputObject = nil

    self:createUI()
    self:setupInput()

    return self
end

function VirtualJoystick:createUI()
    -- Outer ring (background)
    self.outerRing = Instance.new("ImageLabel")
    self.outerRing.Name = "JoystickOuter"
    self.outerRing.Size = UDim2.new(0, self.size, 0, self.size)
    self.outerRing.Position = self.position
    self.outerRing.AnchorPoint = Vector2.new(0.5, 0.5)
    self.outerRing.BackgroundTransparency = 1
    self.outerRing.Image = "rbxassetid://5765786884"  -- Circular outline
    self.outerRing.ImageTransparency = 0.3

    -- Inner stick (thumb)
    self.innerStick = Instance.new("ImageLabel")
    self.innerStick.Name = "JoystickInner"
    self.innerStick.Size = UDim2.new(0, self.size * 0.4, 0, self.size * 0.4)
    self.innerStick.Position = UDim2.new(0.5, 0, 0.5, 0)
    self.innerStick.AnchorPoint = Vector2.new(0.5, 0.5)
    self.innerStick.BackgroundTransparency = 1
    self.innerStick.Image = "rbxassetid://5765786884"  -- Filled circle
    self.innerStick.ImageTransparency = 0.1
    self.innerStick.Parent = self.outerRing

    -- Parent to ScreenGui
    local screenGui = Instance.new("ScreenGui")
    screenGui.Name = "JoystickGui"
    screenGui.ResetOnSpawn = false
    screenGui.DisplayOrder = 100
    screenGui.Parent = game.Players.LocalPlayer:WaitForChild("PlayerGui")

    self.outerRing.Parent = screenGui
    self.gui = screenGui
end

function VirtualJoystick:setupInput()
    self.outerRing.InputBegan:Connect(function(input)
        if input.UserInputType == Enum.UserInputType.Touch then
            self.active = true
            self.inputObject = input
            self:updateStick(input.Position)
        end
    end)

    self.outerRing.InputChanged:Connect(function(input)
        if self.active and input == self.inputObject then
            self:updateStick(input.Position)
        end
    end)

    self.outerRing.InputEnded:Connect(function(input)
        if input == self.inputObject then
            self:release()
        end
    end)
end

function VirtualJoystick:updateStick(touchPosition)
    local center = self.outerRing.AbsolutePosition +
        Vector2.new(self.outerRing.AbsoluteSize.X / 2, self.outerRing.AbsoluteSize.Y / 2)

    local offset = Vector2.new(touchPosition.X, touchPosition.Y) - center
    local maxDistance = self.size / 2

    -- Clamp to circle
    if offset.Magnitude > maxDistance then
        offset = offset.Unit * maxDistance
    end

    -- Update visual
    self.innerStick.Position = UDim2.new(
        0.5, offset.X,
        0.5, offset.Y
    )

    -- Calculate normalized direction
    self.direction = offset / maxDistance

    -- Apply deadzone
    if self.direction.Magnitude < self.deadzone then
        self.direction = Vector2.new(0, 0)
    end
end

function VirtualJoystick:release()
    self.active = false
    self.inputObject = nil
    self.direction = Vector2.new(0, 0)

    -- Reset visual
    self.innerStick.Position = UDim2.new(0.5, 0, 0.5, 0)
end

function VirtualJoystick:getDirection()
    return self.direction
end

function VirtualJoystick:getMagnitude()
    return self.direction.Magnitude
end

function VirtualJoystick:destroy()
    self.gui:Destroy()
end

Dynamic Position Joystick

-- Joystick appears where you touch
local DynamicJoystick = {}
DynamicJoystick.__index = DynamicJoystick
setmetatable(DynamicJoystick, {__index = VirtualJoystick})

function DynamicJoystick.new(config)
    local self = VirtualJoystick.new(config)
    setmetatable(self, DynamicJoystick)

    self.outerRing.Visible = false
    self:setupDynamicInput()

    return self
end

function DynamicJoystick:setupDynamicInput()
    -- Touch zone covers left half of screen
    local touchZone = Instance.new("Frame")
    touchZone.Size = UDim2.new(0.5, 0, 1, 0)
    touchZone.Position = UDim2.new(0, 0, 0, 0)
    touchZone.BackgroundTransparency = 1
    touchZone.Parent = self.gui

    touchZone.InputBegan:Connect(function(input)
        if input.UserInputType == Enum.UserInputType.Touch then
            -- Move joystick to touch position
            self.outerRing.Position = UDim2.new(0, input.Position.X, 0, input.Position.Y)
            self.outerRing.Visible = true

            self.active = true
            self.inputObject = input
            self.startPosition = Vector2.new(input.Position.X, input.Position.Y)
        end
    end)

    UserInputService.TouchMoved:Connect(function(input)
        if self.active and input == self.inputObject then
            self:updateStick(input.Position)
        end
    end)

    UserInputService.TouchEnded:Connect(function(input)
        if input == self.inputObject then
            self:release()
            self.outerRing.Visible = false
        end
    end)
end

Touch Buttons

Action Button

local function createActionButton(config)
    local button = Instance.new("ImageButton")
    button.Name = config.name or "ActionButton"
    button.Size = UDim2.new(0, config.size or 80, 0, config.size or 80)
    button.Position = config.position
    button.AnchorPoint = Vector2.new(0.5, 0.5)
    button.BackgroundTransparency = 1
    button.Image = config.image or "rbxassetid://5765786884"
    button.ImageTransparency = 0.3

    -- Label
    if config.label then
        local label = Instance.new("TextLabel")
        label.Size = UDim2.new(1, 0, 1, 0)
        label.BackgroundTransparency = 1
        label.Text = config.label
        label.TextColor3 = Color3.new(1, 1, 1)
        label.TextScaled = true
        label.Parent = button
    end

    -- Icon
    if config.icon then
        local icon = Instance.new("ImageLabel")
        icon.Size = UDim2.new(0.6, 0, 0.6, 0)
        icon.Position = UDim2.new(0.5, 0, 0.5, 0)
        icon.AnchorPoint = Vector2.new(0.5, 0.5)
        icon.BackgroundTransparency = 1
        icon.Image = config.icon
        icon.Parent = button
    end

    -- Press feedback
    button.InputBegan:Connect(function(input)
        if input.UserInputType == Enum.UserInputType.Touch then
            button.ImageTransparency = 0.1
            button.Size = UDim2.new(0, (config.size or 80) * 0.9, 0, (config.size or 80) * 0.9)
        end
    end)

    button.InputEnded:Connect(function(input)
        if input.UserInputType == Enum.UserInputType.Touch then
            button.ImageTransparency = 0.3
            button.Size = UDim2.new(0, config.size or 80, 0, config.size or 80)
        end
    end)

    return button
end

-- Common action buttons
local function createMobileControls(parent)
    -- Jump button
    local jumpButton = createActionButton({
        name = "JumpButton",
        position = UDim2.new(0.9, 0, 0.7, 0),
        size = 90,
        label = "JUMP"
    })
    jumpButton.Parent = parent

    jumpButton.MouseButton1Down:Connect(function()
        local humanoid = game.Players.LocalPlayer.Character
            and game.Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid")
        if humanoid then
            humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
        end
    end)

    -- Attack button
    local attackButton = createActionButton({
        name = "AttackButton",
        position = UDim2.new(0.75, 0, 0.85, 0),
        size = 70,
        label = "ATK"
    })
    attackButton.Parent = parent

    attackButton.MouseButton1Down:Connect(function()
        -- Fire attack event
        AttackRemote:FireServer()
    end)

    -- Ability buttons
    for i = 1, 3 do
        local abilityButton = createActionButton({
            name = "Ability" .. i,
            position = UDim2.new(0.6 + (i - 1) * 0.1, 0, 0.55, 0),
            size = 60,
            label = tostring(i)
        })
        abilityButton.Parent = parent

        abilityButton.MouseButton1Down:Connect(function()
            AbilityRemote:FireServer(i)
        end)
    end
end

Holdable Button

local function createHoldButton(config)
    local button = createActionButton(config)

    local holding = false
    local holdStartTime = 0

    button.InputBegan:Connect(function(input)
        if input.UserInputType == Enum.UserInputType.Touch then
            holding = true
            holdStartTime = os.clock()

            -- Hold loop
            task.spawn(function()
                while holding do
                    local holdTime = os.clock() - holdStartTime

                    if config.onHold then
                        config.onHold(holdTime)
                    end

                    task.wait()
                end
            end)
        end
    end)

    button.InputEnded:Connect(function(input)
        if input.UserInputType == Enum.UserInputType.Touch then
            holding = false

            if config.onRelease then
                config.onRelease(os.clock() - holdStartTime)
            end
        end
    end)

    return button
end

-- Example: Charge attack
local chargeButton = createHoldButton({
    name = "ChargeAttack",
    position = UDim2.new(0.85, 0, 0.6, 0),
    onHold = function(holdTime)
        -- Update charge UI
        local charge = math.min(holdTime / 2, 1)  -- Max 2 seconds
        chargeBar.Size = UDim2.new(charge, 0, 1, 0)
    end,
    onRelease = function(holdTime)
        local charge = math.min(holdTime / 2, 1)
        ChargeAttackRemote:FireServer(charge)
    end
})

Gesture Recognition

Swipe Detection

local SwipeDetector = {}
SwipeDetector.minSwipeDistance = 50  -- pixels
SwipeDetector.maxSwipeTime = 0.3     -- seconds

local touchStart = {}

function SwipeDetector.init(callback)
    UserInputService.TouchStarted:Connect(function(input)
        touchStart[input] = {
            position = input.Position,
            time = os.clock()
        }
    end)

    UserInputService.TouchEnded:Connect(function(input)
        local startData = touchStart[input]
        if not startData then return end

        local endPosition = input.Position
        local deltaTime = os.clock() - startData.time

        if deltaTime > SwipeDetector.maxSwipeTime then
            touchStart[input] = nil
            return
        end

        local delta = Vector2.new(
            endPosition.X - startData.position.X,
            endPosition.Y - startData.position.Y
        )

        if delta.Magnitude < SwipeDetector.minSwipeDistance then
            touchStart[input] = nil
            return
        end

        -- Determine direction
        local direction
        if math.abs(delta.X) > math.abs(delta.Y) then
            direction = delta.X > 0 and "right" or "left"
        else
            direction = delta.Y > 0 and "down" or "up"
        end

        callback(direction, delta.Magnitude, deltaTime)

        touchStart[input] = nil
    end)
end

-- Usage
SwipeDetector.init(function(direction, distance, time)
    if direction == "up" then
        -- Jump or dodge up
        JumpRemote:FireServer()
    elseif direction == "down" then
        -- Slide or dodge down
        SlideRemote:FireServer()
    elseif direction == "left" then
        -- Dodge left
        DodgeRemote:FireServer("left")
    elseif direction == "right" then
        -- Dodge right
        DodgeRemote:FireServer("right")
    end
end)

Pinch to Zoom

local PinchDetector = {}
local activeTouches = {}

function PinchDetector.init(onZoom)
    UserInputService.TouchStarted:Connect(function(input)
        activeTouches[input] = input.Position
    end)

    UserInputService.TouchMoved:Connect(function(input)
        if not activeTouches[input] then return end

        activeTouches[input] = input.Position

        -- Need exactly 2 touches for pinch
        local touches = {}
        for _, pos in pairs(activeTouches) do
            table.insert(touches, pos)
        end

        if #touches == 2 then
            local distance = (touches[1] - touches[2]).Magnitude

            if PinchDetector.lastDistance then
                local delta = distance - PinchDetector.lastDistance
                onZoom(delta / 100)  -- Normalize
            end

            PinchDetector.lastDistance = distance
        end
    end)

    UserInputService.TouchEnded:Connect(function(input)
        activeTouches[input] = nil
        PinchDetector.lastDistance = nil
    end)
end

-- Usage: Camera zoom
PinchDetector.init(function(zoomDelta)
    local camera = workspace.CurrentCamera
    -- Adjust camera distance
    CameraController.zoomLevel = math.clamp(
        CameraController.zoomLevel - zoomDelta * 5,
        5, 50
    )
end)

Tap vs Long Press

local TapDetector = {}
TapDetector.longPressTime = 0.5  -- seconds
TapDetector.tapMaxMove = 20      -- pixels

function TapDetector.create(element, callbacks)
    local touchData = {}

    element.InputBegan:Connect(function(input)
        if input.UserInputType == Enum.UserInputType.Touch then
            touchData = {
                startPosition = input.Position,
                startTime = os.clock(),
                moved = false
            }

            -- Long press timer
            task.delay(TapDetector.longPressTime, function()
                if touchData.startTime and not touchData.moved then
                    if callbacks.onLongPress then
                        callbacks.onLongPress()
                    end
                    touchData.handled = true
                end
            end)
        end
    end)

    element.InputChanged:Connect(function(input)
        if input.UserInputType == Enum.UserInputType.Touch and touchData.startPosition then
            local moved = (input.Position - touchData.startPosition).Magnitude
            if moved > TapDetector.tapMaxMove then
                touchData.moved = true
            end
        end
    end)

    element.InputEnded:Connect(function(input)
        if input.UserInputType == Enum.UserInputType.Touch then
            if not touchData.handled and not touchData.moved then
                local duration = os.clock() - touchData.startTime

                if duration < TapDetector.longPressTime then
                    if callbacks.onTap then
                        callbacks.onTap()
                    end
                end
            end

            touchData = {}
        end
    end)
end

-- Usage
TapDetector.create(itemButton, {
    onTap = function()
        -- Quick select
        selectItem(itemId)
    end,
    onLongPress = function()
        -- Show item details
        showItemDetails(itemId)
    end
})

Cross-Platform Input

Unified Input System

local InputController = {}
InputController.moveDirection = Vector2.new(0, 0)
InputController.lookDirection = Vector2.new(0, 0)
InputController.actions = {}

-- Mobile joystick
local moveJoystick
local lookJoystick

function InputController.init()
    local platform = getPlatform()

    if platform == "mobile" or platform == "tablet" then
        -- Create touch controls
        moveJoystick = VirtualJoystick.new({
            position = UDim2.new(0.15, 0, 0.75, 0)
        })

        lookJoystick = VirtualJoystick.new({
            position = UDim2.new(0.85, 0, 0.75, 0)
        })

        createMobileControls(moveJoystick.gui)
    end

    -- Keyboard/Gamepad (always active as fallback)
    setupKeyboardInput()
    setupGamepadInput()
end

function InputController.update()
    -- Reset
    InputController.moveDirection = Vector2.new(0, 0)
    InputController.lookDirection = Vector2.new(0, 0)

    -- Mobile joysticks
    if moveJoystick and moveJoystick.active then
        InputController.moveDirection = moveJoystick:getDirection()
    end

    if lookJoystick and lookJoystick.active then
        InputController.lookDirection = lookJoystick:getDirection()
    end

    -- Keyboard WASD (if not using joystick)
    if InputController.moveDirection.Magnitude < 0.1 then
        local keyMove = Vector2.new(0, 0)

        if UserInputService:IsKeyDown(Enum.KeyCode.W) then
            keyMove = keyMove + Vector2.new(0, -1)
        end
        if UserInputService:IsKeyDown(Enum.KeyCode.S) then
            keyMove = keyMove + Vector2.new(0, 1)
        end
        if UserInputService:IsKeyDown(Enum.KeyCode.A) then
            keyMove = keyMove + Vector2.new(-1, 0)
        end
        if UserInputService:IsKeyDown(Enum.KeyCode.D) then
            keyMove = keyMove + Vector2.new(1, 0)
        end

        if keyMove.Magnitude > 0 then
            InputController.moveDirection = keyMove.Unit
        end
    end

    -- Gamepad
    local gamepadMove = UserInputService:GetGamepadState(Enum.UserInputType.Gamepad1)
    for _, input in ipairs(gamepadMove) do
        if input.KeyCode == Enum.KeyCode.Thumbstick1 then
            if input.Position.Magnitude > 0.1 then
                InputController.moveDirection = Vector2.new(input.Position.X, -input.Position.Y)
            end
        elseif input.KeyCode == Enum.KeyCode.Thumbstick2 then
            if input.Position.Magnitude > 0.1 then
                InputController.lookDirection = Vector2.new(input.Position.X, -input.Position.Y)
            end
        end
    end
end

function setupKeyboardInput()
    UserInputService.InputBegan:Connect(function(input, processed)
        if processed then return end

        if input.KeyCode == Enum.KeyCode.Space then
            InputController.actions.jump = true
        elseif input.KeyCode == Enum.KeyCode.E then
            InputController.actions.interact = true
        elseif input.KeyCode == Enum.KeyCode.Q then
            InputController.actions.ability1 = true
        end
    end)

    UserInputService.InputEnded:Connect(function(input)
        if input.KeyCode == Enum.KeyCode.Space then
            InputController.actions.jump = false
        elseif input.KeyCode == Enum.KeyCode.E then
            InputController.actions.interact = false
        end
    end)
end

function setupGamepadInput()
    UserInputService.InputBegan:Connect(function(input, processed)
        if processed then return end

        if input.KeyCode == Enum.KeyCode.ButtonA then
            InputController.actions.jump = true
        elseif input.KeyCode == Enum.KeyCode.ButtonX then
            InputController.actions.interact = true
        elseif input.KeyCode == Enum.KeyCode.ButtonB then
            InputController.actions.attack = true
        end
    end)
end

-- Usage in character controller
RunService.RenderStepped:Connect(function()
    InputController.update()

    local humanoid = LocalPlayer.Character
        and LocalPlayer.Character:FindFirstChildOfClass("Humanoid")

    if humanoid then
        -- Convert 2D input to 3D movement
        local camera = workspace.CurrentCamera
        local cameraForward = camera.CFrame.LookVector * Vector3.new(1, 0, 1)
        local cameraRight = camera.CFrame.RightVector * Vector3.new(1, 0, 1)

        local moveDir = InputController.moveDirection
        local worldDirection = (cameraForward.Unit * -moveDir.Y + cameraRight.Unit * moveDir.X)

        if worldDirection.Magnitude > 0 then
            humanoid:Move(worldDirection.Unit)
        else
            humanoid:Move(Vector3.new(0, 0, 0))
        end

        if InputController.actions.jump then
            humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
        end
    end
end)

Mobile UI Scaling

local function createResponsiveUI()
    local screenGui = Instance.new("ScreenGui")
    screenGui.ResetOnSpawn = false

    -- Different layouts based on platform
    local platform = getPlatform()
    local isPortrait = workspace.CurrentCamera.ViewportSize.Y >
                       workspace.CurrentCamera.ViewportSize.X

    -- Control container
    local controls = Instance.new("Frame")
    controls.Size = UDim2.new(1, 0, 1, 0)
    controls.BackgroundTransparency = 1
    controls.Parent = screenGui

    if platform == "mobile" then
        if isPhone() then
            -- Smaller controls for phone
            controls:SetAttribute("ControlScale", 0.7)
        else
            -- Tablet gets larger controls
            controls:SetAttribute("ControlScale", 1)
        end

        -- Portrait vs landscape
        if isPortrait then
            -- Stack controls vertically
            controls:SetAttribute("Layout", "portrait")
        else
            -- Controls on sides
            controls:SetAttribute("Layout", "landscape")
        end
    end

    -- Update on rotation
    workspace.CurrentCamera:GetPropertyChangedSignal("ViewportSize"):Connect(function()
        local newPortrait = workspace.CurrentCamera.ViewportSize.Y >
                           workspace.CurrentCamera.ViewportSize.X

        if newPortrait ~= isPortrait then
            isPortrait = newPortrait
            updateLayout(controls, isPortrait)
        end
    end)

    return screenGui
end

local function updateLayout(controls, isPortrait)
    -- Reposition elements based on orientation
    local joystickLeft = controls:FindFirstChild("JoystickLeft")
    local joystickRight = controls:FindFirstChild("JoystickRight")
    local buttons = controls:FindFirstChild("ActionButtons")

    if isPortrait then
        -- Portrait: controls at bottom
        if joystickLeft then
            joystickLeft.Position = UDim2.new(0.25, 0, 0.85, 0)
        end
        if joystickRight then
            joystickRight.Position = UDim2.new(0.75, 0, 0.85, 0)
        end
        if buttons then
            buttons.Position = UDim2.new(0.5, 0, 0.7, 0)
        end
    else
        -- Landscape: controls on sides
        if joystickLeft then
            joystickLeft.Position = UDim2.new(0.12, 0, 0.75, 0)
        end
        if joystickRight then
            joystickRight.Position = UDim2.new(0.88, 0, 0.75, 0)
        end
        if buttons then
            buttons.Position = UDim2.new(0.75, 0, 0.5, 0)
        end
    end
end

Safe Areas (Notch/Home Indicator)

local GuiService = game:GetService("GuiService")

local function getSafeAreaInsets()
    local inset = GuiService:GetGuiInset()
    return {
        top = inset.Y,
        bottom = 0,  -- Check specific device
        left = 0,
        right = 0
    }
end

local function applySafeArea(element)
    local insets = getSafeAreaInsets()

    -- Adjust position to avoid notch/home indicator
    local currentPos = element.Position

    element.Position = UDim2.new(
        currentPos.X.Scale,
        currentPos.X.Offset + insets.left,
        currentPos.Y.Scale,
        currentPos.Y.Offset + insets.top
    )
end

-- Or use ScreenGui.SafeAreaCompatibility
local screenGui = Instance.new("ScreenGui")
screenGui.SafeAreaCompatibility = Enum.SafeAreaCompatibility.FullscreenExtension
-- Enum.SafeAreaCompatibility.None - Ignore safe areas
-- Enum.SafeAreaCompatibility.FullscreenExtension - Extend behind notch

Complete Mobile Setup

-- MobileController.lua (LocalScript in StarterPlayerScripts)
local MobileController = {}

function MobileController.init()
    -- Only setup on touch devices
    if not UserInputService.TouchEnabled then
        return
    end

    -- Create main GUI
    local gui = Instance.new("ScreenGui")
    gui.Name = "MobileControls"
    gui.ResetOnSpawn = false
    gui.DisplayOrder = 10
    gui.Parent = game.Players.LocalPlayer:WaitForChild("PlayerGui")

    -- Create joysticks
    MobileController.moveJoystick = DynamicJoystick.new({
        size = isPhone() and 100 or 150,
        position = UDim2.new(0.15, 0, 0.75, 0)
    })
    MobileController.moveJoystick.gui.Parent = gui

    -- Create action buttons
    local actionContainer = Instance.new("Frame")
    actionContainer.Size = UDim2.new(0.3, 0, 0.4, 0)
    actionContainer.Position = UDim2.new(0.95, 0, 0.8, 0)
    actionContainer.AnchorPoint = Vector2.new(1, 1)
    actionContainer.BackgroundTransparency = 1
    actionContainer.Parent = gui

    -- Jump button (bottom right)
    local jumpBtn = createActionButton({
        name = "Jump",
        size = isPhone() and 70 or 90,
        position = UDim2.new(0.5, 0, 0.7, 0),
        icon = "rbxassetid://123456"  -- Jump icon
    })
    jumpBtn.Parent = actionContainer

    -- Attack button (right of jump)
    local attackBtn = createActionButton({
        name = "Attack",
        size = isPhone() and 60 or 80,
        position = UDim2.new(0.15, 0, 0.4, 0),
        icon = "rbxassetid://123457"  -- Attack icon
    })
    attackBtn.Parent = actionContainer

    -- Connect buttons to actions
    jumpBtn.MouseButton1Down:Connect(function()
        local humanoid = game.Players.LocalPlayer.Character
            and game.Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid")
        if humanoid then
            humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
        end
    end)

    attackBtn.MouseButton1Down:Connect(function()
        AttackRemote:FireServer()
    end)

    -- Swipe gestures for dodge
    SwipeDetector.init(function(direction)
        DodgeRemote:FireServer(direction)
    end)

    -- Update loop
    RunService.RenderStepped:Connect(function()
        MobileController.update()
    end)
end

function MobileController.update()
    local moveDir = MobileController.moveJoystick:getDirection()

    if moveDir.Magnitude > 0.1 then
        local camera = workspace.CurrentCamera
        local cameraForward = camera.CFrame.LookVector * Vector3.new(1, 0, 1)
        local cameraRight = camera.CFrame.RightVector * Vector3.new(1, 0, 1)

        local worldDir = (cameraForward.Unit * -moveDir.Y + cameraRight.Unit * moveDir.X)

        local humanoid = game.Players.LocalPlayer.Character
            and game.Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid")

        if humanoid then
            humanoid:Move(worldDir.Unit)
        end
    end
end

return MobileController
Weekly Installs
1
First Seen
Jan 30, 2026
Installed on
cursor1