Voltar para o Blog
Quest Log

Joystick Virtual na Unity para Jogo Mobile

Tela de celular na horizontal com um joystick virtual semitransparente no canto esquerdo e um botão de ação à direita

Joystick virtual na Unity com o On-Screen Stick do Input System, sem asset pago: mesma action do teclado, Canvas Scaler, safe area e versão flutuante em C#.

Ferramenta:UnityC#

Joystick Virtual na Unity para Jogo Mobile

O jeito mais limpo de fazer um Unity joystick virtual é usar o On-Screen Stick que já vem no pacote Input System. Ele transforma uma imagem da UI num analógico de gamepad virtual, então o movimento chega na mesma action Move que o teclado e o controle usam. Nenhum asset pago, nenhum if (mobile) no script do personagem.

Aqui você monta o joystick fixo com o On-Screen Stick, um botão de pulo com o On-Screen Button, posiciona os dois para funcionar em qualquer tela (Canvas Scaler e safe area), cria a versão flutuante que aparece onde o dedo toca e esconde tudo quando o jogo roda no PC. Código para Unity 6.3 LTS com o Input System ativo.

Unity joystick virtual: opções

Existem três caminhos, e cada um tem um custo:

  1. On-Screen Stick do Input System. Grátis, vem no pacote, e alimenta as actions existentes. O visual é seu (duas imagens). Limitação: os modos de comportamento são fixos, e a versão "aparece onde toca" tem pouca personalização.
  2. Joystick Pack (Asset Store). Gratuito, com vários estilos prontos (fixo, flutuante, dinâmico) e sprites, mas sem atualização há anos: confira a data da última versão na página do asset antes de importar. Ele lê os eventos da UI e expõe Horizontal/Vertical num componente, então você escreve a ligação com o personagem e mantém dois caminhos de input no código.
  3. Componente próprio herdando de OnScreenControl. Umas 50 linhas de C#, comportamento exatamente como você quer, e continua alimentando as actions. É o que faço quando o jogo precisa de joystick flutuante.

Para a maioria dos jogos, comece com o 1. Se o design pedir flutuante, vá para o 3. As duas seções a seguir cobrem os dois.

On-Screen Stick e On-Screen Button

Monte a hierarquia:

  1. GameObject > UI > Canvas (se a cena não tem). Confirme que existe um EventSystem com o InputSystemUIInputModule.
  2. Dentro do Canvas, crie uma UI > Image chamada StickBackground: sprite de círculo, 300 x 300 px, alfa uns 40%.
  3. Dentro dela, outra UI > Image chamada StickHandle: círculo menor, 140 x 140 px, centralizado.
  4. No StickHandle, Add Component > On-Screen Stick.
  5. No campo Control Path, clique e escolha Gamepad > Left Stick.
  6. Movement Range: 80. É o raio em pixels (da referência do Canvas) que o handle pode andar a partir do centro.

Para o botão de pulo: UI > Image chamada JumpButton, 180 x 180 px, Add Component > On-Screen Button, Control Path = Gamepad > Button South.

O que está acontecendo por baixo: o pacote cria um gamepad virtual. Mexer o handle escreve no leftStick dele; apertar o botão escreve no buttonSouth. Para o resto do jogo, existe um controle conectado.

O On-Screen Stick tem três modos no campo Behaviour, com os nomes exatos da API:

  • Relative Position With Static Origin: centro fixo, o handle começa em repouso e anda conforme o dedo se move. É o padrão e o mais previsível.
  • Exact Position With Static Origin: centro fixo, mas o handle pula para a posição exata do dedo.
  • Exact Position With Dynamic Origin: o centro vai para onde o dedo encostou, dentro de um raio definido por Dynamic Origin Range.

E a opção Use Isolated Input Actions existe para um problema real: quando o jogo detecta troca de dispositivo (o gamepad virtual contra o touch), a interação com o stick pode ser cancelada. Ligar essa opção faz o stick usar actions locais e evita o cancelamento.

Ligar à mesma action do teclado

Esse passo é quase nada, e é a maior vantagem do método. O asset padrão InputSystem_Actions da Unity 6 já tem Move com binding em <Gamepad>/leftStick e Jump em <Gamepad>/buttonSouth. Então o script de movimento que você já tem continua o mesmo:

Arquivo PlayerMove2D.cs:

using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerMove2D : MonoBehaviour
{
    [SerializeField] private Rigidbody2D rb;
    [SerializeField] private float speed = 5f;
    [SerializeField] private float jumpForce = 8f;

    private InputAction moveAction;
    private InputAction jumpAction;
    private Vector2 moveInput;
    private bool jumpRequested;

    private void Awake()
    {
        moveAction = InputSystem.actions.FindAction("Player/Move", true);
        jumpAction = InputSystem.actions.FindAction("Player/Jump", true);
    }

    private void Update()
    {
        moveInput = moveAction.ReadValue<Vector2>();
        if (jumpAction.WasPressedThisFrame())
        {
            jumpRequested = true;
        }
    }

    private void FixedUpdate()
    {
        rb.linearVelocity = new Vector2(moveInput.x * speed, rb.linearVelocity.y);
        if (jumpRequested)
        {
            rb.linearVelocity = new Vector2(rb.linearVelocity.x, jumpForce);
            jumpRequested = false;
        }
    }
}

Teclado, controle físico e joystick virtual chegam pela mesma moveAction. Se o seu asset é personalizado e não tem binding de gamepad, adicione Gamepad > Left Stick em Move e Gamepad > Button South em Jump. Se quiser que a deadzone do analógico físico também valha para o virtual, o processor Stick Deadzone no binding cobre os dois.

Posicionar com Canvas Scaler e safe area

Joystick com tamanho fixo em pixels fica minúsculo num tablet e enorme num celular pequeno. Selecione o Canvas e, no Canvas Scaler:

  • UI Scale Mode: Scale With Screen Size.
  • Reference Resolution: 1920 x 1080 (jogo em paisagem).
  • Screen Match Mode: Match Width Or Height, Match 0,5.

Agora os 300 px do fundo do stick são 300 px de uma tela de referência, e escalam proporcionalmente. Ancore o StickBackground no canto inferior esquerdo (no Anchor Presets, segure Shift e Alt para levar pivot e posição junto) com margem de uns 120 px, e o JumpButton no inferior direito.

O segundo problema é o notch e as bordas arredondadas. Um joystick encostado no canto some debaixo do recorte da câmera em muitos aparelhos. A Unity informa o retângulo utilizável em Screen.safeArea. Crie um painel SafeArea filho do Canvas, esticado na tela inteira, mova os controles para dentro dele e adicione este script:

Arquivo SafeAreaFitter.cs:

using UnityEngine;

[RequireComponent(typeof(RectTransform))]
public class SafeAreaFitter : MonoBehaviour
{
    private RectTransform panel;
    private Rect lastSafeArea;
    private Vector2Int lastScreenSize;

    private void Awake()
    {
        panel = GetComponent<RectTransform>();
        Apply();
    }

    private void Update()
    {
        Vector2Int screenSize = new Vector2Int(Screen.width, Screen.height);
        if (Screen.safeArea != lastSafeArea || screenSize != lastScreenSize)
        {
            Apply();
        }
    }

    private void Apply()
    {
        Rect safe = Screen.safeArea;
        lastSafeArea = safe;
        lastScreenSize = new Vector2Int(Screen.width, Screen.height);

        Vector2 anchorMin = safe.position;
        Vector2 anchorMax = safe.position + safe.size;
        anchorMin.x /= Screen.width;
        anchorMin.y /= Screen.height;
        anchorMax.x /= Screen.width;
        anchorMax.y /= Screen.height;

        panel.anchorMin = anchorMin;
        panel.anchorMax = anchorMax;
        panel.offsetMin = Vector2.zero;
        panel.offsetMax = Vector2.zero;
    }
}

A checagem no Update cobre a rotação da tela: ao virar o celular, o notch troca de lado e o painel se reajusta. Teste no Device Simulator escolhendo um aparelho com recorte; ele desenha a safe area por cima da Game view.

Até aqui você tem controles na tela que funcionam. Jogo mobile publicado pede mais: HUD que não briga com os controles, feedback de toque, desempenho em aparelho barato, build assinado para a loja. Se o seu objetivo é sair do protótipo e chegar no jogo na mão do jogador, é exatamente esse caminho que o curso de Unity percorre com projetos completos.

Curso de Unity
Quer fazer seu primeiro jogo na Unity?

No curso de Unity 6 da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Unity
Suporte no WhatsAppAcesso vitalícioGarantia de 7 dias

Joystick flutuante

Em jogo de ação mobile, o jogador não olha para o canto da tela: encosta o polegar em qualquer lugar da metade esquerda e espera que o joystick nasça ali. O modo Exact Position With Dynamic Origin faz algo parecido, mas o fundo do joystick não acompanha. Um componente próprio resolve isso e continua alimentando a action Move.

Monte assim: uma UI > Image JoystickArea cobrindo a metade esquerda da tela (anchors de 0 a 0,5 no X), cor com alfa 0 e Raycast Target ligado, pivot no centro. Dentro dela, o StickBackground (anchors no centro) com o StickHandle dentro. Remova o On-Screen Stick do handle e coloque este script na JoystickArea:

Arquivo FloatingJoystick.cs:

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.OnScreen;

public class FloatingJoystick : OnScreenControl, IPointerDownHandler, IDragHandler, IPointerUpHandler
{
    [InputControl(layout = "Vector2")]
    [SerializeField] private string stickControlPath = "<Gamepad>/leftStick";
    [SerializeField] private RectTransform background;
    [SerializeField] private RectTransform handle;
    [SerializeField] private float radius = 100f;

    private RectTransform area;
    private Vector2 origin;

    protected override string controlPathInternal
    {
        get => stickControlPath;
        set => stickControlPath = value;
    }

    private void Awake()
    {
        area = (RectTransform)transform;
        background.gameObject.SetActive(false);
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        RectTransformUtility.ScreenPointToLocalPointInRectangle(area, eventData.position, eventData.pressEventCamera, out origin);
        background.anchoredPosition = origin;
        handle.anchoredPosition = Vector2.zero;
        background.gameObject.SetActive(true);
        SendValueToControl(Vector2.zero);
    }

    public void OnDrag(PointerEventData eventData)
    {
        RectTransformUtility.ScreenPointToLocalPointInRectangle(area, eventData.position, eventData.pressEventCamera, out Vector2 local);
        Vector2 offset = Vector2.ClampMagnitude(local - origin, radius);
        handle.anchoredPosition = offset;
        SendValueToControl(offset / radius);
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        handle.anchoredPosition = Vector2.zero;
        background.gameObject.SetActive(false);
        SendValueToControl(Vector2.zero);
    }
}

Por que isso funciona: OnScreenControl é a classe base do próprio On-Screen Stick. Herdando dela, você ganha o gamepad virtual de graça; só precisa dizer qual controle alimentar (controlPathInternal) e mandar o valor com SendValueToControl. O atributo [InputControl(layout = "Vector2")] faz o Inspector mostrar o seletor de controles filtrado para analógicos.

A conta é a de sempre: a distância do dedo até a origem, limitada pelo raio com Vector2.ClampMagnitude, dividida pelo raio para virar um valor de 0 a 1. Para o anchoredPosition do fundo bater com o ponto local, a JoystickArea precisa de pivot no centro e o StickBackground de anchors no centro.

Esconder no PC

Joystick na tela de quem joga com teclado é poluição visual. A regra mais simples:

Arquivo MobileControlsVisibility.cs:

using UnityEngine;
using UnityEngine.InputSystem;

public class MobileControlsVisibility : MonoBehaviour
{
    [SerializeField] private GameObject mobileControlsRoot;
    [SerializeField] private bool forceShowInEditor = false;

    private void Start()
    {
        bool hasTouch = Touchscreen.current != null;
        bool show = Application.isMobilePlatform || hasTouch || (Application.isEditor && forceShowInEditor);
        mobileControlsRoot.SetActive(show);
    }
}

O forceShowInEditor deixa você testar no editor sem trocar de plataforma. Para notebook com tela de toque, a checagem de Touchscreen.current mostra os controles, o que costuma ser o certo. Se quiser algo mais fino, esconda os controles quando o jogador tocar no teclado ou no controle físico, e mostre de volta no próximo toque.

Erros comuns

O stick não responde. Falta EventSystem na cena, ou ele usa o StandaloneInputModule antigo. O On-Screen Stick depende dos eventos de ponteiro da UI.

O handle mexe mas o personagem não anda. O asset de actions não tem binding de <Gamepad>/leftStick em Move, ou o Player Input está preso num esquema de controle Keyboard&Mouse e ignora o gamepad virtual.

O stick solta sozinho no meio do movimento. Troca de dispositivo detectada entre o toque e o gamepad virtual. Ligue Use Isolated Input Actions no On-Screen Stick.

O botão de pulo ativa junto com o joystick. As duas imagens se sobrepõem, ou a JoystickArea flutuante cobre o botão. Ajuste a área ou a ordem na hierarquia (o último filho fica por cima e recebe o toque primeiro).

Próximo passo

Teste o controle com o polegar de verdade num aparelho: raio do stick, tamanho do botão e margem mudam muito do monitor para a mão. Para gestos além do joystick, veja o post de touch na Unity com EnhancedTouch. Se você está escolhendo engine para o projeto, o mesmo problema resolvido na Godot está em controles touch e joystick virtual na Godot, e o guia de como criar o primeiro jogo mobile ajuda a dimensionar o escopo.

Perguntas frequentes

Preciso comprar asset para fazer joystick virtual na Unity?

Não. O pacote Input System traz os componentes On-Screen Stick e On-Screen Button, que transformam uma imagem da UI num analógico ou botão de um gamepad virtual. Como eles alimentam as mesmas actions do teclado e do controle, o código do personagem não muda.

Como fazer o joystick aparecer onde o dedo toca?

Use o On-Screen Stick com Behaviour em Exact Position With Dynamic Origin e um Dynamic Origin Range grande, ou escreva um componente próprio herdando de OnScreenControl que move o fundo do joystick para o ponto do toque e envia o valor com SendValueToControl. O segundo jeito dá controle total sobre a área e o visual.

O Joystick Pack da Asset Store ainda funciona?

O Joystick Pack segue gratuito na Asset Store em outubro de 2026, mas a última versão listada é de março de 2019. Ele lê os eventos da UI e entrega valores que você precisa passar ao personagem por código próprio, sem integração com as actions do Input System.

Como deixar o joystick fora do notch do celular?

Coloque os controles dentro de um painel ajustado a Screen.safeArea. Um script converte o retângulo seguro em anchorMin e anchorMax do painel, e tudo que estiver dentro dele respeita o notch e as bordas arredondadas.