Voltar para o Blog
Quest Log

Swipe na Unity: Detectar Deslizar o Dedo em Jogo Mobile

Corredor 3D estilizado com três faixas e um rastro de dedo deslizando para a direita sobre a tela

Unity swipe com o Input System: detector para endless runner com distância mínima em centímetros, tempo máximo, direção dominante e teste com mouse no editor.

Ferramenta:UnityC#

Swipe na Unity: Detectar Deslizar o Dedo em Jogo Mobile

Detectar Unity swipe é comparar onde o dedo encostou com onde ele saiu: se andou mais que uma distância mínima, rápido o bastante, é um deslize, e a direção é o eixo com maior deslocamento. Com o Input System, você lê o início e o fim pelo EnhancedTouch e faz essa conta em poucas linhas.

O que separa um swipe que funciona de um que irrita o jogador são três números: distância mínima, tempo máximo e a regra de direção. Aqui você monta um detector com esses três ajustáveis, mede a distância em centímetros (para funcionar igual em qualquer celular), liga num personagem de endless runner que troca de faixa, pula e desliza, e testa tudo com o mouse no editor. Código para Unity 6.3 LTS.

Unity swipe: como funciona

Um swipe tem quatro dados:

  1. Posição inicial: onde o dedo encostou.
  2. Posição final: onde o dedo saiu (ou onde está agora, se você decide durante o movimento).
  3. Tempo: quanto durou o gesto.
  4. Direção: o vetor do fim menos o início.

A decisão é uma sequência de filtros. Se a distância é pequena, foi um toque, não um swipe. Se o tempo é longo, foi um arrasto lento, provavelmente o jogador estava ajustando o dedo. Passou nos dois? Olhe qual eixo domina: se o deslocamento em X é maior que em Y, é esquerda ou direita; senão, cima ou baixo.

Essa regra de "eixo dominante" é o que endless runner usa, porque o jogo só tem quatro respostas possíveis. Um swipe na diagonal de 30 graus vira "direita", sem ambiguidade.

Ler início e fim do toque

O Input System oferece dois jeitos de saber quando o dedo encosta e sai.

O mais direto é Touchscreen.current.primaryTouch, que tem press.wasPressedThisFrame, press.wasReleasedThisFrame, startPosition e position. Funciona, mas só enxerga o primeiro dedo.

O mais robusto é o EnhancedTouch, com os eventos estáticos Touch.onFingerDown e Touch.onFingerUp. Cada um recebe um Finger, que mantém a identidade do dedo do começo ao fim e informa screenPosition. É o que uso no detector, porque se o jogador encostar um segundo dedo sem querer, cada dedo tem o próprio início.

Calcular direção e distância

A conta de direção é aritmética de vetor. O deslocamento é fim - início. Para saber o eixo dominante, compare os valores absolutos:

  • Mathf.Abs(delta.x) > Mathf.Abs(delta.y): horizontal. Sinal de delta.x diz esquerda ou direita.
  • Senão: vertical. Sinal de delta.y diz cima ou baixo (lembrando que em tela o Y cresce para cima).

A distância é delta.magnitude. O problema é a unidade: está em pixels, e 100 pixels num celular de 460 dpi são um terço do que são num tablet antigo de 160 dpi. Um limiar em pixels faz o swipe ser difícil num aparelho e disparar sozinho em outro. A solução é medir em centímetros: Screen.dpi informa pontos por polegada, e uma polegada tem 2,54 cm. Então:

pixelsPorCm = Screen.dpi / 2.54f

O manual da Unity avisa que Screen.dpi devolve 0 quando o sistema não informa a densidade. Por isso o código usa um valor de reserva.

Distância mínima e tempo máximo

Arquivo SwipeDetector.cs:

using System;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.EnhancedTouch;
using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch;

public enum SwipeDirection
{
    Left,
    Right,
    Up,
    Down
}

public class SwipeDetector : MonoBehaviour
{
    public event Action<SwipeDirection> Swiped;

    [SerializeField] private float minDistanceCm = 0.6f;
    [SerializeField] private float maxDuration = 0.5f;
    [SerializeField] private float fallbackDpi = 160f;
    [SerializeField] private bool acceptMouse = true;

    private Vector2 startPosition;
    private float startTime;
    private int activeFingerIndex = -1;

    private void OnEnable()
    {
        EnhancedTouchSupport.Enable();
        Touch.onFingerDown += HandleFingerDown;
        Touch.onFingerUp += HandleFingerUp;
    }

    private void OnDisable()
    {
        Touch.onFingerDown -= HandleFingerDown;
        Touch.onFingerUp -= HandleFingerUp;
        EnhancedTouchSupport.Disable();
    }

    private void Update()
    {
        if (!acceptMouse || Mouse.current == null)
        {
            return;
        }

        if (Mouse.current.leftButton.wasPressedThisFrame)
        {
            BeginGesture(Mouse.current.position.ReadValue());
        }
        else if (Mouse.current.leftButton.wasReleasedThisFrame)
        {
            EndGesture(Mouse.current.position.ReadValue());
        }
    }

    private void HandleFingerDown(Finger finger)
    {
        if (activeFingerIndex != -1)
        {
            return;
        }
        activeFingerIndex = finger.index;
        BeginGesture(finger.screenPosition);
    }

    private void HandleFingerUp(Finger finger)
    {
        if (finger.index != activeFingerIndex)
        {
            return;
        }
        activeFingerIndex = -1;
        EndGesture(finger.screenPosition);
    }

    private void BeginGesture(Vector2 position)
    {
        startPosition = position;
        startTime = Time.unscaledTime;
    }

    private void EndGesture(Vector2 position)
    {
        float duration = Time.unscaledTime - startTime;
        if (duration > maxDuration)
        {
            return;
        }

        Vector2 delta = position - startPosition;
        float dpi = Screen.dpi > 0f ? Screen.dpi : fallbackDpi;
        float minDistancePixels = minDistanceCm * (dpi / 2.54f);
        if (delta.magnitude < minDistancePixels)
        {
            return;
        }

        SwipeDirection direction;
        if (Mathf.Abs(delta.x) > Mathf.Abs(delta.y))
        {
            direction = delta.x > 0f ? SwipeDirection.Right : SwipeDirection.Left;
        }
        else
        {
            direction = delta.y > 0f ? SwipeDirection.Up : SwipeDirection.Down;
        }

        Swiped?.Invoke(direction);
    }
}

Algumas escolhas aí dentro:

  • Time.unscaledTime: se o jogo usar câmera lenta (Time.timeScale baixo) num momento de impacto, o swipe continua com o mesmo tempo máximo real. Com Time.time, o limite de 0,5 s ficaria esticado.
  • activeFingerIndex: só um dedo por vez faz swipe. Um segundo dedo encostando no meio não reinicia o gesto.
  • Evento Swiped: o detector não sabe nada do personagem. Quem quiser reagir assina o evento. Isso deixa você reutilizar o mesmo detector num menu de fases que passa as páginas com o dedo.

Sobre os números: 0,6 cm de distância mínima e 0,5 s de tempo máximo são um bom ponto de partida para runner. Se os jogadores reclamarem que o pulo sai quando só queriam tocar, aumente a distância. Se reclamarem que o swipe "não pega", aumente o tempo.

Swipe em endless runner: trocar de faixa

O personagem tem três faixas (-1, 0, 1). O swipe muda a faixa alvo, e o movimento até ela é suave.

Arquivo LaneRunner.cs:

using UnityEngine;

public class LaneRunner : MonoBehaviour
{
    [SerializeField] private SwipeDetector swipe;
    [SerializeField] private CharacterController controller;
    [SerializeField] private float laneWidth = 2.5f;
    [SerializeField] private float laneChangeSpeed = 12f;
    [SerializeField] private float forwardSpeed = 10f;
    [SerializeField] private float jumpSpeed = 8f;
    [SerializeField] private float gravity = -25f;

    private int currentLane;
    private float verticalVelocity;

    private void OnEnable()
    {
        swipe.Swiped += OnSwiped;
    }

    private void OnDisable()
    {
        swipe.Swiped -= OnSwiped;
    }

    private void OnSwiped(SwipeDirection direction)
    {
        if (direction == SwipeDirection.Left)
        {
            currentLane = Mathf.Max(currentLane - 1, -1);
        }
        else if (direction == SwipeDirection.Right)
        {
            currentLane = Mathf.Min(currentLane + 1, 1);
        }
        else if (direction == SwipeDirection.Up && controller.isGrounded)
        {
            verticalVelocity = jumpSpeed;
        }
        else if (direction == SwipeDirection.Down)
        {
            Debug.Log("Deslizar: dispare a animação de slide e reduza o collider aqui");
        }
    }

    private void Update()
    {
        float targetX = currentLane * laneWidth;
        float newX = Mathf.MoveTowards(transform.position.x, targetX, laneChangeSpeed * Time.deltaTime);
        float deltaX = newX - transform.position.x;

        if (controller.isGrounded && verticalVelocity < 0f)
        {
            verticalVelocity = -2f;
        }
        verticalVelocity += gravity * Time.deltaTime;

        Vector3 move = new Vector3(deltaX, verticalVelocity * Time.deltaTime, forwardSpeed * Time.deltaTime);
        controller.Move(move);
    }
}

Mathf.MoveTowards dá uma troca de faixa em velocidade constante, que é previsível para o jogador; se quiser uma curva mais macia, use Mathf.SmoothDamp. O -2f quando no chão mantém o CharacterController grudado no piso e o isGrounded confiável. A faixa é limitada a -1..1 com Mathf.Max e Mathf.Min, então swipe para fora da pista é ignorado.

O detector e o personagem são o começo de um runner. O jogo de verdade precisa de pista que se gera e recicla, obstáculos com padrão justo, velocidade que sobe, moedas, pontuação, morte e reinício, tudo rodando liso em celular de entrada. É na hora de juntar essas partes que o projeto costuma emperrar, e é esse tipo de jogo completo que o curso de Unity constrói com você do zero até o build.

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

Se você quer o passo a passo do jogo inteiro, o tutorial de como fazer um endless runner na Unity monta a pista infinita, os obstáculos e a pontuação em cima deste mesmo controle.

Disparar antes de soltar o dedo. O detector acima decide no onFingerUp. Para runner competitivo, decidir assim que o dedo passa da distância mínima deixa o jogo mais responsivo. Assine também Touch.onFingerMove, faça a mesma conta com finger.screenPosition como fim e, quando disparar, ligue um bool gestureConsumed. No onFingerUp do mesmo dedo, volte activeFingerIndex para -1 e só chame o EndGesture se gestureConsumed estiver desligado; depois desligue a flag. Assim o gesto não dispara duas vezes e o próximo dedo continua sendo aceito.

Testar com mouse no editor

O detector já aceita mouse: com acceptMouse ligado, clicar, arrastar e soltar com o botão esquerdo chama o mesmo BeginGesture e EndGesture. No editor, o Screen.dpi é o do seu monitor; se o swipe parecer curto ou longo demais com o mouse, é por isso, e no celular ele se comporta pelos centímetros reais.

Duas alternativas que não exigem mudar o código:

  • Device Simulator: na Game view, troque Game por Simulator e escolha um aparelho. O clique vira toque, e o simulador usa a resolução e a densidade daquele modelo, o que deixa o teste de distância mais próximo do real. Se o gesto contar duas vezes no Simulator, desligue o acceptMouse.
  • Touch Simulation: em Window > Analysis > Input Debugger, menu Options, ligue Simulate Touch Input From Mouse or Pen. Aí o mouse dispara o onFingerDown e o onFingerUp. Nesse caso, desligue o acceptMouse para não contar o gesto duas vezes.

Erros comuns

Swipe dispara duas vezes. Touch Simulation ligada junto com o caminho do mouse no detector. Use um ou outro.

Toque simples vira swipe. Distância mínima baixa demais, ou medida em pixels num aparelho de alta densidade. Use centímetros com Screen.dpi.

Diagonal sai na direção errada. É a regra do eixo dominante funcionando. Se o seu jogo precisa de diagonal (oito direções), troque a comparação por ângulo com Mathf.Atan2 e fatias de 45 graus.

Swipe "morre" depois de pausar o jogo. Algum script desligou o EnhancedTouch e não religou, ou o detector foi desativado junto com o jogo e deixou de ouvir os eventos. Mantenha o OnEnable/OnDisable pareados.

Swipe para baixo some em alguns Android. Gesto que começa rente à borda inferior ou superior pode ser capturado pelo sistema (barra de navegação, painel de notificações) e nunca chegar ao jogo, ou chegar como Canceled. Deixe uma margem morta perto das bordas no design da tela, ou aceite o swipe a partir de qualquer ponto mais central, e trate o cancelamento como gesto descartado.

Resposta diferente em 30 e em 60 fps. O detector usa tempo real e posições de tela, então ele não depende da taxa de quadros. Se a troca de faixa parecer mais lenta em celular fraco, o problema está no movimento: confira se ele multiplica por Time.deltaTime, como no LaneRunner.

O botão de pausa também conta como swipe. O dedo apertou a UI e deslizou um pouco. Cheque EventSystem.current.IsPointerOverGameObject() no início do gesto e descarte se for UI.

Próximo passo

Monte uma cena com uma pista simples, o LaneRunner e um cubo como obstáculo, e calibre os três números no aparelho. Se a parte de vetor da direção ainda estiver confusa, a matemática de vetores para jogos explica deslocamento, magnitude e direção. Para decidir escopo e monetização antes de investir meses no runner, leia como criar o primeiro jogo mobile.

Perguntas frequentes

Como detectar swipe na Unity com o New Input System?

Ative o EnhancedTouch, guarde a posição e o horário em Touch.onFingerDown, e em Touch.onFingerUp compare com a posição final. Se a distância passar do mínimo e o tempo ficar abaixo do máximo, é swipe; a direção é o eixo com maior deslocamento.

Qual a distância mínima ideal para um swipe?

Meça em centímetros, não em pixels, porque a densidade de tela varia muito entre aparelhos. Algo entre 0,5 e 1 cm costuma funcionar. Converta com Screen.dpi, e use um valor padrão quando o aparelho devolver 0.

Como testar swipe na Unity sem celular?

Faça o detector aceitar também o botão esquerdo do mouse, chamando as mesmas funções de início e fim do gesto. Outra opção é ligar Simulate Touch Input From Mouse or Pen no Input Debugger ou usar o Device Simulator, que transformam o clique em toque.

Swipe deve disparar ao soltar o dedo ou durante o movimento?

Em endless runner, disparar durante o movimento, assim que passar a distância mínima, deixa o jogo mais responsivo. Disparar ao soltar é mais simples e evita gestos acidentais. O código deste post faz ao soltar e mostra a mudança para disparar antes.