Voltar para o Blog
Quest Log

Tela de Loading na Unity com LoadSceneAsync e Barra

Tela escura de carregamento com uma barra de progresso brilhante quase cheia e um pequeno personagem correndo sobre ela

Unity loading screen real com LoadSceneAsync: barra de progresso, o porquê de travar em 90%, allowSceneActivation, cena intermediária e tempo mínimo na tela.

Ferramenta:UnityC#

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:

  1. Mostrar a tela de loading (uma cena própria ou um painel por cima).
  2. Chamar LoadSceneAsync e guardar o AsyncOperation.
  3. Atualizar a barra a cada frame com o progresso.
  4. 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: true quando a operação inteira terminou, ativação incluída.
  • allowSceneActivation: se false, a cena carrega mas não entra no jogo até você mudar para true.
  • 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:

  • progressoExibido separado 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 com MoveTowards, 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.unscaledTime e unscaledDeltaTime. Se alguém deixou Time.timeScale em zero (saindo de um menu de pausa, por exemplo), a barra continua andando.
  • allowSceneActivation dentro de corrotina. A própria documentação recomenda usar em corrotina e avisa para não tentar no Awake, 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.

Curso de Unity
Quer fazer seu primeiro jogo na Unity?

No curso de Unity 6 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 Unity
Suporte no WhatsAppAcesso vitalícioGarantia de 7 dias

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.