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:
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).- Posicione um pouco abaixo do centro da tela, com fonte legível e um contorno ou sombra para funcionar sobre qualquer fundo.
- Arraste no campo
Prompt Textdo 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.
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:
- Crie uma segunda interface,
IFocusable, comvoid OnFocus()evoid OnBlur(). Separar em outra interface mantém o destaque opcional: objetos que não precisam dele continuam implementando sóIInteractable. - 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 comOnFocus()no novo. - Em cada objeto,
OnFocusliga um destaque: no 2D, trocar o material doSpriteRendererpor 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.



