O que é UI em Godot e por que usar Control e CanvasLayer
Em jogos 2D, a interface (HUD) precisa ficar “presa” à tela: barra de vida, pontuação, mensagens de pausa e game over. Em Godot, a UI é construída principalmente com nós Control, que usam um sistema de layout próprio (anchors, offsets e containers) e são ideais para elementos que devem se adaptar a diferentes resoluções.
Para garantir que a HUD não seja afetada pela câmera (zoom, movimento, limites), é comum colocar a UI dentro de um CanvasLayer. Esse nó desenha seus filhos em uma camada separada do mundo, mantendo a interface estável na tela.
- Control: base para UI (Label, TextureRect, ProgressBar, Panel, etc.).
- CanvasLayer: mantém a UI independente do mundo/câmera.
- Containers: organizam automaticamente o layout (HBoxContainer, VBoxContainer, MarginContainer…).
Conceitos essenciais: anchors, offsets, containers e escalonamento
Anchors e offsets (posicionamento responsivo)
Um Control tem âncoras (anchors) que definem a referência em relação ao retângulo do pai (normalmente a tela). Os offsets definem margens/posições a partir dessas âncoras.
- Anchor Top-Left: ideal para HUD no canto superior esquerdo (vida, score).
- Anchor Top-Right: ideal para minimapa, ícones, etc.
- Anchor Full Rect: útil para painéis que ocupam a tela toda (pause, game over).
Dica prática: no editor, use o menu Layout do Control (ex.: “Top Left”, “Full Rect”) para configurar anchors rapidamente e depois ajuste offsets.
Containers (layout automático)
Containers evitam “posicionamento manual” e facilitam o suporte a múltiplas resoluções. Exemplos comuns:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
HBoxContainer: organiza elementos em linha (vida + score lado a lado).VBoxContainer: organiza em coluna (mensagens empilhadas).MarginContainer: adiciona margens internas sem precisar ajustar offsets manualmente.
Quando você coloca Controls dentro de um Container, o tamanho/posição passa a ser controlado pelo Container. Em geral, você ajusta Size Flags (Fill/Expand) e separação (separation) no Container.
Escalonamento para múltiplas resoluções (Project Settings)
Para UI consistente em diferentes telas, configure o modo de escala do projeto:
- Vá em
Project Settings > Display > Window. - Defina uma resolução base em
Size(ex.: 1280x720 ou 1920x1080). - Em
Stretch, use normalmente:Mode = canvas_itemseAspect = keep(ouexpand, dependendo do seu design).
O objetivo é: o mundo pode “expandir” ou manter proporção, mas a UI deve continuar legível e bem ancorada.
Arquitetura recomendada: UI desacoplada do Player
Um erro comum é a UI acessar diretamente nós internos do Player (ex.: $Player/Health). Isso cria dependências frágeis: se você trocar o Player, renomear nós ou instanciar múltiplos jogadores, a UI quebra.
Uma abordagem mais robusta é usar:
- Sinais para eventos (vida mudou, pontuação mudou, game over).
- Variáveis observadas (via
setgetou propriedades com setter) em um nó “fonte de dados” (ex.:GameState). - Referência única da UI para esse nó de estado, não para nós internos do Player.
Opção prática: GameState como Autoload (Singleton)
Crie um script GameState.gd e registre como Autoload em Project Settings > Autoload. Assim, qualquer cena pode emitir/ler estado sem procurar nós na árvore.
extends Node
signal health_changed(current: int, max: int)
signal score_changed(score: int)
signal pause_changed(paused: bool)
signal game_over
var max_health: int = 100
var _health: int = 100
var _score: int = 0
var _paused: bool = false
var health: int:
get:
return _health
set(value):
_health = clamp(value, 0, max_health)
health_changed.emit(_health, max_health)
if _health <= 0:
game_over.emit()
var score: int:
get:
return _score
set(value):
_score = max(0, value)
score_changed.emit(_score)
var paused: bool:
get:
return _paused
set(value):
_paused = value
get_tree().paused = _paused
pause_changed.emit(_paused)Repare que a UI não precisa saber quem causou o dano ou quem ganhou pontos; ela apenas reage ao estado.
Passo a passo: construindo uma HUD (vida, score e mensagens)
1) Criar a cena de UI com CanvasLayer
Crie uma nova cena chamada HUD.tscn:
- Nó raiz:
CanvasLayer(nome:HUD). - Filho:
Control(nome:Root).
No Root, aplique Layout > Full Rect para ocupar a tela toda. Isso facilita ancoragem e containers.
2) Barra superior com vida e pontuação usando Containers
Estrutura sugerida:
Root (Control)MarginContainer(nome:TopBarMargin)
No TopBarMargin:
- Layout: Top Wide (ou anchors no topo com largura total).
- Configure margens (Theme Overrides > Constants > Margin Left/Top/Right) ou use um filho container para padding.
Dentro de TopBarMargin, adicione:
HBoxContainer(nome:TopBar)
Dentro de TopBar, adicione:
ProgressBar(nome:HealthBar)Label(nome:ScoreLabel)
Ajustes recomendados:
- No
HealthBar: definaMin Value = 0,Max Value = 100(será atualizado via código), e em Size Flags marqueHorizontal = Expand Fillpara ocupar espaço. - No
ScoreLabel: texto inicial “Score: 0”. Em Size Flags, deixe sem expandir para manter tamanho natural. - No
TopBar: ajusteSeparationpara espaçamento entre elementos.
3) Mensagens de Pause e Game Over (overlay central)
Crie um overlay que ocupa a tela e centraliza mensagens:
- Dentro de
Root, adicione umControl(nome:Overlay) e aplique Layout > Full Rect. - Dentro de
Overlay, adicione umCenterContainer(nome:Center) e aplique Full Rect. - Dentro de
Center, adicione umVBoxContainer(nome:Messages). - Dentro de
Messages, adicione doisLabel:PauseLabeleGameOverLabel.
Configuração inicial:
PauseLabel.text = "PAUSED"evisible = falseGameOverLabel.text = "GAME OVER"evisible = false
Se quiser um fundo escurecido, coloque um ColorRect como primeiro filho de Overlay com Full Rect e cor com alpha (ex.: preto com 0.4). Mantenha visible = false e ative junto com as mensagens.
Conectando HUD ao jogo via sinais (sem dependência do Player)
Script da HUD: reagindo ao GameState
Anexe um script ao nó HUD (CanvasLayer) ou ao Root. Exemplo no HUD:
extends CanvasLayer
@onready var health_bar: ProgressBar = $Root/TopBarMargin/TopBar/HealthBar
@onready var score_label: Label = $Root/TopBarMargin/TopBar/ScoreLabel
@onready var overlay: Control = $Root/Overlay
@onready var pause_label: Label = $Root/Overlay/Center/Messages/PauseLabel
@onready var game_over_label: Label = $Root/Overlay/Center/Messages/GameOverLabel
func _ready() -> void:
# Estado inicial
health_bar.max_value = GameState.max_health
health_bar.value = GameState.health
score_label.text = "Score: %d" % GameState.score
_set_pause_ui(GameState.paused)
_set_game_over_ui(false)
# Conexões por sinais
GameState.health_changed.connect(_on_health_changed)
GameState.score_changed.connect(_on_score_changed)
GameState.pause_changed.connect(_on_pause_changed)
GameState.game_over.connect(_on_game_over)
func _on_health_changed(current: int, max: int) -> void:
health_bar.max_value = max
health_bar.value = current
func _on_score_changed(score: int) -> void:
score_label.text = "Score: %d" % score
func _on_pause_changed(paused: bool) -> void:
_set_pause_ui(paused)
func _on_game_over() -> void:
_set_game_over_ui(true)
func _set_pause_ui(paused: bool) -> void:
overlay.visible = paused or game_over_label.visible
pause_label.visible = paused
func _set_game_over_ui(show: bool) -> void:
overlay.visible = show or pause_label.visible
game_over_label.visible = showObserve que a HUD só conhece o GameState. Ela não chama métodos do Player nem acessa nós internos dele.
Como o jogo atualiza o GameState (exemplos de integração)
Em qualquer lugar do jogo (Player, inimigo, pickups, sistema de pontuação), em vez de “avisar a HUD”, você atualiza o estado:
- Ao tomar dano:
GameState.health -= damage - Ao curar:
GameState.health += heal - Ao ganhar pontos:
GameState.score += points
Como health e score têm setter, os sinais são emitidos automaticamente e a HUD atualiza em tempo real.
Prática guiada: barra de vida e rótulo de pontuação em tempo real
Objetivo
- Criar uma
ProgressBarque reflete a vida atual. - Criar um
Labelque mostra a pontuação. - Atualizar ambos em tempo real via sinais do
GameState.
Checklist de implementação
| Item | O que verificar |
|---|---|
| Autoload | GameState.gd registrado em Project Settings > Autoload com nome GameState |
| HUD na cena | HUD.tscn instanciada na cena principal (ou adicionada como filho fixo) |
| Anchors | Root e Overlay em Full Rect; top bar ancorada no topo |
| Containers | HBoxContainer para vida + score; CenterContainer para mensagens centrais |
| Sinais | HUD conectada a health_changed e score_changed |
Teste rápido (sem depender do Player)
Para validar a HUD, você pode criar um script temporário em qualquer nó da cena (ex.: um Node chamado DebugControls) para simular mudanças:
extends Node
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_left"):
GameState.health -= 10
if event.is_action_pressed("ui_right"):
GameState.health += 10
if event.is_action_pressed("ui_up"):
GameState.score += 100
if event.is_action_pressed("ui_down"):
GameState.paused = not GameState.pausedCom isso, você confirma que a barra e o texto reagem imediatamente, e que a mensagem de pause aparece sem a HUD conhecer qualquer detalhe do Player.
Boas práticas para evitar problemas comuns
- Não use caminhos fixos para o Player na HUD (ex.:
get_node("/root/Main/Player")). Prefira um nó de estado (Autoload) e sinais. - Evite posicionar UI “no olho” com valores absolutos. Use anchors e containers para suportar resoluções diferentes.
- CanvasLayer para HUD: se a câmera se mover/zoomer, a UI permanece estável.
- Atualização por evento: sinais são mais eficientes e limpos do que atualizar UI no
_processa cada frame.