segunda-feira, 7 de janeiro de 2013

Conclusão

Chegamos ao final deste blog. Para acessar de uma forma fácil todas as postagens utilize o SUMÁRIO.

Se você estiver interessado em conhecer mais sobre a área de desenvolvimento de jogos dê uma olhada em diversos materiais didáticos que eu já produzi nesta área. Vou comentar sobre alguns deles.

  • Game Design Compendium
A série Game Design Compendium contém 20 apresentações voltada para iniciantes na área de desenvolvimento de jogos e aborda diversos conceitos fundamentais de Game Design.

As apresentações estão disponíveis neste link: Game Design Compendium.



  • MRDX
O MRDX é uma biblioteca para programação de jogos em C/C++ que eu implementei em 2001 com o objetivo de facilitar o ensino nesta área. O MRDX foi apresentado no 1º Workshop Brasileiro de Desenvolvimento de Jogos (WJogos) que ocorreu em Fortaleza no ano de 2002.

Os textos que acompanham o MRDX são bem didáticos e apresentam noções básicas de programação de jogos que são úteis independente da linguagem de programação que você estiver utilizando atualmente.

O MRDX pode ser baixado neste link: MRDX.



  • UnrealScript
UnrealScript é uma linguagem de programação orientada a objetos utilizada para programar jogos 3D na Unreal Engine. Em novembro/2009, a empresa Epic Games lançou o Unreal Development Kit (UDK) que pode ser usado livremente para fins não comerciais.

Criei uma série de 40 artigos em português ensinando a programação de jogos no UDK com UnrealScript. Os artigos podem ser acessados neste link: Sumário UnrealScript.


sexta-feira, 4 de janeiro de 2013

Uso de Arquivo para os Recordes


Os recordes das fases do Mina 2 são gravados em um arquivo quando o programa é finalizado para que estes recordes possam ser recuperados na próxima execução do Mina 2. Se este arquivo não fosse usado, os valores dos Recordes sempre retornariam ao valor padrão em uma nova execução do jogo.

A variável com o nome "frec" é usada para referenciar o arquivo que guarda os recordes no Mina 2. Ela foi definida da seguinte forma:
type tiporecorde = record
                      nome:string[10];
                      score:integer;
                   end;

var frec: file of tiporecorde;

Como pode ser visto no Laço Principal, no começo da execução do programa, a variável "frec" é associada ao nome de arquivo "recordes.mi2" através da procedure assign():
assign(frec,'recordes.mi2');

Logo após é chamada a procedure verificarecordes que irá procurar pelo arquivo "recordes.mi2" e carregar os valores de recordes caso o arquivo seja encontrado. Se o aquivo não existir, serão utilizados os valores padrões para os recordes.

O código da procedure verificarecordes é o seguinte:
procedure verificarecordes;            
var texto1:PathStr;
    ind:integer;
begin
     texto1:=FSearch('recordes.mi2','.');
     if texto1 = '' then
        for ind:=1 to QF do
          begin
            recordes[ind].nome:='Computador';
            recordes[ind].score:=100;
          end
     else
        begin
          reset(frec);
          for ind:=1 to QF do
              read(frec,recordes[ind]);
        end;
end;

A procedure gravarecordes é chamada quando for escolhida a opção "Finalizar" na procedure menu. Esta procedure é responsável por gravar os recordes atuais das fases no arquivo "recordes.mi2".

Código da procedure gravarecordes:
procedure gravarecordes;            
var ind:integer;
begin
  rewrite(frec);
  for ind:=1 to QF do
      write(frec,recordes[ind]);
  close(frec);
end;

A tela de recordes é acessada a partir do Menu Principal do Mina 2. A procedure procrecorde controla esta tela que é apenas informativa. É feito um conjunto de movimentações com os textos para montar a tela. O trecho de código abaixo é responsável por mover os textos que contém o recorde de cada fase da direita para esquerda até sua posição correta.
 {trecho da procedure procrecorde}

  for rec:= 1 to QF do
      begin
           str(recordes[rec].score:3,s);
           str(rec:2,strfase);
           s:= 'FASE '+ strfase+' : '+ s + ' - '+ recordes[rec].nome;
           for ind:=80 downto 25 do
               begin
                    x:=81-ind;
                    if x>26 then x:=26;
                    gotoxy(ind,(rec*2)+6);
                    write(copy(s,1,x));
                    escondecursor;
                    textcolor(14);
                    delay(20);
                    if ind <> 25 then
                       apagalinha((rec*2)+6);
               end;
      end;

quinta-feira, 3 de janeiro de 2013

Procedure fimdejogo


A procedure fimdejogo é chamada a partir da procedure jogo quando o Tempo da fase chega a zero. Esta procedure mostra o Placar da partida com os pontos dos jogadores e indica se o recorde atual da fase foi superado.

O código da procedure fimdejogo foi dividido em três partes nesta postagem para facilitar a explicação.

A 1ª parte realiza uma pequena animação que mostra os textos "PLACAR" e "MINA 2" descendo lentamente a partir do topo da tela.
procedure fimdejogo;
var ind,col,tecla_res:integer;
    s,strfase:string;
begin
  apagatela;
  textcolor(15);

  for ind:=1 to 5 do
      begin
           gotoxy(36,ind);
           write('PLACAR');
           escondecursor;
           textcolor(15);
           delay(100);
           if ind <> 5 then
                  apagalinha(ind);
      end;

  for ind:=1 to 3 do
      begin
           gotoxy(36,ind);
           write('MINA 2');
           escondecursor;
           textcolor(15);
           delay(100);
           if ind <> 3 then
                  apagalinha(ind);
      end;

A 2ª parte escreve os nomes e os pontos dos jogadores na tela. Caso esteja no modo de 2 jogadores é verificado a pontuação de cada jogador para determinar o vencedor ou se a partida acabou em Empate.
  for ind:=1 to qjog do   
   begin
    textcolor(jog[ind].cor);
    if ind=1 then
       col:=47
    else
       col:=23;
    gotoxy(col,8);
    write(extrainfo[ind].nome);
    gotoxy(col+2,10);
    write(extrainfo[ind].score:3);
   end;
   textcolor(15);
   ind:=1;
   if qjog=2 then
    if extrainfo[1].score = extrainfo[2].score then
     begin
       gotoxy(36,12);
       write('EMPATE');
     end
    else
     begin
       if extrainfo[1].score > extrainfo[2].score then
          ind:=1
       else
          ind:=2;
       gotoxy(30,12);
       write('VENCEDOR : '+ extrainfo[ind].nome);
     end;

A 3ª parte verifica se os pontos do vencedor foram maior do que o recorde atual da fase. Se isto acontecer será exibido na tela o texto "NOVO RECORDE!" junto com as informações relacionadas ao recorde. O nome e os pontos do jogador serão armazenados no vetor "recordes[]" que guarda os recordes de todas as fases. No final do código a procedure enfeite é chamada para fazer a animação da borda até que a tecla ENTER seja pressionada.
   if extrainfo[ind].score > recordes[fase].score then
    begin
     textcolor(15+blink);
     gotoxy(32,14);
     write('NOVO RECORDE!');
     textcolor(15);
     recordes[fase].score := extrainfo[ind].score;
     recordes[fase].nome := extrainfo[ind].nome;
     str(recordes[fase].score:3,s);
     str(fase:2,strfase);
     s:= 'FASE '+ strfase+' : '+ s + ' - '+ recordes[fase].nome;
     gotoxy(28,16);
     write(s);
    end;
  gotoxy(25,20);
  write('Pressione ENTER para continuar.');
  repeat
   enfeite;
   tecla_res:=ord(upcase(readkey));
   if tecla_res=0 then tecla_res:=ord(readkey)+255;
  until tecla_res=ENT;
end;

quarta-feira, 2 de janeiro de 2013

Procedures utilitárias

Existem diversas procedures utilitárias no Mina 2 que serão analisadas nesta postagem.

A procedure informa escreve no topo da tela o tempo restante e qual jogador está vencendo. Nas laterais são escritos os nomes e pontos dos jogadores de acordo com a cor de cada jogador como pode ser visto nesta imagem.


Os nomes e os pontos dos jogadores estão armazenados no vetor "extrainfo[]". Este é o código da procedure informa:
procedure informa;
var ind,col:integer;
begin
 for ind:=1 to qjog do   {nome e ponto do jog}
  begin
    apagalinha(5);
    textcolor(jog[ind].cor);
    if ind=1 then
       col:=67
    else
       col:=3;
    gotoxy(col,10);
    write(extrainfo[ind].nome);
    gotoxy(col+2,12);
    write(extrainfo[ind].score:3);
  end;
  textcolor(15);
  if qjog=2 then
    if extrainfo[1].score = extrainfo[2].score then
       msg('     Empate')
    else
     begin
       if extrainfo[1].score > extrainfo[2].score then
          ind:=1
       else
          ind:=2;
       msg(extrainfo[ind].nome + ' esta vencendo.');
     end;
  gotoxy(35,3);
  textcolor(15);
  if tempo<=10 then textcolor(15+blink);
  write('TEMPO: ',tempo:2);
end;

Existe outra procedure chamada mostra que é responsável por exibir na tela os caracteres que representam os jogadores e seus objetos, além das bolas que ameaçam os jogadores. Esta procedure e a procedure informa são chamadas a partir da procedure jogo.

As informações necessárias para a exibição de todos os itens estão nos vetores "jog[]", "obj[]" e "bola[]". Estes vetores são do tipo "geral" como pode ser visto na postagem Definição das variáveis.
procedure mostra;
var ind:integer;
begin
  for ind:=1 to qjog do
    begin
      textcolor(jog[ind].cor);

      gotoxy(obj[ind].col + difcol, obj[ind].lin + diflin);
      write(chr(obj[ind].cara));

      gotoxy(jog[ind].col + difcol, jog[ind].lin + diflin);
      write(chr(jog[ind].cara));
    end;

  if qbola > 0 then
   for ind:= 1 to qbola do
    begin
      textcolor(15); {branco}
      gotoxy(bola[ind].col + difcol, bola[ind].lin + diflin);
      write('0');
    end;
end;

A procedure inicia procura uma posição aleatória na fase que não contenha um bloco. Esta procedure utiliza passagem de parâmetros por referência. Isto é feito usando a palavra "var" na frente dos nomes dos parâmetros. Na passagem por referência, as variáveis originais que foram passadas para a procedure tem os seus valores modificados.
procedure inicia(var nlin, ncol:integer);
begin
   repeat
     nlin := random( altura ) + 1;
     ncol := random( largura) + 1;
   until mapa[ nlin, ncol ] = 0;
end;

A procedure morte é chamada quando ocorre algum tipo de colisão do jogador. Ela desenha um "*" na posição atual do jogador e exibe uma mensagem no topo da tela com o nome do jogador que explodiu. Depois faz uma pequena pausa e subtrai 5 pontos do jogador.
procedure morte(x:integer);
begin
  apagalinha(5);
  sound(50);
  textcolor(jog[x].cor);
  gotoxy(jog[x].col + difcol, jog[x].lin + diflin);
  write('*');
  msg(extrainfo[x].nome + ' explodiu.');
  delay(300);
  nosound;
  delay(400);
  apagalinha(5);
  extrainfo[x].score := extrainfo[x].score - 5;
  jog[x].direcao := 0;
end;

sexta-feira, 21 de dezembro de 2012

Movimento e Colisão

Nesta postagem vamos comentar sobre diversas procedures que são responsáveis por movimentar os jogadores e as bolas e testar a colisão do jogador com diversos elementos do jogo. Estas procedures são chamadas a partir da procedure jogo.

A procedure movecara movimenta os jogadores baseado no valor da variável "direcao" que recebe seu valor atual na procedure controle de acordo com as teclas pressionadas pelos jogadores.

O trecho de código abaixo mostra a lógica do movimento para a direção 1 (para cima). Para as outras direções o código é bem parecido, por isso foram omitidos.
procedure movecara;
var ind:integer;
begin
  for ind:= 1 to qjog do
    begin
      apaga(jog[ind].lin,jog[ind].col);
      case jog[ind].direcao of
        1:
           if mapa[jog[ind].lin-1,jog[ind].col]=1 then
                       morte(ind)
           else
                 jog[ind].lin:=jog[ind].lin-1;

        2: {...}
        3: {...}
        4: {...}
end;

A imagem do jogador em sua posição atual é apagada. Depois, baseado na direção que o jogador está se movendo é verificado se existe um bloco no caminho. Se o caminho estiver livre, a posição atual do jogador será modificada, mas se houver um bloco o jogador irá colidir, perderá pontos e será exibido um "*" na posição do jogador representando a colisão conforme a imagem abaixo. Essas ações estão na procedure morte().



O movimento das bolas é feito pela procedure movebola. As bolas se movimentam em diagonal e mudam de direção quando colidem com os blocos. O trecho de código abaixo mostra a lógica do movimento das bolas para a direção 1.
procedure movebola;
var ok,ind:integer;
begin
  for ind:= 1 to qbola do
   begin
     apaga(bola[ind].lin,bola[ind].col);
     ok:=0;
     repeat
        case bola[ind].direcao of
           1:begin
              if mapa[bola[ind].lin-1,bola[ind].col+1]=1 then
               begin
                 if((mapa[bola[ind].lin-1,bola[ind].col]=1) and
                    (mapa[bola[ind].lin,bola[ind].col+1]=1)) or
                    ((mapa[bola[ind].lin-1,bola[ind].col]=0) and
                    (mapa[bola[ind].lin,bola[ind].col+1]=0)) then
                      bola[ind].direcao:=3    {direcao oposta}
                 else
                  if(mapa[bola[ind].lin-1,bola[ind].col]=1) and
                    (mapa[bola[ind].lin,bola[ind].col+1]=0) then
                      bola[ind].direcao:=2    {muda movimento vertical}
                  else
                   if(mapa[bola[ind].lin-1,bola[ind].col]=0) and
                     (mapa[bola[ind].lin,bola[ind].col+1]=1) then
                       bola[ind].direcao:=4    {muda movimento horizontal}
               end
              else
                begin
                 bola[ind].lin:=bola[ind].lin-1;
                 bola[ind].col:=bola[ind].col+1;
                 ok:=1;
                end;
            end;

           2: {...}
           3: {...}
           4: {...}
end;

Os valores de 1 a 4 representam as seguintes direções para as bolas:


Se houver um bloco no local para onde a bola deveria ir será preciso decidir de que forma a bola irá mudar sua direção. Caso a direção da bola seja 1, o primeiro teste feito é para verificar se as posição acima e a direita possuem blocos ou se as duas posições estão sem blocos. Em ambos os caso a bola moverá na direção oposta.

O segundo teste verifica se a posição acima tem bloco e a direita não. Neste caso a bola muda apenas seu movimento vertical.

O terceiro teste verifica se a posição acima não tem bloco e a direita tem. Neste caso a bola muda apenas seu movimento horizontal.


A procedure testaobj verifica se os jogadores conseguiram pegar os seus itens. O código completo da procedure é este:
procedure testaobj;
var ind:integer;
begin
  for ind:=1 to qjog do
   begin
     if (jog[ind].col=obj[ind].col) and (jog[ind].lin=obj[ind].lin) then
        begin

          extrainfo[ind].score := extrainfo[ind].score + pontoobj;

          if qjog=2 then
            repeat
             inicia(obj[ind].lin,obj[ind].col);
            until (obj[1].lin<>obj[2].lin) or (obj[1].col<>obj[2].col)
          else
             inicia(obj[ind].lin,obj[ind].col);
        end;
   end;
end;

O vetor "obj" guarda as informações relacionadas aos itens que os jogadores devem pegar. Caso a posição do jogador e de seu item seja igual, o jogador ganhará a quantidade de pontos que está armazenada na variável "pontoobj". O valor desta variável é definido na procedure pegadados e depende da velocidade do jogo e da quantidade de bolas.

Depois o item deve reaparecer em outra posição. Isto é feito através da procedure inicia(). Caso 2 jogadores estejam jogando é feito um laço de repetição para garantir que o item não fique no mesmo lugar do item do outro jogador.

A procedure testamina verifica se os jogadores estão na mesma posição de uma das 5 minas que estão escondidas na área do jogo. Se estiver, será chamada a procedure morte() para o jogador e a mina será movida para outra posição. O código da procedure testamina é muito parecido com o da procedure testaobj.
        
A procedure testabola apenas verifica a colisão do jogador com as bolas do jogo e chama a procedure morte() caso ocorra. O seu código é bem simples e está abaixo.
procedure testabola;
var ind,bo:integer;
begin
  for ind:=1 to qjog do
    for bo:= 1 to qbola do
       if (jog[ind].col=bola[bo].col) and (jog[ind].lin=bola[bo].lin) then
              morte(ind);
end;

quinta-feira, 20 de dezembro de 2012

Procedure controle

A procedure controle é chamada quando o jogador pressiona uma tecla durante uma partida do Mina 2. O seu conteúdo é o seguinte:
procedure controle;
begin
  case tecla of
    CI1:jog[1].direcao:=1;
    DI1:jog[1].direcao:=2;
    BA1:jog[1].direcao:=3;
    ES1:jog[1].direcao:=4;
    CI2:jog[2].direcao:=1;
    DI2:jog[2].direcao:=2;
    BA2:jog[2].direcao:=3;
    ES2:jog[2].direcao:=4;
    ESC:sair:=1;

     PA:begin
          apagalinha(5);
          textcolor(15+blink);
          gotoxy(36,5);
          write('PAUSA');
          escondecursor;
          repeat
            tecla:=ord(upcase(readkey));
            if tecla=0 then tecla:=ord(readkey)+255;
          until tecla=PA;
          textcolor(15);
          apagalinha(5);
        end;
  end;
end;

As contantes CI1, DI1, BA1 e ES1 contém os valores ASCII das setas do teclado que são usadas pelo 1º jogador. O 2º jogador usas as teclas W, D, S, e A. Os valores ASCII destas teclas estão armazenadas nas contantes CI2, DI2, BA2 e ES2. Todas essas contantes estão definidas no início do código fonte do Mina 2 como pode ser visto na postagem Definição das variáveis. É importante ressaltar que os códigos ASCII das letras minúsculas e maiúsculas são diferentes. Para evitar este problema na leitura das teclas pressionadas as letras sempre são convertidas para maiúsculas através do uso da função "upcase()".

O pressionamento dessas teclas alteram a direção em que se move um determinado jogador. Após isso a procedure controle é encerrada e o fluxo voltará para a procedure jogo. São usados os valores de 1 a 4 para representar a direção do jogador conforme a imagem abaixo.


Para encerrar a partida basta pressionar a tecla "ESC". É possível também pausar o jogo pressionando a tecla "P". O valor ASCII da tecla "P" está armazenado na constante "PA". Existe um código que é executado quando o jogo está em pausa. Este código escreve na área de mensagem da tela a palavra "PAUSA" piscando e entra em um laço que só será encerrado quando o jogador pressionar a tecla "P" de novo. Para fazer o texto piscar é precisa adicionar o atributo "blink" na chamada da procedure "textcolor()". A imagem abaixo mostra a mensagem de Pausa.


quarta-feira, 19 de dezembro de 2012

Procedure jogo


A procedure jogo gerencia uma partida do Mina 2. A partir dela são chamadas as procedures que movimentam os itens do jogo e testam a colisão do jogador.

A procedure contém um laço while que mantém sua execução enquanto não for pressionada alguma tecla. Quando isso acontecer o fluxo do programa sai da procedure jogo e passa para a procedure controle para depois retornar à procedure jogo. Isto pode ser visto no Laço Principal.

Foi criada uma variável inteira chamada "cont" que serve para contar as iterações do laço while. Esta variável é usada para sincronizar diversas ações durante uma partida.

As variáveis "tempo" e "espera" também são usadas na procedure jogo. Seus valores são determinados na procedure pegadados de acordo com os valores informados pelo jogador. O valor da variável "tempo" é exibida na parte superior da tela e representa o tempo que falta para encerrar a partida. O valor da variável "espera" é o tempo em milissegundos que deve durar uma pausa que é feita no final do laço while.

O código completo da procedure jogo está abaixo.
procedure jogo;
begin
  while not(keypressed) do         {1ª parte}
    begin
      if((tempo mod 5) = 1) then
        for i:= 1 to QM do
          inicia(mina[i].lin,mina[i].col);

      if((cont mod 5) = 1) then     {2ª parte}
        begin
          movecara;
          if qbola > 0 then movebola;
          mostra;
          testaobj;
          testamina;
          if qbola > 0 then testabola;
          informa;
          escondecursor;
        end;

      if((cont mod 50) = 1) then     {3ª parte}
       begin
        tempo:=tempo-1;
        if tempo = 0 then
          begin
            fimdejogo;
            sair:=1;
            exit; {sai da procedure jogo}
          end;
       end;

      delay(espera);             {4ª parte}
      cont:=cont+1;
      if cont = 30000 then cont:=0;
    end;  {fim do while}

    tecla:=ord(upcase(readkey));
    if tecla=0 then tecla:=ord(readkey)+255;
end;

A 1ª parte do código muda a posição das minas a cada 5 unidades de tempo. Isto é feito usando o operador "mod" do Pascal que retorna o resto de uma divisão entre inteiros. No código usamos a expressão (tempo mod 5) que poderá resultar nos valores de 0 a 4. A procedure inicia() procura uma posição vazia na fase e armazena essa posição nas variáveis do item que foi passado como parâmetro.

Na 2ª parte do código, a cada 5 iterações do laço while, são chamadas diversas procedures que realizam a movimentação do jogador e das bolas e que testam a colisão do jogador com os objetos, as minas e as bolas.

A 3ª parte do código é responsável por diminuir a unidade de tempo do jogo que é exibida na tela. Isto ocorre a cada 50 iterações. Quando o valor da variável "tempo" for igual a zero a partida é encerrada e o fluxo do programa vai para a procedure fimdejogo.

Na 4ª parte temos a função "delay(espera)" que faz uma pausa na execução do código baseado no valor da variável "espera". Após isso a variável "cont" é adicionado de 1. Quando a variável "cont" estiver com o valor 30000 ela é zerada para evitar que chegue no valor limite de 32767 (inteiro 16 bit) e se torne um valor negativo.

O código ASCII da tecla que foi pressionada é armazenada na variável "tecla" para ser analisado na procedure controle e executar alguma ação caso exista.