Programação orientada a objetos: Herança e polimorfismo – Parte 2

No primeiro artigo mostrei uma implementação de lista encadeada que explora o uso de herança e polimorfismo. Vimos como este recurso nos permite construir classes coesas e que colaboram entre si para realizar tarefas complexas. Nesta continuação trago a implementação do jogo da cobrinha utilizando os mesmos conceitos.

Jogo da Cobrinha

Modelando a solução

O jogo da cobrinha consiste em controlar uma cobra em uma área de jogo retangular que não pode ser ultrapassada. O objetivo é marcar o maior número de pontos comendo maçãs e impedindo que a cobra ultrapasse a área delimitada ou que encoste em si mesma.

Screenshot de um jogo da cobrinha em Java

Screenshot do jogo da cobrinha em Java

O esquema que implementei segue o seguinte fluxo. Pensando no estado inicial do jogo, temos a cobra com seu tamanho inicial deslocando-se para a direita.

Representação da cobrinha

Representação da cobrinha

Cada quadrado representa um pedaço do corpo da cobra e as setas representam as direções correntes dos nós. Cada pedaço deve se mover para a direita, e o pedaço imediatamente após deve seguí-lo.

Agora, se o primeiro pedaço muda de direção, deve existir um meio de sinalizar aos outros que eles devem fazer uma curva nos próximos movimentos e esta informação deve se propagar até o final, fazendo com que todos os nós da cobra sigam a mesma trajetória.

Colaboração entre os objetos no jogo da cobrinha

Colaboração entre os objetos no jogo da cobrinha

Reparem que o primeiro nó recebe diretamente o comando para mudar de direção, e em seguida propaga este movimento através da mensagem “siga-me” para os nós seguintes. Os nós que pertencem a cauda da cobra se comportam de maneira diferente ao receber a notícia de que devem seguir o nó da frente. Ao contrário da cabeça, que muda sua direção imediatamente e depois move-se, os nós da cauda primeiro movem-se em sua direção atual (indicada pela seta grande), notifica o nó seguinte para que ele o siga na mesma direção, e apenas ao final muda sua direção segundo a orientação do nó da frente. Fazendo isto, os nós da cauda fazem um último movimento na direção corrente antes de fazer a curva na próxima rodada. Após esta rodada a estrutura da cobra deve ficar igual a figura abaixo.

Posição dos pedaços da cobra após a curva

Posição dos pedaços da cobra após a curva

Para fazer com que a cobra aumente em um quadrado o seu tamanho ao comer uma maçã basta que desloquemos todos os blocos menos o último. Isto deixará um espaço vazio entre o último e o penúltimo bloco. Então, basta criar um novo bloco para preencher este espaço.

Colaboração entre os objetos no jogo da cobrinha ao comer uma maçã

Colaboração entre os objetos no jogo da cobrinha ao comer uma maçã

Percebam que quando a notificação chega no último nó, ao invés de ele mover-se para seguir o restante do corpo, ele cria um novo nó igual a ele e executa as movimentações neste nó, fazendo com que o novo nó assuma a posição que o último deveria assumir se a cobra não tivesse comido a maçã. Desta maneira adicionamos mais um bloco ao corpo da cobra.

Implementando a solução

Para implementar esta solução farei uso de uma simples game engine que construí em Java. Esta engine foi construída com o único propósito de demonstração e lhe faltam vários recursos de uma engine com qualidade de produção. Ela possui um game loop básico e capacidades primitivas de renderização que vai permitir que demonstremos a implementação do nosso jogo. Detalharei neste artigo apenas os aspectos relacionados a herança e polimorfismo. Disponibilizarei todo o código fonte caso vocês queiram se aprofundar no restante da implementação.

Considere o diagrama de classes abaixo para a implementação do jogo.

Diagrama de classes do jogo da cobrinha

Diagrama de classes do jogo da cobrinha

Todo objeto no nosso jogo possui uma posição na tela e a capacidade de se desenhar. Representaremos nossos objetos através da classe GameObject. A maçã é um exemplo de objeto simples que possui apenas uma posição e nenhum comportamento. É representada pela classe Apple.

Cada pedaço da cobra é um GameObject, porém possui características específicas como uma direção, a capacidade de se mover e de seguir a trilha do pedaço a sua frente. Representamos os pedaços da cobra através da classe SnakePart. Possuímos três partes específicas distintas que são a cabeça, o corpo e a cauda (pedaço final) representados respectivamente pelas classes Head, Body e Tail. Por fim, temos o personagem do jogo representado pela classe Snake.

Agora, vamos ao código fonte.

public class Snake {

    private static final Color DEFAULT_COLOR = Color.GREEN;

    private Head head;
    private Vector direction;

    public Snake(int x, int y, Vector direction) {
        head = new Head(x, y, direction);
        this.direction = direction;
    }

    public void update(boolean grow) {
        head.follow(direction, grow);
    }

    public Vector nextPosition() {
        return head.getPosition().add(direction);
    }

    public void render(RenderingContext rc) {
        head.render(rc, DEFAULT_COLOR);
    }

    public void turnTo(Vector turnToDirection) {
        if (!turnToDirection.isOpposite(direction)) {
            direction = turnToDirection;
        }
    }

    public boolean contains(Vector position) {
        return head.contains(position);
    }
}

A classe Snake fornece um construtor através do qual a nossa classe de jogo pode inicializar a cobra em uma posição e direção específicos dentro da área de jogo. Toda vez que um game loop acontece, nossa classe de jogo irá pedir para que o objeto Snake atualize sua posição. Para isto, a classe fornece o método update. O método nextPosition é utilizado pela classe de jogo para testar quando a cobra vai colidir com uma maçã ou atingir os limites da área de jogo. O método render é utilizado para renderização. Quando um usuário interage com o jogo, a classe de jogo detecta esta interação e utiliza o método turnTo para sinalizar que a cobra de mudar de direção no seu próximo update. O método contains é utilizado para saber se a cobra está localizada em uma coordenada específica dentro da área de jogo.

public class Head extends SnakePart {

    private SnakePart next;

    public Head(int x, int y, Vector direction) {
        super(x, y, direction);
        next = new Body(x - 1, y, direction).withTail();
    }

    @Override
    public SnakePart follow(Vector direction, boolean grow) {
        changeDirection(direction).move();
        next = next.follow(getDirection(), grow);
        return this;
    }

    @Override
    public boolean contains(Vector position) {
        if (!super.contains(position)) {
            return next.contains(position);
        } else {
            return true;
        }
    }

    @Override
    public void render(RenderingContext rc, Color color) {
        rc.renderObjectAt(this, color);
        next.render(rc, color);
    }
}

O construtor da cabeça da cobra recebe sua posição e direção iniciais e cria o restante de seu corpo, consistindo em um pedaço de corpo e uma cauda no final.

No método follow vemos o polimorfismo em ação. Conforme vimos na seção de modelagem, cada pedaço se comporta de maneira diferente na hora de se mover. A cabeça, no caso do código acima, muda sua direção imediatamente e em seguida se move. Após mover-se, envia uma mensagem para que o próximo pedaço o siga na mesma direção. O método follow possui um boleano que indica se a cobra comeu uma maçã ou não. Isso sinaliza para os pedaços consecutivos que eles devem tomar ações necessárias para crescer. Reparem também como utilizamos uma cadeia de invocação nos métodos render e contains para iterar por todo o corpo da cobra executando ações como renderizar ou fazendo testes para saber se algum pedaço está sobre uma coordenada específica na área de jogo.

public class Body extends SnakePart {

    private SnakePart next;

    public Body(int x, int y, Vector direction) {
        super(x, y, direction);
    }

    public Body withTail() {
        Vector tailPosition = getPosition().add(getDirection().getOpposite());
        next = new Tail(tailPosition.x(), tailPosition.y(), getDirection());
        return this;
    }

    public Body withTail(SnakePart tail) {
        next = tail;
        return this;
    }

    @Override
    public SnakePart follow(Vector direction, boolean grow) {
        next = next.follow(getDirection(), grow);
        move().changeDirection(direction);
        return this;
    }

    @Override
    public boolean contains(Vector position) {
        if (!super.contains(position)) {
            return next.contains(position);
        } else {
            return true;
        }
    }

    @Override
    public void render(RenderingContext rc, Color color) {
        rc.renderObjectAt(this, color);
        next.render(rc, color);
    }
}

O método follow da classe Body, que representa os pedaços intermediários do corpo da cobra, funciona de maneira diferente da cabeça. Ao invés de mudar de direção imediatamente, ele precisa andar uma última vez na sua direção corrente e apenas depois mudar de direção. Mas antes disto, ele se certifica de enviar uma mensagem avisando que o próximo pedaço deve segui-lo.

Percebam mais uma vez como esta cadeia se comporta polimorficamente. A cabeça possuía uma referência para uma SnakePart, e não para um Body. E o mesmo acontece com Body. Não sabemos se next é uma referência para um outro Body ou um Tail. Porém, graças ao polimorfismo, podemos chamar o método follow sabendo que independente de para qual classe concreta next esteja apontando, o método correto será chamado.

public class Tail extends SnakePart {

    public Tail(int x, int y, Vector direction) {
        super(x, y, direction);
    }

    @Override
    public SnakePart follow(Vector direction, boolean grow) {
        SnakePart partToReturn;

        if (grow) {
            partToReturn = new Body(getPosition().x(), getPosition().y(), getDirection()).withTail(this);
        } else {
            partToReturn = this;
        }

        partToReturn.move().changeDirection(direction);
        return partToReturn;
    }

    @Override
    public void render(RenderingContext rc, Color color) {
        rc.renderObjectAt(this, color);
    }
}

Por fim, a implementação do método follow da classe Tail, que é o fim da cauda da cobra, testa o boleano que indica se a cobra comeu ou não uma maçã. Se verdadeiro, a cauda cria mais um pedaço com posição e direção iguais as suas. Ao final, se um novo pedaço foi criado, este deve fazer o movimento, o que fará com que ele ocupe o gap entre a cauda e o resto do corpo. Reparem que a cauda, assim como o corpo, primeiro se move e depois muda de direção.

Código-fonte

https://github.com/marceloandradep/tasafo-oop

Jar executável do Jogo da Cobrinha

https://drive.google.com/file/d/0ByHQ4AlxEonDM3dIUDRza0VMblU/view?usp=sharing

Anúncios

3 comentários sobre “Programação orientada a objetos: Herança e polimorfismo – Parte 2

  1. Pingback: Programação orientada a objetos: Herança e polimorfismo – Parte 1 | Blog do Tá safo!

  2. O link que eu coloquei no post vai te levar pro meu github. Nessa página tem um botão no canto inferior direito escrito “Download ZIP”. Clica nele que o github vai empacotar tudo num zip pra você fazer o download.

    Curtir

O que tu achas?

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s