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.

segunda-feira, 10 de dezembro de 2012

Procedure tela

A Procedure tela é responsável por desenhar a tela relacionada a fase que o jogador escolheu e inicializar todos os itens desta fase. Esta procedure é executada apenas uma vez antes de começar uma partida.

O código desta procedure é extenso mas é simples. As ações que são executadas dentro desta procedure são as seguintes:
procedure tela;
var i:integer;
    s,strfase:string;
begin
    { Inicia dados dos jogadores... }

    { Inicia dados das bolas... }

    { Limpa a matriz mapa que representa a fase atual... }

    { Preenche as bordas da fase na matriz mapa... }

    { Preenche a matriz mapa de acordo com a fase escolhida... }

    { Desenha a fase de acordo com os valores da matriz mapa... }

    { Inicia as minas da fase... }

    { Inicia os objetos que os jogadores tem de pegar... }

    { Monta a string de Recorde e exibe no topo da tela... }

end;

O código abaixo inicia os dados do jogador. Lembre-se que as variáveis "jog" e "obj" são arrays de duas posições do tipo "geral". O tipo "geral" é definido no início do programa e possui as variáveis col, lin, cara, cor e direcao
  for i:= 1 to qjog do
    begin
      jog[i].cara:=2;
      jog[i].direcao:=0;
      extrainfo[i].score:=0;
      if i = 1 then
        begin
          jog[i].col:=28;
          jog[i].lin:=11;
          jog[i].cor:=10;
          obj[i].cara:=4;
          obj[i].cor:=10;
        end
      else
        begin
          jog[i].col:=23;
          jog[i].lin:=7;
          jog[i].cor:=14;
          obj[i].cara:=6;
          obj[i].cor:=14;
        end;
    end;

As variáveis lin e col definem a posição na área do jogo. A variável direcao pode conter os valores de 0 a 4, sendo que 0 (zero) significa que o jogador está parado e os outros valores indicam a direção atual que o jogador está se movendo.

A variável cara contém o valor ASCII do símbolo que deve ser desenhado na tela. Os valores utilizados no Mina 2 são:
☻ (ASCII = 2), ♦ (ASCII = 4) e ♠ (ASCII = 6)

O 1º jogador utiliza a cor verde claro (valor = 10) e o 2º jogador utiliza a cor amarelo (valor = 14). A cor também é usada nos objetos que os jogadores devem pegar.


Em relação as bolas do jogo, elas iniciam em posições predeterminadas na área de jogo mas com um pequeno ajuste aleatório que é obtido usando a função "random()".

A função "random()" recebe como parâmetro um valor inteiro X e retorna um valor aleatório que vai de 0 até X-1. Por exemplo, random(3) pode retornar os valores 0, 1 ou 2.

O trecho de código abaixo mostra como é definida a posição da 1ª bola na área de jogo. As outras bolas são feitas de forma semelhante.
  if qbola > 0 then
    for i:= 1 to qbola do
     begin
      case i of
        1: begin
             bola[i].col:=3+random(3);
             bola[i].lin:=2+random(3);
             bola[i].direcao:=2;
           end;

    ...

Esta imagem mostra como ficaram as posições iniciais das 8 bolas na fase 2.


A matriz mapa[lin,col] guarda o valor "1" para as posições da fase que contém um bloco. Todas as fases possuem o mesmo tipo de borda feito com os blocos. A parte interior da área do jogo é preenchida de acordo com a fase escolhida.

O código abaixo mostra o algoritmo de preenchimento das fases de 2 a 5. A fase 1 não está presente porque não existe nenhum bloco na área central da fase 1.

Por exemplo, a fase 2 que está na imagem acima consiste apenas das bordas e de uma linha central que inicia na posição (lin=9, col=12) e vai até a posição (lin=9, col=39) da matriz mapa.
  case fase of
       2: for i:= 12 to 39 do
              mapa[9,i]:=1;
       3: for i:= 5 to 13 do
            begin
              mapa[i,15]:=1;
              mapa[i,36]:=1;
            end;
       4: for i:= 12 to 39 do
            begin
              mapa[5,i]:=1;
              mapa[13,i]:=1;
              if (i=25) or (i=26) then
                begin
                  mapa[8,i]:=1;
                  mapa[9,i]:=1;
                  mapa[10,i]:=1;
                end;
            end;
       5: begin
            i:=12;
            repeat
              if(odd(i)) then
                for j:=4 to 12 do
                  mapa[j,i]:=1
              else
                for j:=6 to 14 do
                  mapa[j,i]:=1;
              i:=i+9;
            until i>39;
          end;
  end;

O código que desenha os blocos na tela baseado nos valores da matriz mapa é o seguinte:
  textcolor(12);   {cor vermelha}
  for i:= 1 to altura do
    for j:= 1 to largura do
      if mapa[i,j]=1 then
        begin
          gotoxy(j+difcol,i+diflin);
          write('█');
        end;

As diferenças entre as coordenadas da área do jogo e as coordenadas da tela estão definidas nas constantes "diflin = 6" e "difcol = 15", sendo assim a posição (1,1) da matriz mapa será desenhada na coordenada de tela (7,16).

Todas as variáveis que definem as posições do jogador, objetos e bolas, são referentes a posição dentro da área de jogo. Para encontrar a posição real de tela onde serão desenhados esses itens é preciso adicionar os valores das constantes diflin e difcol.