GameSpaceLib & Sprites: personagens 2D com C++

Você conhece a GameSpaceLib? Não? Pois descubra aqui como essa biblioteca pode tornar bem mais fácil sua vida criando jogos em C++! Este artigo é de autoria de André Santee e apresenta os fundamentos para manipular sprites 2D usando esta biblioteca.

Parte I: Instalando o necessário

Passo 1 – Verificando o que é necessário

Este tutorial assume que você possua algum conhecimento em C++ e lógica da programação. Para seguir este tutorial é necessário que você possua alguma versão do Visual C++. Você pode baixar uma versão gratuita do compilador em: http://www.microsoft.com/express/. Este é um excelente compilador com todas as funcionalidades importantes que a versão paga possui.
Para este tutorial eu utilizei a versão Express 9.0 (2008) do Visual C++, mas você pode utilizar qualquer versão superior à 5.0.

Também é necessário que se tenha o DirectX 9.0c ou superior instalado.

Passo 2 – baixando a GameSpaceLib

Uma vez que você possui o Visual C++ instalado, podemos começar. Antes de tudo precisamos de uma biblioteca ou engine que faça o trabalho duro de acesso aos dispositivos de vídeo, áudio e entrada, para que o programador possa preocupar-se apenas com a programação da lógica do jogo ou aplicativo diverso. Neste tutorial vamos utilizar a ferramenta brasileira GameSpaceLib.

O que é GameSpaceLib?

GameSpace é uma biblioteca desenvolvida em C++ para o controle de dispositivos de entrada e saída: vídeo (2D), áudio, teclado, mouse e joysticks. A GameSpaceLib é gratuita e possui o código fonte-aberto.

Quais são as vantagens da GameSpaceLib?

Dentre outras bibliotecas multimídia como SDL e Allegro, a vantagem da GameSpaceLib é que ele possui uma interface muito mais fácil e intuitiva para programação.
Foi programada para acessar diretamente as funções da API, o que o torna veloz, pois utiliza todos os recursos da placa de vídeo e de forma direta, sem passar por outros motores ou frameworks para fazer a mediação.

Para baixar a GameSpaceLib acesse:

http://www.asantee.net/gamespace/ e escolha a versão 1.3.0 ou superior. Após baixado o arquivo, instale o SDK em seu computador.

Passo 3 – Criando o projeto

Utilizando a GameSpaceLib com o Microsoft Visual C++ Express:
1. Abra o Visual C++ Express, clique em File->New->Project.
2. No campo “Project types:” selecione Win32, e no lado esquerdo, no campo “Templates:” selecione “Win32 Console Application”.
3. No campo “Name:” digite o nome do projeto desejado (Ex.: Game2D).
4. Em “Location:” selecione o diretório onde deseja armazenar os dados do projeto.
5. Clique em OK.

Uma nova janela irá abrir. Clique em “Next”, em seguida verifique se a opção “Windows Application” está selecionada e marque o campo “Empty project”. Clique em “Finish”.

Pronto. Seu projeto está criado. Agora você precisa adicionar um arquivo .CPP à ele para entrar com o códito do tutorial. Clique em “Project->Add new item…”. No campo esquerdo “Templates:” selecione “C++ File (.cpp)”, escolha um nome para ele no campo “Name:” (Ex.: main.cpp) e clique em “Add”.
Uma tela em branco surgirá na área principal de seu compilador. É onde você digitará o código do arquivo .CPP principal.

Preparando o projeto para rodar com a GameSpaceLib:
Instale o SDK da GameSpaceLib.
Clique em “Tools->Options”. Amplie o campo “Projects and Solutions” (clique no +) e clique em “VC++ Directories”.
Ao seu lado esquerdo, clique no menu abaixo de “Show directories for:” e selecione “Include files”. Uma lista de diretórios surgirá.
Clique no botão que cria uma nova pasta, logo acima da lista de diretórios, e escolha a pasta “include” do local onde a GameSpaceLib foi instalada (Ex.: C:\Arquivos de programas\GameSpaceLib SDK\Include).
Volte ao menu “Show directories for:” e selecione “Library files”. Repita o mesmo processo anterior mas selecionando a pasta msvc\lib (Ex.: C:\Arquivos de programas\GameSpaceLib SDK\msvc\lib).
Clique em OK.

Abra a pasta onde você instalou o SDK. Então selecione e copie para a pasta de seu projeto (por exemplo: “c:\Jogo2D\Jogo2D\”) os seguintes arquivos:

-GameSpace.dll (contido no diretório \msvc\bin)
-audiere.dll (contido no diretório \msvc\bin)

Utilize a seguinte linha de código no início do arquivo .cpp principal para incluir o arquivo .lib ao projeto:
#pragma comment (lib, “GameSpace.lib”)

Pronto, agora é só começar a programar.

Utilizando a GameSpaceLib com o Dev-C++ 5:
O SDK da GameSpaceLib acompanha um arquivo DevPak que automaticamente instala todos os arquivos necessários para a compilação. O arquivo encontra-se dentro do diretório \mingw\ na pasta onde o SDK foi instalado. Para instalar o SDK ao Dev-C++ 5 basta executar o arquivo .DevPak e seguir as instruções em tela que surgirão em seguida.

Parte II – Desenhando um sprite simples

#pragma comment (lib, "GameSpace.lib")
#include
#include

INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR, INT )
{
     GAMESPACE_VIDEO_HANDLER gsVideo;
     GAMESPACE_INPUT_HANDLER gsInput;
     GAMESPACE_AUDIO_HANDLER gsAudio;

     GS_SPRITE gsSprite;

     if (gsVideo.StartApplication(640, 480, "GameSpace", false))
     {
          gsInput.RegisterInputDevice(NULL);
          gsAudio.RegisterAudioDevice(NULL);
          gsVideo.HideCursor(true);

          gsSprite.LoadSprite(&gsVideo, "character.bmp", GS_ARGB(255,0,255,0));

          while (gsVideo.ManageLoop())
          {
               gsInput.UpdateInputData();

               gsSprite.DrawSprite(GS_VECTOR2(100, 100));
          }
     }
     return 0;
}

Vamos analisar o código:

#pragma comment (lib, "GameSpace.lib")
#include
#include

A primeira linha de código liga o arquivo GameSpace.lib ao projeto. Ele é necessário para que o compilador possa puxar as funções de GameSpace.dll.
Em seguida incluímos os arquivos gamespace.h e windows.h ao código. Eles são necessários para que tudo funcione.

INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR, INT )
{

Esta é a função principal do programa. É aqui que todo o código começa a ser executado. É equivalente à função main() do modo console.

     GAMESPACE_VIDEO_HANDLER gsVideo;
     GAMESPACE_INPUT_HANDLER gsInput;
     GAMESPACE_AUDIO_HANDLER gsAudio;

Nesta parte do código declaramos todos os objetos da GameSpaceLib que serão usados. De vídeo, entrada e áudio respectivamente. Neste tutorial ainda não usaremos as funções de áudio da GameSpaceLib mas vamos declarar o objeto mesmo assim.

     GS_SPRITE gsSprite;

Este é o objeto utilizado para carregar imagens de arquivos BMP, JPEG, PNG ou TGA e desenhá-las na tela. Esta forma de desenhar bitmaps na tela é denominada SPRITE. Num jogo, cada tipo de objeto (personagens, elementos do cenário, itens, etc.) que estiver em arquivos de imagem diferente vai precisar de uma declaração de um objeto GS_SPRITE distinta.

Posteriormente vamos utilizar um método (função membro) deste objeto para carregar o arquivo.

     if (gsVideo.StartApplication(640, 480, "GameSpace", false))
     {

O método GAMESPACE_VIDEO_HANDLER::StartApplication cria a janela ou especifica o modo tela cheia e prepara o fundo para que possamos desenhar os sprites nela.

No primeiro e segundo parâmetros especificamos a largura e altura, em pixels, da janela. Neste caso ela será de 640×480 pixels. O terceiro parâmetro define um título para a janela. Vamos chamá-la de “GameSpace”.

No último parâmetro definimos se queremos nossa aplicação no modo janela (true) ou se a queremos em tela-cheia (false). Neste caso eu escolhi o modo de tela cheia (fullscreen) que é mais comum em jogos.

Este método pode retornar true caso corra tudo bem, ou false caso haja um falha durante sua execução. Se ele tiver sucesso, prosseguimos com as demais instruções:

          gsInput.RegisterInputDevice(NULL);
          gsAudio.RegisterAudioDevice(NULL);

Os métodos GAMESPACE_INPUT_HANDLER::RegisterInputDevice e GAMESPACE_AUDIO_HANDLER::RegisterAudioDevice preparam os objetos para tratar dos dispositivos de entrada (mouse e teclado) e áudio. Devemos especificar NULL para ambos os argumentos.

          gsVideo.HideCursor(true);

Esta função trata de esconder ou exibir o ponteiro do mouse. Neste caso estamos definindo que não queremos exibí-lo (true). Caso queiramos mostrar o cursor especificamos false.

          gsSprite.LoadSprite(&gsVideo, "character.bmp", GS_ARGB(255,0,255,0));

Aqui é onde nós carregamos o sprite. No primeiro parâmetro devemos passar um ponteiro para o objeto de tratamento de vídeo (GAMESPACE_VIDEO_HANDLER) que estamos utilizando. Em seguida informamos o nome do arquivo que queremos carregar. Neste caso eu escolhi o seguinte sprite:

No último parâmetro informamos a cor que queremos ignorar na hora de desenhar o sprite. Neste caso é a cor verde. O macro GS_ARGB(ALPHA,VERMELHO,VERDE,AZUL) transforma três valores RGB mais o canal alpha (de transparência) em um valor de cor 32-bit.

Caso você queira, também pode definir o valor de transparência da figura no próprio arquivo com o formato PNG. Caso o faça, defina este parâmetro como 0×0 (zero em hexadecimal).

Note que a figura utilizada possui um mesmo personagem em diversas posições. Na primeira linha o vemos virado de frente, nas duas linhas seguintes, para os lados e por último, de costas. Se dividirmos essa imagem em pequenos retângulos e utilizarmos cada quadro por vez, teremos um personagem animado. Você verá como fazer isso adiante.

          while (gsVideo.ManageLoop())
          {

Aqui começamos o laço principal do programa. Em jogos ou em qualquer aplicação que envolva gráficos, o laço principal é fundamental para que possa haver a interação e animação.

O método ManageLoop trata automaticamente dos eventos do sistema, exibe o que está no back buffer (se houver algo) e prepara o back buffer para o desenho direto de sprites. Retorna true caso tenha obtido sucesso ou false caso o usuário do programa tenha requisitado seu encerramento. Esse método deve ser usado uma vez a cada rodada do laço principal.

Neste exemplo o programa irá se repetir enquanto ManageLoop retornar true. Com isso temos todo o laço principal pronto com apenas uma única função. Podemos fazer todo o trabalho de desenho de sprite dentro dele.

               gsSprite.DrawSprite(GS_VECTOR2(100, 100));

Esta função desenha o sprite. A estrutura GS_VECTOR2 representa um ponto na tela, com os eixos x e y. GS_VECTOR2(x,y) especifica um ponto, em pixels, onde o sprite deve ser desenhado.

Pronto, enviamos a instrução para desenhar o sprite. Agora precisamos encerrar a cena e exibir na tela do computador o que foi desenhado:

               gsInput.UpdateInputData();

O método GAMESPACE_INPUT_HANDLER::UpdateInputData atualiza a leitura do teclado e mouse. Deve ser chamado uma vez a cada execução do laço principal.

          }
     }
     return 0;
}

As últimas linhas de código tratam de encerrar o laço, a condicional e a função principal.

Pronto! Agora temos uma programa que com algumas linhas de código cria a janela, carrega e desenha um sprite! Caso você queira obter informações mais aprofundadas sobre os objetos e métodos usados, visite: http://www.asantee.net/gamespace/reference.htm

Parte III – Animando o personagem

Agora faremos algumas adições ao código para que ele permita que controlemos o personagem com o teclado. Mudanças no código serão marcadas com negrito e comentadas:

#pragma comment (lib, "GameSpace.lib")
#include
#include

INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR, INT )
{
     GAMESPACE_VIDEO_HANDLER gsVideo;
     GAMESPACE_INPUT_HANDLER gsInput;
     GAMESPACE_AUDIO_HANDLER gsAudio;

     GS_SPRITE gsSprite;

     if (gsVideo.StartApplication(640, 480, "GameSpace", false))
     {
          gsInput.RegisterInputDevice(NULL);
          gsAudio.RegisterAudioDevice(NULL);

          gsVideo.HideCursor(true);

          gsSprite.LoadSprite(&gsVideo, "character.bmp", GS_ARGB(255,0,255,0));
          gsSprite.SetupSpriteRects(4, 4);

          GS_INTERPOLATOR interp;

          const float fStride = 100.0f;
          const float fSpeed  = 1.0f;
          int nDir = 0;
          float nXpos = 100, nYpos = 100;

          while (gsVideo.ManageLoop())
          {
               gsInput.UpdateInputData();

               bool bMoving = false;
               if (gsInput.KeyDown(GSK_LEFT))
               {
                    nDir=1;
                    nXpos-=fSpeed;
                    interp.Set(0, 3, fStride);
                    bMoving = true;
               } else
               if (gsInput.KeyDown(GSK_RIGHT))
               {
                    nDir=2;
                    nXpos+=fSpeed;
                    interp.Set(0, 3, fStride);
                    bMoving = true;
               }
               if (gsInput.KeyDown(GSK_DOWN))
               {
                    nDir=0;
                    nYpos+=fSpeed;
                    interp.Set(0, 3, fStride);
                    bMoving = true;
               } else
               if (gsInput.KeyDown(GSK_UP))
               {
                    nDir=3;
                    nYpos-=fSpeed;
                    interp.Set(0, 3, fStride);
                    bMoving = true;
               }

               if (!bMoving)
               {
                    interp.Set(0, 0, 0.0f);
               }

               gsSprite.SetRect(interp.GetCurrentFrame(), nDir);
               gsSprite.DrawSprite(GS_VECTOR2((float)nXpos, (float)nYpos));

               gsVideo.PrintText(GS_VECTOR2(0,0),
                                             "Utilize as setas para mover o personagem",
                                             "Tahoma", 30.0f, 0xFFFFFFFF);
          }
     }
     return 0;
}

Agora vamos analizar as novas linhas de código:

gsSprite.LoadSprite(&gsVideo, "character.bmp", GS_ARGB(255,0,255,0));
gsSprite.SetupSpriteRects(4, 4);

Você já deve ter notado que a imagem que utilizamos possui 4 linhas e 4 colunas. As linhas especificam uma direção diferente (baixo, esquerda, direita, cima) para a qual o personagem aponta. E em cada coluna temos uma etapa diferente no movimento de “caminhar” do personagem, nós chamaremos cada subdivisão da animação de “quadro”, ou “quadro de animação”:

O que o método GS_SPRITE::SetupSpriteRects faz é criar retângulos que dividem o bitmap em quadros de animação. Nos argumentos do método especificamos que a figura possui quatro colunas de quadros e que cada coluna possui 4 linhas.

GS_INTERPOLATOR interp;

O objeto GS_INTERPOLATOR é usado para criar animações suaves. Mais adiante veremos como ele funciona.

const float fStride = 100.0f;
const float fSpeed  = 1.0f;

Aqui definimos a constante fStride que especifica o tempo de espera entre a troca de um quadro de animação para o próximo. Este valor será usado posteriormente com o objeto GS_INTERPOLATOR.

Em seguida a constante fSpeed especifica uma velocidade de movimento para personagem. Experimente aumentar ou diminuir levemente este valor para ver o resultado.

int nDir = 0;
float nXpos = 100, nYpos = 100;

A variável nDir irá armazenar a direção do personagem (baixo, esquerda, direita ou cima). Este valor pode ir de 0 a 3 e representa na verdade uma das 4 linhas que será usada. Lembre-se de que cada linha de quadros na imagem carregada representa uma direção diferente.

As variáveis nXpos e nYpos vão armazenar a posição do personagem nos eixos X e Y da tela.

bool bMoving = false;

Esta variável receberá o valor true caso o usuário pressione uma tecla para mover o personagem.

if (gsInput.KeyDown(GSK_LEFT))
{
     nDir=1;
     nXpos-=fSpeed;
     interp.Set(0, 3, fStride);
     bMoving = true;
} else
if (gsInput.KeyDown(GSK_RIGHT))
{
     nDir=2;
     nXpos+=fSpeed;
     interp.Set(0, 3, fStride);
     bMoving = true;
}

Aqui utilizamos o objeto GAMESPACE_INPUT_HANDLER pela primeira vez. O método KeyDown retorna true se a tecla especificada como argumento estiver sendo pressionada. Veja a lista de opções de argumento para essa função em: http://www.asantee.net/gamespace/reference.htm#GS_KEY

Caso a seta para esquerda esteja sendo pressionada, a direção do personagem será definida para a linha 2 (lembre-se que a primeira linha é a 0!) e fSpeed será decrementada de nXpos.

O método GS_INTERPOLATOR::Set pode ser usado para criar uma animação suave para animações baseadas em quadros. Nos argumentos do método definimos que os quadros da animação irão do 0 ao 3 (da primeira à quarta coluna na imagem do sprite) e que os quadros devem ser trocados a cada 100 milésimos (veja a declaração de fStride).

Caso a seta para direita seja pressionada, definimos a terceira linha como direção do personagem e incrementamos nXpos com fSpeed.

O mesmo fazemos para as setas CIMA e BAIXO do teclado:

if (gsInput.KeyDown(GSK_DOWN))
{
     nDir=0;
     nYpos+=fSpeed;
     interp.Set(0, 3, fStride);
     bMoving = true;
} else
if (gsInput.KeyDown(GSK_UP))
{
     nDir=3;
     nYpos-=fSpeed;
     interp.Set(0, 3, fStride);
     bMoving = true;
}

Assim controlamos todas as variáveis que vão definir a posição e animação do personagem.

if (!bMoving)
{
     interp.Set(0, 0, 0.0f);
}

Caso o personagem esteja movendo-se, definir a animação para o quadro 0 (primeira coluna).

gsSprite.SetRect(interp.GetCurrentFrame(), nDir);

Com a função GS_SPRITE::SetRect definimos o quadro que queremos utilizar dentro da grade 4×4 em que a imagem foi dividida (com o método GS_SPRITE::SetupSpriteRects).

Primeiro definimos a coluna que queremos. O método GS_INTERPOLATOR::GetCurrentFrame retorna o quadro atual da animação (que foi definida entre 0 e 4 com Set). E nDir especifica qual linha do quadro queremos. Esta linha é especificada por nDir.

Agora que definimos o quadro atual do sprite podemos desenha-lo:

gsSprite.DrawSprite(GS_VECTOR2((float)nXpos, (float)nYpos));

Aqui desenhamos o sprite nas posição nXpos e nYpos. E aí temos nosso personagem interativo!

Para saber mais sobre a GameSpaceLib visite http://www.asantee.net/gamespace/

Você pode baixar o jogo-exemplo com o seu código-fonte aqui: Space Pong com GameSpaceLib.

Sem comentarios

Oi, meu nome é AbraGames

Artigo escrito por Bruno, membro da PDJ

Logotipo da AbraGames

Para quem não sabe a AbraGames (Associação Brasileira das Desenvolvedoras de Jogos Eletrônicos) é uma entidade sem fins lucrativos fundada em abril de 2004. Seu objetivo principal é trabalhar em favor da indústria brasileira de desenvolvimento de jogos eletrônicos. As ações para promover esse desenvolvimento são:

  • Reunir, para a defesa de seus interesses, as empresas que compõem a indústria;
  • Representar a indústria e atuar, em caráter permanente, junto ao governo e demais instituições cujas decisões tenham impacto no desenvolvimento do setor, além de representar a visão de seus membros;
  • Promover, nos mercados nacionais e internacionais, os produtos e serviços de seus associados;
  • Patrocinar atividades que estimulam a expansão de oportunidades de trabalho neste segmento em todo o Brasil;
  • Colaborar com os associados para reter e atrair talentos dentro da indústria de desenvolvimento de jogos nacional;
  • Desenvolver normas e padrões de produção e prestação de serviços destinados ao aprimoramento da qualidade dos associados;
  • Conscientizar a comunidade nacional para a importância econômica, social e cultural das atividades de desenvolvimento de jogos eletrônicos, promovendo o consumo dos jogos nacionais;
  • Promover a inovação, a pesquisa e o desenvolvimento de novas tecnologias de relevante interesse para a indústria;
  • Procurar obter incentivos, governamentais ou privados, para a pesquisa e desenvolvimento de jogos nacionais, bem como atrair investimentos nacionais e estrangeiros para o setor;
  • Realizar, periodicamente, pesquisas e estatísticas com vistas à elaboração de estudos informativos e à avaliação das tendências e da dimensão do mercado;
  • Encaminhar às autoridades governamentais e demais entidades competentes estudos e sugestões visando ao desenvolvimento e fortalecimento da indústria, inclusive defendendo o aprimoramento da legislação relativa às atividades da indústria brasileira de desenvolvimento de jogos em geral e, em especial, à proteção jurídica de seus produtos e normas de incentivo ao desenvolvimento, produção, comercialização, importação e exportação de jogos eletrônicos com vistas a assegurar um tratamento justo e igualitário para produtores e usuários destes serviços;
  • Acompanhar, de maneira ampla, a política governamental em todos os setores que interessem à indústria, pleiteando (em processos legislativos e regulatórios) medidas que beneficiem a evolução do mercado de trabalho e das empresas do setor;
  • Organizar eventos, cursos, seminários e palestras;
  • Manter intercâmbio de caráter cultural e informativo com outras associações e entidades afins nacionais e internacionais promovendo, quando for o caso, atividades conjuntas;
  • Encorajar e promover o sentido de comunidade dentro da Indústria, a harmonia e cooperação entre seus associados e, de modo geral, promover o desenvolvimento da produção de jogos brasileiros;
  • Oferecer, na medida de suas possibilidades, serviços de assistência e aconselhamento profissional aos associados.

Umas das primeiras coisas que a ABRAGAMES fez foi publicar o Plano Diretor da Promoção da Indústria de Desenvolvimento de Jogos Eletrônicos no Brasil, neste documento constam quais medidas tem que ser tomadas para que a indústria de jogos eletrônicos cresça. Esta é uma leitura obrigatória para quem quer ser inteirar mais sobre o assunto.
Acessando o site da ABRAGAMES você encontra uma lista completa de empresas associadas, mural de empregos, pesquisas sobre o mercado nacional, calendário de eventos e muito mais.

Sem comentarios

Como fazer um Space Invaders – Parte 3

Na terceira parte da série nós finalmente vamos começar a programar, infelizmente ainda não vai ser uma programação "pesada", só vamos preparar o programa para que ele possa "suportar" o jogo em si. Ou seja, vamos criar um esqueleto para ele.

Módulo da Lib

Primeiro, vamos criar um programa que inicie a Allegro, entre num loop(que não faz nada) até que se aperte Esc, e depois feche. O importante aqui é ir programando e compilando a medida que se faz o código. Muitas pessoas quando começam a programar tendem a programar várias linhas de código e só depois compilar, mas isso é ruim porquê depois de muitas linhas de código se tiver um erro no seu programa(e provavelmente vai ter) fica díficil saber de one ele veio já que você programou muitas coisas de uma só vez.

Então agora nós só vamos fazer um programa que chama a Allegro e pronto. Mas é importante lembrar que nós vamos separar a Allegro em um outro módulo como eu havia dito. O porquê disso é devido ao fato de organização no código e também pra aumentar a facilidade de portabilidade. Se você depois quiser fazer esse programa em SDL ou outra lib, o jogo vai estar mais isolado da lib utilizando essa organização no código.

Na primeira parte da série eu falei que seria necessário um conhecimento de programação de jogos e como usar a Allegro, então pra quem não conhece existem bons tutoriais por aí sobre ela, mas se quiser continuar lendo sinta-se a vontade ;)

Vamos criar então três arquivos:

  • main.c
  • libAllegro.h
  • libAllegro.c

A main.c é o programa em si, que tera nossa função main() e que controlará todo o resto do jogo. O libAllegro.h e libAllegro.c são os arquivos que vão constituir o módulo da lib da Allegro.

Dentro do libAllegro.h, teremos o seguinte código:

libAllegro.h
  1.  
  2. #ifndef __libAllegro
  3. #define __libAllegro
  4.  
  5. #include <allegro.h>
  6.  
  7. void inicia_allegro();
  8. void finaliza_allegro();
  9.  
  10. #endif

Nesse arquivo não temos nada de muito especial, somente incluimos a allegro, e declaramos duas funções que serão programas no libAllegro.c, pelo nome das funções(inicia_allegro e finaliza_allegro) podemos supor que elas vão servir para iniciar e fechar a Allegro.
Agora vamos para o libAllegro.c programar cada função. Primeiro vamos programar a initAllegro, que será a função que irá inicializar a Allegro. Para inicializar a Allegro nós vamos utilizar o mesmo código que o Dev-C++ cria caso você use o devkpak da Allegro , segue o código comentado:

Inicialização da Allegro
  1. int depth, res;
  2.  
  3. allegro_init();
  4.  
  5. //Quantas cores o jogo tera
  6. depth = desktop_color_depth();
  7. if (depth == 0) depth = 32;
  8. set_color_depth(depth);
  9.  
  10. //Resolucao da tela
  11. res = set_gfx_mode(GFX_AUTODETECT_WINDOWED, 640, 480, 0, 0);
  12. if (res != 0)
  13. {
  14.     allegro_message(allegro_error);
  15.     exit(-1);
  16. }
  17.  
  18. //Inicializacao de submodulos da Allegro
  19. install_timer();
  20. install_keyboard();

Bom, explicando por alto cada parte da função(já que o foco aqui não é Allegro) primeiro nós declaramos duas variáveis que serão utilizadas para armazenar os bits das cores(depth) e outra para verificar se podemos inicializar o programar com a resolução e o modo gráfico definido(res). Depois chamamos a allegro_init() para que a allegro se inicie e para podermos usar todas as outras funções da allegro.

Mais abaixo usamos a variável depth para receber quantas cores utilizamos no desktop, depois disso é feita uma verificação de erro e chamamos a set_color_depth(depth); Mais embaixo inicializamos o modo gráfico com janela na resolução de 640 por 480, mais adiante fazemos uma verificação de erro. E depois chamamos install_timer() e install_keyboard() para podermos usá-los mais tarde. Desculpem não explicar detalhadamente mas como disse acima, o foco não é a Allegro ;)

A função finaliza_allegro() será uma função vazia, já que por hora não tem o que botar nela mesmo. Ela vai servir mais tarde para finalizarmos timers e outras coisas da Allegro.

O arquivo libAllegro.c ficará assim:

libAllegro.c
  1.  
  2. #include "libAllegro.h"
  3.  
  4. void inicia_allegro()
  5. {
  6.  
  7.     int depth, res;
  8.  
  9.     allegro_init();
  10.  
  11.     //Quantas cores o jogo tera
  12.     depth = desktop_color_depth();
  13.     if (depth == 0) depth = 32;
  14.     set_color_depth(depth);
  15.  
  16.     //Resolucao da tela
  17.     res = set_gfx_mode(GFX_AUTODETECT_WINDOWED, 640, 480, 0, 0);
  18.     if (res != 0)
  19.     {
  20.         allegro_message(allegro_error);
  21.         exit(-1);
  22.     }
  23.  
  24.     //Inicializacao de submodulos da Allegro
  25.     install_timer();
  26.     install_keyboard();
  27.  
  28. }
  29.  
  30. void finaliza_allegro()
  31. {
  32.  
  33. }

Bom, falta só chamar essas funções no nosso arquivo main. A estrutura do main vai ser bem simples, ele precisa chamar nosso módulo da Allegro, chamar a função de inicializar, entrar num loop(essa parte do loop na verdade não precisa, mas eu deixei ela aqui para que a gente possa ver a tela preta), depois chamar a função deinitAllegro, e fim :)

main.c
  1.  
  2. #include "libAllegro.h"
  3.  
  4. int main()
  5. {
  6.     inicia_allegro();
  7.  
  8.     while (!key[KEY_ESC])
  9.     {
  10.  
  11.     }
  12.  
  13.     finaliza_allegro();
  14.  
  15.     return 0;
  16. }
  17. END_OF_MAIN()

Se vocês compilarem o programa agora(é bom fazê-lo), ele irá rodar uma tela preta e quando você apertar Esc ele irá sair do programa.

Módulo Jogo

O módulo do jogo vai ser um dos mais importantes porque ele é um módulo central que controla todo o resto do jogo(por isso o nome dele :)). Vai ser um dos módulos onde nós mais vamos voltar e ficar adicionando/mudando código já que ele é o controlador de tudo, então a medida que formos criando as coisas, quem vai botar elas "no palco" vai ser esse módulo.

Primeiro vamos criar o jogo.h:

jogo.h
  1.  
  2. #ifndef __jogo
  3. #define __jogo
  4.  
  5. extern int fimJogo;
  6.  
  7. void inicia_variaveis();
  8. void processamento_jogo();
  9.  
  10. #endif

Ou seja, esse módulo agora não vai ter muita coisa. Só precisamos de uma variável fimJogo que vamos usar na main mais tarde, e duas funções, uma pra inicializar as variáveis relacionadas ao jogo, e a outra é o processamento do jogo em si. Vamos ver elas no jogo.c:

#include "jogo.h"
  1.  
  2. int fimJogo;
  3.  
  4. void inicia_variaveis()
  5. {
  6.     fimJogo = 0;
  7. }
  8.  
  9. void processamento_jogo()
  10. {
  11. }

Por enquanto esse módulo não tem nada de especial também, só atribuimos zero a variável fimJogo. Pode parecer inútil programar esses módulos sem nada só pra ter um programa que aparece uma tela preta, mas construir programas grandes é como se fosse qualquer projeto grande como por exemplo uma casa, primeiro se constroem os alicerces para depois ir "moldando" o resto em cima disso. E é assim que um programa grande funciona, embora no nosso caso o Space Invaders não seja necessariamente grande como os jogos de hoje em dia, ele já apresenta características de um jogo que precisa ser programado dessa maneira.

Se nós não programarmos o jogo projetando e tendo em vista o final dele, mais adiante muito do código terá que ser reescrito e isso além de ser trabalhoso faz com que o jogo fique com muitos bugs porque você quase sempre não se lembra algo que você programou no começo do jogo. Então se você está se perguntando o porquê de tudo isso, relaxe que mais adiante você vai entender ;)

Agora o que nos falta é inserir isso na nossa main, o que eu basicamente vou fazer é chamar a função de inicializar as variáveis, mudar o while e inserir o processamento do jogo:

#include "libAllegro.h"
  1. #include "jogo.h"
  2.  
  3. int main()
  4. {
  5.     inicia_allegro();
  6.  
  7.     inicia_variaveis();
  8.  
  9.     while (!fimJogo)
  10.     {
  11.         //Input — Apagar depois
  12.         if (key[KEY_ESC]) fimJogo = 1;
  13.  
  14.         processamento_jogo();
  15.     }
  16.  
  17.     finaliza_allegro();
  18.  
  19.     return 0;
  20. }
  21. END_OF_MAIN()

Bom, percebam que eu chamei a inicia_variaveis() logo após a inicia_allegro(), essa ordem é importante porque algumas vezes nós vamos precisar iniciar variáveis que são dependentes da lib que estamos usando, então essa função tem que vir depois da inicialização da lib.

Depois eu mudei o while de "while (!key[KEY_ESC])" pra "while (!fimJogo)". Eu fiz isso devido ao fato de que o jogo nem sempre vai fechar quando apertamos ESC, por enquanto ele só vai fechar nessa condição, mas mais adiante ele vai fechar no menu principal, quando apertamos esc, quando der algum erro, etc. Então por isso o loop principal do jogo depende da variável fimJogo.

Adicionei a linha de "if (key[KEY_ESC]) fimJogo = 1 para podermos fechar o jogo, mais adiante nós vamos deletar essa linha já que no lugar dela vai ser chamada uma função para controlar o input de todo o jogo, mas como nós ainda não criamos o módulo de input essa linha de código vai ficar aí temporiariamente para podermos fechar o jogo.

Logo após o nosso falso input eu chamo a função de processamento do jogo, embora não tenha nada, é essa função que vai mexer os objetos do jogo, detectar colisões, verificar quando a fase acabou, processar a IA dos inimigos, ou seja, tudo relativo ao jogo em si. Por enquanto vamos deixar ela vazia mesmo.

Finalizando

Bom, até agora você tem uma bela de uma tela preta! Mas isso é importante já que existem milhares de maneiras de fazer um programa que imprima uma tela preta, e principalmente milhares de maneiras de começar a programar um jogo. Mas eu estou fazendo isso pra que vocês vejam que é importante preparar tudo antes mesmo que o programa não faça nada. E não é só porque o programa não está fazendo nada, que o código por debaixo dele tem que ser ruim. O jogo tem que ser bom tanto para o usuário quanto para o programador.

Por enquanto vamos ficar só com esses dois módulos, no próximo será explicado o módulo de output o de objeto e canhão e input! Então se prepare!

Mas como vocês viram, montar um jogo é saber se preparar, entender os requerimentos dele e saber como programar isso efetivamente. Por isso eu frizei bastante nas 2 primeiras partes da série o planejamento, e como eu disse, é prática! Até a próxima ;)

12 Comentarios

Padrões de Projetos para Jogos – Máquina de Estado

Primeiramente, gostaria de me desculpar pelo atraso, nós estavamos com sérios problemas com o sistema do WordPress pra publicar esse post(que era pra ter saído a umas 2 semanas!) Mas uma simples tag que tinha nele impedia a gente de postar, mas agora ajeitamos e aqui está mais um tutorial escrito por um convidado, Bruno Schifer, que mandou diretamente do fórum e está sendo publicado aqui.

Só lembrando que quem quiser participar também é só postar aqui.

E fiquem com o tutorial:

Nesse tutorial eu vou ensinar uma técnica de programação muito útil para o desenvolvimento de jogos.

Trata-se da máquina de estado que pode ser usada para controle da execução do seu jogo entre outras coisas.

Antes de mais nada, vamos chamar o objeto de estudo deste tutorial corretamente, pois o nome está incompleto.

As máquinas de estado são mais corretamente denominadas Máquina de Estado Finito ou em inglês “Finite State Machine” ou ainda melhor, FSM.

A utilização das FSMs no mundo da programação já é muito difundida e qualquer projeto mais complexo pode se utilizar desta técnica para resolver problemas comuns de lógica.

Com a utilização, foi-se criando um padrão de implementação para esse objeto e não é à toa que os autores Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides, incluíram um padrão que pode ser usado para implementar a FSM em C++ no livro “Design Patterns: Elements of Reusable Object-Oriented Software”.

Eu já encontrei FSMs em implementações de jogos, algoritmos de inteligência artificial, algoritmos de parsing (compiladores) e em muitos outros lugares, ou seja, é muito importante para qualquer desenvolvedor de jogos entender o funcionamento das Máquinas de Estado Finito.

O estudo das FSMs é normalmente conhecido como a Teoria dos Autômatos ou ainda Teoria da Computação e ela é ensinada nas universidades para a implementação de compiladores e algoritmos de parsing, apesar de ter muitas outras aplicações.

Vamos então ao significado do termo: FSM é um modelo de comportamento composto por um número finito de estados, transições e eventos que geram essas transições.

Um modelo é uma interpretação ou abstração de uma realidade para que possamos compreender e utilizar o objeto com maior facilidade.

Portanto, a máquina de estados cria um modelo do comportamento de um objeto para que possamos utilizá-lo em nossas aplicações.

Por número finito de estados queremos dizer que nós podemos contar quantos estados a máquina possui.

Logo, esse modelo de comportamento pode assumir um número limitado de estados.

Com base nisso, nós podemos representar uma FSM através de um “diagrama de transição de estados” ou através de um “diagrama de estados” definido na UML.

Não vou me aprofundar nos diagramas citados acima, pois eles não entram no escopo deste tutorial.

O que eu vim aqui mostrar é uma implementação de uma FSM utilizando o State Pattern definido no livro de Padrões de Projetos do Erich Gamma.

Vamos ao exemplo…

Simplificando o modelo, vamos imaginar um jogo qualquer bem simples. A minha idéia é criar um jogo que possua uma tela de apresentação, uma tela de menu e uma tela com o jogo em si. Dessa forma, concluímos que o número de estados dessa aplicação é 3 (três).

As transições entre os estados são acionadas a partir de certos eventos. Vamos tentar relacioná-los aqui:

Quando o jogo entra na tela de apresentação não é possível que o jogador saia da tela. Ele terá que aguardar 5 segundos para que ocorra a primeira transição de estado. Após os 5 segundos, um evento ocorrerá e a transição para o estado de menu será acionada. Portanto, o primeiro evento de transição é o tempo alcançar 5 segundos após a entrada na máquina.

Dentro do estado de menu, o jogador pode sair da aplicação pressionando o botão saída ou pode entrar no estado jogo, pressionando o botão jogar. As transições nesse estado ocorrem somente com o pressionamento de dois botões.

Por último, o jogador estando dentro da tela de jogo, pode sair da mesma pressionando o botão ESC no teclado. Ao sair desse estado, a máquina volta para o estado menu.

Vamos visualizar em um diagrama de estados da UML o modelo de comportamento desse jogo:

Com esse diagrama, conseguimos ver exatamento o comportamento do jogo mapeado em um modelo de FSM.

Após ter definido nosso modelo, podemos começar a implementar.

Como meu objetivo é mostrar a implementação do State Pattern, não vou apresentar uma aplicação gráfica aqui. Tudo será implementado no console do DOS.

Ao código então…

Crie um projeto vazio com o nome de MaquinaEstado no Dev-C++.

Acrescente os seguintes arquivos no projeto:

- Main.cpp
- Global.h
- Maquina.cpp
- Maquina.h
- Estado.cpp
- Estado.h
- Estados.cpp
- Estados.h

Pronto, agora pegue o arquivo Main.cpp e inclua o seguinte código nele:

#include “Global.h”int main()
{
// Loop principal do jogo
while(1)
{
// Implementação dos eventos: teclas ESC, j e q, e evento de tempo terminado
if(kbhit())
{
int tecla = getch();
if(tecla == 27) // tecla ESC
{
break;
}
} //Limpa a tela // Desenha na tela printf(“Loop”);
}
return 0;
}

Pegue o arquivo Global.h e insira o seguinte código:

#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#include <time.h>


Como eu disse, não estaremos usando nada gráfico aqui.Compile o programa e execute. Esse programa é um modelo simplificado de jogo. Você pode observar no código de Main.cpp o loop principal da aplicação que espera entradas no teclado, comando kbhit() e getch(). Caso ocorra algum pressionar de botão, o programa armazena a tecla pressionada na variável específica e caso a tecla pressionada seja um ESC, o loop é interrompido com o comando break.Mais a frente no código, podemos encontrar um comentario simulando a limpeza no buffer da tela, que poderia ser feito com uma chamada ao sistema operacional, usando system mais um cls, e podemos encontrar o comando printf() que simula o desenhar da tela. Tudo isso dentro de um modelo simplificado que tem como objetivo excluir a complexidade do desenvolvimento de um jogo para que possamos focar o problema na implementação de uma FSM.No código de Global.h você pode encontrar as bibliotecas padrão do C que estamos usando nessa aplicação.Com essa aplicação, nós já podemos receber 3 dos eventos que acionam as transições de estado da máquina:- Pressionar da tecla “j” para jogar – Pressionar da tecla “q” para sair – Pressionar da tecla “ESC”Está faltando somente um dos eventos relacionados anteriormente no tutorial:- limite de tempo de 5 segundos alcançado após a entrada da aplicaçãoAcrescente o código em vermelho na função main para podermos monitorar o tempo:

#include “Global.h”int main()
{
// Tempo
time_t timer_start;
time_t timer_current;
timer_start = time(NULL);double diff = 0; // Loop principal do jogo
while(1)
{
timer_current = time(NULL);
diff = difftime(timer_current, timer_start); if(kbhit())
{
int tecla = getch();
if(tecla == 27)
{
break;
}
}
// Desenha na tela
printf(“Loop: %4.2f\n”, diff);
}
return 0;
}

Pronto, a partir de agora somos capazes de capturar qualquer evento necessário para que as transições existentes no nosso modelo sejam acionadas. Está na hora de começar a implementar os estados da máquina.Segundo Erich Gamma, inicialmente, temos que implementar uma classe abstrata chamada, no meu caso, de Estado e essa classe, basicamente, irá implementar uma interface comum para todos os estados do jogo. As subclasses de Estado, implementam, então, comportamentos específicos a cada um destes estados.O modelo não é complicado. Segue abaixo um diagrama de classes do modelo que irei implementar baseado no livro do Gamma.Nesse modelo, a classe Maquina mantém uma instância da classe Estado e a utiliza para executar operações específicas ao estado do jogo.Toda vez que um estado muda, ou melhor, quando ocorre uma transição de estados, a instância de Estado da classe Maquina muda.Primeiramente, vamos observar o código da classe Estado, pai de todos os estados do jogo. Abra o arquivo Estado.h e acrescente o código abaixo:

#ifndef ESTADO_H
#define ESTADO_H
class Estado
{
public:
// Eventos de mudança de estado
virtual void AoPressionarJogar(Maquina* maquina) {};
virtual void AoPressionarSair(Maquina* maquina) {};
virtual void AoPressionarESC(Maquina* maquina) {};
virtual void AoTerminarTempo(Maquina* maquina) {};
// Executa o evento de entrada do estado
virtual void AoEntrar() {};
// Executa um frame de animação do estado atual
virtual void ExecutaFrame() {};
// Executa o evento de saída do estado
virtual void AoSair() {};
protected:
void ExecutaTransicao(Maquina* maquina, Estado* estado);
};
#endif

A nossa classe Estado possui um método para cada evento tratado pela máquina. Esses métodos são abstratos (virtual) e podem ser reescritos nas classes filhas de Estado, ou seja, cada um dos estados do jogo terá o tratamento de um evento específico para o seu caso. Lembrando que não existe a necessidade de implementação de todos os métodos de evento na classe filha, vou explicar isso mais a frente quando eu mostrar a implementação dessa classe.Como meus estados são estados de um jogo, antes de executar o frame de animação, eu preciso instanciar todos os objetos que irei utilizar na tela e os meus controladores de objetos, além de outras coisas. Por isso que crio dois eventos de estado que são: AoEntrar() e AoSair(). No momento em que executo uma transição de estados, eu faço uma chamada ao método AoSair() do estado atual, pois a máquina irá sair desse estado no momento da transição e após que ocorrer a transição eu faço uma chamada ao método AoEntrar() do estado atual, pois o estado foi alterado para um novo estado. Esses dois métodos são chamados antes e depois da transição e eles são responsáveis por instanciar e liberar os recursos usados no Estado em questão.O método ExecutaFrame() é usado para a lógica contida no loop do jogo referente a um estado específico. É nesse método que eu, por exemplo, no EstadoJogo desenho e atualizo o jogador, os inimigos, os tiros, etc.Por último, o método ExecutaTransicao() é utilizado para chamar os métodos AoSair() e AoEntrar() e para executar a transição de estados em si. Ele é implementado na classe pai Estado, pois a lógica deve ser a mesma para todos os estados filhos desta classe.Vamos observar o código de implementação dessa classe Estado:

#include “Global.h”void Estado::ExecutaTransicao(Maquina* maquina, Estado* estado)
{
maquina->ExecutaTransicao(estado);
}

O único método que temos que implementar aqui é o método ExecutaTransicao(). Ao ser chamado, ele informa à máquina, através do método maquina->ExecutaTransicao(estado) o novo estado que a máquina vai assumir como estado corrente.Vamos observar o codigo da classe Maquina. Abra o arquivo Maquina.h e inclua o seguinte código nele:

#ifndef MAQUINA_H
#define MAQUINA_H
class Estado;class Maquina
{
private:
// Instância única da classe (Singleton)
static Maquina* m_pInstancia;
// Estado atual
class Estado* m_pEstadoAtual;
// Indica se a máquina tem que parar
bool m_Finalizar;
public:
// Construtor da classe
Maquina();
// Cria a instância única da classe (Singleton)
static Maquina* CriaInstancia();
// Executa um frame de animação do estado atual
void ExecutaFrame();
// Executa transição de estados
void ExecutaTransicao(Estado* estado);
// Finaliza a máquina
void Finalizar();
// Pergunta se pode finalizar a máquina
bool PodeFinalizar();
// Eventos de mudança de estado
void AoPressionarJogar();
void AoPressionarSair();
void AoPressionarESC();
void AoTerminarTempo();
};
#endif

Nesse fragmento acima nós temos a classe Maquina que irá controlar as mudanças de estado do meu jogo. Ao criar o objeto Maquina, eu utilizarei o padrão Singleton garantindo que eu tenha somente uma instância dessa classe em toda a execução da minha aplicação. Esse padrão não me deixa cometer o erro de criar duas instâncias dessa classe. Para aprender mais sobre o Singleton consulte o livro do Gamma ou procure na Internet, existem vários sites que mostram como implementá-lo. Esse padrão será assunto de um tutorial futuro aqui na Schifer. O atributo m_pInstancia aponta para a instância única da máquina e o método CriaInstancia() é o responsável pela criação da instância única.O segundo atributo da classe Maquina é uma referência para o estado corrente do meu jogo: m_pEstadoAtual.Em seguida, eu coloco um atributo booleano que informa se a máquina irá finalizar ou não. Caso esse atributo assuma um valor verdadeiro, a máquina irá informar à aplicação que o loop principal deve ser interrompido através de um comando break. Quem informa a aplicação é o método PodeFinalizar(). Esse método é chamado no meio do loop principal. Existe ainda um método Finalizar() que pode ser chamado em qualquer lugar da aplicação informando que a partir de agora, a máquina pode interromper o fluxo de execução, ou seja, esse método informa que já pode finalizar e o método PodeFinalizar() só responde a pergunta, pois em um ponto específico da execução, precisamos perguntar se podemos ou não executar o comando break.Temos, então, o construtor da classe que limpa os ponteiros e faz com que a variável m_Finalizar receba falso, pois quando for verdadeiro, ela irá terminar a execução do loop principal.Após o construtor, temos o método CriaInstancia() do Singleton.Depois do método do Singleton, temos o método ExecutaFrame(). Ele é responsável por chamar o método ExecutaFrame() do estado corrente.Os dois métodos seguintes são os métodos de finalização da máquina já explicado.Por último, nós temos os métodos que lançam os eventos que a máquina trata. Para cada evento tratado, deve existir um método de lançamento desse evento. Eu poderia trocar esses nomes por nomes mais conceituais de jogo, mas para manter a complexidade baixa, eu chamei os métodos com os nomes das teclas que serão pressionadas, mas isso acabe a você decidir como irá implementar.Vamos observar o código de implementação dos métodos acima:

#include “Global.h”Maquina::Maquina()
{
m_pEstadoAtual = 0;
m_Finalizar = false;
}
// Definição do atributo instância
Maquina* Maquina::m_pInstancia = 0;
// Cria a instância única da classe (Singleton)
Maquina* Maquina::CriaInstancia()
{
if(m_pInstancia == 0)
{
m_pInstancia = new Maquina();
}
return m_pInstancia;
}
// Executa uma transição de estado
void Maquina::ExecutaTransicao(Estado* estado)
{
// Executa o evento AoSair() do estado antigo antes de executar a transição
if(m_pEstadoAtual != 0)
{
m_pEstadoAtual->AoSair();
}
m_pEstadoAtual = estado;// Executa o evento AoEntrar() do estado novo logo após executar a transição
m_pEstadoAtual->AoEntrar();
}
// Executa um frame de animação do estado atual
void Maquina::ExecutaFrame()
{
m_pEstadoAtual->ExecutaFrame();
}
void Maquina::Finalizar()
{
m_Finalizar = true;
}
bool Maquina::PodeFinalizar()
{
return m_Finalizar;
}
// Eventos de mudança de estado
void Maquina::AoPressionarJogar()
{
m_pEstadoAtual->AoPressionarJogar(this);
}

void Maquina::AoPressionarSair()
{
m_pEstadoAtual->AoPressionarSair(this);
}

void Maquina::AoPressionarESC()
{
m_pEstadoAtual->AoPressionarESC(this);
}

void Maquina::AoTerminarTempo()
{
m_pEstadoAtual->AoTerminarTempo(this);
}

O código começa com o construtor que já foi explicado anteriormente, assim como o próximo método que é o responsável pela criação do Singleton.O método ExecutaTransicao() é o responsável pela troca de estados. Existe um teste inicial que verifica se o valor do estado corrente é zero, pois se for, um erro será lançado ao se tentar usar um método a partir de um ponteiro para objeto vazio. Esse teste é necessário, pois não podemos chamar o método OnSair() na primeira vez que estivermos executando a máquina. Em seguida ele muda a referência do estado atual e logo em seguida chama o método OnEntrar() para o novo objeto que o ponteiro estará referenciando.Após trocar o estado, temos o método ExecutaFrame() que como foi explicado anteriormente, chama o ExecutaFrame() do estado atual.Temos então os métodos que fazem o controle de finalização da máquina, já explicados, e os métodos que implementam os eventos que a máquina trata. Todos eles chamam seus respectivos métodos nas classes filhas. É nas classes filhas que implementamos a lógica das transições, pois dependendo do evento que ocorrer e do estado atual, o próprio estado indica qual o próximo estado que a máquina assumirá.Vamos implementar agora os 3 estados deste jogo. Copie o código seguinte para o arquivo Estados.h.

#ifndef ESTADOS_H
#define ESTADOS_H
class EstadoApresentacao : public Estado
{
private:
// Instância única da classe (Singleton)
static EstadoApresentacao* m_pInstancia;
public:
// Cria a instância única da classe (Singleton)
static EstadoApresentacao* CriaInstancia();
void AoEntrar();void ExecutaFrame();void AoSair();// Eventos de mudança de estado
void AoTerminarTempo(Maquina* maquina);
};
class EstadoMenu : public Estado
{
private:
// Instância única da classe (Singleton)
static EstadoMenu* m_pInstancia;
public:
// Cria a instância única da classe (Singleton)
static EstadoMenu* CriaInstancia();
void AoEntrar();void ExecutaFrame();void AoSair();// Eventos de mudança de estado
void AoPressionarJogar(Maquina* maquina);
void AoPressionarSair(Maquina* maquina);
};
class EstadoJogo : public Estado
{
private:
// Instância única da classe (Singleton)
static EstadoJogo* m_pInstancia;
public:
// Cria a instância única da classe (Singleton)
static EstadoJogo* CriaInstancia();

void AoEntrar();

void ExecutaFrame();

void AoSair();

// Eventos de mudança de estado
void AoPressionarESC(Maquina* maquina);
};

#endif

Como você pode visualizar no código, todos os meus estados são um Singleton, isso é recomendação do Erich Gamma em seu livro. Portanto, os 3 estados terão os métodos e atributos referentes ao padrão Singleton. O método é o CriaInstancia() e o atributo é o m_pInstancia. Você pode notar que todos os estados possuem esse método e esse atributo.Em seguida temos a declaração dos 3 métodos que foram declarados abstratos na classe Estado: AoSair(), ExecutarFrame() e AoEntrar(). Cada um desses métodos já foi explicado anteriormente, mas o que vou acrescentar é que cada um dos estados possui uma lógica sua de sequência de tarefas que serão executadas, ou seja, a lógica é específica para cada estado. Eu mostrarei isso na implementação do estado.Após declarar os métodos acima, encontramos os eventos da máquina tratados por um estado específico. Os estados não implementam todos os eventos, mas somente aqueles que será capaz de tratar. Por isso que no primeiro estado, você encontra o método AoTerminarTempo() e nos outros estados não. Esse método só é necessário no estado apresentação de acordo com as definições de nosso projeto. Isso vale para os outros estados também, você verá que cada um implementa os eventos necessários para ele.Agora, observe a implementação da classe acima. Acrescente o código abaixo no arquivo Estados.cpp:

#include “Global.h”EstadoApresentacao* EstadoApresentacao::m_pInstancia = 0;EstadoApresentacao* EstadoApresentacao::CriaInstancia()
{
if(m_pInstancia == 0)
{
m_pInstancia = new EstadoApresentacao();
}
return m_pInstancia;
}
void EstadoApresentacao::AoEntrar()
{
}
void EstadoApresentacao::ExecutaFrame()
{
printf(“\nApresentacao\n”);
}
void EstadoApresentacao::AoSair()
{
}
void EstadoApresentacao::AoTerminarTempo(Maquina* maquina)
{
ExecutaTransicao(maquina, EstadoMenu::CriaInstancia());
}
EstadoMenu* EstadoMenu::m_pInstancia = 0;EstadoMenu* EstadoMenu::CriaInstancia()
{
if(m_pInstancia == 0)
{
m_pInstancia = new EstadoMenu();
}
return m_pInstancia;
}
void EstadoMenu::AoEntrar()
{
}

void EstadoMenu::ExecutaFrame()
{
printf(“\nMenu\n”);
printf(“\n- Pressione ‘j’ para jogar”);
printf(“\n- Pressione ‘q’ para sair\n\n”);
}

void EstadoMenu::AoSair()
{
}

void EstadoMenu::AoPressionarSair(Maquina* maquina)
{
maquina->Finalizar();
}

void EstadoMenu::AoPressionarJogar(Maquina* maquina)
{
ExecutaTransicao(maquina, EstadoJogo::CriaInstancia());
}

EstadoJogo* EstadoJogo::m_pInstancia = 0;

EstadoJogo* EstadoJogo::CriaInstancia()
{
if(m_pInstancia == 0)
{
m_pInstancia = new EstadoJogo();
}

return m_pInstancia;
}

void EstadoJogo::AoEntrar()
{
}

void EstadoJogo::ExecutaFrame()
{
printf(“\nJogo\n”);
printf(“\nPressione ‘ESC’ para sair”);
}

void EstadoJogo::AoSair()
{
}

void EstadoJogo::AoPressionarESC(Maquina* maquina)
{
ExecutaTransicao(maquina, EstadoMenu::CriaInstancia());
}

No código acima, nós vemos o padrão Singleton implementado para cada um dos estados (já foi explicado anteriormente). Em seguida, não precisei implementar código para os eventos AoSair() e AoEntrar(), pois a simplicidade do meu projeto não exige inicialização de objetos nos estados, ao contrário do método ExecutaFrame(). Esse último necessita de um código específico, pois aqui eu imprimo a mensagem que indica na tela qual estado está rodando nesse momento.Em seguida, nós encontraremos a implementação dos eventos. Basicamente, nesse tutorial, a lógica da transição de um estado é simplesmente passar a instância da máquina e a instância do novo estado. O método ExecutaTransicao() implementado na classe Estado pai é então chamado recebendo a máquina e o novo estado. Ele chama então, o método ExecutaTransicao() da máquina recebendo a instância do novo estado. Esse método pegará essa instância e indicará que o seu atributo m_pEstadoAtual irá receber a referência para essa instância. Dessa forma, o estado atual muda e a máquina assume um novo estado concreto.Para terminar o tutorial, acrescente o texto em vermelho abaixo no arquivo Global.h e tente compilar o programa.

#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#include <time.h>
#include “Maquina.h”
#include “Estado.h”
#include “Estados.h”

Se você quiser fazer o download do projeto com o código fonte completo deste tutorial no Dev-C++ clique aqui.Com isso, nós terminamos mais um tutorial da Schifer. Se você seguiu o tutorial inteiro e gostou ou não gostou do que leu, tem dúvidas sobre o que leu, ou quer apenas me mandar uma mensagem, sinta-se livre para me enviar um e-mail. Meu e-mail é schifers@hotmail.com. Agradeço a atenção e espero que vocês tenham entendido tudo o que foi explicado aqui. Um abraço a todos e até o próximo tutorial.Bruno Schifer

3 Comentarios

Como fazer um Space Invaders – Parte 2

Desculpem o atraso pessoal, é que estamos tendo problema com o sistema do WordPress e o post que era pra ter sido postado (do Bruno Schifer) teve que ser adiado, então esse imprevisto com o sistema acabou me pegando de surpresa pra escrever esse texto(ainda faltava preparar umas coisas!). Mas aqui está:

Parte 2

Bom, nessa segunda parte do artigo vamos fazer as preparações para podermos começar a programar baseado com a descrição do jogo que foi feita na primeira parte da série.

Módulos

A maneira que irei usar para programar aqui vai ser dividir em módulos o que tinhamos definido na primeira parte da série e algumas coisas especiais que vamos usar pra programar. Na primeira parte nós definimos que iriamos ter os seguintes atores no jogo:

- Canhão
- Naves
- Base(barreira)
- Tiros

Definimos também as variáveis que eles irão usar, se vocês prestaram atenção eles dividem variáveis em comum, como x e y, flag de vivo ou morto, e frame atual de animação. No caso o único que é diferente é a barreira. Então nós iremos criar uma estrutura comum para o Canhão, Naves e Tiros. Vamos chamar ela de estrutura objeto. Nós iremos definir as variáveis dela na próxima parte da série.

Vejam que mesmo definindo que iremos criar uma estrutura em comum para eles, cada um tem um movimento diferente, tem uma imagem diferente, atira de maneira diferente e é representado com uma estrutura de dados diferente(o Canhão é uma estrutura normal, as naves são uma matriz de uma estrutura, e os tiros uma lista, ou vetor de uma estrutura de tiros). Então apesar de cada um dos três dividir a estrutura de objeto, cada um tem uma peculiaridade, ou seja, vamos precisar de um módulo para cada um também.

Quando eu falo a palavra módulo, eu me refiro nada mais que um .h e um .c que contém somente coisas relacionadas ao ator que estamos programando. É quase igual a um objeto de POO(só que sem as vantagens de POO). Com o que definimos até agora vamos precisar de um módulo para o Canhão, outro para as Naves outro para a Barreira outro para os Tiros e mais um para a estrutura de objeto. A idéia desses módulos é que (como objetos de POO) eles são “isolados” o máximo possível para que qualquer alteração no jogo, não precise alterar muitos módulos, mas sim o módulo em questão. Ou seja, vamos tentar fazer com que eles sejam independentes um do outro o máximo possível. Não é 100% possível de fazer isso porque geralmente um utiliza variáveis e estruturas dos outros e alguns vão ser por natureza totalmente dependentes de um outro, como por exemplo a estrutura objeto que é uma estrutura “mãe” da estrutura de canhão nave e tiros. Se vocês não estão entendendo muito bem isso tudo, fiquem mais calmos que quando vocês começarem a programar (na próxima parte da série) vai tudo ficar mais claro.

Outros módulos que vamos precisar vão ser os módulos de output, input, o módulo jogo e mais um módulo para a lib que vamos usar. Esse módulo da lib vai servir pra que possamos tentar separar algumas funções da lib que vamos usar no jogo, pra ficar mais fácil pra quem usa SDL ao invés de Allegro.

Resumindo nós temos os seguintes módulos:

Lib, módulo que terá funções para inicializar/finalizar a lib utilizada.

Objeto, módulo que irá conter estrutura variáveis e funções para manipular objetos(qualquer coisa que se mova) no jogo.(É nesse módulo que vamos criar a função da colisão)

Tiros, módulo que será dependente do módulo objeto e ira conter estruturas de dados, variavéis e funções para manipular os tiros das naves inimigas e do jogador.

Nave, módulo que será dependente do módulo objeto e do modulo tiro e irá conter estrutura variáveis e funções para manipular as naves inimigas do jogo(com base no que tinhamos planejado na série passada, é aqui que vai ter o algoritmo de movimento das naves e o algoritmo de “IA” para atirar)

Canhao, módulo que será dependente do módulo objeto e módulo de tiros, irá conter estrutura variáveis e funções para manipular o canhão do jogo

Barreira, módulo que irá conter estruturas e variáveis para manipular a barreira.

Input, módulo que irá controlar as reações do teclado.

Output, módulo que irá conter funções para controlar audio e video.

Jogo, módulo chave que irá utilizar todos os outros módulos para fazer o jogo funcionar.

Ordem de desenvolvimento

Bom, com base nisso iremos criar a ordem de como iremos programar tudo. De acordo com o que eu tinha dito na parte passada da série, a ordem com que vamos programar tudo depende das dependências(!). Mas nesse caso não são necessariamente as dependências dos módulos. Vejam que o módulo de tiro depende do de objeto, mas ele vai ser um dos ultimos a ser programado já que (como eu tinha dito) se criarmos ele primeiro não iria adiantar muito já que pra um tiro ser disparado precisa de algo que o dispare!

Então a ordem vai ser algo mais ou menos assim:

1º Criar o módulo da Lib
2º Criar o módulo do Jogo
3º Criar o módulo de Output
4º Criar o módulo Objeto e Canhao
5º Criar o módulo de Input
6º Criar o módulo das Naves
7º Criar o módulo dos Tiros
8º Criar o módulo da Barreira

Acredito que a ordem será essa, mas notem que criar um módulo não significa necessariamente que vamos criar ele e nunca mais iremos programar algo nele de novo. Muito pelo contrário, todo módulo que vamos criar vai ser um “esqueleto”, ou seja, no começo não vai ter muita coisa, só o suficiente pra criar as dependências e não precisar ficar reprogramando tudo utilizando Ctrl+C e Ctrl+V ou algo que o valha.

Finalizando

Com isso concluímos essa segunda parte da série. A partir da próxima se preparem que vamos começar a programar ;)

4 Comentarios

Tutorial Básico de Iluminação

Estamos inaugurando o espaço do convidado aqui no PDJBlog. Esse espaço nada mais é do que um espaço onde alguém que não faz parte do time de escritores do Blog escreve um artigo ou tutorial e nós decidimos postar aqui. Em torno de uns 2 ou 3 dias iremos explicar no fórum como proceder.

O escritor de hoje é Marcelo Prado, ou akigames como é conhecido na PDJ. Ele é um dos usuários “das antigas” que com bastante esforço chegou longe na área onde ele queria. É só visitar o site dele: http://marcelo3d.vilabol.uol.com.br/ pra conferir.

Espero que o pessoal se motive vendo como dá pra ir longe com esforço e foco ;)

Light Dome com Scanline Render do 3Ds MAX

Temos aqui a nossa cena renderizada sem qualquer configuração de Iluminação. Uma cena simples com três esferas com Raytrace Material e um plano com um material Standard.

Primeiro criamos uma Spot na TOP View, de forma que o target esteja no centro de nossa cena. Evite deixar a Spot muito próxima da cena. Em seguida desligue o target da Spot e e re posicione o seu PIVOT para o centro da cena. Isso ajudará na hora de copiar a Spot.

Iremos criar um total de 11 instâncias dessa Spot ao redor da cena a cada 30 graus, formando um circulo de Luzes em volta da cena.

Iremos Criar mais 3 instâncias da Spot na vista Front no eixo Z. E depois criar mais 11 instâncias de cada Spot Selecionado na Imagem acima no eixo X. Dessa forma teremos nosso Light Dome Pronto. Só faltando assim um ajuste na fileira central de luzes para interpola-las e ter um sombreamento mais suave.

Teremos assim o seguinte resultado ao renderizar.

 

Precisamos agora configurar as Spots para obtermos o uma Iluminação adequada e sombras difusas. Para esse caso usei a seguinte configuração:

Shadow “on”: Shadow Map
Multipler: 0,05
Specular: Desligado
Shadow Dens.: 1,55
Bias: 0,1
Size: 120
Sample Range: 6

Aqui vai uma pequena explicação do que cada um desses parâmetros.

Shadow Type “Shadow Map”: O Max irá gerar as sombras na cena a partir de um BITMAP criado pelo próprio Max com a silhueta do Objeto. Muito rápido e leve, porém não detecta transparência.

Multipler: Controla a intensidade da Luz.

Specular e Difuse: Controla quais propriedades do Shader a luz irá afetar. Por padrão a luz afeta o Difuse e o Specular do Shader. No nosso caso para o Light Dome desligamos o Specular para que a Luz afete somente o Difuse do Shader iluminando a cena. Caso contrário teríamos várias bolinhas rodeando todos os Objetos com Shaders que possuam Specular.

Shadow Density: Controla a densidade da sombra. Quanto maior a densidade mais escura a sombra tende a ser.

Bias: Controla a distancia da sombra do Objeto que está emitindo-a.

Size: Já que o Shadow Map se basea de um BITMAP calculado pelo Max com a silhueta do Objeto, nesse campo você controla a resolução desse Bitmap. Com um Size de 1024 você terá um BITMAP de 1024×1024 para gerar a sombra. Quanto maior a resolução maior a qualidade e nitidez da sombra.

Sample Range: Aqui você controla a quantidade de “Blur” que você quer no seu BITMAP. Quanto maior o valor do Sample Range mais borrada será a sombra.

Teremos o seguinte resultado. E cadê o Specular da cena? Já que não temos nenhuma Luz gerando Specular na cena vamos criar uma.

Iremos criar uma Omni na cena seguindo as configurações da imagem acima. Logo em seguida com a Omni selecionado clicaremos em Align > Place Highlight. Ao passar o Mouse em cima de umas das esferas aparecerá uma setinha Azul. Essa setinha indica onde a Omni ira gerar o Specular. Com essa ferramenta o Max irá posicionar a Omni no lugar certo para gerar o Specular onde a setinha azul indica. Dessa forma você pode posicionar o Specular da forma que achar melhor para a sua cena.

Agora nossa cena possui Specular.

 

Para finalizar iremos criar a radiosidade vinda do piso. Selecione as Spots que se encontram no topo do Light Dome e crie uma cópia (Cópia, não Instância) e posicione as como na imagem acima. Não iremos mexer muito nessas luzes, apenas desligar a sombra e mudar a cor para uma cor próxima com a cor do piso. Para isso você pode pegar o render e clicar com o botão direito do Mouse. Dessa forma o Max irá criar uma cópia do pixel no qual você clicou em um quadrado de cor que encontrasse no topo da janela do renderizador. Assim você pode copiar essa cor para as luzes que irão gerar a radiosidade.


Render Final com Scanline Render e Light Dome.

Sky Light com Mentalray 3.5 no 3Ds MAX

Basicamente iremos manter tudo que fizemos na cena anterior. Iremos apenas substituir as Spots do Light Dome por uma simples Sky Light. Você pode posicionar a Sky Ligth em qualquer lugar da cena que o resultado será o mesmo. Após criar a Skylight va na janela do Renderizador e na guia “Indirect Illumination” ligue o Final Gather. Para testar você pode usar o preset DRAFT. E para render final você pode usar os presets Medium e High.


Render Final com Skylight no Mentalray

Um Comentario

Como fazer um Space Invaders – Parte 1

“Rather than the programmer saying how about like this, he keeps making the kind of environment which the designer can adjust to whatever their heart’s desire.” – Takuya Seki, Shadow of the Colossus

Essa série é um conjunto de textos com instruções para se programar do zero um Space Invaders. Notem que eu estou usando o termo programar, mas eu vou mencionar uma pequena parte de Game Design, especificamente a parte onde o design se transforma em programação. Isso parece ser bobo mas é um dos (vários) motivos pra um projeto de game design que é bom não dar certo. Os gráficos vão ser uma cópia do original com umas mudanças, o bom de fazer clones de jogos antigos é que os gráficos são simples ;). Os sons vão ser feitos usando um programa free que eu vou mencionar mais adiante nas próximas partes da série.

Para quem essa série é indicada

É essencial que você saiba programar e já tenha tido alguma experiência com programação de jogos antes, no caso, saber usar/criar headers, ponteiros, conhecer algumas estruturas de dados simples, saber o que é game loop, double buffering e etc. Ou seja, se você sabe programar e já programou algum jogo antes mas não acabou, ou teve dificuldades, ou pelo menos sabe usar alguma lib tipo Allegro ou SDL essa série será útil. Se você está aprendendo a programar pode tentar acompanhar já que eu vou dar umas dicas de práticas de programação que não precisam de muito conhecimento, são métodos de programar que eu considero importantes. Eu irei usar C(sem POO) com Allegro, mas o jogo será programado de uma maneira tal que será fácil portar o jogo para outras libs.

Primeiros Passos

No desenvolvimento de um jogo tradicional existem muitos processos a serem feitos até que efetivamente se comece a programar. Quando o motivo incial de fazer um jogo não é a tecnologia dentro dele e sim alguma idéia, se começa pelo design do jogo a partir dessa idéia.
Resumindo um pouco, a partir de uma (ou mais) idéia(s) o game designer vai moldando o jogo na cabeça dele, pensando em alguns detalhes do jogo, mas não nele todo, a partir disso são criados alguns protótipos da funcionalidade do que ele tinha pensado, e a partir disso é definido como o jogo será, ou seja, o design doc. Notem que alguns design docs ainda deixam espaço para experimentação. Nesses casos ele não é um documento que contém TUDO do jogo, já que seria complicado mudar alguma coisa no código caso essa coisa não tenha ficado boa como ele tinha pensado(isso acontece muito).
Mas o importante do design doc é que ele seja um guia para as outras pessoas da equipe(no nosso caso, o programador) para elas saberem o que elas tem que fazer. É importante existir uma comunicação entre o designer e o programador sobre essas partes que podem mudar, já que as vezes é complicado deletar tudo que já tinha sido feito e reprogramar do zero. Quando o programador sabe de antemão quais coisas podem vir a mudar no futuro, ele prepara o código para que ele seja facilmente modificavel. Sabendo disso vamos criar nosso design doc simplificado!

“Design Doc” simplificado

Como nós não estamos criando um jogo do zero e sim um clone, o design doc está praticamente pronto, ou seja, a gente já sabe tudo que o jogo tem, seria só passar pro papel e em teoria começar a programar… mas como a vida nunca é facil, não é assim que nós vamos começar.
Isso acontece porque mesmo se a gente jogasse o jogo todo dia e conhecesse tudo a respeito dele, nós estariamos pensando como jogadores e não como programadores. É muito fácil achar que um jogo é fácil de programar só porque ele parece simples, mas a verdade é outra… SEMPRE tem mais do que parece ter por trás de um jogo, e é isso que eu vou mostrar aqui.

Bom, supondo que todos que estão lendo sabem o que é o space invaders a gente pode fazer um design doc do jogo:

Obs: Eu estou chamando de Design Doc, mas é só uma descrição do jogo pra gente ter idéia de como ele funciona, o que existe dentro dele, quais as regras e etc, ao longo do texto vou usar design doc e descrição mas me refiro a mesma coisa.

screenshot do jogo

Em Space Invaders você controla um canhão que defende a terra de invasores do espaço. O canhão fica na parte inferior da tela, ele pode se mover para esquerda e para a direita. Ele se defende atirando contra os invasores.


Os invasores se movimentam para direita ou para a esquerda. Quando um dos flancos toca um lado da tela, eles descem uma linha e movem para o lado oposto:

movimento dos invasores

Os invasores também se defendem atirando contra o canhão. Eles se movem mais rápido a medida que vão sendo destruidos(mais adiante vai ser explicado como eles são destruidos). Existem 3 tipos de invasores(), e eles ficam organizados como uma matriz.

Eventualmente aparece um nave em cima() que se move para o lado oposto da tela e dá pontos especiais para o jogador.

Os tiros do jogador, se acertam a base, destroem ela parcialmente, se acertam uma nave inimiga destroem ela imediatamente.
Os tiros das naves não acertam naves, mas acertam a base, destruindo ela parcialmente, e quando acertam o canhão o jogador é destruido e ele perde uma vida.

O jogador passa de fase quando todos os invasores da matriz morrem, a cada fase que ele passa os invasores começam uma linha abaixo.
O jogador perde o jogo quando ele perde todas as vidas dele(ele começa com 3).
As fases são infinitas.

Bom, tá meio bagunçado mas descrevemos o jogo, certo? Em partes :) Isso é o que passa pela cabeça de um jogador quando ele descreve o jogo(e na de Game Designers que ainda estão aprendendo), mas falta algumas informações cruciais aí. Por exemplo, eu falei que “Eventualmente aparece uma nave em cima que se move para o lado oposto da tela e dá pontos especiais para o jogador“. Ok, mas, quais são os critérios para a nave aparecer? Ela aparece sempre pela esquerda? Ela aparece sempre pela direita? Se o jogador estiver mais pra direita ela aparece na direita e vice-versa? Ela aparece depois de um determinado tempo? Ela aparece depois de um número de tiros? Só jogando o jogo pra saber, e mesmo assim você ficaria na dúvida.
Mas vamos supor que Space Invaders é um jogo que ainda está sendo criado, que o game designer dele não sabe disso também, ou seja, ele simplesmente teve uma idéia de que seria legal uma nave aparecer em cima dando pontos especiais(geralmente quando a gente tem essas idéias legais, elas sempre são vagas em termos de programação). O game designer vai querer experimentar varias opções durante o jogo, ele vai querer experimentar todas aquelas opções que eu disse acima e mais o que a cabeça dele puder imaginar. você, como programador, na hora que estiver modelando o programa tem que identificar essas partes vagas de um documento de design e conversar com o designer do jogo sobre isso, pra que você não comece a programar algo que seja da sua cabeça, e quando o game designer ver que aquilo não era o que ele tava pensando você vai ter que deletar tudo porque não preparou o código pra mudanças.

Com base nisso vou mostrar aqui algumas coisas que ficaram faltando e outras que o game designer vai querer experimentar:

Canhão:

- Qual a posição inicial do canhão?
- O que acontece quando o jogador leva um tiro? Ele perde uma vida e fica quanto tempo sem se mexer? Ele fica invencível por um tempo? Por quanto tempo?
- Quando ele leva um tiro ele volta pra posição inicial?
- O que acontece quando ele toca um extremo da tela? Ele aparece no outro extremo? Ele para de se mexer?
- Ele tem tiros infinitos? Se tem, ele pode atirar uma vez até acabar o tiro ou quantas vezes ele quiser?

Invasores:

- Como eles ficam organizados? Qual é o tamanho da matriz?
- Qual a velocidade deles? Como é a progressão da velocidade a medida que eles vão morrendo?
- Qual é o critério pra eles atirarem?

Barreira:

- Como que a barreira vai sendo destruida? Ela tem seções que aguentam por exemplo 3 tiros? Ou ela é destruida por terreno(tipo worms)?

Pois é, quando a gente descreve o jogo, ou tem idéias pra um jogo, esse tipo de coisa passa batido, mas quando a gente para pra pensar nesses detalhes que ficam faltando acaba aparecendo um monte de coisas. Mas o importante é que isso apareça AGORA e não quando nós começarmos a programar. Eu vou responder todas essas perguntas agora já que nós estamos programando um clone, mas eu fiz isso porque eu quero que vocês vejam que quando nós estamos criando um jogo do zero, esse tipo de pergunta aparece, e as vezes a gente não sabe como responder e tem que experimentar. E no final das contas é o programador que tem que programar o jogo sem essas respostas, por isso ele tem que saber aonde que o designer quer experimentar pra que ele possa fazer um código flexivel o suficiente pra que possa haver espaço para essas experimentações.

Mas voltando as perguntas:

O canhão começa na esquerda mais ou menos em uns 20% da tela. Quando ele leva um tiro o jogo para e espera até o jogador apertar o botão de atirar e ele volta a posição inicial dele, mas os invasores continuam na posição aonde eles estavam. Quando o canhão se move até um extremo da tela ele para e não se move mais. Ele tem tiros infinitos e só pode dar um tiro por vez.

Os invasores se organizam em uma matriz com 11 colunas e 5 linhas. A velocidade inicial e a progressão é algo que nós vamos ter que experimentar :) Mas no jogo, quando sobra o ultimo invasor aparentemente a velocidade dele dobra quando ele se move pra direita, quando ele se move pra esquerda ela diminui um pouco(acho que é pro jogador ter mais chance, fica realmente muito rápido). O critério pra eles atirarem me parece ser algo assim: nas primeiras fases um deles atira aleatoriamente, enquanto o outro tiro é sempre da nave que está na coluna em que o jogador está, a medida que as fases vão passando vai aumentando o número de tiros aleatórios.

A barreira é destuida por terreno, de acordo com a animação. No jogo original, a barreira era “deletada” de acordo com a animação da explosão, por exemplo: Um tiro acertava a barreira(a colisão aparentemente é pixelperfect), o tiro virava uma explosão, o desenho dessa explosão simplesmente sobrepõe o que tinha sido desenhado na barreira(isso será explicado em detalhes nas próximas partes da série).

Bom, adicionando isso tudo ao “design doc” inicial, já temos coisa suficiente pra planejar algumas coisas de como iremos programar e a ordem de como iremos programar o jogo.

Pensando como programador

Primeiro, nós temos que identificar quantos objetos nós temos. Pela imagem(e pelo que a gente conhece do jogo) a gente sabe que tem o canhão, os inimigos, as 4 bases que defendem o jogador, tem também aquela nave que aparece em cima para dar pontos extras. Com isso nós já detectamos que vamos criar uma estrutura para o jogador, outra para as naves, e mais uma para as barreiras que ficam embaixo. Mas nós temos que lembrar que o jogador atira e as naves de cima também! Ou seja, mais uma estrutura para o tiro. Resumindo nós temos:

- Canhão(jogador)
- Naves
- Base
- Tiros

Note que poderiam ser 4 classes, mas não importa muito já que nós vamos separar por módulos cada estrutura, então mesmo não usando OOP vai ficar muito parecido já que estamos modelando o programa da mesma maneira, a diferença vai ser na hora de representar no código.

Depois nós temos que identificar os comportamentos, ou seja, o que cada um pode fazer e pensar em como isso pode ser programado:

- O canhão atira, leva tiros e se move.
- As naves atiram, levam tiros, se movem.
- A barreira somente leva tiros(coitada hehehe).
- Os tiros se colidem com outros tiros, com o canhão, com as naves e com a barreira.

Aparentemente é simples, mas agora é que vai ficar complicado pois nós vamos refinar isso. Não se espante se você não entender porquê vamos usar tal variável ou tal algoritmo, isso tudo será explicado nas seções mais a frente destinadas a programar cada parte do jogo, mas é importante fazer esse tipo de verificação agora para que não aconteçam imprevistos no futuro. E como que você vai saber qual algoritmo usar em determinada hora? Bom, isso é experiência, e é pra isso que você está lendo esse artigo :D

Canhão:

Nós sabemos que ele dá um tiro até atingir o final da tela ou algum outro objeto. Pra isso vamos precisar de alguma variável que indique se o jogador está atirando ou não.
O canhão se move para esquerda e para direita de acordo com o que o jogador aperta no teclado, isso é bem simples, vamos criar só uma váriavel X e Y e uma Velocidade pra podermos fazer isso.
Quando o jogador leva tiros ele simplesmente é destruido, então uma colisão de bounding boxes nesse caso será útil.

Naves Invasoras:

As naves atiram de acordo com aquele critério mencionado acima, teremos que criar uma função que terá um algoritmo que decidirá quais naves irão atirar.
A colisão delas é parecida com a do canhão, bounding box já que elas são destruidas na hora.
O movimento dos invasores é de acordo com o visto na imagem acima, iremos criar uma função pra mover também de acordo com o que sabemos.
Notem que nós pensamos em criar uma estrutura nave, e de fato nós vamos criar, mas o que importa para nós é o conjunto das naves, ou seja, a matriz! No caso os inimigos vão ser uma matriz da estrutura nave.

Base:

A base só leva tiro, e a colisão é como explicamos acima, o algoritmo dela é meio complicado e será explicado mais adiante, mas o que nós precisamos saber agora é como representar essa base, a base nada mais vai ser do que um bitmap, sem energia sem nada, só vamos precisar do X e Y dela pra poder calcular a colisão.

Tiros:

Os tiros se movem de acordo com quem atirou, por exemplo, se o tiro foi do canhão, o tiro sempre se move pra cima. Se o tiro foi de alguma das naves ele vai se mover sempre pra baixo. A função de movimento deles é bem simples. A colisão também é feita com bounding box(com excessão da colisão de tiro com a barreira).

Uma regra geral pra caso você não saiba que algoritmo usar ou como representar tal objeto é, nessa hora do planejamento, fazer experimentos com partes do programa. Por exemplo, digamos que você ficou em dúvida sobre como fazer o movimento das naves, então crie um programa que SOMENTE tenha essas naves se movendo. Parece uma coisa simples mas isso envolve pesquisa e experimentação, até porquê nem todo programador sabe tudo, então quando um programador começa um projeto grande que ele não sabe como programar determinadas coisas, é nessa hora de planejamento que ele faz essa experiências, demos e pesquisas. Isso é super importante, e é um dos motivos que diferencia um programador preparado de um programador amador que programa “na doida”.

Bom, podemos já ter uma idéia do que vamos programar e como vamos programar. Agora nós precisamos decidir o que vamos programar primeiro. Isso muda de pessoa pra pessoa e de jogo pra jogo, acreditem. Mas como nós estamos programando jogos pequenos e já sabemos o que nós queremos, nós podemos usar uma regra bem simples que se aplica ao nosso caso: Comece programando o que não tem dependências. Ou seja, não dá pra começar programando a colisão se nós não temos ainda as estruturas que vão fazer essa colisão, não dá pra programar o algoritmo de mover as naves se nós ainda não fizemos ela nem desenhamos elas na tela! A idéia é simples.

No nosso caso, eu vou começar programando o canhão e a função de movimento dele. Depois nós poderiamos fazer a função de atirar, mas ia ser ruim porque depois nós iamos ter que programar as barreiras, as naves e depois voltar pra acabar os tiros! Porquê se nós fossemos programar os tiros do canhão logo após termos criado ele e sem nada no jogo, não teria com o quê colidir, então não faria muito sentido. Ou seja, depois de programar o canhão e o movimento dele, vamos criar as naves e o movimento delas, depois a barreira, depois os tiros e por aí vai. Vocês verão mais adiante na proxima parte da série, como isso será feito, por enquanto fiquem com isso que já é até bastante informação =)

Considerações Finais

A primeira parte acaba por aqui, ná próxima nós vamos montar o esqueleto do jogo e programar o canhão. Espero que não tenha ficado tão confuso! Eu tenho tendência a confundir o simples hehehe, mas creio que a partir das próximas partes fique mais fácil de acompanhar já que é uma parte mais prática e sem tanta teoria. Obrigado e espero ver os comentários pra saber as dúvidas e também poder melhorar nás próximas partes da série. ;)

7 Comentarios

Vaga de estágio em programação Flash – RJ

A vaga é para ocupar o meu lugar, pois no meio de abril vence meu contrato e estarei assumindo tempo integral em outra empresa. O estágio é no Centro da cidade, 20 horas semanais com horário bem flexível, sendo oferecido bolsa + benefícios. Quem quizer saber mais sobre com o que irá trabalhar, pode visitar o site Mundo da Criança (www.mundodacrianca.com).

A programação é em flash com ActionScript 2.0.

Quem estiver apto para a vaga, envie currículo para anderson [arroba] mundodacrianca [ponto] com.

Abraços!

Sem comentarios

Developer Begins! – Curso de games na área…

update #3: segundo me informado hoje, a primeira turma LOTOU e quem se mostrou interessado mas não concretizou a matrícula está com reserva para uma segunda turma, que supostamente começará no dia 14 de abril e terminará no dia 30 (mesmo horário… tem tanto laboratório assim disponível na faculdade e eu não sabia?!).

update #2: a data do curso foi alterada para 7 de abril e irá até o dia 29 do mesmo mês.

Algumas vezes acontecem coisas embaixo de seu nariz, mas que pela capacidade limitada de percepção do sujeito em questão, passam completamente despercebidas. Me senti assim neste fim de semana, quando li uma notícia na Unidev sobre um novo curso de games no Rio de Janeiro. Até aí tudo bem, pois moro no Rio de Janeiro e isso fez com que aguçasse a curiosidade para saber mais sobre o tal curso, só que a surpresa ainda estava por vir.

Minha primeira surpresa foi quando li: “trás em parceria com a Tecnohall, gestora do Centro de Treinamento Uniriotec”. Na mesma hora soltei uma expressão linda que não posso transcrever, afinal ela possui convênio com a faculdade onde estou me graduando e isto soava-me como notícia boa. O golpe final veio logo abaixo: “O curso será ministrado no Centro de Treinamento Uniriotec, localizado na Universidade Federal do Estado do Rio de Janeiro (Unirio) – Av. Pasteur, 458 – térreo – Prédio da Escola de Informática Aplicada – Urca.”

Pára tudo! A empresa que possui convênio com a faculdade onde estudo está em parceria na oferta de um curso de games que será oferecido nos laboratórios de lá? Como é que eu só descobri isso agora?

Fiquei um tempo parado até cair a ficha, mas de qualquer maneira era uma ótima notícia! Logo tratei de entrar no site da Magus Ludens e descubro que a notícia saiu dia 25 de fevereiro. Como o curso já inicia na próxima segunda-feira (17 de março), fiquei um pouco apreensivo quando à disponibilidade de vagas, mas e-mail que recebi, ainda está em tempo de se matricular. Confesso que desconheço os professores e o nível de qualidade do curso, mas como estarei praticamente “em casa”, resolvi arriscar.

O curso Developer Begins! tem duração de 40 horas, com aulas de segunda à sexta de 19h às 22h (17 de março a 4 de abril de 2008). O valor do curso é de R$699,00 à vista ou 2x de R$349,00. O contato pode ser feito pelo e-mail treinamento@uniriotec.br ou pelo telefone (21) 3209-1412.

update #1:

Agora vocês podem conferir a ementa do curso:

· História dos Games
o De 1889 a 2008
o Perspectivas para o futuro
o Academia de Artes e Ciências Interativas

· Game Design
o Entretenimento
o Significado e Experiência
o Semiologia
o Sistema
o Interatividade
o Espaço das Possibilidades
o Sistema de Incerteza
o Definição de Game
o Diferenças de um Jogo Digital
o Círculo Mágico
o Regras em 3 níveis
o Flow
o Sistema Cibernético
o Games como Sistema Emergente
o Teoria da Informação
o Sistema de Conflito
o Desenhando Regras para serem quebradas

· Game Design e Roteiro
o Interface
o Game Bible
o Design Iterativo
o Balanceamento
o Prototipagem
o Metaversos
o Liderança
o Básico de Roteiro para Game
o Outras discursões

· Direção de Arte + Concept Art
o Pesquisa de Referência
o Dissonância
o Storyboard
o GUI
o Charts
o Concept Art

· Desenho Técnico
o Aula Prática

· Apresentação do ambiente 3D e Interface do 3ds Max
o Conceitos gerais de um ambiente 3D
o Básico da Interface do 3ds Max

· Modelagem
o Modelagem em lowpoly

· Mapeamento e Textura
o UVW Mapping, Unwrap e Material Editor

· Shaders, Câmera e Iluminação
o Render to Texture
o Câmera
o Iluminação
o Shaders

· Game Engine, Linguagens de programação e Pipeline Gráfico

· Pseudolinguagem e apresentação do XNA

· XNA
o Aula Prática

· Marketing
o Conceito gerais de Marketing
o Análise do mercado e indústria de games no Brasil e no Mundo

3 Comentarios

Uma Introdução a Sistemas de Partículas

Um grande “boa noite” a todos!

Gostaria de pedir desculpas por meu atraso em postar aqui a minha contribuição: somos um grupo de poucos escritores, então, qualquer atraso de um de nós leva a grandes problemas na manutenção da atualização deste blog.

Hoje venho aqui trazer uma breve explanação sobre uma das coisas que estudei durante certo tempo com meu amigo Edmar Souza Jr. (the great SuperSayajin! ) e que me parece ser sempre um assunto bastante atualizado e capaz de oferecer muitos resultados: os sistemas de partículas.
Sistemas de partículas são sistemas criados para gerenciar a criação, movimentação (às vezes também a colisão) e destruição de pequenos elementos que podem compor um cenário do jogo. Você pode escrever sistemas de partículas para simular:
- Chuva;
- Neve;
- Furacões; (este aqui eu não consegui fazer… ainda! ;) )
- Espirais; (semelhante àquelas galáxias vistas nos filmes)
- Explosões;
- Fontes d’água;
- Fogo;
- E muuuuuuitas outras coisas!

Ou seja, um sistema de partículas pode tornar o seu jogo muito mais atraente, já que a posição e movimentação das partículas não é pré-programada (entretanto, quanto mais partículas a serem renderizadas, mais pesada fica a aplicação!).

Certo, certo… Já falamos sobre vantagens e cuidados, agora vamos ver como deve ser criado um sistema… O meu sistema eu criei da seguinte forma:
- A classe (sim, só gosto de trabalhar com orientação a objetos, nem me venham com história de programação estruturada! :D ) controladora do sistema, que mantém uma lista de todas as partículas e as funções para criar partículas, inicializá-las, movimentá-las e destruí-las;
- A classe a que deve pertencer cada partícula, armazenando nela somente informações da partícula especificamente, como: coordenadas retangulares ou polares no espaço (não sabe trabalhar com coordenadas polares? Então está na hora de aprender, pois eu precisei delas para fazer os espirais! ), tempo de vida, contador que auxilie a animação, etc.

Agora, lembre-se que forças externas podem interferir no movimento das partículas, forças como:
- Gravidade (influencia na posição vertical);
- Vento (influencia na posição horizontal);
- Campo elétrico;
- Campo magnético (provido ou não pelas outras partículas).

No meu sistema implementei levando em conta somente a gravidade e o vento.

Os segredos para se desenvolver um bom sistema são:
- Saber quais propriedades a serem atribuídas a uma partícula;
- Saber como inicializar corretamente (criar e posicionar uma partícula);
- Saber como deve ser feita a movimentação (não é muito difícil – estude exatamente como as coisas se comportam na vida real e aplique esses conceitos);
- Saber o momento para destruir uma partícula (geralmente as partículas são destruídas após um determinado tempo transcorrido, ao colidir com algo ou ao atingir determinada distância do ponto de criação).

Uma curiosidade: gotas de chuva caindo em sua simulação não devem possuir uma aceleração devido à gravidade! Isso é uma regra da Física – um corpo caindo chega a uma velocidade máxima e dela não ultrapassa, devido à resistência que o ar oferece e isso ocorre com as gotas de chuva. Os flocos de neve também não sofrem aceleração gravitacional, estes devido à sua aerodinâmica.

As partículas de explosão e da fonte d’água podem ser criadas da mesma forma: por meio de dispersão, ou seja, as partículas possuem uma velocidade inicial que as levam a se afastar de um ponto origem e pode-se aplicar ou não as leis da gravidade e do vento (é óbvio que a fonte sofre essa ação, não é?). Mais uma curiosidade: quando coloco um tempo de vida razoável nas partículas, uma potência (velocidade inicial com que se afastam as partículas) baixa, sem sofrer a ação da gravidade, tenho um efeito semelhante ao da chama de uma vela! :)

Bem, com isso que foi dito já há um pontapé inicial para compreendermos como os sistemas de partículas funcionam, não?

Seguindo estas orientações, vocês podem desenvolver quaisquer sistemas de partículas (é só colocar a cabeça para funcionar!).

 

Bem, agora vou mostrar aqui como criar um sistema de partículas que simule uma chuva em Delphi, ok?


Apresentarei aqui um sistema muito simplificado, não estarão envolvidos aqui tantos parâmetros quanto foram necessários no sistema que desenvolvi, mas já dará um resultado razoável em seus jogos.

Obs: No período em que estava estudando, desenvolvi cada tipo de sistema de partícula como um componente para o DelphiX e, posteriormente, comecei a converter todos os componentes de sistemas de partículas para classes e “encapsulei-as” em um componente principal chamado DXParticleSystemManager, mas infelizmente interrompi a conversão há algum tempo :( .


Como já disse, uma classe (ou tipo, que é o que vou usar agora) deve designar a construção das partículas:

   TGota = record
              x: integer;
              y: integer;
           end;

E uma outra classe (mais uma vez, vou preferir criar um tipo aqui, só para tornar o processo mais simples) para controlar tudo, ou seja, nosso sistema:

   TChuvaSprite = record
                     intensidade: integer;
                     gotas: TList;
                     vento: integer;
                     gravidade: integer;
                     superficie: TCanvas;
                  end;


Bem, defina também um método que inicialize o sistema (CriarChuva) e um método que, a cada vez que for chamado, ele irá desenhar sobre a superfície e recalcular a posição das partículas (MoveGotas).
Pronto! Agora vamos dizer mais ou menos o que deve ser feito na inicialização:



procedure CriarChuva;
var i: integer;
     pgota: ^TGota;
begin
   if intensidade > 0 then
      for i := 0 to intensidade - 1 do
         begin
            new(pgota);
            pgota.x := random(superficie.width);
            pgota.y := random(superficie.height);
            gotas.Add(pgota);
         end;
end;


E agora, vamos ver como deve ser feita a movimentação e desenho de cada ponto. Só lembrando: as partículas de chuva não sofrem aceleração gravitacional, entretanto, em nosso sistema, quanto maior a gravidade, maior a velocidade de queda vertical da gota (só que essa velocidade é constante!!!) e só para facilitar, as gotas de chuva terão a cor ClAqua…


procedure MoveGotas;
var i: integer;
      pgota: ^TGota;
begin
   superficie.Pen.Color := ClAqua;
   for i := 0 to gotas.Count - 1 do
   begin
       pgota := gotas.Items[i];
       superficie.MoveTo(pgota.x, pgota.y);
       superficie.LineTo(pgota.x + vento, pgota.y + gravidade);
       pgota.x := pgota.x + 2*vento;
       pgota.y := pgota.y + 2*gravidade;
       if pgota.x <> superficie.width then
           pgota.x := 0;
       if pgota.y <> superficie.height then
            pgota.y := 0;
   end;
   superficie.release;
end;

Certo, agora, alguns detalhes: se você colocar o código assim, desse jeito, pode dar algum problema na hora de redesenhar a superfície. Eu trabalho com os valores x e y como reais, mas daí tem que tomar cuidado nas validações e conversões, senão acaba ficando tudo uma porcaria e idem quanto a gravidade negativa, pode trabalhar com ela, mas talvez tenha que fazer ajustes no algoritmo, de acordo com componentes e bibliotecas que estiver usando.

Antes que me perguntem: e por que não já colocou um código perfeito para usarmos, sem falhas e com muitos efeitos?
Simples: meu objetivo aqui é MOSTRAR COMO FAZER e não só chegar e já passar o código-fonte, senão ninguém estaria aprendendo, não é? E já que o objetivo é aprender, temos que fazer restrições nas funcionalidades e tentarmos criar algo bem genérico e de baixa complexidade, como isto!

Em outro momento eu mostro algo aqui um pouco mais avançado, mas quem quiser implementar algo um pouco melhor, uma dica: mesmo em 2D, podemos inserir mais uma coordenada às gotas, uma coordenada z, que quanto maior esse z, mais próxima a gota está da tela, portanto maior ela ficaria, mais rápido ela cairia, ok?


Pronto, por enquanto é isso! Agora é com vocês: recriem o sistema de partículas para chuvas e implementem, por exemplo, o sistema para neve, pois é também simples, mas introduz uma nova característica: o movimento das partículas não é retilíneo, mas sim, algo similar a uma senóide. :)

Ah, para que eu não esqueça, estou subindo também o executável que implementei no período em questão, com vários sistemas de partículas diferentes.

Para baixar, é só clicar aqui: Uma Introdução a Sistemas de Partículas

Agora sim: até mais!

Sem comentarios