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 oIEnumeratorsem tipo genérico. - Chamar
SayThings()direto não faz nada. Tem que serStartCoroutine(SayThings()). - A referência de
StartCoroutinerecomenda a versão que recebe oIEnumerator, 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): esperatsegundos de tempo escalado. A documentação diz que o tempo real de espera étdividido porTime.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 otimeScale. 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:
- Guardar o
CoroutineemblinkRoutine. Se o personagem toma dano de novo durante o piscar, a coroutine antiga é parada antes de começar outra. Sem isso, duas coroutines alternariam oenabledao mesmo tempo e o sprite poderia terminar invisível. - Restaurar no fim. O laço pode terminar com o sprite desligado, então a última linha liga de volta.
- 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.
Parar coroutine e objeto desativado
Formas de parar:
StopCoroutine(handle): para a coroutine cujoCoroutinevocê guardou. É o jeito certo, como noHurtBlink.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 umreturn.
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 noOnEnable.- Destruir o objeto: as coroutines dele morrem junto.
O que não para:
enabled = falseno componente: a referência deStopCoroutinedeixa claro que desabilitar o script não interrompe as coroutines. OUpdatepara, 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
asyncnão: se o objeto for destruído no meio da espera, o código continua e tenta mexer numSpriteRendererdestruído. OdestroyCancellationTokendoMonoBehaviouré cancelado quando ele é destruído, e oWaitForSecondsAsynclançaOperationCanceledExceptionse o token for cancelado durante a espera. Daí otry/catch. - Desativar não para. Diferente da coroutine,
SetActive(false)não interrompe oasync. - Não reaproveite o mesmo Awaitable. A documentação diz que as instâncias vêm de um pool e não é seguro dar
awaitduas vezes nelas. - Vantagens: pode devolver valor (
Awaitable<int>), funciona comtry/catchnormalmente, e encaixa com código de rede, arquivos eInstantiateAsync.
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.



