Voltar para o Blog
Quest Log

Sistema de Interação na Unity: Portas, Itens e Tecla E

Visão em primeira pessoa de uma porta de madeira com um aviso flutuante de tecla E no centro da tela, uma chave sobre uma mesa ao lado

Unity interaction system reutilizável: interface IInteractable, trigger no 2D ou raycast no 3D, prompt da tecla na tela e porta, item e NPC no mesmo código.

Ferramenta:UnityC#

Sistema de Interação na Unity: Portas, Itens e Tecla E

Um unity interaction system reutilizável tem três peças: uma interface IInteractable que todo objeto interativo implementa, um detector no jogador que descobre qual objeto está ao alcance (trigger no 2D, raycast no 3D) e um prompt na tela mostrando a tecla. Com isso, porta, item no chão e NPC usam exatamente o mesmo código do lado do jogador, e criar um objeto interativo novo vira escrever um script pequeno que implementa a interface. Este tutorial monta tudo em C# para o Unity 6.3 LTS com o Input System.

O ganho real não é a porta funcionar. É que o centésimo objeto interativo do seu jogo não exige tocar no script do jogador.

Unity interaction system: a ideia

O jeito ingênuo de fazer interação é o jogador conhecer cada tipo de objeto:

if (hit.collider.CompareTag("Door")) { /* abre porta */ }
else if (hit.collider.CompareTag("Item")) { /* pega item */ }
else if (hit.collider.CompareTag("NPC")) { /* conversa */ }

Funciona com três tipos. Com quinze (alavanca, baú, placa, cama, máquina de venda), esse if vira o arquivo mais perigoso do projeto, porque qualquer objeto novo mexe nele.

A inversão: o jogador não sabe o que o objeto faz. Ele só sabe que o objeto "é interagível" e pede para ele "fazer a sua coisa". Cada objeto decide o próprio comportamento. Em C#, esse contrato é uma interface. É um caso direto do princípio de programar para abstrações, que aparece em vários dos padrões de projeto para jogos.

Interface IInteractable

Crie IInteractable.cs:

using UnityEngine;

public interface IInteractable
{
    string Prompt { get; }

    bool CanInteract(GameObject interactor);

    void Interact(GameObject interactor);
}

Três membros, cada um com um motivo:

  • Prompt: o texto da ação ("Abrir porta", "Pegar chave", "Falar"). O objeto sabe o que ele faz, então ele fornece o texto.
  • CanInteract: permite recusar. Uma porta trancada sem chave, um NPC que já falou tudo, um baú vazio. O detector usa isso para não mostrar o prompt quando a interação não é possível.
  • Interact: a ação em si. Recebe quem interagiu, para que o item saiba em qual inventário entrar e o NPC saiba para onde olhar.

Uma interface não é um componente; quem vai no GameObject é a classe que a implementa (um MonoBehaviour). O TryGetComponent<IInteractable> encontra essa classe pelo contrato, sem saber o nome dela.

Detectar o objeto: trigger ou raycast

No 3D (primeira ou terceira pessoa), raycast da câmera. O jogador interage com o que está olhando. Um raio curto do centro da tela resolve.

No 2D (plataforma ou top-down), trigger em volta do jogador. Não existe "olhar" para um objeto; o que importa é estar perto. Um collider trigger no jogador lista o que está dentro e escolhe o mais próximo.

O detector 3D, Interactor3D.cs, vai no jogador, com referência à câmera:

using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;

public class Interactor3D : MonoBehaviour
{
    [SerializeField] private Camera playerCamera;
    [SerializeField] private float interactDistance = 2.5f;
    [SerializeField] private LayerMask interactableMask;
    [SerializeField] private TMP_Text promptText;

    private InputAction interactAction;
    private IInteractable current;

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

        interactAction = InputSystem.actions.FindAction("Interact");
    }

    private void Update()
    {
        current = FindTarget();
        UpdatePrompt();

        if (current != null && interactAction.WasPressedThisFrame())
        {
            current.Interact(gameObject);
        }
    }

    private IInteractable FindTarget()
    {
        Ray ray = playerCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0f));

        if (!Physics.Raycast(ray, out RaycastHit hit, interactDistance, interactableMask, QueryTriggerInteraction.Collide))
        {
            return null;
        }

        if (hit.collider.TryGetComponent(out IInteractable interactable) && interactable.CanInteract(gameObject))
        {
            return interactable;
        }

        return null;
    }

    private void UpdatePrompt()
    {
        if (promptText == null)
        {
            return;
        }

        bool show = current != null;
        promptText.gameObject.SetActive(show);

        if (show)
        {
            string key = interactAction.GetBindingDisplayString(InputBinding.MaskByGroup("Keyboard&Mouse"));
            promptText.text = $"[{key}] {current.Prompt}";
        }
    }
}

ViewportPointToRay(0.5, 0.5) lança o raio do centro exato da tela, onde fica a mira. interactDistance de 2,5 metros é o alcance de um braço com folga. A interactableMask limita o raio a uma layer Interactable, para que ele não pare na mão do personagem ou num efeito de partícula.

O detector 2D, Interactor2D.cs, precisa de um CircleCollider2D com Is Trigger marcado no jogador (raio de 1 unidade é um bom começo):

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class Interactor2D : MonoBehaviour
{
    private readonly List<Collider2D> nearby = new List<Collider2D>();
    private InputAction interactAction;

    public IInteractable Current { get; private set; }

    private void Awake()
    {
        interactAction = InputSystem.actions.FindAction("Interact");
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.TryGetComponent(out IInteractable _))
        {
            nearby.Add(other);
        }
    }

    private void OnTriggerExit2D(Collider2D other)
    {
        nearby.Remove(other);
    }

    private void Update()
    {
        Current = FindClosest();

        if (Current != null && interactAction.WasPressedThisFrame())
        {
            Current.Interact(gameObject);
        }
    }

    private IInteractable FindClosest()
    {
        IInteractable best = null;
        float bestDistance = float.MaxValue;

        for (int i = nearby.Count - 1; i >= 0; i--)
        {
            Collider2D candidate = nearby[i];
            if (candidate == null)
            {
                nearby.RemoveAt(i);
                continue;
            }

            float distance = ((Vector2)candidate.transform.position - (Vector2)transform.position).sqrMagnitude;
            if (distance < bestDistance
                && candidate.TryGetComponent(out IInteractable interactable)
                && interactable.CanInteract(gameObject))
            {
                best = interactable;
                bestDistance = distance;
            }
        }

        return best;
    }
}

O laço percorre a lista de trás para frente para poder remover entradas destruídas (um item que foi pego some da cena sem disparar OnTriggerExit2D). Para o prompt no 2D, reaproveite o UpdatePrompt do detector 3D lendo Current.

Mostrar o prompt na tela

O prompt é um texto de UI que aparece quando há alvo:

  1. GameObject > UI > Text - TextMeshPro. No Unity 6, o TextMeshPro faz parte do pacote de UI; na primeira vez a Unity pede para importar os recursos essenciais (Import TMP Essentials).
  2. Posicione um pouco abaixo do centro da tela, com fonte legível e um contorno ou sombra para funcionar sobre qualquer fundo.
  3. Arraste no campo Prompt Text do detector.

O detalhe que faz diferença: o texto da tecla não é fixo. GetBindingDisplayString devolve o nome da tecla configurada na ação, filtrado pelo grupo de controle Keyboard&Mouse. Se o jogador remapear o botão, ou se você trocar de E para F, o prompt acompanha. Para mostrar o botão do controle quando o jogador estiver usando um, troque o grupo para Gamepad.

Interação parece pequena, mas é ela que liga o personagem ao resto do jogo: inventário, diálogo, quests, save, puzzles. Num projeto real esses sistemas precisam conversar sem que cada um conheça todos os outros, e é aí que a maioria dos projetos vira um emaranhado. Aprender a desenhar essas conexões dentro de um jogo inteiro, e não em exemplos isolados, é o que o curso de Unity trabalha em projeto.

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

Porta, item e NPC com o mesmo sistema

Cada objeto é só um script que implementa IInteractable. Lembre de pôr os três na layer Interactable e de dar um collider a cada um.

Porta, Door.cs, num objeto Hinge (dobradiça) que é pai da malha da porta e fica no canto onde ela gira:

using UnityEngine;

public class Door : MonoBehaviour, IInteractable
{
    [SerializeField] private float openAngle = 90f;
    [SerializeField] private float rotateSpeed = 240f;

    private bool isOpen;
    private Quaternion closedRotation;
    private Quaternion openRotation;

    public string Prompt => isOpen ? "Fechar porta" : "Abrir porta";

    private void Awake()
    {
        closedRotation = transform.localRotation;
        openRotation = closedRotation * Quaternion.Euler(0f, openAngle, 0f);
    }

    public bool CanInteract(GameObject interactor)
    {
        return true;
    }

    public void Interact(GameObject interactor)
    {
        isOpen = !isOpen;
    }

    private void Update()
    {
        Quaternion target = isOpen ? openRotation : closedRotation;
        transform.localRotation = Quaternion.RotateTowards(transform.localRotation, target, rotateSpeed * Time.deltaTime);
    }
}

Como o collider está na malha filha e o script na dobradiça, o TryGetComponent direto no collider não acharia a porta. Duas saídas: coloque o collider no mesmo objeto do script, ou troque no detector por hit.collider.GetComponentInParent<IInteractable>(), que sobe na hierarquia até achar.

Item, ItemPickup.cs:

using UnityEngine;

public class ItemPickup : MonoBehaviour, IInteractable
{
    [SerializeField] private string itemId = "chave_porao";
    [SerializeField] private string displayName = "chave";

    public string Prompt => $"Pegar {displayName}";

    public bool CanInteract(GameObject interactor)
    {
        return true;
    }

    public void Interact(GameObject interactor)
    {
        Debug.Log($"{interactor.name} pegou {itemId}");
        Destroy(gameObject);
    }
}

O Debug.Log é o ponto onde entra o seu inventário: interactor.GetComponent<Inventory>().Add(itemId). A estrutura de dados do inventário (slots, pilhas, itens como ScriptableObject) está no guia de sistema de inventário para RPG.

NPC, NpcTalk.cs:

using UnityEngine;

public class NpcTalk : MonoBehaviour, IInteractable
{
    [SerializeField] private string npcName = "Ferreiro";
    [SerializeField, TextArea] private string[] lines;

    private int nextLine;

    public string Prompt => $"Falar com {npcName}";

    public bool CanInteract(GameObject interactor)
    {
        return lines != null && lines.Length > 0;
    }

    public void Interact(GameObject interactor)
    {
        Debug.Log($"{npcName}: {lines[nextLine]}");
        nextLine = (nextLine + 1) % lines.Length;
    }
}

Para caixa de diálogo de verdade, com texto letra por letra e escolhas, o post de sistema de diálogo na Unity parte exatamente desse ponto: o Interact do NPC passa a abrir o diálogo.

Repare que nenhum dos três exigiu mudar Interactor3D ou Interactor2D.

Interação com Input System

Projetos novos do Unity 6 trazem o asset InputSystem_Actions com uma ação Interact no mapa Player, ligada ao E no teclado. Ela vem configurada com uma interação Hold, ou seja, só é considerada "performed" depois de o botão ficar apertado por um tempo.

Isso explica um susto comum: quem escreve interactAction.WasPerformedThisFrame() acha que a tecla está com atraso. A documentação de InputAction.WasPressedThisFrame diz que ele devolve verdadeiro no frame em que o botão passou do limiar de aperto, mesmo que uma interação na ação ainda não tenha concluído. Por isso o código usa WasPressedThisFrame: resposta imediata, com o asset padrão intocado.

O Hold vira recurso quando você quer: para "segure E para abrir o cofre", use WasPerformedThisFrame só nesse objeto. Se não quer Hold em lugar nenhum, abra o asset, selecione a ação Interact e remova a interação no painel Interactions.

Destacar o objeto em foco

O prompt diz o que vai acontecer, mas o jogador também precisa ver qual objeto está em foco, principalmente quando há dois itens lado a lado. O caminho mais simples é avisar o objeto quando ele ganha e perde o foco:

  1. Crie uma segunda interface, IFocusable, com void OnFocus() e void OnBlur(). Separar em outra interface mantém o destaque opcional: objetos que não precisam dele continuam implementando só IInteractable.
  2. No detector, guarde o alvo do frame anterior. Se o alvo mudou, teste if (antigo is IFocusable a) a.OnBlur(); e faça o mesmo com OnFocus() no novo.
  3. Em cada objeto, OnFocus liga um destaque: no 2D, trocar o material do SpriteRenderer por um com contorno ou clarear a cor; no 3D, ativar um objeto filho com um contorno ou aumentar levemente a emissão do material.

Esse retorno visual reduz muito o "apertei E e nada aconteceu", porque o jogador sabe antes de apertar se está mirando no lugar certo. Em jogos com controle, onde a mira é menos precisa, ele é quase obrigatório.

Erros comuns

O prompt não aparece. O objeto não está na layer da interactableMask, não tem collider, ou o collider está num filho e o script no pai (use GetComponentInParent).

TryGetComponent com interface não acha nada. O script que implementa a interface está em outro objeto da hierarquia (pai ou filho do collider atingido). Note o contrário também: TryGetComponent encontra componentes desativados, então desmarcar o script não desliga a interação; use o CanInteract para isso.

A tecla responde com atraso. WasPerformedThisFrame com a interação Hold do asset padrão.

Interage através da parede. O raio está com uma máscara que ignora paredes. Inclua a layer das paredes no raycast e confira se o primeiro objeto atingido é interagível.

No 2D, o item pego continua aparecendo no prompt. O objeto destruído ficou na lista. O laço do FindClosest remove entradas nulas.

Dois objetos dão prompt ao mesmo tempo. O detector 2D escolhe o mais próximo; o 3D, o que está na mira. Se o seu jogo precisa de prioridade (NPC antes de item), adicione um int Priority à interface.

Próximo passo

Crie quatro objetos novos sem tocar no detector: uma alavanca que abre uma porta à distância, uma placa com texto, um baú que só abre com a chave do ItemPickup (use o CanInteract para conferir o inventário) e uma cama que salva o jogo. Se os quatro saírem sem mexer no jogador, o sistema está bem desenhado.

Perguntas frequentes

Como fazer o personagem interagir com objetos apertando E na Unity?

Crie uma interface IInteractable com um método Interact, implemente-a em cada objeto interativo e, no jogador, detecte o objeto mais próximo (trigger no 2D, raycast no 3D). Quando a ação Interact do Input System for apertada, chame Interact no objeto detectado.

TryGetComponent funciona com interface na Unity?

Sim. TryGetComponent<IInteractable>(out IInteractable alvo) encontra qualquer componente no objeto que implemente a interface. É o que permite o mesmo código do jogador funcionar com porta, item e NPC sem saber qual é qual.

Por que a ação Interact padrão demora para responder?

No asset InputSystem_Actions dos projetos novos do Unity 6, a ação Interact vem com uma interação Hold, então ela só é considerada performed depois de segurar o botão. Use WasPressedThisFrame, que reage ao aperto e ignora a interação, ou remova o Hold na janela de actions.

Como fazer o script de abrir porta na Unity?

Faça a porta implementar IInteractable e, no Interact, girar uma dobradiça (um objeto pai no canto da porta) até o ângulo aberto com uma coroutine ou Quaternion.RotateTowards. Um bool guarda se ela está aberta, e um segundo Interact fecha.