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.
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.
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.
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.
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.
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.
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.
[sourcecode language=\\\”java\\\”]
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);
}
}
[/sourcecode]
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.
[sourcecode language=\\\”java\\\”]
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);
}
}
[/sourcecode]
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.
[sourcecode language=\\\”java\\\”]
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);
}
}
[/sourcecode]
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.
[sourcecode language=\\\”java\\\”]
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);
}
}
[/sourcecode]
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
Deixe um comentário