Voltar para o Blog
Quest Log

Transicao de Cena com Fade no Godot 4: Autoload, Tween e Zero Corte Seco

Tela de jogo escurecendo em fade preto durante a troca entre duas cenas no Godot

Aprenda transicao de cena godot com fade no Godot 4: autoload com CanvasLayer e ColorRect, tween de fade in e out, e como evitar travada em cena pesada.

Ferramenta:Godot

Transicao de Cena com Fade no Godot 4: Autoload, Tween e Zero Corte Seco

Troca de cena com corte seco é uma daquelas coisas que ninguém elogia quando está boa, mas todo mundo sente quando está ruim. O jogador encosta na porta, a tela pisca, e de repente ele está em outro lugar. Funciona, mas parece protótipo. Uma transicao de cena godot bem feita é um fade de meio segundo: a tela escurece, a cena troca por baixo do pano, a tela clareia. Pronto, o jogo parece outro.

A boa notícia é que isso custa um script de umas quarenta linhas. A receita do Godot 4 é um autoload com um CanvasLayer e um ColorRect preto por cima de tudo, dois tweens via create_tween() e o change_scene_to_file() no meio do sanduíche. Nesse artigo eu monto isso do zero, explico cada decisão e fecho com o problema que aparece depois: a travada quando a cena de destino é pesada, e o que dá pra fazer sobre ela. O mesmo autoload, aliás, serve pra suavizar a ida pra tela de game over, que é a troca de cena que o jogador mais vê num jogo de ação.

Por que a transicao de cena godot vive num autoload

O change_scene_to_file() destrói a cena atual inteira e instancia a nova. Qualquer node que esteja dentro da cena atual morre junto, incluindo um eventual ColorRect de fade que você tenha colocado nela. Se o fade vive na cena, ele desaparece no exato momento em que mais precisava estar na tela. Por isso o fade precisa morar fora da árvore de cenas trocáveis, e o lugar pra isso no Godot é um autoload.

Autoload é um node que o Godot adiciona como filho direto da raiz quando o jogo abre, e que sobrevive a toda troca de cena. Se o conceito ainda é nebuloso pra você, eu destrinchei o padrão em autoload e singleton no Godot, vale a leitura antes de seguir. Aqui o que importa é: o overlay preto fica no autoload, então ele continua na tela enquanto a cena embaixo dele é destruída e recriada.

O segundo ingrediente é o CanvasLayer. Um ColorRect comum desenha na camada da cena e disputa ordem com o resto da UI. Dentro de um CanvasLayer com layer alto, tipo 100, ele desenha por cima de absolutamente tudo: HUD, menus, partículas, tudo. É a garantia de que o fade cobre a tela inteira sem depender da estrutura de cada cena do jogo.

Montando o autoload de transição

Crie uma cena nova com esta estrutura:

SceneTransition (CanvasLayer)
└── ColorRect

No SceneTransition, defina layer = 100. No ColorRect, pinte de preto, use o preset de âncora Full Rect pra cobrir a tela toda, e um detalhe que derruba muita gente: mude mouse_filter pra Ignore. Sem isso, mesmo invisível, o retângulo engole todo clique do jogo, porque alpha zero não desliga input.

Salve como res://scene_transition.tscn e o script:

extends CanvasLayer

@onready var color_rect: ColorRect = $ColorRect

@export var fade_duration: float = 0.3

var is_transitioning := false

func _ready() -> void:
    # Começa transparente; o ColorRect só aparece durante a troca.
    color_rect.modulate.a = 0.0

func change_scene(path: String) -> void:
    if is_transitioning:
        return
    is_transitioning = true

    # Fade out: escurece a tela.
    var tween := create_tween()
    tween.tween_property(color_rect, "modulate:a", 1.0, fade_duration)
    await tween.finished

    # Tela preta: troca a cena escondido do jogador.
    get_tree().change_scene_to_file(path)

    # Fade in: revela a cena nova.
    tween = create_tween()
    tween.tween_property(color_rect, "modulate:a", 0.0, fade_duration)
    await tween.finished

    is_transitioning = false

Registre em Projeto, Configurações do Projeto, aba Autoload, com o nome SceneTransition, apontando pra cena (não pro script solto, porque o autoload precisa do ColorRect junto).

A partir daí, qualquer lugar do jogo troca de cena com uma linha:

func _on_porta_body_entered(body: Node2D) -> void:
    if body.is_in_group("player"):
        SceneTransition.change_scene("res://fases/fase_02.tscn")

Três decisões do script que merecem explicação. Primeiro, animei modulate:a em vez de color:a porque modulate funciona igual pra qualquer node, então se um dia você trocar o ColorRect por uma TextureRect com uma arte de transição, nada muda. Segundo, o is_transitioning é a trava contra clique duplo: sem ela, o jogador que encosta em duas portas no mesmo frame dispara duas transições sobrepostas e o resultado é fade brigando com fade. Terceiro, o tween via create_tween() é criado pelo autoload, que nunca sai da árvore, então o tween nunca é morto no meio da animação, que é exatamente o bug que aconteceria se o tween fosse criado por um node da cena que está sendo destruída.

Curso de Godot
Quer fazer seu primeiro jogo na Godot?

No curso de Godot da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Godot
Suporte no WhatsAppAcesso vitalícioGarantia de 7 dias

O fade in esconde um detalhe: a cena nova já está rodando

Repare na ordem: o change_scene_to_file() roda com a tela preta, e o fade in acontece por cima da cena nova já viva. Isso significa que o _ready() da cena nova, a física, os inimigos, tudo já está processando durante o fade in. Na maioria dos jogos isso é o comportamento desejado, o mundo "acorda" enquanto a tela clareia.

Mas se a sua cena tem uma cutscene de entrada, ou o player pode tomar dano no primeiro meio segundo, talvez você queira segurar o jogo até o fade terminar. O jeito limpo é a própria cena esperar um sinal do autoload. Adicione no topo do script de transição:

signal transition_finished

E emita depois do fade in, logo antes de liberar a trava:

    transition_finished.emit()
    is_transitioning = false

Na cena que precisa esperar:

func _ready() -> void:
    if SceneTransition.is_transitioning:
        await SceneTransition.transition_finished
    iniciar_cutscene()

Outro detalhe de robustez: change_scene_to_file() retorna um código de erro. Em projeto pequeno dá pra ignorar, mas custa pouco logar quando o caminho está errado:

    var err := get_tree().change_scene_to_file(path)
    if err != OK:
        push_error("Falha ao trocar para a cena: " + path)

Erro de digitação em caminho de cena é silencioso sem isso, e você fica olhando uma tela preta sem pista nenhuma.

Evitando a travada em cena pesada

Agora o problema que o fade não resolve sozinho: o change_scene_to_file() carrega a cena de destino de forma síncrona, no mesmo frame. Se a fase tem texturas grandes, muitos nodes ou shaders pra compilar, o jogo congela durante o carregamento. Com o fade, o congelamento fica escondido atrás da tela preta, o que já melhora bastante a percepção, mas a música engasga e a janela pode até ser marcada como sem resposta se passar de alguns segundos.

A solução do Godot 4 é o ResourceLoader.load_threaded_request(), que carrega a cena numa thread enquanto o jogo continua rodando. A versão do change_scene com carregamento em background:

func change_scene_async(path: String) -> void:
    if is_transitioning:
        return
    is_transitioning = true

    # Dispara o carregamento em thread antes mesmo do fade.
    ResourceLoader.load_threaded_request(path)

    var tween := create_tween()
    tween.tween_property(color_rect, "modulate:a", 1.0, fade_duration)
    await tween.finished

    # Espera o carregamento terminar sem congelar o jogo.
    while ResourceLoader.load_threaded_get_status(path) == ResourceLoader.THREAD_LOAD_IN_PROGRESS:
        await get_tree().process_frame

    var packed_scene: PackedScene = ResourceLoader.load_threaded_get(path)
    get_tree().change_scene_to_packed(packed_scene)

    tween = create_tween()
    tween.tween_property(color_rect, "modulate:a", 0.0, fade_duration)
    await tween.finished

    is_transitioning = false

Dois pontos importantes aqui. O pedido de carregamento é disparado antes do fade out, então a thread já trabalha durante os 0.3 segundo de escurecimento, que em fase média costuma ser tempo suficiente pra terminar. E como o resultado chega como PackedScene, a troca usa change_scene_to_packed() em vez de change_scene_to_file(), que aceita a cena já carregada e instancia na hora.

Se a fase é tão pesada que nem isso esconde a espera, o passo seguinte é uma tela de carregamento assíncrona com barra de progresso lendo load_threaded_get_status() com o array de progresso, mas pra maioria dos jogos 2D o fade com carregamento em thread já elimina o engasgo por completo.

Uma observação de escopo: tudo isso vale pra trocar a cena raiz inteira. Se o que você quer é colocar uma cena dentro de outra, tipo spawnar uma bala ou abrir um menu por cima do jogo, o caminho é outro, é instantiate() com add_child(), e eu cubro isso em como instanciar cenas no Godot. Um lugar onde esse autoload de fade rende muito é na tela de seleção de fases, onde cada botão leva pra uma cena diferente. O menu principal do jogo é outro caso clássico, quando o botão de iniciar leva o jogador pra primeira fase com o mesmo fade em vez de um corte seco.

Passando dados entre cenas na troca

Cedo ou tarde você vai precisar levar informação de uma cena pra outra durante a transição: a porta que leva pra fase 2 precisa dizer em qual ponto o player entra, ou o placar da fase precisa sobreviver até a tela de resultado. Como o change_scene_to_file() destrói a cena atual inteira, qualquer variável que estava nela morre junto. A tentação do iniciante é usar variável global solta, mas existe um lugar melhor: o próprio autoload de transição, que já sobrevive à troca.

Dá pra passar um dicionário de dados junto com o pedido de troca e guardá-lo no autoload até a cena nova pedir:

var dados_proxima_cena: Dictionary = {}

func change_scene_com_dados(path: String, dados: Dictionary = {}) -> void:
    dados_proxima_cena = dados
    change_scene(path)

E a cena de destino lê o que precisa logo no _ready():

func _ready() -> void:
    var dados: Dictionary = SceneTransition.dados_proxima_cena
    var ponto_entrada: String = dados.get("ponto_entrada", "padrao")
    var pontuacao: int = dados.get("pontuacao", 0)
    posicionar_player_no_ponto(ponto_entrada)

O Dictionary.get() com valor padrão é o detalhe que evita erro quando a cena é aberta direto no editor, sem ninguém ter preenchido os dados: ela simplesmente usa o padrão em vez de quebrar. Isso mantém cada cena testável sozinha, que é meio caminho pra não sofrer na depuração.

Atenção ao escopo: esse dicionário no autoload serve pra dados voláteis da sessão atual, como o ponto de entrada ou o placar da fase. Ele não substitui salvar progresso em disco. Para o que precisa persistir entre aberturas do jogo, como configurações e save de progresso, o caminho é gravar em arquivo, e o guia de salvar opções e progresso com ConfigFile mostra como. Dado de sessão vive no autoload; dado permanente vive no disco.

Variações que custam uma linha

Com a estrutura pronta, personalizar é barato. Duração diferente por contexto: como fade_duration é @export, dá pra ajustar no Inspector do autoload, ou aceitar como parâmetro opcional em change_scene(path, duration). Fade pra branco em vez de preto, pra flashback ou teleporte: muda a cor do ColorRect antes do tween. Curva de aceleração: encadeie set_trans(Tween.TRANS_SINE) no tween pra um fade que começa suave e termina suave, em vez do linear padrão.

E se um dia você quiser uma transição de círculo fechando estilo Zelda, ou um dissolve com ruído, a arquitetura não muda nada: o ColorRect ganha um ShaderMaterial e o tween passa a animar um parâmetro do shader em vez do alpha. O autoload, a trava, o sinal e o carregamento em thread continuam exatamente iguais. É por isso que vale montar essa base direito uma vez só.

Fechando

Transição de cena com fade no Godot 4 se resume a quatro peças: um autoload pra sobreviver à troca, um CanvasLayer com ColorRect pra cobrir a tela, dois create_tween() pra escurecer e clarear, e o change_scene_to_file() escondido no meio. Pra cena pesada, troque o carregamento síncrono pelo load_threaded_request e a travada some atrás da tela preta.

Meu conselho prático: implemente isso na primeira semana do projeto, não na última. Trocar todos os change_scene_to_file() espalhados pelo código por SceneTransition.change_scene() depois de seis meses é chato; começar com o autoload no lugar custa dez minutos e todo novo nível do jogo já nasce com transição decente.