sexta-feira, 8 de maio de 2015

Threads em C# e Co-rotinas em Unity

Mais um projeto foi dado à nossa classe de POO, envolvendo animações, e meu grupo optou por fazer um jogo utilizando Unity. Se você não conhece Unity, vale a pena se informar (este link é interessante). Dando uma breve descrição, é uma game engine que facilita muito o desenvolvimento de jogos, já que une elementos gráficos, física e possui API própria, ou seja, ela tem um conjunto de funções que simplificam muitas partes difíceis que não queremos lidar quando desenvolvemos jogos, como colisões entre objetos.

Entre outras vantagens de Unity, cresce rápido e tem uma vasta documentação disponível para quem quer aprender, além de permitir criar projetos para diferentes plataformas e ainda ser de "graça" (existe a versão profissional paga, mas a gratuita oferece recursos mais que suficientes para quem não tem projetos gigantes). Um exemplo do potencial está no vídeo abaixo:



Se você já usou alguma framework (usei Microsoft XNA), vai ver que Unity é bem mais intuitivo. Por exemplo, ele já computa automaticamente a gravidade desde que você opte por isso.

Mas, voltando ao projeto, decidimos fazer um protótipo de um jogo Worms (aquele joguinho em que várias minhocas devem atirar entre si, dê uma olhada aqui). É um jogo complexo, então nosso protótipo não ficou totalmente completo. Utilizamos C# (unity permite outras linguagens), que é muito parecido com Java.

Um conceito cuja utilização foi solicitada foi o de Thread. Threads são como um "programa", com início, meio e fim, mas que, na verdade, são desenvolvidos dentro de um programa. É um fluxo sequencial, e cada programa geralmente apresenta um principal e único. No entanto, é possível que queiramos executar duas tarefas simultâneas, não sequenciais.

Nesse caso, definimos uma aplicação multi-thread, em que várias threads são executadas simultaneamente, com divisão das tarefas na CPU inclusive, o que é uma vantagem para computadores com vários processadores. Um exemplo real é que conseguimos executar simultâneas tarefas no computador graças ao recurso.

Em C#, threads são levemente diferentes de Java (no fundo, só muda a forma de escrever):
using System.Threading;
class Teste {
    public void Rodar() 
    {
        Console.WriteLine("Rodando")
    }
    public static int Main() 
    {
        Thread newThread = new Thread(new ThreadStart(Rodar));
        newThread.Start(); //Iniciei a thread
    }
}
A diferença é que se usa um delegate em C#. Não se aprofundando (mais detalhes aqui), delegate é um tipo que encapsula um método, a ideia é bem simples de ser usada. Por exemplo:
delegate void Metodo(string frase);
class Teste {
    static void Printar(string entrada)
    {
        Console.WriteLine(frase);
    }

    static void Main()
    {
        Metodo func = Printar;
        func("Hey"); //Chama a funcao Printar
    }
}
OK, mas e qual a aplicação de thread no jogo? Unity funciona com uma thread principal executando uma rotina de Update a uma quantidade determinada de frames por segundos (fps). A cada frame, colisões, inputs de jogadores, if's pré-definidos são detectados. Às vezes, queremos que um processo ocorra gradualmente, como quando um objeto deve desaparecer gradualmente da tela. Não podemos definir uma transparência e fazer um for para diminui-la, já que isso seria executado em um só frame e não haveria esse processo gradual.

A opção então é definir um processo paralelo executado simultaneamente ao Update da thread principal. Se o que precisamos fazer exige memória computacional, não há outra solução, precisamos de outra thread. Se precisamos que os processos terminem juntos, porém, temos um problema: dado que as execuções são independentes, é possível que haja dessincronia entre elas.

Além disso, as funções próprias de Unity são restritas à thread principal. A game engine, no entanto, fornece uma opção: as co-rotinas. Estas são diferentes de thread, mas tem semelhanças.

Na co-rotina, não há divisão do processador, tudo é executado em sequência, mas intercalado, o que dá a impressão de que são paralelos (ou seja, não há ganho real de velocidade). O que acontece é que, ao criar uma co-rotina, a função salva o último ponto em que parou, e no frame seguinte, executa de onde parou. Com um exemplo fica mais fácil:
IEnumerator Corotina() {
    Debug.Log("Comeco");
    yield WaitForSeconds(5);
    Debug.Log("Meio");
    yield return 0;
    Debug.Log("Fim");
    yield return 0;
}

void Start() {
    StartCoroutine("Corotina");
}

void Update() {
    Debug.Log("Novo frame!");
}
Yield é um retorno que salva a posição de término, e basicamente diz "parei aqui, e no próximo frame executo a partir deste ponto". WaitForSeconds é uma função especial que faz o programar esperar por 5 minutos, mas como é rodado na co-rotina, essa espera não altera o resto do programa. Nos primeiros frames então, a função espera até complementar 5 segundos, e depois termina de rodar o programa, com uma execução a cada frame.

A co-rotina é, portanto, mais adequada na maioria das situações em que não é realmente necessária uma divisão do processador para aumento de velocidade, principalmente porque é a implementação é bem simples. Em casos de cálculos pesados, o ideal é definir uma thread.

Em breve, detalhes do protótipo. Até mais!

2 comentários: