MAC-499
Projeto de
Formatura Supervisionado.
Aluno: Ricardo Bueno
Cordeiro
Supervisor: Marco Dimas
Gubitoso
Responsável: Carlos Eduardo
Ferreira
5.3 Implementação
da interface
5.4 Porte para Linux I - Como
funcionam sons digitais.
Desde o início da
faculdade eu e meus amigos tivemos o sonho de fazer um jogo de computador.
Durante nosso tempo na faculdade tivemos varias idéias para realizar esse
sonho, uma delas chegou a progredir um pouco.
Um dos estilos de jogos que mais
gostamos chama-se adventure, esse tipo de jogo é como se fosse um filme onde o
jogar interage com os outros personagens. Chegamos a escrever um roteiro
rudimentar para um jogo desse tipo, chamado Ball's Quest, e ate escrevemos um
pouco de código, mas aprendemos um dos maiores problemas que programadores
enfrentam quando desejam fazer um jogo, a arte. De que adianta ter toda a
técnica de programação e a vontade de se criar um jogo desse tipo se você não
possui gráficos para exibir na tela, alguns tipos de jogos de computador exigem
uma grande quantidade de arte gráfica, esse era um deles. O único com tal dom
era o Meio, mas infelizmente ele não domina a habilidade para criar animações
de um personagem. Isso foi um fator fundamental para desistirmos do projeto,
pois devido a impossibilidade de testar nossos algoritmos com algo visual
acabamos ficando desanimados.
Mas nós ainda tínhamos a vontade e a
idéia para se fazer um jogo, o que estava faltando era tempo para sentar em
grupo e resolvermos todos os problemas antes de começarmos a projetar algo de
verdade. Com o surgimento da matéria MAC-499, em nossos currículos, encontramos
a oportunidade de realizar esse sonho.
Durante algumas conversas soltas com
alguns dos membros do grupo decidimos usar um estilo de jogo que todos gostamos
e que resolveria o problema da parte de arte, o conceito do nosso jogo surgiu
com lembrança de um antigo jogo para Apple II, chamado "Castle of
Wolfstein". Ele é de um estilo de jogo que chamamos de "covert
action", onde o jogador tem a experiência de invadir alguma instalação,
como uma base militar, e sair sem que ninguém soubesse. A vantagem de tal
estilo é que os gráficos são bem mais simples do que outros jogos, ele consiste
de uma grande imagem, o cenário, onde outras imagens, os personagens e efeitos
especiais, são desenhados por cima, isso torna a necessidade de animações
complexas bem menor, algo que nosso amigo Meio poderia copiar de algum lugar.
Então formamos um grupo composto por
seis pessoas: Ricardo (eu), Tiago, Luiz Gustavo (Gus), Marcos (Meio),
Roberto(Guto) e Ricardo(Skubs). E começamos a realizar um sonho.
Tínhamos um sonho,
uma idéia, uma matéria e um problema.
Como desenvolver um jogo?
Bom, para fazer um jogo sabíamos que
iríamos precisar de alguma funcionalidade implementada que nos daria os
recursos iniciais: desenhar imagens na tela e tocar sons. Obviamente se tratava
de uma camada independente do jogo, mas extremamente importante. Portanto a
primeira resposta que tivemos para nossa duvida foi: uma biblioteca gráfica e
sonora. Esse foi o ponto de partida, havíamos começado a fazer uma biblioteca
com as funcionalidades básicas que seriam necessárias.
Quando começamos a falar com nosso
coordenador, em março, recebemos a sugestão de pensarmos em um roteiro para o
nosso jogo, a partir desse roteiro poderíamos descobrir algumas partes importantes
da interface do jogo e algumas funcionalidades que ele deveria possuir. Essa
foi nossa segunda meta: criar um roteiro para o nosso jogo.
Nosso jogo foi, eventualmente,
batizado de GANNSO devido ao nome do jogo que criamos, PATTO, para realizar testes
na nossa biblioteca.
A biblioteca que
iríamos criar seria a camada entre nosso jogo e o sistema operacional abaixo.
Com esse conceito em mente acabamos enxergando uma das características mais
importantes dessa biblioteca, se ela possuísse uma funcionalidade simplificada,
ou seja, se a parte gráfica fosse composta apenas pelas funções básicas para
desenho como colar uma imagem em cima de outra, se a parte de som apenas
carregasse o um som simples e tocasse ele, então tal biblioteca seria facilmente
implementada para qualquer sistema operacional, ou seja ela seria portável.
TTodo programa escrito para ela poderia ser compilado para outra plataforma
trocando apenas o código dela.
Essa característica que nos levou a
dar o nome de MIP, que significa: Mip Is Portable.
Apesar da portabilidade ser algo
bom, ter uma interface composta apenas por funções simples leva todas as
camadas de software que estão acima dessa biblioteca a pagar um preço de
performance, pois qualquer grupo de instruções que forma um código complexo
terá que fazer chamadas sucessivas a biblioteca, desperdiçando tempo precioso.
Inicialmente escolhemos o Windows
como sistema operacional para criar o MIP, em seguida iríamos porta-lo para o
Linux. Outra decisão importante foi a linguagem em que ele seria implementado,
para que ele fosse rápido o suficiente iríamos utilizar linguagens de baixo
nível, a primeira candidata foi C, porem a biblioteca seria criada por varias
pessoas e seus usuários finais deveriam ter facilidade em utiliza-la, para isso
precisávamos de uma linguagem que tornasse fácil a modularização da biblioteca
e simples de usar. Decidimos, então, usar C++, a biblioteca seria orientada a
objetos, cada contribuidor dessa biblioteca teria apenas que implementar seu
módulo da maneira que achasse mais eficiente e os demais membros do grupo
apenas usariam a interface disponível.
A decisão da linguagem a ser
utilizada, aparentemente, iria causar problemas de performance, mas durante seu
desenvolvimento ela foi testada através de um jogo simples, chamado PATTO, e
demonstrou ser rápida o suficiente para as nossas necessidades.
Para implementar o MIP no Windows
utilizamos o DirectX, para implementar o MIP no linux foi utilizado o X.
O desenvolvimento do MIP foi
dividido de acordo com suas funcionalidades: gráficos, sons e sistema.
A idéia do MIP é
esconder o sistema operacional do usuário final, porém descobrimos que não
seria necessário esconder tudo através do MIP, como a biblioteca foi feita
inteiramente em C++, que seria obviamente a linguagem que usaríamos para fazer
o jogo, tínhamos em nossas mãos um grande conjunto de funcionalidades que não
precisariam ser portadas, esse conjunto é composto por todas as funções de C
que pertencem ao padrão ANSI C.
Para que o usuário utilizasse o
sistema operacional, foi necessária a criação de uma interface que
disponibilizasse algumas funcionalidades do sistema operacional não cobertas
pelo ANSI C.
Todas essas funcionalidades foram
implementadas em uma classe chamada GSystem, essa classe também comporta a
interface para retornar referências a objetos das outras áreas do MIP.
Essa parte é
responsável por todo o processo de criação objetos que representam imagens,
chamados de superfícies, e sua utilização.
Essas superfícies possuem métodos
para se copiar umas nas outras, tanto inteiramente como parcialmente. Para que
essas superfícies sejam desenhadas na tela existe um metido na classe GSystem
que retorna uma referencia a uma superfície representando a tela do monitor. De
modo simplificado, tudo que o usuário tem que fazer é carregar as superfícies
necessárias para seu software e copiá-las para a superfície do monitor.
Essa foi a área do
MIP que eu implementei, ela possui duas partes importantes, o gerenciador de
buffers de sons, responsável por carregar arquivos de som e atualizá-los
periodicamente, e os buffers propriamente ditos, que são literalmente os
arquivos carregados na memória.
Apesar de sua simplicidade no
Windows, uma vez que o DirectSound, módulo de som do DirectX, cuida de quase
toda a parte complicada, a versão para Linux foi um grande desafio que durou
dois meses de desenvolvimento.
Para testar as
funcionalidades que eram conceituadas e implementadas no MIP, desenvolvemos um
jogo chamado PATTO.
Esse jogo é bem simples e baseado em
jogos antigos, ele consiste em controlar uma espaço nave por um campo de
asteróides, que são completamente inofensivos para a nave, a atirar em inimigos
alienígenas e nos asteróides, ocasionalmente ganhando vidas. O jogo possui
também um placar, para que o jogador tenha uma contagem dos pontos que ele
acumulou destruindo os inimigos, um barra de life e um numero de vidas, uma
vida é perdida quando a barra de life esvazia completamente.
Felizmente até um jogo desse porte é
capaz de testar todas as funcionalidades do MIP. Todos os recursos disponíveis
no MIP foram usados no PATTO, talvez esse seja o motivo pelo qual o PATTO ficou
mais "acabado" que o próprio GANNSO.
Como utilizar o
MIP? Ele aparenta ser complicado mas no fundo o processo é bem simples.
Toda vez que um programa utilizando
o MIP é inicializado e execução começa dentro do código do MIP, esse código
trata toda a burocracia com o sistema operacional, necessária para fazer a inicialização
de alguns dos seus módulos internos, em seguida a execução é passada para o
código do usuário que faz algumas inicializações restritas, devido a ainda não
terminada inicialização do MIP, esse pedaço da execução que roda no código do
usuário precisa existir pois é nele que esse usuário instala suas funções de
callback para tratar eventos e também ele tem a opção de direcionar o resto da
inicialização do MIP para atender as necessidades desejadas, como por exemplo,
selecionar resolução e profundidade de cores, do monitor, a ser utilizada.
Na semana seguinte
em que falamos com o Gubi, quando ele sugeriu um roteiro, nós começamos a
pensar em possíveis tramas para o nosso jogo. Durante uma aula de Álgebra II,
em que todos os alunos começam a enxergar coisas que não existem, eu e o Meio
tivemos a idéia básica para o nosso roteiro.
Como criamos a estória do roteiro?
Por ser um jogo de "covert action" sabíamos que tipo de cenas
gostaríamos que existissem no jogo, como a invasão de uma base militar e outras
mais simples, como por exemplo: "Nosso herói irá enfrentar um inimigo que
também está invadindo a base militar".
A partir dessas idéias começamos a nos fazer as perguntas básicas:
"Por que?", "Como?", "Quem?", "Quando?"
e "Onde?". E através das respostas que adquirimos fomos gerando novas
perguntas e no final quando juntamos todas as repostas e as organizamos
havíamos criado uma estória cheia de fantasia e detalhes bizarros.
Infelizmente tudo
que temos escrito sobre a estória são cópias das mensagens do ICQ que
mandávamos uns aos outros durante a fase de criação do roteiro e atualmente não
fazem muito sentido para quem não participou da criação.
Mas esse roteiro, altamente cru e rudimentar, auxiliou a extrair algumas
das funcionalidades que o jogo deveria possuir.
O meu envolvimento
na criação do jogo pode ser separado em duas partes, minha contribuição para o
MIP e minha contribuição para o GANNSO.
Em relação ao MIP fiquei encarregado
de criar toda a interface de som, mas de onde eu iria partir para desenvolver
tal codificação, os passos que eu acabei seguindo, talvez não o melhor caminho,
foram os seguintes: aprender a utilizar o DirectSound, planejar a interface do
som, implementar essa interface usando o DirectSound de forma eficiente, porte
para Linux I - compreender como funcionam sons digitais, porte para o Linux II.
Isso não foi
nenhum desafio, tudo o que tive de fazer foi ler um livro sobre o DirectX, em
implementar alguns dos exemplos contidos no livro.
O DirectSound ‚ algo realmente
simples, inicialmente ‚ necessário criar um referencia a um objeto do tipo
DirectSound, a partir dele ‚ possível alterar as configurações da placa de som:
freqüência do som a ser usada, stereo ou mono. Também ‚ possível setar um nível
de cooperação com os outros aplicativos rodando no sistema.
A partir dessa classe ‚ possível
criar dois tipos de buffers de som: static e streaming, buffers do tipo static
são carregados inteiramente na memória, geralmente são usados para sons curtos
que serão tocados com freqüência, os buffers streaming são algo um pouco mais
complicado, eles servem para tocar sons que são grandes de mais para caberem na
memória, eles são buffers onde uma agulha fica lendo seu conteúdo constantemente
e ciclicamente, quando eles estão tocando o usuário fica encarregado de
constantemente copiar blocos do som para um setor que não esta sendo lido.
Ambos os buffers tocam sons no
formato mais básico possível, o PCM (Pulse Modularization Code), esse ‚ o
formato da maioria dos arquivos do tipo wave.
Querendo esconder
do usuário a parte de ficar copiando blocos do som para a memória
periodicamente, criei uma interface muito simples, o usuário carrega os sons a
partir dos arquivos e tem a disposição métodos para tocar, parar, rebobinar e
ver o tempo atual dos buffers. Tanto para buffers static como para streaming
essa interface é igual.
O primeiro desafio
da implementação foi aprender o formato dos arquivos de som wave, após alguns
dias procurando na internet e foi capaz de achar um excelente site
(www.wotsit.org) que possui a descrição de centenas de formatos conhecidos,
entre eles o wave.
A implementação dos buffers de som
static foi razoavelmente simples, foi necessário apenas repassar as chamadas
dos métodos para o DirectSound. Uma característica interessante ‚ que os
buffers static guardam internamente uma lista ligada de buffers
DirectSoundBuffer, isso para que quando um usuário usar o método play para esse
buffer e ele já estiver tocando, então uma nova instância desse som irá ser
tocada, essa lista ligada permite então que varias instancias do mesmo som
sejam tocadas ao mesmo tempo, isso se torna interessante quando usamos buffers
que tocam efeitos de explosões ou tiros.
O desafio maior nessa parte foi a
implementação dos buffers streaming, para que o usuário não se preocupa-se em
ficar atualizando os buffers streaming foi criada uma lista ligada de buffers
desse tipo e toda vez que um buffer streaming começasse a tocar ele era
inserido nessa lista. Foi criado internamente ao MIP um timer para, a cada meio
segundo, percorrer essa lista e atualizar todos os buffers, quando necessário.
No caso de algum desses buffers ter parado de tocar ele era removido pelo atualizador
da lista.
Implementar essa funcionalidade foi
algo tedioso, pois era necessário manter uma contabilidade dos bytes do som
tocado, quantos bytes faltavam para ler do arquivo e quantos bytes de
"silêncio" deveriam ser introduzidos no final do buffer quando o
arquivo terminasse.
Nessa altura da implementação
aprendi um problema interessante do Windows, quando criamos um timer, para
mandar uma mensagem periodicamente para a função tratadora de mensagens, o
Windows não trata tais mensagens como normalmente faz, seqüencialmente, elas em
particular são tratadas concorrentemente do resto do programa. Isso estava
causando um problema seriíssimo na atualização de buffers streaming, quando o
programa ‚ fechado e ainda existem buffers de som tocando, o MIP deleta os
buffers da memória e depois desligava o timer, mas o problema era que os
buffers estavam sendo deletados ao mesmo tempo em que estavam sendo
atualizados, o que causava uma falha de segurança no Windows e o programa era
terminado com erro. A solução para esse problema foi criar um controle simples
de concorrência.
Quando comecei a
planejar o porte da parte de som para o Linux encontrei uma situação curiosa,
para portar o código para o Linux eu iria precisar de duas camadas de software,
uma semelhante a do Windows e uma semelhante a do DirectSound, foi ai que
percebi que o que deveria ser portado não era o código que eu havia criado, ele
seria o mesmo! O que realmente eu deveria portar era a funcionalidade do
DirectSound, com isso em mãos eu poderia utilizar praticamente o mesmo código
usado no Windows, a única diferença seria a utilização de threads para fazer as
atualizações dos buffers streaming.
Então comecei a implementar a
interface do DirectSound para o Linux, ele era bem semelhante ao código que eu
havia criado na camada superior com algumas pequenas diferenças: todos os
buffers eram iguais, a menos do tamanho, e era necessário fazer a mixagem dos
sons antes de jogar para a placa de som.
Mas como fazer a mixagem? Para isso
aprendi uma das coisas mais interessantes entre todas as do jogo: como
funcionam sons digitais.
Um som ‚ apenas uma onda que se
propaga pela matéria, um som digital não passa de uma amostragem dessa onda, ou
seja, para cada segundo da onda pegamos um numero grande de fatias dela e
calculamos a amplitude do som nessa fatia, geralmente são usadas 11005, 22010
ou 44020 fatias, essas amplitudes são então convertidas para números de 8 bits
ou 16 bits. Um som no formato PCM não passa de uma grande seqüência dessas
fatias, chamadas de samples. Mas então, como fazer a mixagem? Quando dois sons
se misturam suas ondas causam interferência tanto construtiva, a amplitude
aumenta, ou destrutiva, a amplitude diminui.
Vamos dar dois exemplos simples para
tentar explicar esses efeitos:
·
Suponha que temos um som tocando a 440
hz, ou seja, se desenharmos um gráfico da amplitude pelo tempo, temos então 440
picos e buracos em um segundo. Se tocarmos outro som na mesma fase, ou seja, os
picos e buracos dos dois sons coincidem, então temos uma interferência
construtiva, o volume aumenta.
·
Imagine os mesmos dois sons do exemplo
anterior, mas com uma pequena diferença, eles estão em fases opostas, ou seja,
os picos de um coincidem com os buracos do outro, temos aqui uma interferência
destrutiva, nenhum som ‚ tocado.
Portanto para
fazer a mixagem tudo o que temos que fazer ‚ somar as samples que devem ser
tocadas no mesmo instante, considerando que picos e buracos têm amplitudes
opostas.
A alma do porte
para o linux se resume em uma função, o mixer, ele ‚ encarregado de percorrer
todos os buffers que existem, fazer a mixagem em uma porção de todos eles e
jogar o resultado para a placa de som, geralmente essas porções tinham por
volta de 2048 bytes de tamanho.
A maior frustração de todo o projeto
aconteceu nesse momento. O mixer deve ser muito rápido e não pode consumir
tempo do processador, o primeiro teste que foi realizado acusou que 12% da CPU
foi gasto para o mixer tocando apenas um buffer de som. Quando eram utilizados
20 buffers a performance do sistema caia consideravelmente, pois o mixer
gastava em torno de 50% a 70% da CPU.
Apos milhares de
otimizações no mixer, incluindo a geração e analise do código assembly, cheguei
ao ponto que maiores otimizações necessitariam de programação direta em
assembly. Porém nesse ponto, a performance havia atingido um valor apropriado,
um buffer som tocando gastava apenas 0.8% da CPU e 30 buffers gastavam 16%
aproximadamente.
Agora tínhamos o
MIP em nossas mãos, ele ainda não estava funcionando perfeitamente, mas já
possuía todos os recursos de que precisávamos para começar o nosso jogo. Mas
tínhamos um detalhe a resolver, que ângulo de câmera deveríamos usar. Durante o
jogo, o jogador, está sempre com o monitor centrado no personagem principal,
como se estivesse acima dele, olhando para baixo, mas para que as paredes do
jogo pudessem ser vistas, é necessário que o centro da câmera não fique
perpendicular com o jogador, mas sim que possua uma pequena inclinação, assim
todos os objetos do jogo podem ter suas laterais vistas pelo jogador.
Sentamos então para começarmos a
discutir sobre como faríamos o desenvolvimento do jogo, queríamos definir quais
seriam os módulos principais do jogo, suas funcionalidades e complexidades,
para que pudéssemos dividir nosso grupo por essas áreas.
Dividimos então o
GANNSO em três grandes áreas: gráficos, IA e gerenciamento interno.Nosso grupo
foi também dividido para podermos atacar as três áreas ao mesmo tempo.
Apesar do nome levar a entender que
essa área trata apenas de imagens e sua manipulação ela faz muito mais do que
isso. A partir das imagens que formam o cenário, são criadas duas máscaras,
máscaras são estruturas que possuem informações não visuais sobre o mapa: uma
para teste de colisão, para cada coordenada do mapa a mascara informa se esse
pixel é transponível ou não, e uma máscara de profundidade, usada no
"falso" efeito de tridimensionalidade do jogo.
Como nossos mapas chegam a ser quatros
vezes maiores do que a resolução do monitor usada no jogo, essa área
disponibiliza funções para desenhar os objetos do jogo na tela, o famoso
rendering, bem como transformar coordenadas relativas ao mapa para a tela e
vice versa.
Uma das coisas mais fascinantes
dessa área é sem duvida o algoritmo usado para criar a sensação de
profundidade.
Se não me engano esse algoritmo foi
criado por Tiago, mas eu o achei tão fascinante que decidi dedicar um pouco a
descreve-lo.
Nós temos vários objetos espalhados
pelo cenário, quando vamos desenhar um pixel de um personagem na tela, como
saber se o pixel que já está lá não pertence a um objeto que deve estar na
frente desse personagem e, portanto, encobrir o pixel do personagem.
A primeira solução diz que devemos,
então, desenhar todos os personagens em ordem decrescente de profundidade, os
que estão no fundo são desenhados primeiro e os outros são desenhados por cima
deles. Essa solução é válida e funciona. Os personagens que estão mais ao fundo
são aqueles que possuem menor coordenada y, as coordenadas y crescem de cima
para baixo no mapa. Mas esse algoritmo não funciona quando temos um cenário
inteiro em uma grande imagem, ou seja, o personagem pode passar por trás de
paredes, o custo de ter o cenário inteiro quebrado em blocos e desenhar um a um
em ordem é muito alto e portanto inviável, sem levar em conta a tempo gasto
para manter a ordenação de todas as partes.
Qual seria a solução?
A idéia é razoavelmente simples.
Todo pixel da imagem usada como cenário pertence ao chão ou a um objeto fixo no
cenário, se ele esta no chão então o personagem nunca poderá estar
"atrás" dele, porem se tal pixel pertence a um objeto sabemos então
qual é a coordenada da base desse objeto. Então o que fazemos é simples,
fazemos um pré-processamento dos objetos do cenário e geramos uma máscara que
guarda para cada pixel do mapa qual a coordenada y da base dele.
Quando vamos desenhar um pixel na
tela, um pedaço do personagem, pegamos a coordenada da base do pixel do mapa e
comparamos com a coordenada da base do pixel do personagem, coordenada do pé
dele, e usamos essa comparação para decidir quem está atrás de quem, e então
desenhamos, se o personagem está na frente desenhamos o pixel dele, se ele
estiver atrás, fazemos a transparência entre os dois pixeis.
Acreditamos que uma das partes mais
complexas e difíceis da programação de jogos é a inteligência artificial. Criar
ou simular comportamentos humanos sempre foi um desafio para a computação,
nessa área esse problema foi mais uma vez enfrentado.
Inicialmente a IA criou algoritmos
de path-finding, que são usados pelos inimigos para encontrar caminhos pelo
mapa, esses algoritmos são capazes de calcular parcialmente os valores, gerando
resultados conforme o personagem se desloca pelo mapa.
Outra funcionalidade da IA é a de
criar uma maquina de estados para definir como um NPC (Non Player Character)
irá reagir a um problema.
Essa foi a área em que tive menos
contato.
Como sempre dissemos, como saber se
um tiro acertou alguém no jogo?
O gerenciamento interno do jogo é
responsável por simular toda a camada do jogo que representa a física do nosso
jogo. Ela também é responsável por inicializar todos os módulos do jogo e
tratar todos os eventos gerados pelo MIP.
O desenvolvimento dessa área foi
feito em passos, que serão descritos a seguir.
A primeira parte a ser desenvolvida
foi o tratamento de eventos, enviados pelo MIP. O GANNSO possui uma interface
bem simples para a entrada de dados, os únicos comandos que ele reconhece são:
quatro teclas para a movimentação do personagem, uma tecla para disparar tiros
e
a movimentação do
mouse para controlar a mira. Todas as teclas são configuradas através de um arquivo
de configurações que é lido e interpretado durante a inicialização do jogo.
Toda vez que uma tecla é apertada
uma flag é acionada para dizer que esse botão está clicado, assim guardamos um
vetor com o estado de todas as teclas relevantes ao jogo.
Aqui foi encontrado mais um problema
interessante, quando o Windows manda eventos de clique de botões do mouse ele
não utiliza o mesmo padrão entre os eventos de BUTTON_DOWN e de BUTTON_UP, a
parâmetro que informa qual o botão que foi clicado possui valores diferentes
entre nos dois eventos. Para consertar isso, toda vez que um botão é clicado
nós armazenamos o botão que foi clicado e o próximo evento de BUTTON_UP é
considerado para esse botão.
Após a leitura do mouse e do teclado
terem sido implementadas; iniciamos a parte de movimentação do personagem
principal.
Para isso criamos uma lista ligada
com todos os objetos do jogo, no início da implementação do gerenciamento, essa
lista continha apenas um objeto, o personagem principal.
O gerenciamento, a cada 100
milisegundos, percorre essa lista chamando o método de atualização de todos os
objetos da lista, esse método é responsável por setar o novo estado desse
objeto.Atualmente existem poucas possibilidades de estados para um personagem,
ele informa em qual direção o personagem está olhando, são oito possíveis, se
ele está andando ou se ele esta atirando.
Para setar o novo estado do
personagem basta analisar o vetor de estados do teclado, ele possui todas as
informações sobre o estado atual do personagem.
O MIP chama uma função de callback
do jogo chamada Idle com a maior freqüência possível, dentro do código dessa
função que fazemos as atualizações mencionadas acima são feitas. Essa função
realiza outras tarefas que serão descritas abaixo.
Para que exista animação dos
personagens no jogo, toda vez que a função Idle é chamada nós percorremos a
lista de objetos, analisando seus estados e, se esse estado for de
movimentação, deslocamos o personagem de um número determinado de pixeis na
direção do movimento, sempre respeitando a máscara de colisão.
Depois que a parte de movimentação
estava pronta, nós criamos os NPCs, a classe deles é derivada da mesma classe
do personagem principal e portanto possuem a mesma interface, portanto foi algo
simples implementar os NPCs, tudo o que era necessário já estava pronto e
portanto as animações com os NPCs funcionou perfeitamente.
A única diferença é que o método de
atualização da classe dos NPCs chama a atualização do módulo de IA, que por sua
vez atualiza o estado do NPC, para que a implementação da IA fosse acoplada ao
jogo, bastava apenas trocar o modulo de IA usado pelo NPC para o da área de IA.
Após o personagem e os NPCs estarem
prontos, partimos para a implementação dos tiros no jogo. Como funcionam tiros?
Quando um tiro é disparado ele causa dano em objetos que estão próximo a ele,
ou em cima dele, no caso de uma bala. Mas como implementar essa característica?
Toda vez que um tiro é disparado no jogo, ele cria um dano, esse dano é
acrescentado a uma lista de danos. Todo dano dentro do jogo possui quatro
propriedades: localização, dano, raio do efeito e persistência. Através da
persistência podemos saber qual o tempo de atuação do dano. A cada passada do
ciclo de atualização dos objetos do jogo, o gerenciamento percorre a lista de
danos e faz uma atualização dos danos, isso causa o decréscimo do valor da
persistência, e quando essa atinge zero o dano é removido da lista.
Assim podemos simular vários tipos
de efeitos em um jogo, como por exemplo, fogo, o fogo teria as seguintes
propriedades: sua localização, uma quantidade de energia que ele tira
periodicamente de todos os personagens que estão em contato com ele, um raio de
efeito equivalente ao tamanho da animação gráfica que representa ele e um
grande tempo de persistência, que indica quanto tempo ele leva para apagar.
Quando o tempo acaba, ele pode informar a animação que ela não existe mais, e, portanto,
na próxima atualização dos objetos, ela será removida da lista e não irá para a
tela.
Toda vez que um personagem atinge
uma quantidade de energia inferior um igual a zero ele é considerado morto, o
que causa a remoção dele da lista de objetos do jogo. Isso ocorre com o
personagem principal também, porem, mesmo desaparecido, ele pode continuar
dando tiros, esse erro deve ser consertado ainda.
Para que nosso jogo fosse mais
amigável, era necessário que o jogador, de alguma maneira pudesse recuperar sua
energia perdida devido a danos que ele sofreu. Para isso foram criados os
medkits. Eles são kits de primeiro socorros que ficam espalhados pelo cenário
do jogo, quando o personagem passa por cima deles, e se ele estiver ferido,
então esse medkit desaparece e a barra de energia é aumentada.
Depois de algum tempo passado, após
o personagem ter pegado um deles, os medkits voltam a aparecer no mesmo local,
esse efeito é chamado de respawn.
Essa parte da implementação causou
um dos bugs mais engraçados de todo o desenvolvimento. Quando um tiro é
disparado, alem do dano que é criado para a lista de danos, criamos um objeto
no jogo para representar a animação da faísca desse tiro, esse objeto é
adicionado à lista de objetos e estava sujeito a atualizações como todos os
outros. Para que isso fosse possível, tal objeto da faísca deveria derivar da
classe base de objetos e, portanto, herdou todas as propriedades e métodos
dela. Alguns métodos foram sobrecarregados para que suas funcionalidades fossem
desativadas, porém os métodos que não eram virtuais continuaram a existir.
As animações usadas para a faísca
possuem seis quadros (imagens), diferentes. Para mostrar todas elas, a
propriedade de vida da classe foi usada como um contador de tempo antes da
animação “morrer”. A cada atualização da lista esse contador era diminuído de
uma unidade e era usado para determinar qual o quadro a ser mostrado na próxima
renderização da tela.
Mas, ai estava um problema. Quando
um tiro era disparado em um medkit, na próxima atualização dos objetos era
detectada a colisão da faísca com o medkit e portanto essa faísca era “curada”,
seu vida era aumentada, através de um método que não foi sobrecarregado, e então
na próxima renderização da tela esse valor de vida era usado para determinar
qual o quadro a ser exibido, mas tal quadro não existia pois o valor da vida não
estava entre um e seis.
Isso causava um
acesso ilegal de memória que levava à terminação do jogo.
Para solucionar tal problema,
criamos uma nova propriedade à classe de objetos, uma propriedade para
armazenar o tipo do objeto daquela classe, e, então, toda operação a ser
executada em objetos era efetuada mediante a confirmação do tipo do objeto.
Nossos planos iniciais eram de ter o
GANNSO inteiramente funcional para a apresentação dos trabalhos, infelizmente, devido
a alguns problemas, tivemos um atraso do programa inteiro.
Todos os membros do grupo estavam
trabalhando e, portanto, eram raros os momentos em que estávamos juntos para
programar, nossa forma de comunicação foi a lista de mail que criamos para
divulgar nossos avanços um para os outros. Isso dificultou a troca de informação,
código e idéias. A IA recebeu a primeira versão do jogo um pouco tarde e, portanto,
não tiveram condições de testar completamente todos os seus algoritmos.
Nas vezes em que todos estavam
reunidos com micros para trabalhar na programação, o desenvolvimento rendia de
maneira espetacular. Como todos ficavam na mesma sala e trocavam idéias, a cada
dificuldade encontrada alguém vinha com uma solução e o problema era resolvido.
Como o Meio disse certa vez: “Se não
estivéssemos reunidos hoje esse problema levaria uma semana para ser resolvido,
eu iria falar para o Tiago que o personagem estava atravessando as paredes, ele
ia ficar testando seus algoritmos por pelo menos dois dias, depois ia falar que
a culpa estava no gerenciamento, então vocês iriam revisar o código e descobrir
que na verdade o problema era meu, pois a mascara de colisão estava errada”.
Mas felizmente o que conseguimos ter
completo já era alguma que iria satisfazer, parcialmente, nosso sonho. Agora
temos um roteiro e um princípio de jogo.
Todo o código foi criado em editores
de texto como UltraEdit e Emacs.
Toda a programação para o Windows foi feita usando o compilador freeware da
Borland, o Borland 5.5. Porém encontramos algumas dificuldades com esse
compilador, não conseguíamos compilar o jogo nele e depois roda-lo em um
computador que tivesse ele instalado. Por alguma razão que desconhecemos as
bibliotecas dinâmicas do DirectX não conseguem ser carregadas para a execução
do jogo com sucesso e portanto o programa trava antes de começar.
Para o Linux, na parte do som do MIP, usei o compilador g++, para gerar o código,
infelizmente, após a modificação da rede linux, o device que era usado para
gravar as samples de som na placa de som, “/dev/dsp”, foi fechado para escrita
e, portanto, não sabemos se o jogo continua com o som funcionando.
Muitos se perguntam quais foram as matérias estudas no curso que mais nos
ajudaram a desenvolver o jogo. Na verdade todas as matérias ajudaram um pouco,
mas as que realmente usamos em grande quantidade foram:
·
Inteligência artificial: Ajudou em todo o processo de criação dos NPCs do GANNSO
·
Estrutura de dados: Como poderíamos ter feito esse jogo sem saber o que é uma lista ligada. Uma
das mais simples de todas as estruturas de dados que aprendemos durante nosso
curso, as listas ligadas, sem duvida foram fundamentais.
·
Programação concorrente: A maioria dos bugs que encontrei foram criados devida à concorrência. Essa
mmatéria ajudou a enxergar de uma maneira diferente a execução de um
programa, e ajudou e perceber que tipo de erros estavam ocorrendo quando o
programa travava.
·
Geometria computacional: Uma das funções do gerenciamento que não foram implementados
completamente, era a passagem de estímulos para a IA dos personagens, um deles
em particular era o do NPC estar enxergando o personagem principal do jogo,
para calcular isso com precisão é necessário o conhecimento básico de geometria
computacional.
Para o Linux, na parte do som do MIP, usei o compilador g++, para gerar o código,
Mesmo agora que não sou mais aluno de MAC-499, pretendo continuar com o
projeto MIP/PATTO/GANNSO, inclusive já conversei com os membros da equipe e
todos aceitaram continuar com os projetos, o Gus em particular disse que a IA
do jogo se tornou questão de honra para ele.
·
MIP: Apesar de já estar funcionando
esperamos consertar algumas funcionalidades e otimizar o código para melhor
desempenho.
·
PATTO: Ele já está praticamente pronto,
mas ainda temos que dar uma acabada geral, como colocar um final para o jogo.
·
GANNSO: Esse merece atenção,
conversando com o Tiago já decidimos mudar completamente a estrutura das
classes dos objetos do jogo e do gerenciamento interno. A IA com certeza vai
sofrer grandes melhoras.
Esse projeto não foi a realização de um sonho, mas sim o princípio da
realização.
Acho que seja importante
agradecermos a algumas empresas que nos permitiram usar seus recursos para
desenvolver esse jogo:
·
NextIS
·
OxTech
·
DirectTalk
Durante todo
o desenvolvimento do projeto foram usados alguns livros para consulta.
·
SCHILDT, Herbert. Borland C++
Completo e Total. Makron Books. 1998
·
SCHILDT, Herbert. Programando em
Windows 95: Segredos e Soluções.
Makron Books. 1997
·
BARGERN, Bradley e Peter Donnelly. Inside DirectX. Microsoft
Press 1998