Voltar para o Blog
Quest Log

Como Montar um HUD (Vida e Municao) no Godot 4

Tela de um jogo 2D no Godot mostrando barra de vida e contador de municao no canto da tela

Aprenda a montar um HUD Godot com barra de vida, contador de municao e CanvasLayer fixo na tela, atualizando tudo por signals e mantendo o codigo desacoplado.

Um HUD Godot bem feito mostra a informacao certa na hora certa sem que o jogador precise parar para pensar. Vida e municao sao os dois indicadores mais comuns, e tambem os que mais costumam ficar mal montados: barras que somem quando a camera se move, contadores que so atualizam um frame depois, e codigo que busca o player a cada quadro. Neste post a gente monta um HUD que fica fixo na tela, atualiza por signals e nao depende de ficar procurando nos pela arvore.

Como Montar um HUD (Vida e Municao) no Godot 4

A ideia central e simples: a interface nao deve saber onde o player esta, e o player nao deve saber como a interface desenha as coisas. Eles se comunicam por mensagens. Quando a vida muda, o player avisa. Quando a municao muda, o player avisa. O HUD escuta e redesenha. Esse desacoplamento e o que permite trocar a arte da barra, mover o contador de canto ou ate testar a interface sozinha, sem precisar de um player na cena.

Por que CanvasLayer mantem o HUD fixo na tela

Se voce colocar a barra de vida direto como filha de um Control solto na cena 2D, ela vai se mover junto com a camera. Isso acontece porque ela esta no mesmo espaco de mundo dos inimigos e do cenario. O CanvasLayer resolve isso: ele desenha seus filhos em uma camada separada, que ignora a transformacao da camera.

A estrutura minima da cena do HUD fica assim:

  • HUD (CanvasLayer)
    • MarginContainer
      • VBoxContainer
        • BarraVida (TextureProgressBar)
        • LabelMunicao (Label)

O MarginContainer garante um respiro nas bordas da tela, e o VBoxContainer empilha os elementos. Use as ancoras dos containers para grudar o conjunto no canto que voce quiser. Como tudo esta dentro do CanvasLayer, a posicao na tela permanece estavel mesmo com a camera seguindo o player pelo mapa.

Vale lembrar que o CanvasLayer tem uma propriedade layer (um numero inteiro). Camadas com numero maior ficam por cima. Deixe o HUD em um valor alto, tipo 10, para garantir que nada do mundo passe na frente dele. Se voce ainda esta organizando como a interface se comporta em telas diferentes, vale combinar isso com o que cobrimos em UI responsiva para varias resolucoes no Godot.

A barra de vida com TextureProgressBar

O TextureProgressBar existe justamente para barras de status. Ele aceita uma textura de fundo (texture_under) e uma textura de preenchimento (texture_progress), e voce controla o quanto aparece pela propriedade value, dentro de um intervalo min_value e max_value.

No inspetor, configure o max_value igual a vida maxima do player (por exemplo, 100) e o value inicial igual ao maximo. O modo de preenchimento (fill_mode) define se a barra esvazia da esquerda para a direita, de cima para baixo ou de forma radial. Para uma barra de vida horizontal comum, deixe no modo da esquerda para a direita.

O script do HUD fica enxuto. Ele so expoe metodos que recebem valores e atualizam os nos:

extends CanvasLayer

@onready var barra_vida: TextureProgressBar = $MarginContainer/VBoxContainer/BarraVida
@onready var label_municao: Label = $MarginContainer/VBoxContainer/LabelMunicao

func configurar_vida(vida_maxima: int) -> void:
    barra_vida.max_value = vida_maxima
    barra_vida.value = vida_maxima

func atualizar_vida(vida_atual: int) -> void:
    barra_vida.value = vida_atual

func atualizar_municao(municao_atual: int, municao_maxima: int) -> void:
    label_municao.text = "%d / %d" % [municao_atual, municao_maxima]

Repare que o HUD nao tem nenhuma referencia ao player. Ele so sabe receber numeros e mostra-los. Quem decide quando chamar esses metodos e quem conhece o player, que vamos ver a seguir.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

O contador de municao com Label

O Label resolve o contador de forma direta. A unica decisao real e o formato do texto. Mostrar so a municao no pente (12), o pente e a reserva (12 / 36) ou municao e maximo do pente (12 / 30) muda a percepcao do jogador. No exemplo acima usei %d / %d, que e a formatacao do GDScript para inteiros.

Um detalhe de polimento: se voce quiser que o numero pisque em vermelho quando a municao chega a zero, faca isso no HUD, nao no player. O player so informa o valor. A decisao visual pertence a interface:

func atualizar_municao(municao_atual: int, municao_maxima: int) -> void:
    label_municao.text = "%d / %d" % [municao_atual, municao_maxima]
    if municao_atual == 0:
        label_municao.modulate = Color.RED
    else:
        label_municao.modulate = Color.WHITE

Manter essa logica dentro do HUD reforca a separacao: amanha voce muda a cor, troca por um icone ou adiciona um som de pente vazio sem tocar em uma linha sequer do player.

Atualizar via signals em vez de buscar o player

Aqui esta o ponto que diferencia um HUD que escala de um que vira dor de cabeca. A tentacao comum e fazer o HUD rodar algo assim no _process:

func _process(_delta: float) -> void:
    var player = get_tree().get_first_node_in_group("player")
    barra_vida.value = player.vida

Isso funciona, mas tem dois problemas. Primeiro, ele consulta a arvore a cada frame, mesmo quando nada mudou. Segundo, ele cria uma dependencia direta: o HUD precisa que o player exista, esteja no grupo certo e tenha uma variavel chamada vida. Qualquer mudanca quebra a interface.

A alternativa e o player emitir signals quando seu estado muda. O HUD se conecta a esses signals uma vez e reage so quando ha mudanca real. Se voce ainda nao usa signals no dia a dia, vale ler signals no Godot e GDScript antes de seguir.

No player, declare os signals e emita nos momentos certos:

extends CharacterBody2D

signal vida_alterada(vida_atual: int)
signal municao_alterada(municao_atual: int, municao_maxima: int)

@export var vida_maxima: int = 100
@export var municao_maxima: int = 30

var vida: int
var municao: int

func _ready() -> void:
    vida = vida_maxima
    municao = municao_maxima

func receber_dano(quantidade: int) -> void:
    vida = max(vida - quantidade, 0)
    vida_alterada.emit(vida)

func disparar() -> void:
    if municao <= 0:
        return
    municao -= 1
    municao_alterada.emit(municao, municao_maxima)

O player so se preocupa com a propria logica. Ele nao sabe que existe um HUD, nem que existe uma barra ou um label. Ele apenas anuncia: minha vida mudou, minha municao mudou. Isso e o oposto de buscar o player toda hora, e e o que mantem as duas partes independentes.

Conectando as duas pontas sem acoplar

Falta o ponto de encontro. Alguem precisa ligar os signals do player aos metodos do HUD. O lugar natural para isso e a cena que contem os dois, por exemplo a cena de nivel ou um no de gerenciamento. Esse no conhece os dois lados e faz a ponte, sem que player e HUD se conhecam diretamente:

extends Node2D

@onready var player: CharacterBody2D = $Player
@onready var hud: CanvasLayer = $HUD

func _ready() -> void:
    hud.configurar_vida(player.vida_maxima)
    hud.atualizar_vida(player.vida)
    hud.atualizar_municao(player.municao, player.municao_maxima)

    player.vida_alterada.connect(hud.atualizar_vida)
    player.municao_alterada.connect(hud.atualizar_municao)

As tres primeiras chamadas no _ready sincronizam o estado inicial, para o HUD ja nascer mostrando os valores corretos. As duas linhas seguintes conectam os signals. Dali em diante, sempre que o player emitir vida_alterada ou municao_alterada, o metodo correspondente do HUD roda automaticamente, recebendo os argumentos do signal.

Esse arranjo tem uma vantagem pratica de teste. Voce pode abrir so a cena do HUD, chamar atualizar_vida(50) no editor ou em um script de teste e ver a barra reagir, sem precisar de player, inimigos ou nivel carregado. A interface vira uma peca isolada e verificavel.

Cuidados que evitam retrabalho

Alguns detalhes pequenos poupam tempo depois. Conecte os signals no codigo, como acima, em vez de arrastar conexoes no editor; assim a ligacao fica visivel e versionada junto com a logica. Use @export para os valores maximos no player, para ajustar vida e municao pelo inspetor sem mexer no script.

Sobre suavizacao da barra: se quiser que ela desca de forma animada em vez de pular direto para o novo valor, faca a animacao com um Tween dentro do atualizar_vida do HUD. O valor que chega pelo signal continua sendo o alvo, e a interpolacao e responsabilidade da interface, nunca do player.

Por fim, pense no HUD como parte da experiencia, nao so como numeros na tela. Tamanho da fonte, contraste da barra e posicao dos elementos afetam o quanto o jogador absorve a informacao em meio segundo. Se quiser ir mais fundo nessa parte, da uma olhada em UI e UX em jogos e tambem em como construir um sistema de vida e dano no Godot que conversa bem com o HUD que voce acabou de montar.

Com CanvasLayer para fixar na tela, TextureProgressBar para a vida, Label para a municao e signals para a comunicacao, voce tem um HUD que fica estavel, atualiza so quando precisa e nao prende a interface ao player. Esse e o tipo de base que aguenta o jogo crescer sem virar uma teia de dependencias.