Voltar para o Blog
Quest Log

Posição do Mouse na Unity: Input System e ScreenToWorldPoint

Cursor de mouse sobre uma grade de jogo 2D com uma linha pontilhada ligando a tela ao ponto no mundo

Posição do mouse no Unity Input System: Mouse.current.position, ScreenToWorldPoint em 2D com o Z certo, raycast em 3D e como ignorar cliques em cima da UI.

Ferramenta:UnityC#

Posição do Mouse na Unity: Input System e ScreenToWorldPoint

No Unity Input System, a posição do mouse vem de Mouse.current.position.ReadValue(): um Vector2 em pixels, com (0, 0) no canto inferior esquerdo da tela. Para usar esse ponto no jogo, você converte para o mundo com Camera.ScreenToWorldPoint (em 2D) ou com um raio da câmera (em 3D). A conversão é onde quase todo mundo erra, porque o Z que você passa muda o resultado.

Aqui você vai ter o código de cada caso pronto para Unity 6.3 LTS: posição em tela, posição no mundo 2D, o bug do Z que faz o ScreenToWorldPoint devolver a posição da câmera, mouse no mundo 3D com raycast, leitura pela action Point (que funciona com toque também) e como ignorar o clique quando o cursor está em cima de um botão.

Unity input system mouse position: em tela

A leitura mais direta usa o dispositivo Mouse:

Arquivo MouseScreenPosition.cs:

using UnityEngine;
using UnityEngine.InputSystem;

public class MouseScreenPosition : MonoBehaviour
{
    private void Update()
    {
        Mouse mouse = Mouse.current;
        if (mouse == null)
        {
            return;
        }

        Vector2 screenPos = mouse.position.ReadValue();
        Vector2 delta = mouse.delta.ReadValue();

        if (mouse.leftButton.wasPressedThisFrame)
        {
            Debug.Log("Clique em " + screenPos + " (tela " + Screen.width + "x" + Screen.height + ")");
        }

        if (delta.sqrMagnitude > 0f)
        {
            Debug.Log("Mouse moveu " + delta + " pixels neste frame");
        }
    }
}

O que esse valor significa:

  • Unidade é pixel da janela do jogo, não do monitor. No editor, é o tamanho da Game view.
  • Origem é inferior esquerda. O canto superior direito é (Screen.width, Screen.height).
  • mouse.delta é quanto o mouse andou desde o último frame, útil para câmera em primeira pessoa.

A checagem Mouse.current == null parece exagero no PC, mas o mesmo script rodando num build de celular sem mouse quebraria com NullReferenceException logo no primeiro frame.

Converter para o mundo em 2D

Num jogo 2D com câmera ortográfica, você quer saber em que ponto do mundo o cursor está para mirar, posicionar uma construção ou mover o personagem até lá.

Arquivo MouseWorld2D.cs:

using UnityEngine;
using UnityEngine.InputSystem;

public class MouseWorld2D : MonoBehaviour
{
    [SerializeField] private Camera cam;
    [SerializeField] private Transform cursorMarker;

    private void Awake()
    {
        if (cam == null)
        {
            cam = Camera.main;
        }
    }

    private void Update()
    {
        if (cursorMarker == null)
        {
            return;
        }

        cursorMarker.position = GetMouseWorldPosition();
    }

    public Vector3 GetMouseWorldPosition()
    {
        if (Mouse.current == null)
        {
            return Vector3.zero;
        }

        Vector2 screen = Mouse.current.position.ReadValue();
        float distance = -cam.transform.position.z;
        Vector3 world = cam.ScreenToWorldPoint(new Vector3(screen.x, screen.y, distance));
        world.z = 0f;
        return world;
    }
}

Duas linhas carregam o método todo. A primeira calcula distance = -cam.transform.position.z: a câmera padrão 2D fica em z = -10 e o jogo acontece em z = 0, então a distância até o plano do jogo é 10. A segunda zera o z do resultado, para o objeto ficar no mesmo plano dos sprites e não atrás da câmera.

Guarde a câmera num campo em vez de chamar Camera.main em todo lugar. Ela é encontrada pela tag MainCamera; se você trocar a câmera ou esquecer a tag, o campo no Inspector deixa claro qual está sendo usada.

O bug do Z no ScreenToWorldPoint

A versão que aparece em todo fórum é esta:

Vector3 world = Camera.main.ScreenToWorldPoint(Mouse.current.position.ReadValue());

O Vector2 vira Vector3 com z = 0, e a documentação do Camera.ScreenToWorldPoint diz que o z do ponto é a distância em unidades de mundo à frente da câmera. Distância zero é o próprio plano da câmera. O que acontece em cada tipo de câmera:

  • Perspectiva: todos os raios saem de um ponto só. Na distância zero, qualquer pixel da tela vira a posição da câmera. É o sintoma "o ScreenToWorldPoint sempre devolve a posição da câmera, não importa onde eu clico".
  • Ortográfica: x e y saem certos, porque os raios são paralelos. Mas o z do resultado é o z da câmera (-10). Se você põe um objeto nesse ponto, ele fica no plano da câmera e some da tela, ou é cortado pelo Near Clip Plane.

A correção é a do script anterior: passe a distância certa no z e, em 2D, zere o z do resultado. Em câmera perspectiva olhando para um jogo 2.5D, a distância certa depende de onde está o plano do jogo, e aí o raio da próxima seção é mais seguro.

Mouse no mundo 3D com raycast

Em 3D não existe "o ponto" do mouse no mundo: existe um raio que sai da câmera passando pelo pixel. Você decide onde ele para.

Arquivo MouseWorld3D.cs:

using UnityEngine;
using UnityEngine.InputSystem;

public class MouseWorld3D : MonoBehaviour
{
    [SerializeField] private Camera cam;
    [SerializeField] private LayerMask groundMask;
    [SerializeField] private float maxDistance = 200f;

    public bool TryGetMouseHit(out RaycastHit hit)
    {
        hit = default;
        if (Mouse.current == null)
        {
            return false;
        }

        Vector2 screen = Mouse.current.position.ReadValue();
        Ray ray = cam.ScreenPointToRay(screen);
        return Physics.Raycast(ray, out hit, maxDistance, groundMask);
    }

    public bool TryGetPointOnFloor(out Vector3 point)
    {
        point = Vector3.zero;
        if (Mouse.current == null)
        {
            return false;
        }

        Ray ray = cam.ScreenPointToRay(Mouse.current.position.ReadValue());
        Plane floor = new Plane(Vector3.up, Vector3.zero);
        if (floor.Raycast(ray, out float enter))
        {
            point = ray.GetPoint(enter);
            return true;
        }
        return false;
    }

    private void Update()
    {
        if (Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame)
        {
            if (TryGetMouseHit(out RaycastHit hit))
            {
                Debug.Log("Clicou em " + hit.collider.name + " no ponto " + hit.point);
            }
        }
    }
}

São dois métodos para dois usos. TryGetMouseHit usa Physics.Raycast e precisa de collider: serve para clicar em unidade, em inimigo, em terreno com MeshCollider. O LayerMask evita que o raio pare no próprio jogador ou num gatilho invisível. TryGetPointOnFloor usa um Plane matemático em y = 0, sem collider nenhum: é o jeito barato de fazer um top-down 3D mirar no chão, e funciona mesmo onde não há geometria.

Pela action Point

Ler Mouse.current prende o código ao mouse. Se o jogo também roda em celular ou tablet com caneta, a action Point do asset padrão já resolve: no map UI do InputSystem_Actions da Unity 6 ela está ligada à posição do mouse, da caneta e do toque.

Arquivo PointerWorld2D.cs:

using UnityEngine;
using UnityEngine.InputSystem;

public class PointerWorld2D : MonoBehaviour
{
    [SerializeField] private Camera cam;

    private InputAction pointAction;

    private void Awake()
    {
        if (cam == null)
        {
            cam = Camera.main;
        }

        pointAction = InputSystem.actions.FindAction("UI/Point", true);
    }

    public Vector3 GetPointerWorldPosition()
    {
        Vector2 screen = pointAction.ReadValue<Vector2>();
        Vector3 world = cam.ScreenToWorldPoint(new Vector3(screen.x, screen.y, -cam.transform.position.z));
        world.z = 0f;
        return world;
    }
}

O código de conversão é o mesmo; só a fonte mudou. Se preferir manter as actions de gameplay todas no map Player, crie uma action Point lá (Action Type Value, Control Type Vector2) e adicione os bindings <Pointer>/position. O path Pointer cobre mouse, caneta e toque de uma vez.

Ler a posição é o fácil. O que dá trabalho num jogo inteiro é tudo que depende dela ao mesmo tempo: mira, câmera que acompanha o cursor, construção em grade, UI que bloqueia o clique, controle que substitui o mouse. Juntar isso num projeto que fecha é o que o curso de Unity ensina construindo jogos completos, em vez de cada peça num tutorial separado.

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

Mouse sobre a UI: ignorar cliques

Clássico: o jogador clica no botão "Inventário" e o personagem também atira, porque o script de tiro leu o mesmo clique. A correção é perguntar ao EventSystem se o ponteiro está sobre a UI.

Arquivo ClickToShoot.cs:

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;

public class ClickToShoot : MonoBehaviour
{
    [SerializeField] private MouseWorld2D mouseWorld;

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

        if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject())
        {
            return;
        }

        Vector3 target = mouseWorld.GetMouseWorldPosition();
        Debug.Log("Atirando em " + target);
    }
}

Três condições para isso funcionar:

  1. O EventSystem da cena usa o InputSystemUIInputModule. Se o Inspector mostrar o StandaloneInputModule antigo, clique no botão de substituir pelo módulo do Input System.
  2. A chamada acontece no Update. Dentro de um callback de action (performed +=), o IsPointerOverGameObject responde com o estado da UI do frame anterior, e o pacote avisa isso no Console.
  3. O elemento de UI tem Raycast Target ligado. Uma imagem decorativa com Raycast Target ligado bloqueia cliques que você queria no mundo; um painel com ele desligado deixa passar.

Encaixar o mouse na grade e trocar o cursor

Dois pedidos que aparecem logo depois de "pegar a posição do mouse": construir em grade e trocar a setinha do sistema por um cursor do jogo.

Para grade, arredonde o ponto do mundo. Com tiles de 1 unidade (o padrão de um sprite de 16 px com Pixels Per Unit 16), Mathf.Floor(world.x) + 0.5f coloca o fantasma da construção no centro da célula. Se a cena usa um componente Grid com Tilemap, use grid.WorldToCell(world) para obter a célula como Vector3Int e grid.GetCellCenterWorld(cell) para voltar ao centro dela. Assim a conta continua certa se alguém mudar o Cell Size do Grid.

Para o cursor, você tem duas opções. Cursor.SetCursor(textura, hotspot, CursorMode.Auto) troca o cursor de hardware, que não tem atraso e respeita a sensibilidade do sistema; a textura precisa estar importada como Cursor no Texture Type, e o hotspot é o pixel que conta como "ponta" (o centro, numa mira). A outra opção é esconder o cursor com Cursor.visible = false e desenhar um sprite na posição do mundo, como o cursorMarker do exemplo 2D. O sprite permite animação e brilho, mas segue o mouse com o atraso de um frame de renderização, e em jogo de mira rápida isso se sente.

Para jogo em primeira pessoa, Cursor.lockState = CursorLockMode.Locked prende o cursor no centro da janela. A partir daí position fica parado e quem interessa é Mouse.current.delta, que continua informando o movimento.

Erros comuns

O objeto segue o mouse com atraso de um frame. Você move no Update e a câmera se move no LateUpdate (Cinemachine, por exemplo). Converta e posicione no LateUpdate, depois da câmera.

Posição certa no editor e errada no build. A Game view do editor tem uma resolução e o build outra; se você usou números fixos de pixel em algum lugar, eles quebram. Trabalhe sempre com o valor convertido para o mundo ou relativo a Screen.width e Screen.height.

Cursor sobre um Canvas no modo Screen Space Camera. A posição do mouse está em pixels de tela e o Canvas tem a própria escala. Para posicionar algo dentro do Canvas, use RectTransformUtility.ScreenPointToLocalPointInRectangle em vez de ScreenToWorldPoint.

Input.mousePosition lançando exceção. O projeto está com o Input System ativo e esse código é do sistema antigo. Troque por Mouse.current.position.ReadValue().

Mira tremendo quando o personagem anda. A câmera segue o personagem e o ponto no mundo muda mesmo com o mouse parado. Isso é correto; se incomoda, suavize a rotação da arma, não a leitura do mouse.

Próximo passo

Com a posição no mundo em mãos, o uso mais comum é girar a arma para o cursor. O post de mirar no mouse em 2D usa a mesma conversão daqui (ScreenToWorldPoint com o Z certo) e calcula o ângulo com Mathf.Atan2. Se a parte de vetor e distância ainda está nebulosa, a matemática de vetores para jogos explica direção, magnitude e normalização com exemplos de jogo. E se você também usa Godot, o Input Map da Godot mostra como a outra engine trata ações e ponteiro.

Perguntas frequentes

Como pegar a posição do mouse no New Input System?

Use Mouse.current.position.ReadValue(). Ele devolve um Vector2 em pixels, com a origem no canto inferior esquerdo da tela. Confira antes se Mouse.current não é null, porque em celular ou console pode não existir mouse.

Por que o ScreenToWorldPoint sempre devolve a posição da câmera?

Porque você passou um ponto com Z igual a zero. O Z do ScreenToWorldPoint é a distância à frente da câmera. Em câmera perspectiva, distância zero é a própria posição da câmera. Passe a distância até o plano do jogo, normalmente o valor positivo do Z da câmera, ou use um raio.

Como pegar a posição do mouse no mundo 2D na Unity?

Leia Mouse.current.position, monte um Vector3 com x e y da tela e z igual à distância entre a câmera e o plano do jogo, chame Camera.ScreenToWorldPoint e zere o z do resultado. Em câmera ortográfica isso devolve o ponto exato do cursor no plano do jogo.

Como saber se o mouse está em cima de um botão da UI?

Chame EventSystem.current.IsPointerOverGameObject() no Update, com o InputSystemUIInputModule no EventSystem. Se devolver true, o cursor está sobre um elemento de UI e você ignora o clique no mundo. Evite chamar dentro de callback de action, porque ali ele lê o estado do frame anterior.