Otimizando seu jogo com Coroutines

Otimizando seu jogo com Coroutines
Imagem de destaque #cover

Quando estamos fazendo um jogo do tipo tower defense cada torre dentro do nosso jogo precisa verificar qual o inimigo mais próximo dela, com o intuito de atacar ele. Para isso, podemos implementar um comportamento de Radar nessas torres.

Como saber qual é o inimigo mais próximo?

Precisamos ir de inimigo em inimigo verificando a distância daquele inimigo em relação a menor distância encontrada até aquele momento. Dentro da Unity, podemos marcar os inimigos com uma tag "inimigo" e assim buscar todos eles dentro do nosso código de busca.

Banner da Escola de Programação: Matricula-se na escola de Programação. Junte-se a uma comunidade de mais de 500 mil estudantes. Na Alura você tem acesso a todos os cursos em uma única assinatura; tem novos lançamentos a cada semana; desafios práticos. Clique e saiba mais!

Para simular, termos muitos inimigos na cena, utilizaremos um "gerador" que irá criar 10.000 inimigos assim que o jogo iniciar.

Como queremos procurar referências a objetos que estejam na cena, utilizaremos o método Start para buscar todos os inimigos e colocá-los dentro de uma lista de "posições".

Com essa lista preenchida, iniciamos o processo de busca desse radar. Queremos que o método IniciarBuscaMaisProximo seja executado a cada dois segundos e, para isso, usamos o comando InvokeRepeating - chamar repetidamente - que automatiza essa chamada de acordo com o tempo que passamos como parâmetro.


[SerializeField]

private List<Transform> inimigos;

private Transform inimigoMaisProximo;

private void Start()

{

    this.inimigos = new List<Transform>();

    var inimigosGO = GameObject.FindGameObjectsWithTag("Inimigo");

    foreach (var encontrado in inimigosGO)

    {

        this.inimigos.Add(encontrado.GetComponent<Transform>());

    }

    InvokeRepeating("ProcurarMaisProximo", 2f, 2f);

}

 O método InvokeRepeating recebe como parâmetros o nome da função que ele deve chamar, o tempo que ele deve esperar para começar as chamadas e o intervalo entre cada chamada.

Com isso, temos o comportamento de busca acontecendo depois de dois segundos. E nesse caso, o IniciarBuscaMaisProximo vai chamar o método que realmente procura o alvo mais próximo e atribuir ele à propriedade inimigoMaisProximo.


private void IniciarBuscaMaisProximo()

{

    this.ProcurarMaisProximo();

}

private Transform ProcurarMaisProximo()

{

    Transform maisProximo = null;

    float distanciaMinima = Mathf.Infinity;

    for (var i = 0; i < this.inimigos.Count; i++)

    {

        var distancia = Vector3.Distance(this.transform.position,this.inimigos[i].position);

        if (distancia < distanciaMinima)

        {

            maisProximo = this.inimigos[i];

            distanciaMinima = distancia;

        }

    }

    this.inimigoMaisProximo = maisProximo;

}

Com esse método de busca pelo mais próximo, temos um pico de processamento dentro do jogo, a cada 2 segundos.

Veja que o profiler da Unity marca que, quando temos esse pico nosso FPS cai de aproximadamente 1000 para 100 quadros por segundo. Isso com apenas esse script sendo executado.

Isso é problemático porque nosso jogo tem que executar com pelos menos 60 quadros por segundo e se temos uma queda tão brusca dessa taxa de quadros executando apenas esse script, na hora que adicionarmos toda lógica dos inimigos, interface gráfica, etc… o jogo vai travar por alguns segundos enquanto ele tenta achar qual o inimigo mais próximo.

Como podemos evitar esse tipo de pico e termos um processamento mais uniforme?

Se esse método só é executado a cada 2 segundos, não precisamos que ele inicie e termine sua execução em um único frame. Pensando dessa maneira, podemos dividir todo esse processamento em vários frames.

Vamos então, criar um novo método que faça isso. Ele será chamado de ProcurarMaisProximoComCoroutine. Como o próprio nome diz, esse método será executado como uma coroutine pela Unity e para isso esse método precisa retornar um IEnumerator.


private IEnumerator ProcurarMaisProximoComCoroutine()

{

}

Podemos pensar em uma coroutine como sendo uma linha de produção. Se, por exemplo, trabalhamos em uma gráfica e recebemos um pedido de 1000 exemplares de um livro e conseguimos produzir apenas 100 exemplares em um dia, o que fazemos?

Como o pedido é maior do que a produção de um dia, no primeiro dia produzimos 100 exemplares. No dia seguinte, produzimos mais 100 e guardamos no estoque junto com os livros já produzidos. Fazemos isso até, que no final do décimo dia, tenhamos 1000 livros produzidos e podemos entregar o pedido para o cliente.

Essa mesma ideia de produzir/processar informações por partes é aplicada ao código quando utilizamos coroutines. Precisamos fazer com que a Unity inicie a execução desse método e, depois de um tempo(dia de produção), ela pare e então, no frame seguinte (dia seguinte), ela volte para esse método a partir do ponto em que parou. Como faremos isso?

Normalmente, quando queremos interromper a execução de um método ou função, usamos a palavra-chave return, e assim o compilador já sabe que ele deve sair daquela função. Mas dessa vez, não queremos que ele somente saia, precisamos que o computador salve o ponto onde ele parou para poder voltar depois, e por isso, precisamos usar outra palavra-chave junto com o return.

Essa palavra-chave é o yield. Como não queremos retornar nenhum valor de fato, vamos apenas adicionar a linha yield return null no final do loop for, assim, toda vez que o computador executar essa linha, ele deve sair dessa função e aguardar o próximo frame para continuar executando ela.


private IEnumerator ProcurarMaisProximoComCoroutine()

{

    Transform maisProximo = null;

    float distanciaMinima = Mathf.Infinity;

    for (var i = 0; i < this.inimigos.Count; i++)

    {

        var distancia = Vector3.Distance(this.transform.position, this.inimigos[i].position);

        if (distancia < distanciaMinima)

        {

            maisProximo = this.inimigos[i];

            distanciaMinima = distancia;

        }

        yield return null;

    }

    this.imigoMaisProximo = maisProximo;

}

Para executarmos uma coroutine utilizamos o método StartCoroutine, passando a função como parâmetro.


private void IniciarBuscaDoMaisProximo()

{

    StartCoroutine(ProcurarMaisProximoComCoroutine());

}

Dessa forma, o loop dará apenas uma volta antes de parar, e isso acaba demorando demais para completar. Podemos dar mais voltas nesse loop antes de pararmos.


private IEnumerator ProcurarMaisProximoComCoroutine()

{

    Transform maisProximo = null;

    float distanciaMinima = Mathf.Infinity;

    for (var i = 0; i < this.inimigos.Count; i++)

    {

        var distancia = Vector3.Distance(this.transform.position, this.inimigos[i].position);

        if (distancia < distanciaMinima)

        {

            maisProximo = this.inimigos[i];

            distanciaMinima = distancia;

        }

        if (i % 250 == 0)

            <strong>yield return null;</strong>

    }

    this.imigoMaisProximo = maisProximo;

}

Assim, temos um processamento mais uniforme, e se olharmos de novo na janela do Profiler da Unity, veremos que o framerate do nosso jogo fica ali em torno dos 4000 quadros por segundo.

Dessa forma, podemos utilizar coroutines para dividir o processamento de tarefas em mais de um frame e assim não temos uma queda brusca na atualização do jogo porque alguma função está demorando muito para ser executada.

Veja os cursos de jogos que temos aqui na Alura, com certeza você vai achar algo que te interesse. Além dos cursos de Unity temos outras ferramentas como Cocos Creator e TIC-80.

Ricardo Bugan Debs
Ricardo Bugan Debs

Ricardo é designer de jogos, programador e instrutor. Trabalha desenvolvendo jogos desde 2012 e está sempre em busca de novas quests. Como instrutor, vê jogos como mundos interativos onde as pessoas entram para aprender.

Veja outros artigos sobre Programação