Voltar para o Blog
Quest Log

Coroutines na Unity: WaitForSeconds, Timers e Sequências

Personagem de jogo piscando após levar dano com uma ampulheta e uma fila de eventos numerados ao fundo

Unity coroutine na prática: piscar após dano, contagem regressiva, sequência de spawn, como parar a coroutine certa e onde entra o Awaitable do Unity 6.

Ferramenta:UnityC#

Coroutines na Unity: WaitForSeconds, Timers e Sequências

Unity coroutine é um método que pausa e continua depois, sem travar o jogo. Você escreve uma sequência no tempo como se fosse uma lista de passos (pisca, espera 0,1 segundo, pisca de novo), declara o método como IEnumerator, usa yield return new WaitForSeconds(0.1f) para esperar e inicia com StartCoroutine. A Unity cuida de continuar a execução no momento certo.

Este tutorial cobre o uso prático no Unity 6.3 LTS: piscar o personagem depois do dano, uma contagem regressiva, uma sequência de spawn em ondas, como parar exatamente a coroutine que você quer, o que acontece quando o objeto é desativado, e onde o Awaitable da Unity 6 entra para quem prefere async e await.

Unity coroutine: o que é

Um método normal roda do começo ao fim dentro de um frame. Se você colocar um laço de espera nele, o jogo inteiro congela até o laço terminar. Uma coroutine resolve isso: ela roda até um yield return, devolve o controle para a engine e continua dali num frame futuro.

Por baixo, uma coroutine é um iterador do C#. A Unity guarda o iterador e, a cada frame, verifica se a condição de espera foi cumprida. Se foi, avança até o próximo yield. Você não precisa saber disso para usar, mas explica por que o tipo de retorno é IEnumerator e por que não dá para usar return valor nela.

Quando usar coroutine e quando usar um timer simples no Update? Timer no Update é ótimo para uma espera só (cooldown de tiro). Coroutine brilha quando há uma sequência: espera, faz algo, espera de novo, faz outra coisa. Escrever isso no Update exige variáveis de estado para lembrar em que passo você está, e a coroutine guarda esse estado sozinha.

StartCoroutine e yield return

A estrutura mínima. Arquivo HelloCoroutine.cs:

using System.Collections;
using UnityEngine;

public class HelloCoroutine : MonoBehaviour
{
    private void Start()
    {
        StartCoroutine(SayThings());
    }

    private IEnumerator SayThings()
    {
        Debug.Log("Começou em " + Time.time);
        yield return new WaitForSeconds(1f);
        Debug.Log("Um segundo depois: " + Time.time);
        yield return new WaitForSeconds(2f);
        Debug.Log("Mais dois segundos: " + Time.time);
    }
}

Pontos que pegam iniciante:

  • O using System.Collections; é obrigatório, porque é de lá que vem o IEnumerator sem tipo genérico.
  • Chamar SayThings() direto não faz nada. Tem que ser StartCoroutine(SayThings()).
  • A referência de StartCoroutine recomenda a versão que recebe o IEnumerator, como acima, em vez da versão que recebe o nome do método como string, por ter menos custo.

O StartCoroutine devolve um objeto Coroutine, que você pode guardar para parar depois ou para esperar dentro de outra coroutine com yield return.

WaitForSeconds, null e WaitUntil

As instruções de espera mais usadas:

  • yield return null: espera o próximo frame. Dentro de um laço, faz algo uma vez por frame.
  • yield return new WaitForSeconds(t): espera t segundos de tempo escalado. A documentação diz que o tempo real de espera é t dividido por Time.timeScale, e que a coroutine retoma no primeiro frame depois de passado o tempo, então ela pode atrasar um pouco em relação ao valor exato.
  • yield return new WaitForSecondsRealtime(t): mesma coisa ignorando o timeScale. Para menus e jogo pausado.
  • yield return new WaitUntil(() => condicao): espera até a condição ser verdadeira. WaitWhile é o inverso.
  • yield return new WaitForFixedUpdate(): retoma depois do passo de física.
  • yield return StartCoroutine(OutraCoroutine()): espera outra coroutine terminar.

Um exemplo com WaitUntil, uma porta que só abre quando todos os inimigos da sala morrem:

using System.Collections;
using UnityEngine;

public class RoomDoor : MonoBehaviour
{
    [SerializeField] private Transform enemiesParent;
    [SerializeField] private GameObject doorBlock;

    private IEnumerator Start()
    {
        yield return new WaitUntil(() => enemiesParent.childCount == 0);
        yield return new WaitForSeconds(0.5f);
        doorBlock.SetActive(false);
        Debug.Log("Sala limpa, porta aberta.");
    }
}

O Start pode ser ele mesmo uma coroutine (a documentação confirma), e aí nem precisa de StartCoroutine.

Exemplo: piscar ao tomar dano

O caso mais clássico. Depois de tomar dano, o personagem pisca e fica invencível por um tempo. Arquivo HurtBlink.cs:

using System.Collections;
using UnityEngine;

public class HurtBlink : MonoBehaviour
{
    [SerializeField] private SpriteRenderer spriteRenderer;
    [SerializeField] private float invincibleTime = 1f;
    [SerializeField] private float blinkInterval = 0.1f;

    private Coroutine blinkRoutine;

    public bool IsInvincible { get; private set; }

    public void OnHurt()
    {
        if (blinkRoutine != null)
        {
            StopCoroutine(blinkRoutine);
        }

        blinkRoutine = StartCoroutine(BlinkRoutine());
    }

    private IEnumerator BlinkRoutine()
    {
        IsInvincible = true;
        float elapsed = 0f;

        while (elapsed < invincibleTime)
        {
            spriteRenderer.enabled = !spriteRenderer.enabled;
            yield return new WaitForSeconds(blinkInterval);
            elapsed += blinkInterval;
        }

        spriteRenderer.enabled = true;
        IsInvincible = false;
        blinkRoutine = null;
    }

    private void OnDisable()
    {
        spriteRenderer.enabled = true;
        IsInvincible = false;
        blinkRoutine = null;
    }
}

O script de vida chama OnHurt() e só aplica dano quando IsInvincible é falso. Três decisões aqui:

  1. Guardar o Coroutine em blinkRoutine. Se o personagem toma dano de novo durante o piscar, a coroutine antiga é parada antes de começar outra. Sem isso, duas coroutines alternariam o enabled ao mesmo tempo e o sprite poderia terminar invisível.
  2. Restaurar no fim. O laço pode terminar com o sprite desligado, então a última linha liga de volta.
  3. Restaurar no OnDisable. Se o objeto for desativado no meio do piscar, a coroutine morre (próxima seção), e sem esse método o personagem voltaria invisível ou invencível para sempre.

Para o flash de cor em vez de piscar, o mesmo esqueleto funciona com Color.Lerp. Esse tipo de detalhe é parte do que game feel e juice chama de feedback.

Contagem regressiva e sequência de spawn

Contagem de "3, 2, 1, já" antes da partida, que funciona mesmo com o jogo congelado:

using System.Collections;
using UnityEngine;

public class StartCountdown : MonoBehaviour
{
    [SerializeField] private int from = 3;

    private IEnumerator Start()
    {
        Time.timeScale = 0f;

        for (int i = from; i > 0; i--)
        {
            Debug.Log(i);
            yield return new WaitForSecondsRealtime(1f);
        }

        Debug.Log("Já!");
        Time.timeScale = 1f;
    }
}

Com timeScale em 0, um WaitForSeconds comum nunca terminaria. A versão Realtime conta o tempo de verdade.

Sequência de spawn em ondas, onde coroutine mostra a maior vantagem sobre o Update. Arquivo WaveSequence.cs:

using System.Collections;
using UnityEngine;

public class WaveSequence : MonoBehaviour
{
    [SerializeField] private GameObject enemyPrefab;
    [SerializeField] private Transform spawnPoint;
    [SerializeField] private Transform enemiesParent;
    [SerializeField] private int waves = 3;
    [SerializeField] private int enemiesPerWave = 4;
    [SerializeField] private float spawnInterval = 0.6f;
    [SerializeField] private float pauseBetweenWaves = 3f;

    private IEnumerator Start()
    {
        for (int wave = 1; wave <= waves; wave++)
        {
            Debug.Log("Onda " + wave);

            for (int i = 0; i < enemiesPerWave; i++)
            {
                Instantiate(enemyPrefab, spawnPoint.position, Quaternion.identity, enemiesParent);
                yield return new WaitForSeconds(spawnInterval);
            }

            yield return new WaitUntil(() => enemiesParent.childCount == 0);
            yield return new WaitForSeconds(pauseBetweenWaves);
        }

        Debug.Log("Todas as ondas vencidas.");
    }
}

Leia de cima para baixo e você entende a fase: para cada onda, solta um inimigo a cada 0,6 s, espera a sala limpar, descansa 3 s. No Update, isso viraria um enum de estados, três timers e um contador.

Coroutines resolvem bem cada sequência isolada. A dificuldade aparece no jogo inteiro: ondas que dependem do chefe, diálogos que pausam o spawn, menus que congelam o tempo, e uma coroutine esquecida rodando num objeto que já devia ter parado. Organizar tudo isso num projeto que fecha é o ponto em que estudar sozinho costuma travar.

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

Parar coroutine e objeto desativado

Formas de parar:

  • StopCoroutine(handle): para a coroutine cujo Coroutine você guardou. É o jeito certo, como no HurtBlink.
  • StopAllCoroutines(): para todas as coroutines iniciadas por aquele componente. Útil num reset, perigoso se o componente tem mais de uma coroutine que deveria continuar.
  • yield break: encerra a coroutine de dentro dela, como um return.

O que para sem você pedir:

  • gameObject.SetActive(false): o manual da Unity diz que a coroutine para quando o GameObject é desativado, e ela não volta sozinha quando ele é reativado. Se precisa continuar, inicie de novo no OnEnable.
  • Destruir o objeto: as coroutines dele morrem junto.

O que não para:

  • enabled = false no componente: a referência de StopCoroutine deixa claro que desabilitar o script não interrompe as coroutines. O Update para, a coroutine continua. Essa diferença é fonte de bug: você "desliga" o inimigo desabilitando o script, e a coroutine de ataque continua atirando.

E um cuidado com WaitForSeconds: a documentação de Time.timeScale diz que, em 0, coroutines esperando WaitForSeconds ficam suspensas. Se a sua pausa congela o tempo, as coroutines de jogo pausam junto, que é quase sempre o que você quer. As de UI devem usar WaitForSecondsRealtime. A relação completa entre tempo escalado e real está em Time.deltaTime na Unity.

Coroutine vs async e Awaitable

A Unity 6 trouxe a classe Awaitable, que permite escrever o mesmo tipo de sequência com async e await. Os métodos estáticos documentados no Unity 6.3 incluem NextFrameAsync, WaitForSecondsAsync, FixedUpdateAsync, EndOfFrameAsync, MainThreadAsync e BackgroundThreadAsync.

O piscar reescrito com Awaitable:

using System;
using UnityEngine;

public class HurtBlinkAsync : MonoBehaviour
{
    [SerializeField] private SpriteRenderer spriteRenderer;
    [SerializeField] private float invincibleTime = 1f;
    [SerializeField] private float blinkInterval = 0.1f;

    public async Awaitable BlinkAsync()
    {
        float elapsed = 0f;

        try
        {
            while (elapsed < invincibleTime)
            {
                spriteRenderer.enabled = !spriteRenderer.enabled;
                await Awaitable.WaitForSecondsAsync(blinkInterval, destroyCancellationToken);
                elapsed += blinkInterval;
            }
        }
        catch (OperationCanceledException)
        {
            return;
        }

        spriteRenderer.enabled = true;
    }
}

Diferenças que importam na prática:

  • Cancelamento é manual. Uma coroutine morre com o objeto. Um método async não: se o objeto for destruído no meio da espera, o código continua e tenta mexer num SpriteRenderer destruído. O destroyCancellationToken do MonoBehaviour é cancelado quando ele é destruído, e o WaitForSecondsAsync lança OperationCanceledException se o token for cancelado durante a espera. Daí o try/catch.
  • Desativar não para. Diferente da coroutine, SetActive(false) não interrompe o async.
  • Não reaproveite o mesmo Awaitable. A documentação diz que as instâncias vêm de um pool e não é seguro dar await duas vezes nelas.
  • Vantagens: pode devolver valor (Awaitable<int>), funciona com try/catch normalmente, e encaixa com código de rede, arquivos e InstantiateAsync.

Regra prática: para sequência de jogo presa a um objeto (piscar, ondas, diálogo), coroutine é mais simples e mais segura. Para carregar algo, esperar resposta de servidor ou trocar de thread, Awaitable.

Erros comuns com coroutine

Chamar sem StartCoroutine. BlinkRoutine(); compila e não faz nada.

WaitForSeconds com o jogo pausado. A coroutine fica parada para sempre se a pausa nunca termina. Use Realtime no que deve rodar em pausa.

Duas coroutines iguais ao mesmo tempo. Guarde o handle e pare a anterior antes de iniciar outra.

Esperar que enabled = false pare a coroutine. Não para. Use StopCoroutine ou desative o GameObject.

Iniciar coroutine em objeto inativo. StartCoroutine num GameObject desativado gera erro. Ative antes, ou inicie a coroutine em outro objeto que esteja ativo.

Laço infinito sem yield. while (true) { } sem nenhum yield return dentro trava a Unity, porque a coroutine nunca devolve o controle.

Próximo passo

Pegue o HurtBlink e o WaveSequence e coloque numa cena com um jogador que toma dano dos inimigos das ondas. Depois teste os dois cenários que mais geram bug: desative o jogador no meio do piscar e reative, e pause o jogo no meio de uma onda. Se a arquitetura começar a ficar emaranhada, com ondas, vida e UI chamando umas às outras, o padrão Observer em padrões de projeto em jogos ajuda a separar quem avisa de quem reage.

Perguntas frequentes

O que é uma coroutine na Unity?

É um método que pode pausar no meio e continuar depois, sem travar o jogo. Ele retorna IEnumerator, é iniciado com StartCoroutine e pausa em cada yield return. Serve para sequências no tempo, como piscar, contar, esperar e repetir.

O que faz yield return null?

Pausa a coroutine até o próximo frame. É útil para repetir algo uma vez por frame dentro de um laço, como uma animação feita por código, sem precisar de um Update separado.

WaitForSeconds funciona com o jogo pausado?

Não. WaitForSeconds usa o tempo escalado, então com Time.timeScale em 0 a coroutine fica parada. Para esperar com o jogo pausado, como num menu, use WaitForSecondsRealtime.

Coroutine ou async na Unity 6?

As duas funcionam. Coroutine é mais simples e para sozinha quando o GameObject é desativado. O Awaitable da Unity 6 permite usar async e await, devolver valores e combinar com cancelamento, mas exige cuidar do cancelamento quando o objeto é destruído, com destroyCancellationToken.