Godot do Zero: Interface do usuário (UI) básica com Control e CanvasLayer

Capítulo 12

Tempo estimado de leitura: 8 minutos

+ Exercício

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:

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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_items e Aspect = keep (ou expand, 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 setget ou 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: defina Min Value = 0, Max Value = 100 (será atualizado via código), e em Size Flags marque Horizontal = Expand Fill para ocupar espaço.
  • No ScoreLabel: texto inicial “Score: 0”. Em Size Flags, deixe sem expandir para manter tamanho natural.
  • No TopBar: ajuste Separation para 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 um Control (nome: Overlay) e aplique Layout > Full Rect.
  • Dentro de Overlay, adicione um CenterContainer (nome: Center) e aplique Full Rect.
  • Dentro de Center, adicione um VBoxContainer (nome: Messages).
  • Dentro de Messages, adicione dois Label: PauseLabel e GameOverLabel.

Configuração inicial:

  • PauseLabel.text = "PAUSED" e visible = false
  • GameOverLabel.text = "GAME OVER" e visible = 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 = show

Observe 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 ProgressBar que reflete a vida atual.
  • Criar um Label que mostra a pontuação.
  • Atualizar ambos em tempo real via sinais do GameState.

Checklist de implementação

ItemO que verificar
AutoloadGameState.gd registrado em Project Settings > Autoload com nome GameState
HUD na cenaHUD.tscn instanciada na cena principal (ou adicionada como filho fixo)
AnchorsRoot e Overlay em Full Rect; top bar ancorada no topo
ContainersHBoxContainer para vida + score; CenterContainer para mensagens centrais
SinaisHUD 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.paused

Com 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 _process a cada frame.

Agora responda o exercício sobre o conteúdo:

Ao criar uma HUD em um jogo 2D na Godot, qual abordagem mantém a interface estável na tela e reduz acoplamento com o Player?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

O CanvasLayer desenha a HUD em uma camada separada, evitando influência da câmera. Usar um GameState (Autoload) com sinais permite que a UI reaja a eventos sem depender de caminhos/nós internos do Player.

Próximo capitúlo

Godot do Zero: Menus e navegação com estados de jogo

Arrow Right Icon
Capa do Ebook gratuito Godot do Zero: Criando seu Primeiro Jogo 2D com GDScript
71%

Godot do Zero: Criando seu Primeiro Jogo 2D com GDScript

Novo curso

17 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.