Pontifícia Universidade Católica do Rio Grande do Sul
Faculdade de Informática — Laboratório de Programação II
Trabalho Prático 2
Tank Escape
1 Instruções
-
O trabalho deve ser feito em duplas ou individualmente.
-
Cada dupla deve fazer o download do arquivo zip com a estrutura básica necessária para implementar este trabalho, que está disponível neste link.
-
ATENÇÃO: para quem for trabalhar em máquina pessoal no Linux, é preciso instalar os seguintes pacotes: libsdl1.2-dev e libsdl-image1.2-dev
-
As classes já implementadas não devem ser alteradas, em especial as classes que manipulam a parte gráfica do trabalho - exceto as classes mencionadas na definição abaixo.
2 Enunciado
O trabalho consiste em implementar o jogo Tank Escape, onde o
jogador controla um tanque e tem o objetivo de fugir de um ambiente
definido. Porém, há diversos adversários no caminho - estes podem ser
evitados, ou destruídos. Outro objetivo do trabalho é praticar a
programação C++, com herança e polimorfismo. As próximas seções
descrevem os diversos aspectos do trabalho.
2.1 Ambiente
O ambiente do jogo é representado na forma de uma matriz, onde cada
elemento equivale a uma posição no mapa de jogo. Neste mapa pode haver
dois tipos de objetos:
-
áreas em branco, onde se pode caminhar livremente;
-
paredes, que bloqueiam a passagem e a visão.
O jogador sai de uma posição específica e precisa chegar na saída (veja a figura
1↑ para entender). O tanque do jogador pode se deslocar nas 4 direções, usando as setas do teclado.
2.2 Adversários
Há 3 tipos de adversários nesse ambiente:
-
Tanque comum: fica andando aleatoriamente para qualquer direção. Se
"enxergar" o tanque do jogador, entra no modo de ataque. Esse modo
consiste em 1) tentar chegar o mais próximo possível do jogador e 2)
atirar, se houver uma linha de tiro livre. O tanque inimigo só atira um
projétil por vez.
-
Torre de comando: é fixa no ambiente, porém é capaz de atirar diversos
projéteis simultaneamente. É muito mais resistente do que um tanque
inimigo.
-
Tanque robô: esse tipo de tanque é capaz também de "ouvir" os movimentos
do jogador, e entra no modo de ataque se isso acontecer.
O deslocamento dos tanques inimigos no modo de ataque pode ser feito de
várias formas, melhores ou piores. Para fazer da forma 100% correta,
seria preciso o estudo de algoritmos de Inteligência Artificial, como o
A* - são os chamados algoritmos de
pathfinding -
não é essa a nossa intenção. Portanto, o objetivo aqui é improvisar, criando um algoritmo que seja razoável o suficiente para o jogo.
"Enxergar" consiste em se ter uma linha de visada direta até o jogador.
Traça-se então uma linha imaginária do inimigo ao jogador: se não houver
uma parede no caminho, o inimigo está "enxergando" o jogador.
"Ouvir" segue o mesmo princípio, porém o adversário "ouve" o jogador se
este se movimentar enquanto o adversário estiver próximo o suficiente.
Essa distância pode ser definida por experimentação (por exemplo, 5 ou
10 unidades).
3 Detalhes técnicos
Deve ser obrigatoriamente utilizado o
framework fornecido no link a seguir:
tank-escape-base.zip. Este
framework
implementa a funcionalidade básica de jogo, incluindo controle da
janela gráfica, comandos via teclado, suporte para desenho de imagens,
etc. O trabalho deve explorar os conceitos de herança e polimorfismo.
O framework fornece um conjunto de classes básicas para realizar
todas essas funções, porém algumas estão incompletas e outras devem ser
implementadas integralmente no trabalho. As seções a seguir descrevem
essas classes.
O controle principal do jogo é feito pela classe CGame, porém esta não deve ser alterada: a implementação da lógica de jogo em si deve ser feita na classe Play.
Um mapa (ambiente de jogo) é definido por uma classe abstrata Mapa:
class Mapa
{
public:
virtual void loadMap(string arquivo) = 0;
virtual void draw() = 0;
virtual bool isEmpty(const Point& ponto) const = 0;
virtual int getWidth() = 0;
virtual int getHeight() = 0;
virtual bool checkCollision(CImage* obj) = 0;
};
Essa classe é estendida pela classe MapaTexto, que implementa boa parte dessa funcionalidade. Você deve apenas implementar o método loadMap, que carrega a definição do mapa a partir de um arquivo texto com o seguinte formato:
dim 30 20
1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1
1 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 1 0 0 1 0 1 0 0 1 0 1
1 0 0 0 0 0 0 0 1 0 0 0 0 0 1 1 1 1 0 1 0 0 1 0 1 0 0 1 0 1
1 0 1 1 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 1 0 0 1 0 1
1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 0 1 0 1
1 0 0 0 0 0 0 0 1 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 1 0 0 1 0 1
1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1
1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1
1 0 0 0 0 1 0 0 0 0 0 1 1 1 1 0 0 1 0 1 1 1 1 1 1 0 0 0 0 1
1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1
1 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 1
1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 1
1 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 1 1 1 0 0 1 0 0 1
1 0 0 0 0 0 0 1 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 0 0 1
1 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 1
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
Observe que após as informações numéricas vem o desenho do mapa, feito
com '0's e '1's: obviamente, '1's representam as paredes. Por uma
questão de princípio, o jogador SEMPRE começa na parte de cima do mapa
(primeira linha), na primeira coluna que estiver vazia. A saída do mapa é
qualquer outro local onde não exista uma parede na borda (no exemplo, o
último caractere da penúltima linha).
3.3 Inimigo
A partir da classe abstrata
Enemy, você deve implementar os 3 tipos de inimigos descritos na seção
2.2↑:
class Enemy
{
public:
virtual void draw() = 0;
virtual void move() = 0;
private:
CImage* img;
};
Ou seja, deve haver um método draw capaz de desenhar o inimigo, e um método move
capaz de movimentá-lo (as torres não se movimentam). Observe que
provavelmente será necessário ter acesso ao mapa, bem como à posição
atual do jogador - para implementar isso, você pode alterar a classe
abstrata como achar mais conveniente.
Não esqueça que o jogo precisa ter uma lista de inimigos para desenhar e
movimentar - provavelmente, o melhor lugar para incluir isso é na
classe Play.
3.4 Imagens
A classe CImage implementa uma imagem que pode ser livremente
posicionada pelo cenário. Ela pode ser usada para montar ambientes, ou
mesmo para desenhar personagens (nosso caso). Por esse motivo há um
ponteiro para CImage na classe Enemy: presume-se que qualquer tipo de inimigo terá um desenho associado. No método draw, você pode apenas desenhar essa imagem, ou fazer algo mais.
Métodos úteis dessa classe incluem:
-
loadImage: recebe um nome de arquivo, e tenta carregar a imagem do arquivo correspondente no disco.
-
setPosition: permite ajustar a posição (x,y) da imagem - estão disponíveis também setX e setY, para ajustes individuais.
-
setXspeed, setYspeed: permite ajustar a velocidade de deslocamento da imagem (em pixels por segundo).
-
draw: desenha a imagem na posição corrente
-
update: atualiza a posição, de acordo com a velocidade atual. O
método precisa receber o tempo decorrido desde a última chamada (medido
em milissegundos) - o framework já implementa isso, através da consulta getUpdateInterval da classe CGame.
-
bboxCollision: verifica a colisão entre a imagem e outra imagem
passada como parâmetro. A colisão é feita considerando as dimensões de
cada imagem, e não apenas as partes não-transparentes.
Por fim, vale a recomendação tradicional: lembre-se que uma das
vantagens do paradigma de OO é a facilidade de modelar, implementar e
testar cada classe individualmente: se o problema parece complicado
demais a princípio, tente imaginar as suas várias partes, e modele as
classes mais simples em um primeiro momento.
4 Critérios de Avaliação
Leia com atenção os critérios de avaliação:
4.1 Funcionamento
-
Carrega e exibe corretamente o mapa a partir de um arquivo texto (use os arquivos no .zip para testar sua aplicação).
-
Implementa adequadamente o comportamento de cada inimigo.
-
Finaliza o jogo quando o tanque do jogador chegar na saída, ou quando for pego.
4.2 Código
-
Toda comunicação com o usuário deve ser realizada através da interface
gráfica, não deve haver entrada nem saída de dados via console.
-
A implementação deve seguir as orientações dadas em aula quanto a
convenções C++ para nomes de identificadores e estrutura das classes (ou
seja, nomes de classes começam com letra maiúscula, atributos, métodos e
variáveis em minúsculas).
-
É obrigatório explorar herança e polimorfismo na implementação.
-
O código deve ser indentado (o Code::Blocks e qualquer IDE moderna faz
isso automaticamente). Código não indentado sofrerá descontos na nota.
-
Não serão aceitos trabalhos com erros de compilação. Programas que não compilarem corretamente terão nota ZERO automaticamente.
4.3 Entrega
-
Data de entrega no Moodle e apresentação: Conforme agenda da disciplina até o horário da aula (impreterivelmente, não haverá adiamentos).
-
Os trabalhos são em duplas. Os arquivos contendo o projeto completo do Code::Blocks devem ser compactados e submetidos pelo Moodle até a data e hora especificadas. ENVIE APENAS ARQUIVOS .ZIP, ou seja, não envie 7z, rar, tar.gz, tgz, tar.bz2, etc.
-
A nota do trabalho depende da apresentação deste no laboratório, na data
marcada. Trabalhos entregues mas não apresentados terão sua nota
anulada automaticamente. Durante a apresentação será avaliado o domínio
da resolução do problema, podendo inclusive ser possível invalidar o
trabalho quando constatada a falta de conhecimento sobre o código
implementado.
-
A cópia parcial ou completa do trabalho terá como consequência a atribuição de nota ZERO ao trabalho dos alunos envolvidos. A verificação de cópias é feita inclusive entre turmas.