[POO] Herança na Unity

Herança é um conceito do paradigma de programação conhecido como Programação Orientada a Objetos , que tem como objetivo construir e trabalhar com abstrações de objetos do mundo real, em um nível de código. A Herança dá a capacidade de uma classe/objeto herdar comportamentos previamente definidos em outra classe, sem a necessidade de duplicação do código, permitindo a extensão e a sobrescrita de comportamentos já existentes, de acordo com a necessidade do novo objeto/classe. Para definir uma relação de herança no C#, a "palavra" reservada : (dois pontos) deve ser utilizada na definição da classe que estamos criando, indicando de qual outra classe o nosso script deve herdar. public class Jogador : MonoBehaviour { // Classe Jogador que herda da classe MonoBehaviour } Por padrão, todos os scripts criados na Unity herdam de MonoBehaviour, que é a classe base da Unity para Scripts que serão associados aos GameObjects do jogo. Embora essa definição de herança

Adicionando uma cena de carregamento

Em alguns jogos, é comum que uma cena da Unity seja utilizada para representar um grande cenário/fase, fazendo com que o jogo possa apresentar uma lentidão durante a transição entre essas cenas e mudanças de fases. Isso acontece porque durante a transição de uma cena, a Unity precisa destruir (remover) todos os objetos da cena atual e carregar (criar) todos os objetos da nova cena. Como vocês sabem, esse processo de criar e destruir objetos são pesados (lentos) e caso esse tempo de carregamento seja muito grande, pode gerar uma sensação ruim no jogador, fazendo parecer que o jogo travou por alguns instantes. E de fato, travou.

Uma forma de solucionar esse problema, ou pelo menos não deixá-lo tão aparente, é adicionar uma tela de carregamento para ser exibida enquanto a nova cena é carregada. Ficou interessado? Então vamos lá!

Para agilizar a construção do projeto de exemplo, vamos aproveitar o nosso post anterior sobre transição entre cenas e entender melhor como funciona a construção dessa tela de carregamento para transições de fases. Para isso, vamos alterar o nosso script FluxoCena para utilizar um método para realizar o carregamento assíncrono de cenas com o AsyncOperation e SceneManager.LoadSceneAsync. Complicou? Não precisa se preocupar. Vamos por partes:

AsyncOperation

A palavra reservada AsyncOperation representa um tipo de dados (um tipo de variável) que, como o próprio nome diz, controle operações assíncronas no C#.

...mas o que é uma operação assíncrona?

Operações síncronas

Bom, antes de chegarmos em operações assíncronas, vamos falar um pouco sobre outro tipo de operações, as operações síncronas.
Operações síncronas são operações que após iniciada a sua execução, precisamos aguardar que ela seja concluída antes de podermos executar uma nova operação. Ou seja, o nosso jogo aguarda a operação síncrona ser concluída para só em seguida continuar executando outras operações do jogo. É exatamente assim que funciona o método LoadScene, que utilizamos no exemplo anterior. O método LoadScene é responsável por carregar uma nova cena no jogo. Quando esse carregamento é feito de forma síncrona, todas as operações do jogo param de ser executadas enquanto a Unity destrói (remove) todos os GameObjects da cena atual e em seguida, constrói os objetos da nova cena. Por ser uma operação síncrona, o jogo fica aguardando a conclusão da operação dando a impressão para o jogador de que o jogo teve um pequeno travamento. A duração desse "pequeno" travamento depende do tamanho/complexidade da cena e do dispositivo (computador, celular e etc.) onde o jogo está sendo executado.

Operações assíncronas

Por outro lado, um operação assíncrona não necessita que o jogo pare a execução de todas as outras operações, enquanto espera a operação assíncrona ser concluída, ou seja, o jogo consegue, por exemplo, iniciar o carregamento de uma nova cena enquanto executa outras operações ao mesmo tempo (paralelamente). Já conseguiu entender o grande benefício de utilizar uma operação assíncrona?

Pois é! Como uma operação assíncrona permite que outras operações sejam executadas em paralelo, é possível que o nosso jogo seja desenvolvido para minimizar o tempo de espera de um jogador enquanto transita entre cenas, ou de forma ainda mais simples, tornar essa transição um pouco mais agradável para o jogador, passando a sensação de progresso, mesmo durante um momento de espera. Essa sensação de progresso durante uma espera é o que conhecemos como uma tela de carregamento, a famosa tela de loading.
É graças ao fato de podermos executar uma operação (exibição e atualização do progresso de carregamento) em paralelo ao carregamento de uma nova cena, que conseguimos construir um telas de loading.
Tela de carregamento

O exemplo

O projeto utilizado como base, desenvolvido no post Transição de Fases (navegação entre Cenas) na Unity, possuía uma única classe chamada FluxoCena, que tem como objetivo executar o método SceneManager.LoadScene quando um botão era clicado pelo jogador, solicitando a transição entre fases/cenas. Ao clicar em um dos botões de transição de cena, o método Mudar é executado para iniciar o carregamento da nova cena. O código original pode ser visto abaixo:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class FluxoCena : MonoBehaviour
{

    [SerializeField]
    private string nomeCena;



    public void Mudar() {
        SceneManager.LoadScene(this.nomeCena);
    }

}

Neste novo exemplo, vamos criar uma nova classe chamada CarregamentoCena que será responsável por controlar a nossa tela de carregamento. O código da tela de carregamento (script CarregamentoCena) também é bem simples. Vamos analisar essa classe por partes. Primeiro, as duas variáveis textoCarregamento e barraProgresso que servem como referência para o campo de texto onde a mensagem carregando x% é exibida e para a representação visual (componente slider da Unity) do percentual carregado, respectivamente.

    [SerializeField]
    private Text textoCarregamento;

    [SerializeField]
    private Slider barraProgresso;

Em seguida, temos os métodos Start, Exibir, Esconder e Atualizar que servem respectivamente para definir os valores mínimo e máximo da barra de progresso (de 0 até 100), exibir (tornar visível) a tela de carregamento, esconder (tornar invisível) a tela de carregamento e o atualizar, bem....o atualizar, atualiza as informações na tela.

O método Start é executado na primeira vez que a tela fica ativa no jogo (quando exibida pela primeira vez para o jogador) e será utilizado para definir os valores limites (mínimo e máximo) da barra de progresso.

    private void Start()
    {
        this.barraProgresso.minValue = 0;
        this.barraProgresso.maxValue = 100;
    }

O método Exibir será executando antes de iniciar o carregamento da nova cena e serve exclusivamente para ativar a tela (gameObject) de carregamento, através do método SetActive com o parâmetro true.

    /// <summary>
    /// Exibe a tela de carregamento ao ativar o GameObject
    /// </summary>
    public void Exibir()
    {
        this.gameObject.SetActive(true);
    }

O método Esconder, como você deve imaginar, serve exclusivamente para desativar a tela (gameObject) de carregamento, também através do método SetActive, porém, com o parâmetro false. No caso do método Esconder, ele será utilizado ao iniciar o jogo, para garantir que a tela de carregamento não esteja sendo exibida antes de iniciar o carregamento de cena.

    /// <summary>
    /// Esconde a tela de carregamento ao desativar o GameObject
    /// </summary>
    public void Esconder() {
        this.gameObject.SetActive(false);
    }

E por fim, temos o método Atualizar, responsável por atualizar a exibição do texto de carregamento e a barra de progresso de acordo com o percentual já carregado, identificado pelo parâmetro progresso.

    /// <summary>
    /// Atualiza a exibição do progresso de carregamento de uma cena
    /// </summary>
    /// <param name="progresso"></param>
    public void Atualizar(float progresso)
    {
        this.textoCarregamento.text = ("carregando " + progresso + "%");
        this.barraProgresso.value = progresso;
    }

Os métodos Exibir, Esconder e Atualizar da classe CarregamentoCena serão utilizados no script FluxoCena, que decidirá quando a tela de carregamento deve ser exibida/escondida e quando os valores de progresso do carregamento devem ser atualizados. O código completo da classe CarregamentoCena pode ser visualizado abaixo:

using UnityEngine;
using UnityEngine.UI;

public class CarregamentoCena : MonoBehaviour
{
    [SerializeField]
    private Text textoCarregamento;

    [SerializeField]
    private Slider barraProgresso;



    private void Start()
    {
        this.barraProgresso.minValue = 0;
        this.barraProgresso.maxValue = 100;
    }

    /// <summary>
    /// Exibe a tela de carregamento ao ativar o GameObject
    /// </summary>
    public void Exibir()
    {
        this.gameObject.SetActive(true);
    }

    /// <summary>
    /// Esconde a tela de carregamento ao desativar o GameObject
    /// </summary>
    public void Esconder() {
        this.gameObject.SetActive(false);
    }

    /// <summary>
    /// Atualiza a exibição do progresso de carregamento de uma cena
    /// </summary>
    /// <param name="progresso"></param>
    public void Atualizar(float progresso)
    {
        this.textoCarregamento.text = ("carregando " + progresso + "%");
        this.barraProgresso.value = progresso;
    }

    
}

Agora que conhecemos a classe CarregamentoCena, vamos dar uma olhada nas alterações necessárias na classe FluxoCena, para utilizarmos os benefícios do carregamento assíncrono de cenas.

A primeira mudança na classe FluxoCena é a adiciona de uma nova variável do tipo CarregamentoCena para armazenar a referência para a tela de carregamento. É através dessa novas variável que poderemos executar os métodos Exibir, Esconder e Atualizar mencionados anteriormente. No trecho de código abaixo, é possível visualizar a nova variável, chamada carregamento, sendo utilizada no método Start do script FluxoCena para esconder a tela de carregamento logo após o jogo ser iniciado.

    /// <summary>
    /// Tela de carregamento
    /// </summary>
    [SerializeField]
    private CarregamentoCena carregamento;



    private void Start() {
        this.carregamento.Esconder();
    }

Além disso, precisamos também alterar a implementação do método Mudar para carregar a nova cena de forma assíncrona. A execução assíncrona de um método, na Unity, pode ser realizada utilizando uma Coroutine. Uma Coroutine é um tipo de método que tem a habilidade de parar a sua execução em um ponto específico e retornar para este mesmo ponto em um próximo frame do jogo.

No nosso exemplo, temos o objetivo de carregar uma nova cena de forma assíncrona, por isso, criamos um método chamado Carregar na classe FluxoCena, que funcionará como uma Coroutine, sendo executado assincronamente.

Lembra da AsyncOperation que iniciou todo esse assunto lá no começo do post? Pois é...é hora de voltarmos a falar dela. Ao utilizar o método SceneManager.LoadSceneAsync, estamos solicitando para o gerenciador de cenas da Unity (SceneManager) que inicie o carregamento de uma nova cena de forma assíncrona. O retorno deste método é um operação assíncrona (AsyncOperation) que representa o processo de carregamento assíncrono da cena e contém as informações necessárias para atualizarmos a nossa tela de carregamento.

    /// <summary>
    /// Carrega uma nova cena de forma assíncrona
    /// </summary>
    /// <returns></returns>
    private IEnumerator Carregar()
    {
        this.carregamento.Exibir();

        // Inicia o carregamento da nova cena de modo assíncrono
        AsyncOperation carregamentoAssincrono = SceneManager.LoadSceneAsync(this.nomeCena);

        float progresso;
        // Verifica se o carregamento da cena foi concluído.
        // Enquanto não for concluído, atualiza o progresso do carregamento
        while (!carregamentoAssincrono.isDone) {
            // Calcula o progresso do carregamento em uma escala de 0 até 100
            progresso = ((carregamentoAssincrono.progress / 0.9f) * 100);
            // Atualiza a exibição do progresso na tela de carregamento
            this.carregamento.Atualizar(progresso);
            yield return null;
        }
    }

Como pode ser visto no código acima, após iniciar o carregamento assíncrono da nova cena, o nosso script inicia uma repetição (um loop while) que aguardará até a conclusão do carregamento da nova cena. A variável carregamentoAssincrono que armazena a referência para a operação de carregamento assíncrono possui o método isDone que identifica se o carregamento já foi concluído e também permite a consulta do percentual de progresso do carregamento, através da variável progress.

Note também que o método Exibir (carregamento.Exibir) da classe CarregamentoCena é executando antes de iniciar o carregamento assíncrono, para exibir a tela de carregamento, enquanto o método Atualizar (carregamento.Atualizar), também da classe CarregamentoCena, é executado a cada ciclo da repetição, enquanto o carregamento não for concluído, para exibir o progresso atual do carregamento da cena.

Se você parou para ler o código com calma e fazer as contas, deve ter achado algo estranho na seguinte linha de código:

    // Calcula o progresso do carregamento em uma escala de 0 até 100
    progresso = ((carregamentoAssincrono.progress / 0.9f) * 100);

Como assim "Calcula o progresso do carregamento em uma escala de 0 até 100" fazendo uma divisão por 0,9? Por que isso é necessário?

De acordo com a própria Unity, conforme descrito em sua documentação sobre operação assíncrona (AsyncOperation), o valor da variável progress varia entre 0 e 1, sendo o valor 1 que representa a conclusão do carregamento só é atingido após a nova cena ser ativada/exibida, ou seja, durante o carregamento, o valor limite da variável progress é 0,9. Dessa forma, para garantir que o carregamento será visualmente representado de corretamente, nós dividimos o progresso atual do carregamento por 0,9 e em seguida multiplicamos por 100, para obter um valor entre 0% e 100% como indicador "real" do progresso do carregamento.

Agora que esclarecemos os detalhes matemáticos, precisamos saber...mas como é que eu uso uma Coroutine?

Isso é bem simples. Para iniciar a execução de uma Courotine, precisamos apenas utilizar o método StartCoroutine. No nosso exemplo, alteramos o método Mudar para iniciar um Coroutine que executará o método Carregar, também da classe FluxoCena. Como o método Mudar é executado ao clicar nos botões de transição entre cenas, sempre que um desses botões for clicado pelo jogador, o carregamento assíncrono de uma nova cena será iniciado.

    public void Mudar()
    {
        StartCoroutine(Carregar());
    }

O código completo da classe FluxoCena, com todas as alterações realizadas, pode ser visualizado a seguir:

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class FluxoCena : MonoBehaviour
{

    /// <summary>
    /// Nome da cena que deve ser carregada
    /// </summary>
    [SerializeField]
    private string nomeCena;

    /// <summary>
    /// Tela de carregamento
    /// </summary>
    [SerializeField]
    private CarregamentoCena carregamento;



    private void Start() {
        this.carregamento.Esconder();
    }

    public void Mudar()
    {
        StartCoroutine(Carregar());
    }

    /// <summary>
    /// Carrega uma nova cena de forma assíncrona
    /// </summary>
    /// <returns></returns>
    private IEnumerator Carregar()
    {
        this.carregamento.Exibir();

        // Inicia o carregamento da nova cena de modo assíncrono
        AsyncOperation carregamentoAssincrono = SceneManager.LoadSceneAsync(this.nomeCena);

        float progresso;
        // Verifica se o carregamento da cena foi concluído.
        // Enquanto não for concluído, atualiza o progresso do carregamento
        while (!carregamentoAssincrono.isDone) {
            // Calcula o progresso do carregamento em uma escala de 0 até 100
            progresso = ((carregamentoAssincrono.progress / 0.9f) * 100);
            // Atualiza a exibição do progresso na tela de carregamento
            this.carregamento.Atualizar(progresso);
            yield return null;
        }
    }

}

Para que esse código funcione em nosso projeto, precisamos configurar dois GameObjects na Unity. Primeiro, vamos adicionar o script CarregamentoCena no GameObject que representa a nossa tela de carregamento.

Adicionando o script CarregamentoCena no GameObject da tela de carregamento

Após adicionar o script CarregamentoCena na tela de carregamento, precisamos agora associar o campo de texto e a barra de progresso com as variáveis da nossa classe, para podermos controlá-los a partir do nosso script. Essa associação pode ser feita arrastando os componentes da hierarquia (Hierarchy) para o Inspector da tela de carregamento (GameObject chamado Carregamento) que possui o script CarregamentoCena, conforme pode ser visto abaixo:

Associando os componentes (Text e Slider) ao script CarregamentoCena

Agora que temos o script CarregamentoCena configurado, basta associá-lo ao nosso script FluxoCena para controlarmos a exibição e a atualização da tela de carregamento.

Associando a tela de carregamento (script CarregamentoCena) ao script FluxoCena

Como resultado, já podemos realizar a transição da cena Menu para a cena Fase 1 de forma assíncrona, com a tela de carregamento.

Transição entre duas cenas com tela de carregamento

Como você pôde notar, o tempo de carregamento depende muito do tamanho/complexidade da cena que está sendo carregada e do dispositivo onde o jogo está sendo executado. No exemplo exibido anteriormente, a tela de carregamento aparece e desaparece em uma fração de segundo, sendo impossível visualizar o seu carregamento completo. Ao utilizar uma cena mais complexa ou executar o jogo em uma dispositivo mais simples/modesto, o resultado da tela do carregamento será mais ou menos assim:

Exemplo do funcionamento da tela de carregamento



O mais importante é que independente do tamanho das cenas utilizadas, o código de exemplo desenvolvido funcionará perfeitamente para a exibição da tela de carregamento. Quer testar com cenas mais complexas? Baixe aqui o nosso projeto de exemplo e experimente.



Comentários

Postagens mais visitadas deste blog

Transição de Fases (navegação entre Cenas) na Unity

GetComponent - Obtendo a referência para outros componentes do GameObject

[POO] Herança na Unity