segunda-feira, 28 de abril de 2014

O War Legacy - Parte 1

Olá pessoal, aqui é o Lince. Bem vindos de volta.
Como vocês estão?

Eu tinha outro post planejado para hoje, mas ele estava demandando muita pesquisa e não tive tempo de terminá-lo a tempo. Então, como estou prestes a mudar novamente o paradigma de programação do War Legacy, resolvi falar sobre os tipos de programação usado nos desenvolvimento de jogos, com ênfase nos que eu usei com o WL.

Eu sempre programei em C++ usando a engine Allegro.

O War Legacy nasceu de uma forma bem peculiar, eu estava começando a trabalhar com o Allegro e quis explorar suas possibilidades. Uma delas, a que fez nascer o War Legacy, foi uma tentativa de mover o cursor usando o joystick: não me lembro de ter conseguido ou não fazer isso, eu parei de tentar porque fiquei fascinado com a experiência com o joystick e acabei me distraindo bastante com isso.

A principio criei figuras geométricas que se moviam na tela a comando do controle, depois implementei um sistema de inercia para os corpos, e por ultimo resolvi arma-los. Agora eram vários triângulos que poderiam possuir uma arma simples ou dupla para atingir os outros triângulos. Consequentemente, as (agora chamadas) naves também possuíam um medidor de vida, e, as que não eram controladas por mim, uma inteligência artificial.

Como as primeiras versões do jogo surgiram de um amontado de ideias, o código resultante é um amontado de algoritmos e lógicas pouco explicativas. Apesar de possuir classes, não posso dizer que o código é orientado a objetos. Não existia nenhum tipo de hierarquia de herança, nem comunicação entre classes, nem nenhum outro conceito inerente à programação orientada a objetos. As classes eram simples recipientes de dados e métodos para lidar com os dados de acordo com as entradas dadas.

Por exemplo, a classe “Player”, que lida com todas as informações relacionadas às naves no jogo, possui diversas informações como: identificador único, posição da nave, quantidade de vida máxima e restante, velocidade, direção, se está vivo ou morto e uma série de outras informações (trinta no total). Essa classe também possui métodos como show() que recebe com parâmetro uma imagem na qual a nave deve ser renderizada, shoot() que verifica se a nave está pronta para atirar e, caso esteja, instância balas nas posições devidas, e detect_collide_b() que recebe como parâmetro uma bala instanciada e retorna verdadeiro ou falso para se a nave está colidindo com a bala.

Tudo bem simplório até então. Já ouvi dizer que um sistema para jogos é bom se funcionar, meu sistema funciona, mas eu não estava satisfeito com isso. Era extremamente difícil dar manutenção ao código e adicionar coisas novas quando queria. Tudo era muito duro e pouco padronizado.

Outro detalhe importantíssimo é que, antes do War Legacy, eu nunca havia estudado programação para desenvolvimento de jogos, então não conhecia nenhuma prática ou técnica que pudesse me ajudar nesse aspecto, tudo foi feito de forma bem empírica e sem nenhum fundamento orientado. Devo dizer que fico feliz por tudo ter funcionado mesmo assim.

Aqui está um pseudocódigo do laço principal do jogo:


Algumas partes do código foram ocultadas ou simplificadas ao máximo para não tornar a leitura enfadonha, enfim.

Não sei se todos que leem aqui estão familiarizados com psedocódigos ou com programação em geral, então tentarei ser o mais didático possível.

De qualquer forma, não vou me extender muito na questão básica de programação, meu foco aqui é a programação usada no War Legacy. Se alguém precisar de aulas base de lógica, algoritmos ou C/C++ para entender o conteúdo postado aqui é só pedir nos comentários, ficarei feliz em fazer alguns posts só com esses temas.

Pseudocódigos podem ser escritos em forma de texto como eu fiz ou em forma de fluxograma:

Fluxograma do primeiro bloco do pseudocódigo

Para finalizar essa primeira parte vou explicar algumas peças chave do laço principal:

Cada objeto "Player" possui três variáveis centrais para o controle da vida da nave durante o jogo: vida, vidaMaxima e vivo? (Este último está com uma interrogação porque quero especificar que é uma variável booleana, ou seja, adquire o valor verdadeiro ou falso).
Quando o jogo começa, todas as naves possuem vida = vidaMaxima = quantidade de vida configurada para as naves no início da partida, além de vivo? ter o valor verdadeiro.
Em cada iteração do laço, as funções de movimento e renderização de cada nave só é chamada caso o valor de vivo? seja verdadeiro.
Sempre que a nave toma dano é retirado um valor da variável vida. Quando o valor dessa variável chega a zero, coloca-se o valor falso para vivo? e reproduz um som de explosão.
Eu poderia checar se a nave está "viva" apenas verificando se o valor de vida é maior que zero, mas com essa abordagem eu não poderia tocar o som de explosão. A explosão só deve ser ouvida uma vez, que é o exato momento em que a nave, ainda considerada "viva" (pois sua variável vivo? está verdadeira), verifica que tem um valor para vida igual ou menor que zero e "percebe" que deve explodir naquele momento.
Basicamente a mesma coisa acontece com as naves controladas por A.I.

O sistema de colisões está logo em seguida. É feito uma série de laços encadeados para relacionar cada nave com cada possível bala instanciada. O detalhe aqui está apenas em que uma nave só pode ser atingida se a bala tiver sido instanciada por uma nave de um time adversário. Considera-se também que uma nave com valor falso para vivo? não retorna verdadeiro para a função que detecta colisões, mais tarde eu acabo imbutindo isso no próprio código do laço ficando
para cada jogador vivo B cujo time seja diferente do de A faça
na linha 25.

Por último, um aspecto do jogo que sumiu lá pela versão 0.05: Power-ups;
Existiam dois tipos de Power-up, o Bom (Medikit), que cura uma certa quantidade de vida de quem pegasse, e o Mal (Anti-Medikit) que fazia justamente o contrário, tirando uma certa quantidade de vida de quem pegasse.
O código relacionado aos power-ups estão divididos em duas partes:
Instanciamento: que verifica o tempo decorrido desde o ultimo instaciamento de power-up e, caso tenha passado x segundos, instancia um power-up de algum dos dois tipos em uma posição aleatória do mapa.
Renderização e Detecção de Colisão: que desenha os power-ups instanciados e verifica se as naves estão colidindo com eles para aplicar os devidos efeitos e desinstanciando-os logo em seguida.
É importante desinstanciar os power-ups logo após alguma nave coletá-lo para evitar que duas naves peguem o mesmo power-up e, mais importante porém menos tangível, o efeito do power-up seja aplicado na mesma nave diversas vezes seguidas.

Ufa... Ficou bem grandinho esse aqui, e é só a parte 1. Se segurem aí que semana que vem tem mais não posso prometer, mas estou animado com o assunto então não deve demorar.

Então é isso, como sempre vocês podem me encontrar no tuiter @LinceAssassino para qualquer dúvida, sugestão, crítica ou simplesmente se quiser trocar uma ideia.
Abraços do Lince.

Nenhum comentário:

Postar um comentário