UI Toolkit Unity para UI de jogo
Estrutura em UXML, aparência em USS, comportamento em pequenos controladores C# que reagem a eventos do jogo. Destilado de um projeto real (HUD de terror-tiro); a lista de solução de problemas é a parte que mais provavelmente economizará tempo.
1. Antes de escrever código
Pergunte apenas o que está faltando, em um lote curto:
- API de UI.
[RequireComponent(typeof(PanelRenderer))]+RegisterUIReloadCallback= workflow PanelRenderer (semUIDocument, semrootVisualElement).UIDocument+rootVisualElement= clássico. Infira a partir do código colado; se não estiver claro, peça um controlador funcional e seu UXML. Nunca misture os dois (causaNullReferenceException). Um arquivo funcional do usuário é melhor que sua memória da API. - Gênero/tema (com capturas de tela ou paleta), plataforma e entrada, resolução/aspecto de referência.
- Os scripts do jogo que a UI escuta. Peça por eles, leia os eventos/propriedades públicas reais, vincule-se a eles. Se um evento estiver faltando, proponha a menor adição ao script do jogo.
- Responda no idioma do usuário; escreva comentários de código no idioma que os scripts existentes usam.
2. Guia de estilo primeiro
Consistência é o que os usuários notam primeiro. Antes de construir muitas partes: proponha um guia curto, construa UMA parte pequena, deixe o usuário reagir e transforme o feedback deles em regras ("sem caixa de fundo", "mais compacto"). Salve-o no projeto (por exemplo, Assets/UI/UI_STYLE_GUIDE.md) e diga ao usuário para colá-lo em futuras solicitações de UI. Atualize-o quando partes ou regras mudarem. Conteúdo: linguagem de design (tom, contêineres, densidade, movimento), tabela de paleta (texto, suave, aviso, crítico, sombra), tipografia (tamanhos por função, regra de sombra), layout (zonas, margens, Configurações de Painel), nomes de classes e estados no USS, regras de arquitetura de código, pegadinhas do UXML, lista de partes existentes, checklist para uma nova parte.
3. Arquitetura
- Uma parte = um controlador + um UXML + um USS, com uma fonte de dados. Sem mega-controlador.
- Partes que devem ficar juntas compartilham um contêiner flex UXML (dois controladores podem compartilhar um GameObject/PanelRenderer; cada um encontra seus próprios elementos por nome).
- Controladores apenas encontram elementos, definem texto/valores e alternam classes USS. Cores, tamanhos e animações ficam no USS.
- PanelRenderer: a raiz chega via
RegisterUIReloadCallback(OnUIReload)(registre em OnEnable, cancele em OnDisable). Ele é acionado quando a UI está pronta e a cada recarregamento, então TODAS as buscasQ<T>("nome"), cache, vinculação de eventos e redefinição de estado vão lá. Faça proteção nula emUpdate()até que os elementos existam. - Eventos do jogo podem disparar antes da UI existir: armazene em cache os valores mais recentes em campos, reaplique no final de
OnUIReload. - Objetos do jogador/jogo podem surgir tarde: referência serializada, senão
FindFirstObjectByTypelimitada (~0,5 s), então vincule; cancele inscrição em OnDisable. - Empurre, não pergunte. Inscreva-se em eventos e escreva apenas na mudança. Consulte apenas valores sem evento (energia contínua, um bool de recarga), alternando classes na mudança. Sem construção de string por quadro.
- Construa elementos repetidos (pips, segmentos) no código uma vez; reconstrua apenas quando sua fonte mudar; limpe listas em cache no recarregamento.
- Temporizadores/pulsos:
element.schedule.Execute(...).Every(ms)criado pausado, retomado apenas enquanto necessário. Flashes de um disparo:.StartingIn(ms). - Elementos de HUD:
picking-mode="Ignore"em todos os lugares, sem controles focalizáveis. Menus são a exceção. - Nome do arquivo = nome da classe MonoBehaviour.
4. Layout (fonte da maioria dos bugs visuais)
- Nunca coloque partes relacionadas com deslocamentos de pixel adivinhados separadamente: dois painéis absolutos sobrepostos porque
bottomfoi estimado às cegas. Coloque-os em um contêiner flex e posicione apenas esse contêiner. - Um contêiner absolutamente posicionado por zona da tela; filhos fluem com flex e margens.
- Esconda com
opacity+ transição quando o layout não deve pular;display:noneapenas quando o espaço deve colapsar. - Largura fixa em rótulos numéricos para que 9 para 10 para 100 não desloque vizinhos.
- Caixas são border-box: largura da linha de segmento =
contagem x (largura + margem) - margem. Alinhe widgets relacionados pela borda esquerda idêntica e largura calculada. - Configurações de Painel: Scale With Screen Size (ex: 1920x1080, match 0.5); para HUDs desligue Clear Color e Clear Depth Stencil. Use o Sort Order do PanelRenderer para camadas de partes (vinheta em tela cheia abaixo do texto).
5. USS e estado
- Estado = classe alternada do C# (
EnableInClassList); animação = transição USS. Nunca defina cores ou anime no código. - Nomeie estados por significado:
--low,--critical,--hidden,--hit,--pulse,--unavailable. Prefira BEM para novas partes. - Regras de sobreposição (segmentos gastos, flash) vêm DEPOIS das regras que elas substituem; especificidade igual, a última vence.
- Uma transição usa a duração/atraso do estado de destino:
.x--hidden { opacity:0; transition-duration:.5s; transition-delay:.9s; }desaparece tarde e aparece instantaneamente;.x--flash { transition-duration:.05s; }liga rápido, desaparece na velocidade base. - Texto flutuante sem fundo precisa de
text-shadow. Semtext-transform(maiúsculas no C#); sem gradientes radiais (veja vinheta). - Para UI re-skinable, coloque cores/tamanhos em variáveis USS em uma classe raiz (
--c-text, usado comovar(--c-text)); verifique se a versão do Unity do usuário suporta.
6. Esqueleto do controlador (PanelRenderer)
[RequireComponent(typeof(PanelRenderer))]
public class PartController : MonoBehaviour
{
[SerializeField] private SourceComponent source; // vazio = encontrar em tempo de execução
private PanelRenderer _pr; private VisualElement _block; private Label _value;
private bool _bound; private float _v = -1f; private int _shown = -1; private float _nextFind;
private void Awake() => _pr = GetComponent<PanelRenderer>();
private void OnEnable() { _pr.RegisterUIReloadCallback(OnUIReload); TryBind(); }
private void OnDisable() { _pr.UnregisterUIReloadCallback(OnUIReload); Unbind(); _block = null; _value = null; }
private void Update()
{
if (_bound || Time.unscaledTime < _nextFind) return;
_nextFind = Time.unscaledTime + 0.5f; TryBind();
}
private void TryBind()
{
if (_bound) return;
if (source == null) source = FindFirstObjectByType<SourceComponent>();
if (source == null) return;
source.OnChanged += OnChanged; _bound = true;
_v = source.Current; Apply(); // puxa o estado atual uma vez
}
private void Unbind() { if (_bound && source != null) source.OnChanged -= OnChanged; _bound = false; }
private void OnUIReload(PanelRenderer r, VisualElement root)
{
_block = root.Q<VisualElement>("part"); _value = root.Q<Label>("part-value");
if (_block == null || _value == null) { Debug.LogError("Part: elements not found in UXML (check Source Asset).", this); _block = null; return; }
_shown = -1; Apply(); // nova árvore: força reescrita do cache
}
private void OnChanged(float v) { _v = v; Apply(); }
private void Apply()
{
if (_block == null || _v < 0f) return;
int n = Mathf.CeilToInt(_v);
if (n != _shown) { _shown = n; _value.text = n.ToString(); } // escreve apenas na mudança
_block.EnableInClassList("part--low", _v <= 0.3f);
}
}
7. Snippets reutilizáveis
- Segmentos/pips: em
OnUIReload,row.Clear(); list.Clear();então adicione NVisualElement{pickingMode=Ignore}com classeseg; na mudançalist[i].EnableInClassList("seg--spent", i >= filled). Mostre pelo menos 1 preenchido enquanto valor > 0:filled = v<=0 ? 0 : Clamp(CeilToInt(v*N-0.001f),1,N). - Pulso:
_pulse = _block.schedule.Execute(() => _block.ToggleInClassList("part--pulse")).Every(400); _pulse.Pause();Retome ao entrar no estado, Pause + remova classe ao sair; rastreie um bool_pulsinge redefina no recarregamento. - Flash no evento:
var b=_block; b.AddToClassList("part--hit"); b.schedule.Execute(()=>b.RemoveFromClassList("part--hit")).StartingIn(150); - Vinheta: gere uma vez em Awake uma
Texture2D128x128 (RGBA32, Clamp, Bilinear,HideFlags.HideAndDontSave) onde alpha =SmoothStep(0,1,InverseLerp(inner,outer,dist/1.4142))a partir do centro, RGB branco; defina_el.style.backgroundImage = new StyleBackground(tex)em OnUIReload; destrua em OnDestroy. USS: elemento absoluto em tela cheia,opacity:0,-unity-background-scale-mode: stretch-to-fill,-unity-background-image-tint-colorpara a cor, classes--low/--critical/--pulse/--flashdefinindo opacidade (flash por último, duração rápida). Sort Order menor que partes de texto.
8. Gêneros e plataformas
A arquitetura é idêntica entre gêneros; o guia de estilo e o modelo de entrada mudam. Arcade: números grandes e encorpados, contagem crescente, pop de escala, brilho de combo, transições rápidas de 0,08-0,25 s, contêineres bem-vindos. Retrô/pixel: fonte pixel, escala inteira, texturas com filtro Point, bordas de sprite 9-slice. Mobile: alvos de toque de cerca de 44-48 dp, área segura do dispositivo como padding da raiz (verifique a API atual para a versão do Unity do usuário), sem estados hover, fonte mínima maior, teste em dispositivo real. Console/gamepad: elementos focalizáveis com um estilo :focus forte, foco inicial explícito, margens TV-safe (3-5 por cento), texto maior, glifos que trocam com o dispositivo ativo. VR/espaço-mundial: depende da versão do Unity, pesquise primeiro. Esses pontos de plataforma são práticas gerais sólidas, mas não testadas em um projeto real: diga isso e peça ao usuário para verificar no dispositivo. Mantenha rótulos de entrada (nome da tecla ou glifo) em dados que o controlador lê, não codificados em UXML.
9. Entregando uma parte
Em ordem: UXML completo, USS completo, controlador completo, etapas de configuração (GameObject, PanelRenderer + Panel Settings compartilhado + Source Asset, Sort Order, referências a preencher, o que ver em Play), botões de ajuste (qual valor USS altera tamanho/posição/cor/limites) e uma entrada curta no guia de estilo. Ao alterar arquivos existentes, forneça arquivos inteiros se a maior parte do arquivo mudar (colagens parciais causaram erros reais).
10. Solução de problemas
| Sintoma | Causa / correção |
|---|---|
NullReference em rootVisualElement / GetComponent<UIDocument>() | O código usa UIDocument, mas o objeto tem PanelRenderer. Use RegisterUIReloadCallback, vincule em OnUIReload |
| Controlador registra "element not found" | Source Asset errado, incompatibilidade de nome com Q<>(), ou UXML inválido |
| UI Builder "Failed to open asset" | UXML inválido (duplicata </ui:UXML>, <Style> ruim, GUID placeholder, caracteres ocultos). Corrija, então clique com o botão direito no arquivo > Reimport. Leia a mensagem do console acima do erro genérico |
| Elementos sem estilo | <Style> ausente ou caminho errado. No UI Builder: StyleSheets > + > Add Existing USS (escreve project://database/...guid=... completo) |
| Tudo desapareceu após uma edição | UXML quebrou, ou a referência Source Asset ficou obsoleta após renomear. Corrija, Reimport, reatribua. Teste com background-color: rgba(255,0,0,.3) na raiz |
| Parte mal posicionada ou sobrepondo outra | GameObject obsoleto ainda desenhando UXML antigo, ou deslocamentos absolutos separados. Exclua objeto antigo, mescle vizinhos em um contêiner |
| Texto placeholder na visualização Game | O modo Edição desenha o UXML; pressione Play |
| Script não pode ser adicionado | Nome do arquivo != nome da classe. Renomeie dentro da janela Project do Unity para que o .meta siga |
Correções não válidas: a tag de stylesheet é <Style src="..."/> SEM o prefixo ui: (<ui:Style> é silenciosamente ignorado); nunca invente ou deixe um GUID placeholder; use o cabeçalho padrão <ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False">.
11. Trabalhando sem ver o editor
Peça uma captura de tela em reclamações de layout e leia atentamente; resolveu bugs que a adivinhação não conseguia. Admita erros brevemente e corrija com arquivos completos. Para erros de compilação, peça o texto exato e a versão do Unity em vez de adivinhar novamente. Não prometa comportamento do dispositivo ou números de desempenho; construa a estrutura e peça ao usuário para testar.