O Submundo de Wumboss é um jogo criado em Java, pensado como uma continuação do jogo O mundo de Wumpus. Nele, o herói cai em um buraco do primeiro jogo, mas milagrosamente consegue sobreviver. Agora, sua missão é explorar as cavernas abaixo do mundo de Wumpus, coletando itens que o ajudarão em sua jornada, matando inimigos e finalmente, derrotando Wumboss, que guarda a saída da caverna.
- Victor Costa Dominguite - RA 245003
- Thiago Donato Ferreira - RA 194300
Vídeo de apresentação inicial do jogo
Vídeo de apresentação final do jogo
Slides de apresentação inicial do jogo
Slides de apresentação final do jogo
Ao longo do desenvolvimento do jogo, não houve mudanças fundamentais em relação ao design inicial planejado. Porém, vale ressaltar que a complexidade de se implementar o jogo se demonstrou maior do que esperada, o que fez com que, durante seu desenvolvimento, fosse necessária a criação de mais sub-componentes (como pode ser observado no diagrama de componentes) e interfaces para conectar efetivamente os principais componentes do jogo (isto é, model, view e controller).
Algumas dificuldades enfrentadas envolveram a conexão entre componentes, sem que houvesse uma interdependência muito grande entre classes. Ou seja, a divisão das funções em componentes que funcionam independentemente um do outro foi um desafio.
Além disso, também houve difiuldade em sincronizar o view e o model, para que as mudanças que ocorressem internamente no jogo fossem imediatamente atualizadas na interface gráfica.
Como nenhum dos integrantes do grupo havia anteriormente utilizado interfaces gráficas, essa foi uma grande novidade, que gerou dúvidas em sua implementação, porém, por fim, acabou somando como um aprendizado. Semelhantemente, a noção de definir um projeto de jogo com uma arquitetura específica também foi algo novo, que gerou certas dificuldades, porém, ao fim, foi algo positivo, uma vez que reforçou a importância de se pensar melhor na organização geral do programa como um todo, antes de começar a implementá-lo, para evitar que seja necessário realizar muitos ajustes no meio do desenvolvimento.
A movimentação dos personagens foi feita baseada em direções, que foram implementadas como um "enum", facilitando consideravelmente a comunicação dos comandos do controller para o model, além de também facilitar a implementação da movimentação internamente no model.
public enum Direcao {
NORTE, LESTE, SUL, OESTE;
public static Direcao randomDir(Random r) {
return Direcao.values()[r.nextInt(4)];
}
...
public static Direcao fromString(String s) {
if(s.equals("up") || s.equals("cima") || s.equals("norte"))
return Direcao.NORTE;
if(s.equals("down") || s.equals("baixo") || s.equals("sul"))
return Direcao.SUL;
if(s.equals("left") || s.equals("esquerda") || s.equals("oeste"))
return Direcao.OESTE;
if(s.equals("right") || s.equals("direita") || s.equals("leste"))
return Direcao.LESTE;
return null;
}
...
}
O espaço do jogo é uma caverna composta de 10 salas de espaço interno 9 x 9. A formação da caverna é feita com base em 30 arquivos modelos de salas, fazendo com que a experiência do jogador seja diferente a cada vez que o jogo é rodado, uma vez que a caverna é montada de maneira diferente toda vez que o jogo é iniciado.
public static Caverna montar() {
Caverna cave = new Caverna();
ArrayList<Integer> tiposSalas = new ArrayList<Integer>();
for (int i = 0; i < Constantes.NUM_SALAS_CAVERNA - 1; i ++) {
tiposSalas.add(i);
}
// Faz com que as salas de cada tipo estejam sempre em ordem diferente
Collections.shuffle(tiposSalas);
...
// As salas de índice i são montadas com base nos tipos de sala
// que constam na lista "tiposSalas"
for (int i = 1; i < Constantes.NUM_SALAS_CAVERNA - 1; i++) {
Sala atual = SalaFactory.montar(i, tiposSalas.get(i));
cave.setSala(i, atual);
}
...
// Conecta as salas subsequentes, com passagens geradas em lugares
// aleatórios ao longo de sua borda
for (int i = 0; i < Constantes.NUM_SALAS_CAVERNA - 1; i++) {
anterior = criarPassagem(cave.getSala(i), cave.getSala(i + 1), anterior);
}
return cave;
}
...
public String[][] readSala(int tipo) throws TipoDeSalaInvalido, IOException {
String[][] res;
String path;
// Há 3 modelos disponíveis para cada tipo de sala. O modelo da sala
// é definido aleatoriamente
int modelo = Constantes.rng.nextInt(3) + 1;
if(tipo < 10)
path = dataPath + "tipo0" + tipo + "/sala0" + modelo + ".csv";
else
path = dataPath + "tipo" + tipo + "/sala" + modelo + ".csv";
try {
res = fileIO.readCSV(path);
} catch(FileNotFoundException e) {
throw new TipoDeSalaInvalido(tipo);
}
if(res == null)
throw new IOException();
return res;
}
No jogo, quando os inimigos entram no campo de visao do herói, eles são alertados e passam a perseguir o herói. Assim, toda vez que o herói se move, os inimigos também se movem em sua direção. Porém, alguns inimigos mais pesados se movem mais lentamente. O destaque do trecho a seguir se deve à simplicidade com que a atualização da movimentação dos inimigos em ritmos diferentes é implementada.
public void atualizarVisaoEInimigos() {
...
// Faz com que todos os inimigos em alerta na sala se movam em direção ao
// herói, caso o seu movimento não estiver em cooldown
for (IInimigo i : inimigosAlerta) {
// globalTimer é incrementado a cada movimento do herói e o cooldown do
// movimento simboliza com que frequência (1 ou 2) o inimigo se move
if (i != null && globalTimer % i.getCooldownMovimento() == 0)
i.moverEmDirecaoA(heroiX, heroiY);
}
}
Foi utilizado o design pattern Model - View - Controller (MVC)
Na seção Documentação dos Componentes mais abaixo, é possível encontrar diagramas que ilustram detalhadamente a conexão feita entre Model, View e Controller, que determina o pattern utilizado no desenvolvimento do jogo.
A trasmissão de comandos entre os componentes Controller e Model tem como intermediária a classe ModelAction.
public class ModelAction implements IActionParser {
private HashMap<String, IActionExecutor> mappings;
public ModelAction() {
mappings = new HashMap<String, IActionExecutor>();
}
...
// A partir de uma string padronizada, o comando é obtido e repassado
private void parseMessage(String message) {
String[] splitMessage = message.split(" ");
IActionExecutor actor = mappings.get(splitMessage[0]);
if(actor == null)
throw new MensagemInvalida("Controller", "ModelAction", message);
String action = splitMessage[1];
String[] args = new String[splitMessage.length - 2];
for(int i = 2; i < splitMessage.length; i++)
args[i-2] = splitMessage[i];
// O comando é passado para ator especificado, o qual comunicará a ação
// ao seu correspondente no model
sendAction(actor, action, args);
}
...
}
A interface gráfica do jogo foi dividida em 3 seções principais: um painel para o inventário do herói, um painel de informaçoes gerais e um para a visualização da caverna em si. Cada um desses precisa ser atualizado conforme o decorrer do jogo. Essa atualização foi unificada pela interface Observer, que permite que a classe EventCreator, responsável pela atualização do View, não tenha que saber qual painel está sendo referenciado para atualizá-lo.
public interface Observer {
public void onUpdate();
public void onUpdate(boolean reinscrever);
public String[] getInfo();
}
public abstract class EventCreator implements IEventCreator{
protected ArrayList<Observer> listeners;
// Listeners engloba todos aqueles que precisam ser atualizados
...
A construção da caverna é feita completamente usando Factories. A caverna como um todo é montada pela CaveFactory. Sendo a caverna formada de salas, a CaveFactory utiliza da SalaFactory para formar as salas, a qual, por sua vez, utiliza da CelulaFactory. Essa última, em muitos casos contém alguma Entidade, como um personagem ou item e, dessa forma, utiliza do ForegroundFactory para montar esses elementos.
public class CaveFactory {
...
public static Caverna montar() {
Caverna cave = new Caverna();
...
// SalaFactory cuidará da formação de uma sala específica
cave.setSala(0, SalaFactory.montar(0, tiposSalas.get(0)));
...
return cave;
}
...
}
public class SalaFactory {
...
public static Sala montar(int id, int tipo) {
...
Sala s = new Sala(id, Constantes.TAM_SALAS, Constantes.TAM_SALAS);
int x = 0, y = 0;
try {
String[][] template = io.readSala(tipo);
// As células da sala são montadas com base no arquivo lido
for(String[] linha : template) {
for(String celula : linha) {
// CelulaFactory cuidará da formação de uma célula específica
s.setCelula(x, y, CelulaFactory.montar(x, y, celula));
x += 1;
}
x = 0;
y += 1;
}
...
return s;
}
}
public class CelulaFactory {
...
public static ICelula montar(int x, int y, String repr) {
// A célula é montada com base na String passada a ela que a representa
Celula c = new Celula(x, y, decodeRawEntity(repr));
// Caso houver algum item ou entidade viva nessa célula, a
// ForegroundFactory é responsável por criá-lo
Entidade e = ForegroundFactory.decodeRawEntity(repr);
if (e != null) {
e.connect(Space.getInstance());
c.pushEntidade((IEntidadeDinamica) e);
}
return c;
}
}
public class ForegroundFactory {
...
public static Entidade decodeRawEntity(String repr) {
// A entidade (item ou criatura) é criada a partir da string
// que a representa
Class<? extends Entidade> classe = tabela.get(repr);
if(classe == null) return null;
Entidade result = null;
try {
result = classe.getConstructor().newInstance();
}
...
return result;
}
}
Repensando o processo de implementação do jogo, chegamos à conclusão de que a arquitetura poderia ter sido mais bem planejada previamente ao início da escrita do código, de forma a tornar o programa mais robusto e com maior independência das classes e componentes.
Ademais, há certas funcionalidades que deixamos de implementar no jogo por falta de tempo, mas que possivelmente ainda implementaremos no futuro. Dentre essas: a opção de salvar o progresso no jogo e retomar em seguida, a sala do Wumboss não sendo necessariamente a última, mais personagens, tamanho variável das salas, movimentação mais inteligente dos inimigos, geração completamente aleatória e independente de arquivos modelo da caverna, entre outras.
De maneira geral, aprendemos bastante sobre a prática de orientação a objetos com esse projeto, além de também termos tido nosso contato inicial com interfaces gráficas e com a noção de arquiteturas. Também pode-se mencionar a aquisição de uma maior prática e o desenvolvimento de nossas habilidades com a linguagem de programação Java.
item | detalhamento |
---|---|
Classes | src.src.model.GameModel, src.src.model.ModelAction |
Autores | Thiago e Victor |
Interface | IGameModel |
item | detalhamento |
---|---|
Classe | src.src.view.GameView |
Autores | Thiago e Victor |
Interface | IGameView |
item | detalhamento |
---|---|
Classe | src.src.controller.Controller |
Autores | Thiago e Victor |
Interface | IController |
OBS: As informações a respeito das interfaces associadas a cada componente encontram-se na seção abaixo
Interface provida pelo Conroller para receber comandos externos e ler arquivos
public interface IController extends IActionCreator{
public String[][] readSala(int tipo) throws TipoDeSalaInvalido, IOException;
public BufferedImage readIcon(String name) throws IOException;
public File hackFontFile(String mode);
public void setKeyboardMappings(InputMap im, ActionMap am);
public void setButtonMappings(JButton b);
}
Método | Objetivo |
---|---|
readSala |
Retorna uma matriz de strings que corresponde a uma sala lida de um arquivo CSV |
readIcon |
Retorna a imagem correspondente ao ícone solicitado |
hackFontFile |
Retorna o arquivo de uma fonte |
setKeyboardMappings |
Mapeia as teclas que devem ser observadas |
setButtonMappings |
Adiciona um botão que deve ser observado |
Interface provida pelo Model para inicializar o jogo, atualizar suas informações e obter informações do seu estado atual.
public interface IGameModel {
/* Inicializacao */
public void start();
public void setControl(IController c);
/* Interacao com os outros componentes */
/* Observer pattern */
public void subToLocal(int x, int y, Observer e);
public void subToHeroi(String info, Observer e);
public void subToItem(String item, Observer e);
/* Requerir informacoes */
/* Retorna o estado da caverna em dada posição, na seguinte forma
* String[3] = {nome do background, nome do foreground, estado}
* */
public String[] getCaveState(int x, int y);
/* Retorna o estado do herói relevante ao View
* O retorno eh apenas um numero em formato de String
* na primeira posicao do array
* */
public String[] getHeroState(String item);
public String[] getPossibleCollectableItems();
public String[] getPossibleCumulativeItems();
/* Retorna o item de nome dado, na seguinte forma
* String[3] = {descricao, estaColetado?, estaEquipado?}
* */
public String[] getItemState(String itemName);
}
Método | Objetivo |
---|---|
start |
Inicializa o jogo |
setControl |
Conecta Model ao Controller |
subToLocal |
Indica quando algo se altera no espaço |
subToHero |
Indica quando algo se altera no Heroi |
subToItem |
Indica quando algo se altera em um item do inventário |
getCaveState |
Retorna informações sobre o estado atual de uma posição na sala ativa |
getHeroState |
Retorna o estado atual do herói |
getPossibleCollectableItems |
Retorna uma lista de todos os itens únicos do inventário |
getPossibleCumulativeItems |
Retorna uma lista de todos os itens que acumulam do inventário |
getItemState |
Retorna informações sobre um certo item do inventário |
Interface provida pela espaço para atualizar suas informações, assim como obtê-las
public interface ISpace extends IActionExecutor{
public void connectHero(IHeroi hero);
public void disconnectHero();
public void destroy();
public void subToLocal(int x, int y, Observer e);
public boolean moverEntidade(int x, int y, Direcao dir);
public void addEntidade(int x, int y, IEntidadeDinamica e);
public IEntidadeDinamica removerEntidade(int x, int y);
public void atacar(IEntidadeViva e, int x, int y);
public int distanciaAte(int xIni, int yIni, int xFim, int yFim);
public void atualizarVisaoEInimigos();
public String[] estadoAtual(int x, int y);
public void refreshLocal(int x, int y);
}
Método | Objetivo |
---|---|
connectHeroi |
Obtém uma referência ao herói |
disconnectHero |
Desvincula o herói ao espaço |
destroy |
Destrói o espaço criado para recomeçar o jogo |
subToLocal |
Indica quando algo se altera no espaço |
moverEntidade |
Move uma Entidade para uma certa direção |
addEntidade |
Adiciona uma Entidade em uma certa posição da sala atual |
removerEnridade |
Remove uma Entidade de uma certa posição da sala atual |
atacar |
Faz uma entidade atacar outra |
distânciaAte |
Retorna a distância entre dois pontos na sala atual |
atualizarVisaoEInimigos |
Atualiza a visão do herói após se movimentar e a movimentação e ataque de inimigos |
estadoAtual |
Retorna informações sobre uma célula (x, y) da sala atual |
refreshLocal |
Atualiza a visibilidade de uma célula (x,y) da sala atual |
Interface provida pela caverna para obter informações básicas
public interface ICaveProperties{
public ISala getSalaAtiva();
public String[] estado(int x, int y);
}
}
Método | Objetivo |
---|---|
getSalaAtiva |
Retorna a sala ativa atualmente no jogo |
estado |
Retorna informações sobre uma célula (x, y) da sala ativa |
Interface provida pela caverna para realizar alterações nela
public interface ICave extends ICaveProperties{
public boolean moveEntidade(int x, int y, Direcao dir);
public void addEntidade(int x, int y, IEntidadeDinamica e);
public IEntidadeDinamica removeEntidade(int x, int y);
public void atacar(IEntidadeViva e, int x, int y);
public void subToLocal(int x, int y, Observer e);
public void destroy();
}
Método | Objetivo |
---|---|
moveEntidade |
Move uma Entidade para uma certa direção |
addEntidade |
Adiciona uma Entidade em uma certa posição da sala atual |
removeEnridade |
Remove uma Entidade de uma certa posição da sala atual |
atacar |
Faz uma entidade atacar outra |
subToLocal |
Indica quando algo se altera no espaço |
destroy |
Destrói a instância da caverna |
Interface provida pela Sala para obter informações a seu respeito e alterá-las
public interface ISala {
public int getTamX();
public int getTamY();
public int getID();
public String[] estado(int x, int y);
public void inativar();
public void addEntidade(int x, int y, IEntidadeDinamica e);
public IEntidadeDinamica removeEntidade(int x, int y);
public boolean outOfBounds(int x, int y);
public ICelula getCelula(int x, int y);
public void subToLocal(int x, int y, Observer e);
public void destroy();
}
Método | Objetivo |
---|---|
getTamX |
Retorna a largura da sala |
getTamY |
Retorna a altura da sala |
getID |
Retorna o ID da sala |
estado |
Retorna informações sobre uma célula (x, y) |
inativar |
Torna a sala inativa |
addEntidade |
Adiciona uma Entidade em uma certa posição |
removeEnridade |
Remove uma Entidade de uma certa posição |
outOfBounds |
Verifica se uma posição (x,y) está dentro da sala |
getCelula |
Retorna a célula numa posição (x,y) |
subToLocal |
Indica quando algo se altera na sala |
destroy |
Destrói a sala |
Interface provida pela Célula para obter informações a seu respeito e alterá-las
public interface ICelula {
public IEntidadeEstatica getBackground();
public void setBackground(IEntidadeEstatica e);
public void pushEntidade(IEntidadeDinamica ent);
public IEntidadeDinamica popEntidade();
public IEntidadeDinamica peekEntidade();
public boolean isVisivel();
public void setVisivel(boolean visivel);
public void inativar();
public void destroy();
public void subscribe(Observer o);
public String[] estado();
}
Método | Objetivo |
---|---|
getBackground |
Retorna a Entidade de fundo da célula |
setBackground |
Altera a Entidade de fundo da célula |
pushEntidade |
Altera a Entidade principal da célula |
peekEntidade |
Retorna a Entidade principal da célula |
isVisivel |
Retorna se a célula está visível |
setVisivel |
Altera o valor de "visível" |
inativar |
Torna a célula inativa |
destroy |
Destrói a célula |
subscribe |
Indica quando algo se altera na célula |
estado |
Retorna informações sobre a célula |
Interface provida por alguém que receba os comandos e os repasse para um ActionParser
public interface IActionCreator{
public void connect(IActionParser agent);
public void disconnect();
}
Método | Objetivo |
---|---|
connect |
Conceta o ActionCreator a um ActionParser |
disconnect |
Desconceta o ActionParser |
Interface provida por alguém que repasse as ações para um ActionExecutor
public interface IActionParser{
public void connect(String name, IActionExecutor agent);
public void disconnectFromAll();
public void sendMessage(String action, String... args);
}
Método | Objetivo |
---|---|
connect |
Conceta o ActionParser a um ActionExecutor |
disconnectFromAll |
Desconceta todos ActionExecutor |
sendMessage |
Manda uma ação para um ActionExecutor |
Interface para passagem da ação para o objeto que a realizará
public interface IActionExecutor{
/* Send a message to this object */
public void sendMessage(String action, String... args);
}
Método | Objetivo |
---|---|
sendMessage |
Manda uma ação para o objeto que irá realizá-la |
Interface parao manuseio de listeners
public interface IEventCreator {
public void subscribe(Observer e);
public void disconnectAll();
}
Método | Objetivo |
---|---|
subscribe |
Adiciona um listeners à lista de observers |
disconnectAll |
Limpa a lista de observers |
Interface para a atualização de observers
public interface Observer {
public void onUpdate();
public void onUpdate(boolean reinscrever);
public String[] getInfo();
}
Método | Objetivo |
---|---|
onUpdate |
Faz com que o Observer atualize |
getInfo |
Obtém informações a respeito do Observer |
Interface provida pelo View para sua inicialização e alteração
public interface IGameView {
public void setControl(IController c);
public void setModel(IGameModel g);
public void montarView();
public void showView();
public void rebuild();
public static void setFeedMessage(String message) {
InfoPanel.setFeed(message);
}
public static void setFeedMessage(String message, Color c) {
InfoPanel.setFeed(message, c);
}
Método | Objetivo |
---|---|
setControl |
Conecta ao Controller |
setModel |
Conecta ao Model |
montarView |
Monta os painéis do view |
showView |
Torna os painéis visíveis |
rebuild |
Remonta o View |
setFeedMessage |
Altera a mensagem mostrada no painel de informações do jogo |
As demais interfaces foram utilizadas como forma de desacoplar as partes do programa, mas não possuem um papel tão significativo para a estrutura do jogo como as descritas acima, por isso, não foram mencionadas.
Todas as exceções são independentes, não herdando uma da outra.
Classe | Descrição |
---|---|
IDInvalido | Indica que houve tentativa de acesso a uma sala de ID inválido na caverna. |
MensagemInvalida | Indica que foi passada uma String inválida contendo o comando de ação interpretado pelo Controller |
SemControllerNaMontagem | Indica que houve tentativa de montagem da caverna sem haver Controller disponível para ler arquivos |
SemReferenciaAComponente | Indica que não há referência ao Controller ou View no Model |
ErroDeInteracao | Indica que houve erro de interação entre Entidades no Model |