Tela de Carregamento na Godot 4 com Barra de Progresso Real

Aprenda a criar uma tela de carregamento godot com barra de progresso usando ResourceLoader e carregamento em segundo plano na Godot 4, passo a passo.
Uma tela de carregamento godot bem feita esconde o tempo que o jogo leva para preparar a próxima fase e ainda dá ao jogador a sensação de que algo está acontecendo. Neste tutorial você vai montar, do zero na Godot 4, uma tela de loading com barra de progresso de verdade, usando ResourceLoader.load_threaded_request() para carregar a cena em segundo plano sem travar a interface. No fim, mostro também uma abordagem mais simples de carregamento fake para jogos pequenos, quando a solução completa é exagero.
Todos os exemplos usam GDScript tipado e APIs reais da Godot 4. Cole e rode.
Por que carregar em segundo plano
Quando você usa o clássico get_tree().change_scene_to_file("res://fase_2.tscn"), a Godot carrega a cena inteira de uma vez, na thread principal. Se a fase é pequena, ninguém percebe. Se a fase tem muitos nós, texturas grandes, áudio e sub-cenas instanciadas, a thread principal fica ocupada durante todo o carregamento. O resultado é aquele congelamento: a imagem trava, o jogo parece ter fechado, e só depois a cena nova aparece.
O carregamento em segundo plano resolve isso jogando o trabalho pesado para uma thread separada. Enquanto essa thread lê o disco e monta os recursos, a thread principal continua livre para desenhar a interface, animar um spinner e, principalmente, atualizar a barra de progresso. O jogador vê movimento e entende que o jogo está trabalhando, não travado.
A Godot 4 expõe isso através de quatro funções do ResourceLoader:
load_threaded_request(path): inicia o carregamento em segundo plano.load_threaded_get_status(path, progress): consulta o estado e preenche o progresso.load_threaded_get(path): devolve o recurso pronto quando terminou.- As constantes de estado como
ResourceLoader.THREAD_LOAD_LOADED.
Entendendo o ResourceLoader threaded
Vamos ver as peças isoladas antes de montar a cena. O fluxo básico é sempre o mesmo: pedir o carregamento, consultar o status todo frame e reagir ao resultado.
Iniciando o pedido
var caminho_cena: String = "res://cenas/fase_2.tscn"
var erro: Error = ResourceLoader.load_threaded_request(caminho_cena)
if erro != OK:
push_error("Falha ao iniciar o carregamento de %s" % caminho_cena)
O load_threaded_request() retorna um Error. Se der OK, a Godot começou a carregar numa thread de fundo. A partir daí, você não bloqueia o jogo. Só precisa perguntar de tempos em tempos se já ficou pronto.
Consultando o status e lendo o progresso
Aqui está o detalhe que mais confunde quem começa. O load_threaded_get_status() recebe um Array como segundo argumento e escreve o progresso dentro dele. Você precisa criar esse array, passar ele, e ler a posição [0], que vem como um float entre 0.0 e 1.0.
var progresso: Array = []
var estado: ResourceLoader.ThreadLoadStatus = ResourceLoader.load_threaded_get_status(caminho_cena, progresso)
match estado:
ResourceLoader.THREAD_LOAD_IN_PROGRESS:
var porcentagem: float = progresso[0] * 100.0
print("Carregando: %.0f%%" % porcentagem)
ResourceLoader.THREAD_LOAD_LOADED:
print("Pronto!")
ResourceLoader.THREAD_LOAD_FAILED:
push_error("Carregamento falhou")
ResourceLoader.THREAD_LOAD_INVALID_RESOURCE:
push_error("Caminho invalido")
Os quatro estados possíveis são:
THREAD_LOAD_INVALID_RESOURCE: o caminho não corresponde a um recurso válido.THREAD_LOAD_IN_PROGRESS: ainda carregando, useprogresso[0]para a barra.THREAD_LOAD_FAILED: algo deu errado durante o carregamento.THREAD_LOAD_LOADED: terminou, pode pegar o recurso.
Pegando o recurso pronto
Quando o estado vira THREAD_LOAD_LOADED, você recupera a cena carregada e troca para ela:
var cena_pronta: PackedScene = ResourceLoader.load_threaded_get(caminho_cena)
get_tree().change_scene_to_packed(cena_pronta)
Note que usamos change_scene_to_packed(), e não change_scene_to_file(). A diferença é fundamental: como já carregamos a PackedScene na thread de fundo, passamos o pacote pronto. Se usássemos change_scene_to_file(), a Godot carregaria tudo de novo do zero, jogando fora todo o trabalho da thread e recriando o congelamento que queríamos evitar.
Montando a cena de loading com ProgressBar
Agora juntamos tudo numa cena reutilizável. A ideia é ter uma cena loading.tscn que recebe qual cena carregar, mostra a barra enquanto trabalha e troca sozinha no fim.
Estrutura de nós
Crie uma cena nova com esta hierarquia:
Loading (Control)
├── ColorRect (fundo escuro, cobrindo a tela toda)
├── CenterContainer
│ └── VBoxContainer
│ ├── Label (texto "Carregando...")
│ └── ProgressBar (min_value = 0, max_value = 100)
O nó raiz é um Control. O ColorRect cobre o fundo para o jogador não ver a cena antiga por baixo. A ProgressBar vai de 0 a 100, então convertemos o float de progresso (0.0 a 1.0) multiplicando por 100.
O script da tela de carregamento
Anexe este script ao nó raiz Loading:
extends Control
@onready var barra: ProgressBar = $CenterContainer/VBoxContainer/ProgressBar
@onready var rotulo: Label = $CenterContainer/VBoxContainer/Label
var caminho_alvo: String = ""
var carregando: bool = false
# Guarda o caminho antes de trocar para esta cena.
static var proxima_cena: String = ""
func _ready() -> void:
if proxima_cena.is_empty():
push_error("Nenhuma cena definida para carregar")
return
iniciar_carregamento(proxima_cena)
func iniciar_carregamento(caminho: String) -> void:
caminho_alvo = caminho
var erro: Error = ResourceLoader.load_threaded_request(caminho_alvo)
if erro != OK:
push_error("Nao foi possivel iniciar o carregamento: %s" % caminho_alvo)
return
carregando = true
barra.value = 0.0
func _process(_delta: float) -> void:
if not carregando:
return
var progresso: Array = []
var estado: ResourceLoader.ThreadLoadStatus = ResourceLoader.load_threaded_get_status(caminho_alvo, progresso)
match estado:
ResourceLoader.THREAD_LOAD_IN_PROGRESS:
var pct: float = progresso[0] * 100.0
barra.value = pct
rotulo.text = "Carregando... %.0f%%" % pct
ResourceLoader.THREAD_LOAD_LOADED:
carregando = false
barra.value = 100.0
finalizar()
ResourceLoader.THREAD_LOAD_FAILED, ResourceLoader.THREAD_LOAD_INVALID_RESOURCE:
carregando = false
push_error("Falha ao carregar %s" % caminho_alvo)
func finalizar() -> void:
var cena: PackedScene = ResourceLoader.load_threaded_get(caminho_alvo)
get_tree().change_scene_to_packed(cena)
O truque da variável static var proxima_cena é passar informação para a cena de loading antes dela existir. Como change_scene_to_packed e change_scene_to_file não aceitam argumentos, usamos uma variável estática compartilhada pela classe. Qualquer script define proxima_cena e depois manda trocar para o loading.
Disparando a partir de um botão ou trigger
De qualquer lugar do jogo, você chama a troca assim:
func ir_para_proxima_fase() -> void:
Loading.proxima_cena = "res://cenas/fase_2.tscn"
get_tree().change_scene_to_file("res://cenas/loading.tscn")
A cena de loading aparece na hora (ela é minúscula e carrega instantâneo), lê o proxima_cena, e começa a carregar a fase pesada em segundo plano. A barra sobe suave, e no fim a fase pronta entra no lugar.
Suavizando a barra de progresso
Um detalhe de polimento: o progresso[0] costuma vir em saltos, não de forma contínua. Ele pode pular de 0.3 para 0.7 de uma vez, dependendo de quantos sub-recursos a cena tem. A barra fica com movimento engasgado.
A solução é interpolar o valor da barra em direção ao alvo, em vez de setar direto. Assim ela sempre desliza:
var progresso_alvo: float = 0.0
func _process(delta: float) -> void:
if not carregando:
return
var progresso: Array = []
var estado: ResourceLoader.ThreadLoadStatus = ResourceLoader.load_threaded_get_status(caminho_alvo, progresso)
if estado == ResourceLoader.THREAD_LOAD_IN_PROGRESS:
progresso_alvo = progresso[0] * 100.0
elif estado == ResourceLoader.THREAD_LOAD_LOADED:
progresso_alvo = 100.0
if barra.value >= 99.5:
carregando = false
finalizar()
# Interpola suave em direcao ao alvo.
barra.value = lerp(barra.value, progresso_alvo, delta * 8.0)
rotulo.text = "Carregando... %.0f%%" % barra.value
Usamos lerp() com um fator ligado ao delta para a barra acompanhar o alvo com suavidade. Repare que só chamamos finalizar() depois que a barra visualmente chegou perto de 100, para o jogador ver a barra encher por completo antes da troca. É um detalhe pequeno que faz a experiência parecer muito mais caprichada.
Carregamento fake ou mínimo para jogos pequenos
Nem todo projeto precisa de threads e ResourceLoader. Se o seu jogo é uma jam de fim de semana, um projeto de estudo ou um protótipo cujas cenas carregam em uma fração de segundo, montar toda essa infraestrutura é exagero. Pior: uma cena que carrega em 50 milissegundos vai fazer a barra ir de 0 a 100 num piscar de olhos, o que fica estranho.
Nesses casos, um carregamento mínimo com tempo fixo entrega a sensação de transição sem complexidade. A ideia é simples: mostrar a tela de loading, esperar um tempo curto (ou animar uma barra fake), e trocar de cena com o change_scene_to_file normal.
extends Control
@onready var barra: ProgressBar = $CenterContainer/VBoxContainer/ProgressBar
static var proxima_cena: String = ""
var duracao_fake: float = 1.2
var tempo: float = 0.0
func _process(delta: float) -> void:
tempo += delta
var t: float = clamp(tempo / duracao_fake, 0.0, 1.0)
barra.value = t * 100.0
if t >= 1.0:
set_process(false)
get_tree().change_scene_to_file(proxima_cena)
Aqui a barra é puramente cosmética: ela enche em duracao_fake segundos, independentemente do carregamento real, que acontece instantâneo no change_scene_to_file. Para o jogador, a experiência é idêntica à de uma barra real, e você economizou dezenas de linhas de gerenciamento de thread.
Uma variação ainda mais enxuta é só um fade preto entre cenas, sem barra nenhuma, usando um AnimationPlayer ou um Tween no ColorRect. Para menus e transições rápidas, muitas vezes é tudo que o jogo precisa.
A regra prática é honesta: use o carregamento em segundo plano quando o tempo de carga é real e perceptível (acima de meio segundo, digamos). Use o carregamento fake quando você só quer a estética de transição. Medir antes de otimizar vale aqui como em qualquer decisão técnica, e se você ainda está decidindo se o motor combina com seu projeto, vale ler nossa análise sobre se vale a pena aprender Godot antes de investir tempo em sistemas avançados.
Cuidados e boas práticas
Alguns pontos que evitam dor de cabeça na hora de colocar isso em produção:
- Não chame
load_threaded_getantes do estadoTHREAD_LOAD_LOADED. Se você pegar o recurso antes de terminar, ele pode vir nulo ou incompleto. - Um pedido por caminho. Não dispare
load_threaded_requestduas vezes para o mesmo caminho sem consumir o primeiro. Controle isso com o seu booleanocarregando. - Persistência de dados entre cenas. Se você precisa manter o estado do jogador entre fases, a tela de carregamento é um ótimo momento para salvar. Combine com um sistema robusto de save e load na Godot para não perder progresso na transição.
- Threads e a linguagem. O comportamento de threads do
ResourceLoaderé o mesmo independentemente de você escrever em GDScript ou C#. Se você tem dúvida sobre qual usar no seu projeto, dá uma olhada no comparativo entre C# e GDScript na Godot. - Sub-recursos. Texturas e áudios referenciados pela cena entram na conta do progresso automaticamente, então o número já reflete o trabalho total. Você não precisa somar nada manualmente.
Conclusão
Montar uma tela de carregamento godot decente na versão 4 é mais simples do que parece quando você entende o fluxo do ResourceLoader threaded: pedir com load_threaded_request, consultar todo frame com load_threaded_get_status lendo o array de progresso, e trocar com change_scene_to_packed assim que o estado vira THREAD_LOAD_LOADED. A interface fica responsiva, a barra sobe de verdade, e o jogador nunca vê o jogo congelado.
Para projetos pequenos, não tenha vergonha de usar a versão fake com tempo fixo. Ela entrega a mesma sensação com uma fração do código. O importante é que a transição pareça intencional e polida, seja o carregamento real ou apenas estético. Escolha a ferramenta certa para o tamanho do seu jogo e siga desenvolvendo aqui no CursoGame.Dev.
Perguntas frequentes
Por que a barra de progresso da minha tela de carregamento fica travada em 0?
Provavelmente você chamou load_threaded_request mas não está lendo o array de progresso no load_threaded_get_status a cada frame. O progresso só é preenchido quando você passa um Array por referência como segundo argumento do get_status e lê progress[0].
Qual a diferença entre load_threaded_request e o ResourceLoader.load normal?
O load normal é síncrono e trava a thread principal até terminar, congelando a tela. O load_threaded_request carrega em segundo plano numa thread separada, deixando a interface responsiva para animar a barra de progresso.
Preciso de tela de carregamento em um jogo pequeno?
Nem sempre. Se as cenas carregam em menos de um segundo, uma tela de loading real pode ser desnecessária. Nesses casos um fade ou um loading mínimo de tempo fixo dá a sensação de transição sem complexidade de threads.
Como troco de cena depois que o carregamento termina na Godot 4?
Quando load_threaded_get_status retorna THREAD_LOAD_LOADED, você pega o recurso com load_threaded_get e chama get_tree().change_scene_to_packed(pacote) passando a PackedScene carregada.
A tela de carregamento funciona no HTML5 (web export) da Godot 4?
O ResourceLoader com threads funciona em desktop de forma confiável. Na web, o suporte a threads depende de o servidor enviar os cabeçalhos COOP/COEP corretos; sem eles, o carregamento pode cair para modo síncrono.


