Entendendo e utilizando Coroutines na Unity3D

Entendendo e utilizando Coroutines na Unity3D

Introdução

Se você já tentou fazer algum jogo na Unity, em algum momento você provavelmente sentiu a necessidade de controlar o tempo da execução de alguns pedaços do código. Seja isso na forma de estados de uma animação, delay para mostrar texto na tela, uma skill que regenera vida a cada x segundos. Enfim, é possível encontrar infinitos usos para Coroutines dentro de jogos na Unity, mas isso também não quer dizer que você deva fazer tudo uma Coroutine.

Para quem já tem experiencia em programação, já deve imaginar que uma Coroutine seja algo parecido com uma Thread, onde é possível pausar a execução do código por um dado período de tempo, e retomar a execução no mesmo estado que foi deixado antes da pausa. Ao mesmo tempo que é possível traçar semelhanças entre Threads e Coroutines, uma diferença extremamente importante precisa ser feita: Coroutines não são threads. Enquanto Threads permitem que código seja executado em paralelo, Coroutines NÃO são executadas em paralelo apesar de darem essa impressão. A API da Unity não é considerada thread-safe, ou seja, não esta preparada para ser acessada simultaneamente por Threads diferentes. Thread-safety é um assunto bem complexo e, enquanto essa explicação não é, tecnicamente falando, muito adequada, deve ser o suficiente para o escopo do post.

Na pratica, Coroutines devolvem o controle do código para a Unity, desta forma é possível executar toda a logica restante para aquele frame mesmo que a Coroutine não tenha terminado. Em seguida em um próximo frame, a logica da Coroutine pode ser resumida, ate que a mesma devolva novamente o controle para a Unity. Esse conceito ficara um pouco mais claro nos exemplos que darei com código mais abaixo no post.

Para finalizar um pouco da introducao do que são Coroutines, vale a pena citar a definicao da documentacao da Unity:

It is essentially a function declared with a return type of IEnumerator and with the yield return statement included somewhere in the body. The yield return line is the point at which execution will pause and be resumed the following frame.

 

Usos

Apos entender a ideia básica por trás das Coroutines, podemos listar alguns usos bem comuns:

  • Código que precisa ser executado em um intervalo diferente de todo Update () ou FixedUpdate ().
  • Distribuir uma computação que utiliza a API da Unity em vários frames. (vale a pena observar que isso é possível mesmo sem Coroutines, mas é necessário um código bem diferente, com Coroutines essa distribuição segue uma logica bem mais simples).
  • Controlar timing de eventos dentro do código em geral (animações, sons, lerping, GUI, etc…).

Detalhes

Algumas observações que valem a pena serem citadas:

  • Coroutines não devem ser utilizadas como uma substituição do Update () ou FixedUpdate () já que existe um custo para que elas sejam executadas.
  • Não existe uma forma simples para retornar valores (igual uma função normal), mas isso não quer dizer que isso é impossível.
  • Como mencionado na definição, uma Coroutine é uma função que retorna a classe IEnumerator e possui um yield return dentro de seu código.

Código

Como uma Coroutine é definida

IEnumerator HelloWorld()
{
    Debug.Log("Hello from Coroutine!");
    yield return new WaitForSeconds(1f);
    Debug.Log("Bye from Coroutine!");
}

Como uma Coroutine é iniciada (método do IEnumerator)

void Start()
{
    StartCoroutine(HelloWorld());
}

Como uma Coroutine é iniciada (método da string)

void Start()
{
    Debug.Log("Hello from Start()");
    StartCoroutine("HelloWorld");
    Debug.Log("Bye from Start()");
}

Como uma Coroutine é parada (método do IEnumerator e string)

void StopHelloWorld()
{
    StopCoroutine(HelloWorld());  // Método do IEnumerator
    StopCoroutine("HelloWorld"); // Método da String
}

Quais tipos de retornos uma Coroutine tem?

IEnumerator ReturnTypes()
{
    // Espera o final do frame após todas as câmeras e GUI estejam renderizadas,
    // mas antes de mostrar o frame na tela.
    yield return new WaitForEndOfFrame();

    // Espera até a próxima função FixedUpdate()
    yield return new WaitForFixedUpdate();

    // Espera x segundos utilizando o tempo escalado 
    yield return new WaitForSeconds(1f);

    // Espera x segundos utilizando o tempo não escalado
    yield return new WaitForSecondsRealtime(1f);

    // Espera até que a função fornecida retorne true
    yield return new WaitUntil(() => { return Time.time > 10f; });

    // Espera até que a função fornecida retorne false
    yield return new WaitWhile(() => { return Time.time <= 15f; });
}

Mais informações sobre Time.timeScale na Scripting API.

Exemplo simples com saída do console

using System.Collections;
using UnityEngine;

public class Coroutines : MonoBehaviour
{

    void Start()
    {
        Debug.Log("Hello from Start()");
        StartCoroutine("HelloWorld");
        Debug.Log("Bye from Start()");
    }

    IEnumerator HelloWorld()
    {
        Debug.Log("Hello from Coroutine!");
        for (int i = 0; i < 5; i++) {
            yield return new WaitForSeconds(0.1f);
            Debug.Log("Doing some work: " + i);
        }
        yield return new WaitForSeconds(1f);
        Debug.Log("Bye from Coroutine!");
    }
}

Deste exemplo podemos ver o resultado no console, utilizando o asset Console Enhanced Free para uma melhor analise do timing dos Debug.Log(). Para quem não conhece, a primeira coluna é a mensagem que o console normal mostra, seguida de qual frame e o tempo (baseado no começo da execução da aplicação toda) em que essa mensagem foi enviada para o console. Indispensável para quem esta aprendendo os timings da Unity.

Podemos observar que assim que a Coroutine atinge o primeiro yield return na linha 18, a linha 11 pode ser executada, já que a Unity retomou o controle enquanto a Coroutine espera ser resumida. Observe também que mesmo depois da função Start () ter terminado a Coroutine é resumida, e a linha 19 é executada no frame 3. Isso continua até que o for loop termine, onde a Coroutine aguarda mais 1 segundo, e termina completamente de executar no frame 208.

Note também que os intervalos não são perfeitos e constantes, existe uma irregularidade já que a Unity ainda trabalha em função de frames. Em especial nos primeiros frames a diferença é bem grande já que acontece inúmeras inicializações.

Retorno no console de um exemplo básico de Coroutines

Quando Coroutines são resumidas?

Para domínio completo das Coroutines, é necessário saber em quais momentos a Unity resume a execução de Coroutines pausadas. A documentacao da Unity fornece um Flowchart completo onde fica bem claro esses momentos de resumo.

Evitando geração de GC

Lendo outros posts sobre Coroutines antes de começar a escrever meu próprio post, me deparei com uma dica bem interessante do Unity Geek. Essa dica consiste em evitar alocações desnecessárias (e consequentemente geração de GC toda vez que uma Coroutine pausar) salvando a referencia do objeto que você retorna, por exemplo:

IEnumerator HelloWorld()
{
    Debug.Log("Hello from Coroutine!");
    yield return new WaitForSeconds(1f);
    Debug.Log("Bye from Coroutine!");
}

Neste exemplo, fica claro que estamos alocando um novo WaitForSeconds, que por si só não é um problema, mas caso você esteja fazendo isso diversas vezes durante a vida do seu código ou em aplicações mobile, a geração de GC pode ser problemática. A solução?

public class Coroutines : MonoBehaviour
{
    private WaitForSeconds wait;

    void Start()
    {
        wait = new WaitForSeconds(1f);
    }

    IEnumerator HelloWorld()
    {
        Debug.Log("Hello from Coroutine!");
        yield return wait;
        Debug.Log("Bye from Coroutine!");
    }
}

Note a diferença nas linhas destacadas, durante a inicialização do script, alocamos o WaitForSeconds e salvamos essa referencia. Toda vez que chamamos yield return trocamos o new WaitForSeconds(1f) por wait. Desta forma evitando alocação desnecessária do mesmo objeto. Essa dica não serve para casos onde o intervalo de espera varia muito, mas é ter isso em mente sempre sera útil para evitar GC.

Conclusão

Ainda preciso fazer um conclusão, escrever mais algumas coisas e acentuar MUITA coisa dentro desse post.

Enquanto não termino esse post, deixo essa quote que li em outro blog sobre as Coroutines

If you can’t follow your coroutine logic, you’ve probably used one where you shouldn’t have.

 

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *