Programação orientada a objeto fazendo um simulador de contaminação

O objetivo aqui é apresentar programação orientada a objeto com uma visão mais material, reduzindo a abistração. Para isso será demonstrado como construir o modelo de propagação do COVID-19 apresentado pelo The Washington Post (Por que surtos como o coronavírus se espalham exponencialmente e como “achatar a curva”). Os resultados neste projeto não devem ser utilizados como prova científica para o controle da dormia de COVID-19.

O grupo VisioRob está testando uma ferramenta a Unity 3D para simulação e controle de robôs. Como primeiro passo, para entendermos o funcionamento da ferramenta e aproveitando o momento da pandemia de COVID-19, foi criado o simulador de contaminação apresentado a seguir.



Para esse simulador foi criado 2 cenas. A que aparece primeiro possui 4 sliders que permitem definir: O número de pessoas; o tempo, em segundos que cada pessoa permanece doente; A porcentagem de pessoas que irão se isolar, ficarem paradas; e a chance de se contaminar ao entrar em contato com algum doente.

Já na segunda cena é apresentado um conjunto de círculos, colocados de forma aleatório. Estes círculos representam as pessoas e a quantidade deles dependem do que foi informado na cena anterior. Na parte inferior é apresentado a quantidade de pessoas saldáveis, pessoas doentes e pessoas que se recuperaram da doença. Junto com a quantidade é apresentado um gráfico que permite a visualização dos dados ao longo do tempo.

Para descrever a programação orientada a objeto vou explicar o funcionamento que dei aos elementos que chamei de pessoas.

O que é Programação Orientada a Objetos (POO)

Programação orientada a objetos (POO) é um paradigma de programação, ou seja, é uma forma de visualizar a construção do software. Neste caso o software é visto como um conjunto de blocos (objetos) independentes que juntos fazem o programa funcionar.

Estes blocos são chamados de objetos ou classes. Eles recebem este nome porque há uma tentativa de abstrair o funcionamento destes blocos relacionando eles com objetos do mundo físico (fora do computador). Entretanto, nem todos os objetos criados por programadores existem no mundo físico como, por exemplo, um socket. Entendo o que é um socket, sei para quê serve, já usei um, mas se alguém pedir para que eu desenhe um socket eu não saberei fazer já que a idealização dele é muito abstrata.

Uma outra forma muito comum de visualizar a construção de um software é através da programação estruturada, nesta os programadores abstraem o programa como uma sequência de funções a serem executadas. Ou seja, cada linha de código é uma ordem que o programador está dando para o software realizar, o que torna a visão do software algo mais concreto.

Dentre as vantagens da POO é a divisão do software em objetos, partes, com comportamento distintos. Isso permite um maior controle do software, no que diz respeito ao desenvolvimento e manutenção.

Ou seja, na simulação acima, enquanto era desenvolvido o código do comportamento do objeto Person preocupava-se apenas descrever como este objeto deveria se comportar sem se importar de como objeto GraphicBar deve se comportar. A única relação entre estes dois objetos é que o objeto Person deve informar ao objeto Settings quando ele muda alguma característica (se está saudável, doente ou curado) e o objeto GraphicBar consulta o objeto Settings para descrever o tamanho das barras do gráfico.

Caso seja necessário alterar o algum comportamento do objeto Person, seja por colocar alguma nova funcionalidade ou corrigir um comportamento inadequado não compromete o comportamento do objeto GraphicBar. Isso facilita que desenvolvedores distintos possam trabalhar no mesmo projeto, cada um sendo responsável por um objeto.

Outra vantagem é a facilidade de substituir/trocar os objetos. Por exemplo, se quisermos representar uma pessoa com outro formato, que não o circulo, ou usar um carro, que tem limite de distância que pode percorrer, basta substituir o objeto Person, sem prejuízo ao restante do software.

Por fim, temos a possibilidade de utilizar um determinado objeto diversas vezes. No caso desse simulador o comportamento do objeto Person foi descrito apenas uma vez e o software cria diversas cópia (instâncias) deste objeto e os coloca em posições aleatória dentro da área de simulação.

Descrevendo o objeto Person

O código que descreve o objeto Person é apresentado na sequência. Após a apresentação do código será apresentado uma explicação de cada parte dele, justificando os usos. A linguagem de programação usada na Unity3D é C#, mais todos os conceitos utilizados que serão apresentados estão presentes em todas as linguagens de programação orientada a objetos como Java, Python e outras.

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

public class Person : MonoBehaviour
{
    private CircleCollider2D playerCollider;
    private GameObject otherPerson;
    [SerializeField]
    private GameObject settings;
    [SerializeField]
    private Settings settingsScript;
    private Rigidbody2D rb2d;
    private Vector2 movement;
    private float movmentNorm;
    [SerializeField]
    private float sickTime;
    [SerializeField]
    private float isolationPorcet;
    [SerializeField]
    private float chanceToMove;
    public bool isHeath = true;
    public bool isSick;
    public bool wasSick;
    [SerializeField]
    private bool fist;
    [SerializeField]
    private bool movable;
    
    // Start is called before the first frame update
    void Start()
    {
        settings = GameObject.Find("Settings");
        settingsScript = settings.GetComponent<Settings>();
        rb2d = GetComponent<Rigidbody2D>();
        movement = new Vector2(Random.Range(-1f, 1f), Random.Range(-1f, 1f));
        if (Mathf.Abs(movement.x) < 0.2f)
        {
            movement.x = 1f * Mathf.Sign(movement.x);
        }
        if (Mathf.Abs(movement.y) < 0.2f)
        {
            movement.y = 1f * Mathf.Sign(movement.y);
        }
        movmentNorm = Mathf.Sqrt(Mathf.Pow(movement.x, 2) + Mathf.Pow(movement.y, 2));
        sickTime = settingsScript.sickTime;
        isolationPorcet = settingsScript.isolationPorcet;
        chanceToMove = Random.Range(0f, 1f);
        if (fist)
        {
            chanceToMove = 2;
            GetSick();
        }
        if (isolationPorcet > chanceToMove)
        {
            movable = false;
            settingsScript.stoppedPeople++;
        }
        else
        {
            movable = true;
            settingsScript.movingPeople++;
        }
        if (movable)
        {
            rb2d.AddForce(new Vector2(movement.x * 100, movement.y * 100));
            rb2d.constraints = RigidbodyConstraints2D.None;
            rb2d.constraints = RigidbodyConstraints2D.FreezeRotation;
        }
        gameObject.GetComponent<CircleCollider2D>().enabled = true;
    }
    
    // Update is called once per frame
    void Update()
    {
        if (rb2d.velocity.magnitude < 2f)
        {
            rb2d.AddForce(rb2d.velocity);
        }
        if (isSick)
        {
            sickTime -= Time.deltaTime;
            if (sickTime < 0)
            {
                GetBetter();
            }
        }
    }

    void GetSick()
    {
        float myInfectionChance = Random.Range(0f, 1f);
        if (isHeath)
        {
            if ((settingsScript.chanceToGetSick > myInfectionChance) || fist)
            {
                settingsScript.sickPeople++;
                settingsScript.healthPeople--;
                isHeath = false;
                isSick = true;
                SpriteRenderer color = GetComponent<SpriteRenderer>();
                color.color = new Color32(219, 166, 55, 255);
            }
        }
    }

    void GetBetter()
    {
        if (isSick)
        {
            settingsScript.sickPeople--;
            settingsScript.curedPeople++;
            isSick = false;
            wasSick = true;
            SpriteRenderer color = GetComponent<SpriteRenderer>();
            color.color = new Color32(248, 141, 210, 255);
        }
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.tag == "Player")
        {
            if (collision.gameObject.GetComponent<Person>().isSick)
            {
                GetSick();
            }
        }
    }
}

Nas linhas de 1 a 3 é apresentado o namespace que é um conjunto de outros objetos que serão utilizados na programação. O namespace é equivalente às bibliotecas das programações estruturadas (os famosos #include da linguagem C, por exemplo).

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

Como em C# tudo é objeto o namespace é importante para permitir que usemos alguns tipos de atributos no nosso objeto Person. Podendo virar os atributos CircleCollider2D, GameObject e outros que são são definidos nos namespace sure estamos usando.

Na linha 5 iniciamos a descrição do nosso objeto Person. O nosso objeto terá todos os atributos e métodos descrito entre o conjunto de chaves {} (linda 6 e linha 130).

public class Person : MonoBehaviour

A linha 5 apresenta dois dos quartos pilares da POO, o encapsulamento e a herança.

A palavra public está relacionada ao encapsulamento. O encapsulamento está relacionado ao nível de segurança dos dados, permissão de acesso. Ficará mais claro quando formos falar dos atributos. Em C# os encapsulamento pode ser do tipo public, private, protected, internal e protected internal, porém classes só podem ser do tipo public ou internal, com exceção a classes definida dentro de outra classes.

A palavra class informa aquilo que estamos definindo. Neste caso é uma classe (objeto) chamado Person. Por convenção, quando defini-se o nome de um objeto a primeira letra é em maiúsculo.

Após o nome da classe encontra-se : (dois pontos) que é a simbologia em C# para indicar herança e, neste caso, o objeto Person herda os atributos e métodos do objeto MonoBehaviour.

O objeto MonoBehaviour está contido dentro do namespace UnityEngine e é responsável pelo comportamentos para o funcionamento de jogos feitos com a Unity3D.

Por causa da herança o objeto Person possui alguns métodos do objeto MonoBehaviour como, por exemplo, o método GetComponent<Rigidbody2D>() apresentado na linha 34.

Ao fazer uma inspeção cuidadosa do código do objeto Person não é possível encontrar como foi definido tal método, a pesar dele possuí-lo. Isso se deve ao fato de ser um método herdado de MonoBehaviour e a descrição do método encontra-se em MonoBehaviour. Assim como métodos um objeto também pode herdar atributos.

Iniciado a nossa classe Person podemos definir os atributos do nosso objeto. Os atributos podem ser entendidos como as propriedades que o objeto possui.

Como estamos fazendo algo semelhante ao apresentado no The Washington Post o nosso objeto precisa colidir com outros objetos e, para isso, vamos anexar um atributo que é um objeto chamado CircleCollider2D e chamá-lo de playerCollider (linha 7), pois este tipo de objeto faz parte da física da Unity3D e permite identificar as colisões. Esse atributo foi definido como private uma vez que os outros objetos não precisam interagir com este atributo.

Os objetos Person podem se interagir entre eles. caso um Person saudável entre em contato com o Person doente ele terá a chance de ficar doente. Então, vamos dar um atributo GameObject chamado otherPerson (linha 8) para que ele possa interagir com outra Person. Na Unity3D todos os objetos na cena são GameObject.

Para que o nosso objeto Person possa conhecer as configurações (se vai ficar parado ou se move, o tempo que ficará doente, e outras configurações feitas no início) vamos anexar dois atributos, o settings do tipo GameObject (linha 10) e o settingsScript do tipo Settings (linha 12).

Para que o objeto Person tenha conhecimento da física aplicada nele devemos adicionar o atributo rb2d do tipo Rigidbody2D (linha 13). Da física temos o interesse que o objeto Person tenha conhecimento da direção de seu movimento, inclusive para que ele inicie com movimento aleatório, foi adicionado os atributos movement do tipo Vector2 (linha 14) e movmentNorm do tipo float (Linda 15).

Continuando a descrição do objeto o Person, ele precisa de alguns atributos numéricos que definem o comportamento dele, como sickTime do tipo float (linha 17) que será utilizado para computar o tempo que estará doente, isolationPorcet do tipo float (linha 19) que é a taxa de isolamento (fornecido pela configuração) que será comparada com chanceToMove do tipo float (linha 21). Caso chanceToMove seja maior que isolationPorcet o objeto Person terá movimento, caso contrário não.

O objeto Person também precisa informar aos outros objetos o sei estado de saúde. Para isso foram definidos três atributos do tipo bool, sendo isHeath (linha 22) responsável por indicar que está saudável, isSick (linha 23) que indica que está doente e wasSick (linha 24) para indicar que já se curou. É importante notar que esses atributos são public, isso porque outros objeto necessitam acessar essas informações. Por exemplo, quando dois objetos Person colidem eles precisam saber se um deles está doente (isSick) e, caso um deles esteja, o que estiver saudável deve adoecer.

Para finalizar a descrição dos atributos, o objeto Person tem os atributos first (Linda 28), para indicar se é o primeiro pois ele sempre deve iniciar doente e poder se mover, e o movable (Linda 30), que indica se o objeto pode ser mover ou não.

A parte do código que contém os atributos é apresentado a seguir. No código é possível visualizar, em diversos momentos, o campo [SerializeField]. A função deste campo é permitir que a Unity3D apresente os atributos private no editor dela, o que facilita o desenvolvimento e correções do código.

private CircleCollider2D playerCollider;
private GameObject otherPerson;
[SerializeField]
private GameObject settings;
[SerializeField]
private Settings settingsScript;
private Rigidbody2D rb2d;
private Vector2 movement;
private float movmentNorm;
[SerializeField]
private float sickTime;
[SerializeField]
private float isolationPorcet;
[SerializeField]
private float chanceToMove;
public bool isHeath = true;
public bool isSick;
public bool wasSick;
[SerializeField]
private bool fist;
[SerializeField]
private bool movable;

Para finalizar o objeto Person deve ser atribuído a ele alguns métodos. Os métodos podem entendidos como ações que o objeto fazem utilizados os atributos, podendo ou não notificá-los.

O primeiro método do nosso objeto é o Start() (linha 31). Este método é chamado quando o objeto é criado na cena. Isto acontece pois o objeto Person herda outros métodos de MonoBehaviour.

Como o método Start() é executado logo após a criação do objeto é nele em que os atributos que necessitam acessar outros objetos podem assumir os dados. No caso desse projeto o método Start() foi usado para iniciar alguns atributos, indicar uma direção aleatória do movimento inicial e verifica se o objeto pode se mover.

Na sequência temos o método Update() (linha 74) que é chamado pelo MonoBehaviour cada vez que os frames da cena são atualizados. Como este método é executado baseado no tempo de execução ele foi usado para calcular o tempo que o objeto Person permanece doente e, regularmente, verifica a velocidade do objeto para acelerar ele caso ele fique muito lento.

Tanto os métodos Start() e Update() dependem de sinais enviados pela herança do MonoBehaviour. Mais podemos criar métodos exclusivos do nosso objeto que não dependem de heranças e nem de outros objetos. A exemplo temos, no objeto Person, os métodos GetSick() (linha 90) e GetBetter() (linha 107).

No método GetSick() é descrito todas as ações que o objeto Person deve realizar quando colide com um objeto doente e se adoece. Uma ação importante é atualizar o contador de pessoas saudáveis e doentes que tem no objeto Settings e, consequentemente, o gráfico.

Já o método GetBetter() descreve todas as ações que devem ser executadas quando o objeto Person deixa de estar doente que, também, inclui atualizar os contadores do objeto Settings.

Por fim, o nosso objeto Person possui o método OnCollisionEnter2D() (linha 120). Este método recebe um sinal do objeto CircleCollider2D que foi adicionado ao Person através do atributo playerCollider (linha 7). O Person executa este método sempre que colide. Caso a comissão seja com outro Person o método verifica se ele está doente e, se necessário, executa o método GetSick().

Todos os métodos do objeto Person podem ser vistos a seguir.

// Update is called once per frame
void Start()
{
    settings = GameObject.Find("Settings");
    settingsScript = settings.GetComponent<Settings>();
    rb2d = GetComponent<Rigidbody2D>();
    movement = new Vector2(Random.Range(-1f, 1f), Random.Range(-1f, 1f));
    if (Mathf.Abs(movement.x) < 0.2f)
    {
        movement.x = 1f * Mathf.Sign(movement.x);
    }
    if (Mathf.Abs(movement.y) < 0.2f)
    {
        movement.y = 1f * Mathf.Sign(movement.y);
    }
    movmentNorm = Mathf.Sqrt(Mathf.Pow(movement.x, 2) + Mathf.Pow(movement.y, 2));
    sickTime = settingsScript.sickTime;
    isolationPorcet = settingsScript.isolationPorcet;
    chanceToMove = Random.Range(0f, 1f);
    if (fist)
    {
        chanceToMove = 2;
        GetSick();
    }
    if (isolationPorcet > chanceToMove)
    {
        movable = false;
        settingsScript.stoppedPeople++;
    }
    else
    {
        movable = true;
        settingsScript.movingPeople++;
    }
    if (movable)
    {
        rb2d.AddForce(new Vector2(movement.x * 100, movement.y * 100));
        rb2d.constraints = RigidbodyConstraints2D.None;
        rb2d.constraints = RigidbodyConstraints2D.FreezeRotation;
    }
    gameObject.GetComponent<CircleCollider2D>().enabled = true;
}

// Update is called once per frame
void Update()
{
    if (rb2d.velocity.magnitude < 2f)
    {
        rb2d.AddForce(rb2d.velocity);
    }
    if (isSick)
    {
        sickTime -= Time.deltaTime;
        if (sickTime < 0)
        {
            GetBetter();
        }
    }
}

void GetSick()
{
    float myInfectionChance = Random.Range(0f, 1f);
    if (isHeath)
    {
        if ((settingsScript.chanceToGetSick > myInfectionChance) || fist)
        {
            settingsScript.sickPeople++;
            settingsScript.healthPeople--;
            isHeath = false;
            isSick = true;
            SpriteRenderer color = GetComponent<SpriteRenderer>();
            color.color = new Color32(219, 166, 55, 255);
        }
    }
}

void GetBetter()
{
    if (isSick)
    {
        settingsScript.sickPeople--;
        settingsScript.curedPeople++;
        isSick = false;
        wasSick = true;
        SpriteRenderer color = GetComponent<SpriteRenderer>();
        color.color = new Color32(248, 141, 210, 255);
    }
}

private void OnCollisionEnter2D(Collision2D collision)
{
    if (collision.gameObject.tag == "Player")
    {
        if (collision.gameObject.GetComponent<Person>().isSick)
        {
            GetSick();
        }
    }
}

Considerações Finais

Espera-se que com uma descrição mais concreta do que é um objeto em programação orientada a objeto, através de um exemplo lúdico como a criação de um jogo, possa facilitar o entendimento sobre o que é o objeto.

O foco, aqui, foi na descrição de apenas o objeto mais concreto utilizado no projeto e, mesmo assim, ainda foi descrito o uso de objetos muito abstratos, como o MonoBehaviour, GameObject, Rigidbody2D e outros. Entretanto, mesmo não conhecendo o comportamento total de todos os objetos foi possível desenvolver este projeto.

O código completo do projeto se no nosso repositório do GitHub https://github.com/visiorob/simuladordecontaminacao

Sobre Marcelo Lemos Rossi 1 Artigo
Líder do grupo de pesquisa VisioRob e apaixonado por Engenharia. Possui interesse em processamento digital de sinais, visão computacional e robótica.

Seja o primeiro a comentar

Faça um comentário

Seu e-mail não será publicado.


*