MAC-499

Projeto de Formatura Supervisionado.

 

Aluno: Ricardo Bueno Cordeiro

Supervisor: Marco Dimas Gubitoso

Responsável: Carlos Eduardo Ferreira

 

1       Introdução   2

2     O Pesadelo.. 2

3     O MIP.. 2

3.1          Sistema   3

3.2          Gráficos  3

3.3      Sons. 4

3.4      PATTO.. 4

3.5      Idéia básica de uso   4

4     O roteiro.. 4

5     Minha participação no MIP.. 5

5.1          DirectSound.. 5

5.2          Interface  5

5.3          Implementação da interface. 5

5.4      Porte para Linux I - Como funcionam sons digitais. 6

5.5      Porte para o Linux II 7

6     GANNSO.. 7

6.1          Gráficos  7

6.1.1             Profundidade. 8

6.2      IA.. 8

6.3          Gerenciamento interno.. 8

6.3.1             Tratamento dos eventos. 9

6.3.2             Movimentação  9

6.3.3             Criação de NPCs  9

6.3.4             Criação de tiros  10

6.3.5             Criação de medkits  10

7       Cronograma   11

8       Ferramentas   11

9     BCC.. 11

10          Continuação   12

11          Agradecimentos.. 12

12          Bibliografia   12

 

1         Introdução

 

      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.

2         O Pesadelo

 

      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.

 

3         O MIP

 

      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.

 

3.1      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.

 

3.2      Gráficos

 

      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.

 

3.3      Sons

 

      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.

 

3.4      PATTO

 

      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.

 

3.5      Idéia básica de uso

 

      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.

 

4         O roteiro

 

      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.

 

5         Minha participação no MIP

 

      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.

 

5.1      DirectSound

 

      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.

 

5.2      Interface

 

      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.

 

5.3      Implementação da interface

 

      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.

 

5.4      Porte para Linux I - Como funcionam sons digitais

 

      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.

 

5.5      Porte para o Linux II

 

      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.

 

6         GANNSO

 

      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.

 

6.1      Gráficos

 

            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.

 

6.1.1      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.

 

6.2      IA

 

            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.

 

6.3      Gerenciamento interno

 

            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.

 

6.3.1      Tratamento dos eventos

 

            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.

 

6.3.2      Movimentaçã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.

 

6.3.3      Criação de NPCs

 

            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.

 

6.3.4      Criação de tiros

 

            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.

 

6.3.5      Criação de medkits

 

            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.

 

7         Cronograma

 

            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.

 

8         Ferramentas

 

            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.

 

9         BCC

 

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.

10    Continuação

 

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.

 

11    Agradecimentos

 

            Acho que seja importante agradecermos a algumas empresas que nos permitiram usar seus recursos para desenvolver esse jogo:

·         NextIS

·         OxTech

·         DirectTalk

12    Bibliografia

 

            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