Um dos meus estilos de jogos favoritos é o "turn-based tactics RPG", que consiste em batalhas onde cada personagem (ou tipo de personagem) tem características e habilidades diferentes, que sendo bem exploradas, podem garantir a vitória. Além disso, existem as táticas "militares", que são a essência do jogo. Por exemplo: uma boa formação tática consiste em posicionar a infantaria na vanguarda e a artilharia, que não tem uma boa defesa, na retaguarda, de onde pode disparar projéteis nos inimigos em segurança.
Jogos desse tipo são mais ou menos como jogos de tabuleiro, mas sem a chatice de ficar escrevendo em fichas de papel ou guardar todas as pecinhas no final do jogo. Mesmo assim, há quem ache esse tipo de jogo chato, e realmente, tem horas que fica chato, mas depois passa. :)
Você já ouviu falar da série Final Fantasy Tactics ou Fire Emblem? São exemplos de jogos desse gênero. Veja abaixo o gameplay de Final Fantasy Tactics Advance 2 e entenda melhor:
Observe o que acontece quando um personagem é selecionado: uma área azul aparece. Ela marca quais os tiles (posições no cenário) para onde esse personagem pode se mover ou atacar. Essa área é influenciada pelo relevo, pelos personagens próximos e os atributos do próprio personagem, entre outros fatores. E o algoritmo que cria essa área não é tão complicado. Veja abaixo como ele funciona:
O grid
Primeiro é preciso criar uma matriz, que vai representar o cenário do jogo. Neste exemplo criei uma matriz 6 x 5 e populei com zeros. Eles representam tiles vazios, enquanto outros números representam obstáculos. Para esse exemplo vou considerar que não existem obstáculos:
var grid = [
[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,0]
];
E graficamente, essa matriz pode ser representada da seguinte forma, com o personagem no tile [2,2] (veja a referência no gráfico adjacente):
Podemos agora adicionar obstáculos, como pedras, água, ou outros personagens, que nesse exemplo simples serão representados pelo valor "1". Em casos mais complexos, o tipo de terreno, como deserto e pântano, também pode influenciar negativamente na movimentação. Isso também pode variar de acordo com o tipo de personagem. Por exemplo, personagens montados em cavalos podem ter sua movimentação reduzida em terrenos arenosos (ver Shinning Force 2). Já personagens voadores não sofrem nenhuma influência. Legal, hein? :)
var grid = [
[0,0,1,1,1,1],
[0,0,0,0,1,1],
[1,0,0,0,0,1],
[0,0,1,0,0,0],
[0,0,0,0,0,0]
];
Assim:
Onde os tiles com "1" não podem ser acessados.
A posição
A posição na matriz é dada pelos valores X e Y no grid, de acordo com o primeiro gráfico acima. Então, o personagem está na posição (2,2). Esses dois valores são passados como parâmetros para o algoritmo, que vai colorir a área de acordo com eles. Mas só isso não basta. O algoritmo precisa saber quando parar de colorir os tiles.
O personagem selecionado deve possuir atributos, como força, defesa, etc. O atributo necessário para nosso algoritmo funcionar é o movimento, que vai determinar o quão grande deve ser a área selecionada. Vamos supor dois casos em que esse personagem tenha um movimento igual a 1, e 2, respectivamente. A áreas serão coloridas da seguinte forma:
O algoritmo deve saber quais tiles deve colorir, tendo como base apenas a posição e o atributo Movimento do nosso personagem, seguindo o padrão "losango" acima. Ele também deve evitar casos como o ilustrado abaixo, com obstáculos:
O código
Uma das possíveis soluções para este problema é:
- Começando a partir do tile onde o personagem está, verificar se é acessível e está dentro do limite do cenário.
- Repetir o processo acima para os quatro tiles adjacentes, e assim por diante, até atingir a área máxima.
Vamos criar uma função recursiva (i.e. chama a si mesma) para essa tarefa. Ela vai receber os valores x, y
e mov
como parâmetros.
function tile_walk(x, y, mov) { }
Agora escreveremos os testes para assegurar que os tiles corretos sejam selecionados. O primeiro teste verifica se o tile atual está além dos limites do cenário, onde WIDTH
e HEIGHT
armazenam os valores 6 e 5, respectivamente, no nosso exemplo:
if (0 > tile.x >= WIDTH || 0 > tile.y >= HEIGHT)
return;
O segundo teste simplesmente verifica se o tile está livre. Obviamente, essa regra depende muito do jogo.
if (tile.blocked)
return;
Finalmente, um último teste para saber se o tile atual ainda não está na lista de tiles "acessados", o que significa que ele ainda não foi verificado pelo algoritmo. Se não estiver, o adiciona. Neste exemplo, essa lista serve apenas para lembrar quais tiles foram marcados para depois apagá-los quando uma nova área precisar ser selecionada. Logo após, chama-se a função tile_walk
para os quatro tiles adjacentes. Os testes acima serão aplicados novamente para cada um deles, e assim sucessivamente, até que alguma das condições não seja atendida.
E finalmente, o código completo e a demonstração:
function tile_walk(x, y, mov) {
if (0 > tile.x >= WIDTH || 0 > tile.y >= HEIGHT)
return;
if (tile.blocked)
return;
if (tile not in open_tiles ) {
open_tiles.insert(tile);
tile_walk(x+1, y, mov-1);
tile_walk(x-1, y, mov-1);
tile_walk(x, y+1, mov-1);
tile_walk(x, y-1, mov-1);
}
}
tile_walk(X, Y, MOV);
Demonstração em Javascript
O código dessa demonstração difere em alguns pontos, mas não altera a ideia. Clique nos tiles para testar!