Tela de Loading na Unity com LoadSceneAsync e Barra
Uma unity loading screen de verdade usa SceneManager.LoadSceneAsync, que carrega a próxima cena em segundo plano e devolve um AsyncOperation com o progresso. Você lê operacao.progress todo frame, joga na barra e, quando chega ao fim, a cena nova assume. O detalhe que confunde todo mundo: se você segura a ativação com allowSceneActivation = false, o progresso para em 0.9 e nunca chega a 1. A barra trava em 90% e parece bug.
Aqui você monta uma tela de loading com barra fiel ao progresso, entende o 0.9, faz o "pressione para continuar", decide entre cena intermediária e overlay, garante um tempo mínimo para a tela não piscar e vê a mesma lógica em corrotina e em Awaitable, no Unity 6.3 LTS.
Como funciona uma tela de loading na Unity
SceneManager.LoadScene (síncrono) congela o jogo até a cena nova estar pronta. Numa cena pequena, ninguém percebe. Numa fase com muita textura e áudio, a tela para por alguns segundos e o jogador acha que travou. Para o básico de trocar de cena, o post de trocar de cena na Unity cobre o LoadScene e a lista de build.
O LoadSceneAsync faz o mesmo trabalho em etapas, ao longo de vários frames, e o jogo continua rodando enquanto isso. Esse "continua rodando" é o que permite uma barra animada, uma dica na tela ou um personagem correndo.
O fluxo de uma tela de loading tem quatro passos:
- Mostrar a tela de loading (uma cena própria ou um painel por cima).
- Chamar
LoadSceneAsynce guardar oAsyncOperation. - Atualizar a barra a cada frame com o progresso.
- Liberar a ativação quando tudo estiver pronto e esconder a tela.
LoadSceneAsync e AsyncOperation
O AsyncOperation que o LoadSceneAsync devolve tem quatro membros que importam:
progress: de 0 a 1. Com a ativação liberada, chega a 1. Com a ativação segurada, para em 0.9.isDone:truequando a operação inteira terminou, ativação incluída.allowSceneActivation: sefalse, a cena carrega mas não entra no jogo até você mudar paratrue.completed: evento disparado no fim.
Se a cena pedida não estiver na Scene List do File > Build Profiles, o LoadSceneAsync registra um erro no Console e devolve null. Por isso o script abaixo checa o retorno antes de usar.
Barra de progresso: por que trava em 90%
O carregamento tem duas fases. Os primeiros 90% são carregar os dados da cena do disco para a memória. Os últimos 10% são ativar a cena: criar os objetos, chamar Awake e OnEnable, descarregar a cena anterior. Com allowSceneActivation = false, a Unity para em 0.9 porque a segunda fase depende da sua permissão. Nas palavras da documentação: o progresso para em 0.9 e isDone fica falso.
A correção é tratar 0.9 como 100% na barra:
float progressoDaBarra = Mathf.Clamp01(operacao.progress / 0.9f);
Assim a barra enche por completo quando os dados terminaram de carregar, e você decide a hora de ativar.
A cena de loading com corrotina
A estrutura mais comum usa uma cena intermediária: o jogo vai da fase atual para a cena Loading, e ela carrega a fase seguinte. Para a cena Loading saber qual cena carregar, uma classe estática guarda o destino.
Arquivo CarregadorDeCena.cs:
using UnityEngine.SceneManagement;
public static class CarregadorDeCena
{
public static string CenaDestino { get; private set; }
public static void Carregar(string nomeDaCena)
{
CenaDestino = nomeDaCena;
SceneManager.LoadScene("Loading");
}
}
Em qualquer lugar do jogo, troque SceneManager.LoadScene("Fase02") por CarregadorDeCena.Carregar("Fase02"). A cena Loading é pequena (um Canvas, uma barra, um texto), então carrega instantaneamente com o LoadScene normal.
Na cena Loading, monte um Canvas com uma Image de fundo, uma barra (uma Image com Image Type em Filled, Fill Method Horizontal) e um TextMeshPro para a porcentagem. Adicione este script num objeto da cena:
Arquivo TelaDeLoading.cs:
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class TelaDeLoading : MonoBehaviour
{
[SerializeField] private Image barra;
[SerializeField] private TMP_Text textoPorcentagem;
[SerializeField] private float tempoMinimoNaTela = 1f;
[SerializeField] private float velocidadeDaBarra = 1.5f;
private void Start()
{
StartCoroutine(CarregarCena(CarregadorDeCena.CenaDestino));
}
private IEnumerator CarregarCena(string nomeDaCena)
{
AsyncOperation operacao = SceneManager.LoadSceneAsync(nomeDaCena);
if (operacao == null)
{
yield break;
}
operacao.allowSceneActivation = false;
float inicio = Time.unscaledTime;
float progressoExibido = 0f;
while (true)
{
float progressoReal = Mathf.Clamp01(operacao.progress / 0.9f);
progressoExibido = Mathf.MoveTowards(progressoExibido, progressoReal, velocidadeDaBarra * Time.unscaledDeltaTime);
barra.fillAmount = progressoExibido;
textoPorcentagem.SetText("{0}%", Mathf.RoundToInt(progressoExibido * 100f));
bool dadosCarregados = operacao.progress >= 0.9f;
bool tempoMinimoPassou = Time.unscaledTime - inicio >= tempoMinimoNaTela;
bool barraCheia = progressoExibido >= 1f;
if (dadosCarregados && tempoMinimoPassou && barraCheia)
{
break;
}
yield return null;
}
operacao.allowSceneActivation = true;
}
}
Por que cada escolha:
progressoExibidoseparado do real. O progresso real pula (0, 0.3, 0.9 em poucos frames). A barra exibida corre atrás dele a uma velocidade constante comMoveTowards, então anda de forma contínua.- Três condições para ativar. Dados carregados, tempo mínimo e barra cheia. Sem a terceira, a cena entra com a barra em 70%, e o jogador sente que algo foi cortado.
Time.unscaledTimeeunscaledDeltaTime. Se alguém deixouTime.timeScaleem zero (saindo de um menu de pausa, por exemplo), a barra continua andando.allowSceneActivationdentro de corrotina. A própria documentação recomenda usar em corrotina e avisa para não tentar noAwake, que roda uma vez só.
Isso funciona bem num projeto de teste com duas cenas. O peso real aparece no jogo inteiro: a música que precisa continuar entre a fase e o loading, o jogador que precisa chegar na fase nova com a vida certa, o save que precisa gravar antes de trocar. Encaixar a troca de cena com tudo isso, sem ninguém para revisar, é o ponto em que muito projeto empaca.
allowSceneActivation e "pressione para continuar"
Jogos com dicas na tela de loading costumam esperar o jogador terminar de ler. Com o allowSceneActivation isso é trivial: troque a condição de saída do loop.
bool apertouContinuar = UnityEngine.InputSystem.Keyboard.current != null
&& UnityEngine.InputSystem.Keyboard.current.anyKey.wasPressedThisFrame;
if (dadosCarregados && barraCheia && apertouContinuar)
{
break;
}
Mostre o texto "Pressione qualquer tecla" só quando dadosCarregados e barraCheia forem verdadeiros. Para aceitar controle também, cheque Gamepad.current com o botão sul.
Um cuidado da documentação: enquanto a ativação está segurada, a fila de operações assíncronas fica parada. Se outro script chamar SceneManager.UnloadSceneAsync nesse meio-tempo, ele só começa depois que você liberar a ativação. Se algo no seu loading "não termina nunca", procure uma segunda operação esperando a primeira.
Cena de loading intermediária vs overlay
Cena intermediária (a do exemplo): a cena antiga sai inteira antes de a nova começar a carregar, então a memória não precisa segurar as duas fases ao mesmo tempo. É a escolha certa para jogos com fases grandes ou para mobile com pouca memória.
Overlay: um Canvas de loading que vive entre cenas (com DontDestroyOnLoad) e cobre a tela enquanto a fase atual chama LoadSceneAsync da próxima. A transição fica mais suave, com fade e sem cena extra, mas durante o carregamento as duas fases estão na memória. Funciona bem para cenas médias.
Para jogos pequenos, o overlay com fade é mais simples de manter. Quando as fases crescem, a cena intermediária evita picos de memória. Se performance já preocupa no seu projeto, o post sobre otimização de performance para 60 FPS mostra como medir antes de decidir.
Corrotina ou Awaitable: qual usar no Unity 6
O Unity 6 tem a classe Awaitable, que permite escrever o mesmo loop com async e await. A versão equivalente:
Arquivo TelaDeLoadingAsync.cs:
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class TelaDeLoadingAsync : MonoBehaviour
{
[SerializeField] private Image barra;
[SerializeField] private TMP_Text textoPorcentagem;
private async void Start()
{
AsyncOperation operacao = SceneManager.LoadSceneAsync(CarregadorDeCena.CenaDestino);
if (operacao == null)
{
return;
}
operacao.allowSceneActivation = false;
while (operacao.progress < 0.9f)
{
float progresso = Mathf.Clamp01(operacao.progress / 0.9f);
barra.fillAmount = progresso;
textoPorcentagem.SetText("{0}%", Mathf.RoundToInt(progresso * 100f));
await Awaitable.NextFrameAsync();
}
barra.fillAmount = 1f;
textoPorcentagem.SetText("100%");
float fimDaEspera = Time.unscaledTime + 0.3f;
while (Time.unscaledTime < fimDaEspera)
{
await Awaitable.NextFrameAsync();
}
operacao.allowSceneActivation = true;
}
}
Na prática, as duas fazem o mesmo. Corrotina é o que a maioria dos tutoriais e da documentação usa, e para automaticamente se o objeto for destruído. Awaitable lê mais linear e se encaixa melhor quando o resto do seu código já é async. A espera final usa Time.unscaledTime num loop de NextFrameAsync, pelo mesmo motivo da corrotina: não depender do Time.timeScale. Um cuidado com async void no Start: se der exceção, ela vai para o Console mas não para nada. Escolha uma das duas e use no projeto todo.
Erros comuns: loading que pisca e jogo congelando na troca
Loading que pisca. A cena é pequena, carrega em um frame e a tela aparece e some. O tempoMinimoNaTela resolve. Um fade de 0.2 s na entrada e na saída deixa ainda mais suave.
Barra parada em 90%. Você está usando operacao.progress direto com allowSceneActivation = false. Divida por 0.9.
Barra pula de 0 para 100. No editor, a cena já está em cache e carrega quase instantâneo. Teste a velocidade real numa build.
Jogo congela no fim do loading mesmo com async. A ativação roda no thread principal. Se a cena nova tem scripts pesados no Awake ou Start (gerar mapa, instanciar centenas de objetos), o engasgo acontece ali. Espalhe esse trabalho por vários frames ou faça antes, durante o loading.
Loading que nunca termina. Outra operação assíncrona está na fila esperando a sua ativação, ou a condição de saída nunca fica verdadeira (por exemplo, progress >= 1f com a ativação segurada).
Próximo passo: loading com transição
Pratique acrescentando um fade preto de entrada e saída com um CanvasGroup e dicas aleatórias numa lista de strings. Depois, meça: faça uma build, coloque um Debug.Log com Time.realtimeSinceStartup no começo e no fim do carregamento e veja quanto tempo a sua fase maior leva de verdade. Esse número diz se você precisa mesmo de barra ou se um fade curto já basta. Se você também trabalha no Godot, compare com a tela de carregamento no Godot, que resolve o mesmo problema com ResourceLoader.load_threaded_request.
Perguntas frequentes
Por que a barra de loading da Unity trava em 90%?
Com allowSceneActivation em false, a Unity para o AsyncOperation.progress em 0.9 e isDone fica false até você liberar a ativação. Os 10% finais correspondem à ativação da cena. Divida o progress por 0.9 para a barra chegar a 100%.
LoadSceneAsync deixa o jogo sem travar nenhum frame?
Ele carrega os dados em segundo plano, mas a ativação da cena (Awake e Start dos objetos, por exemplo) acontece no thread principal. Uma cena com muito trabalho no Awake ainda pode causar um engasgo na troca.
Posso usar async e await em vez de corrotina para o loading no Unity 6?
Sim. O Unity 6 tem a classe Awaitable, e dá para esperar o próximo frame com await Awaitable.NextFrameAsync() dentro de um loop que lê o progress. A corrotina continua funcionando e é a forma que a documentação recomenda para usar allowSceneActivation.
Como fazer o loading não piscar quando a cena é pequena?
Defina um tempo mínimo de tela, por exemplo 1 segundo, e só libere allowSceneActivation quando o carregamento terminou e esse tempo passou. Um fade de entrada e saída também esconde a troca.



