A programação do jogo

Veja como foi estruturada as principais operações do jogo

Uma vez estabelecidos os principais parâmetros do jogo, definidas e produzidas as imagens, o passo seguinte é a programação. É claro que, durante este trabalho, algumas modificações nas imagens podem ser necessárias, para que o conjunto todo funcione de forma mais homogênea. Pequenos ajustes são sempre imprescindíveis.

Partindo da concepção, definimos que a área visual do jogo, ou seja a "tela" onde acontecerá a principal ação, terá as seguintes dimensões: 524 pixels de largura por 254 pixels de altura. A razão dessas medidas? Simples, não teremos uma tela exageradamente larga a ponto de demorar muito para a travessia do tanque, nem uma tela minúscula, que não dê pelo menos três chances ao jogador de acertar o alvo (desde que ele já saiba em que direção atirar). Nem tão lá, nem tão cá, essas medidas ainda tem a vantagem de serem operacionais se algum usuário estiver com seu Windows em modo 640 x 480.

A tela é basicamente uma TImage (Image1) com essas dimensões, que no início do programa é aproveitada para mostrar a figura de abertura do jogo. Este procedimento tem uma dupla função: mostrar como o jogo é sensacional (as imagens de abertura sempre dão uma idéia mais eloqüente do que é, ou poderia ser, o jogo de fato) e garantir que a tela seja montada, na memória, ajustada como true-color, para que possamos visualizar todas as cores do nosso programa sem nenhuma perda.

A estrutura do sistema que operará as animações é bem simples: iremos criar um buffer de tela, com as mesmas dimensões e características da TImage da tela, na memória. Um TBitmap que será chamado de "Buf". Todas as figuras, tanques, armas, efeitos, etc, serão compiladas como TImage e serão transferidas para este buffer de tela, para montar uma cena do jogo. Daí, quando todas as figuras e informações estiverem no Buf, ele é transferido para a tela. Com isso evitamos atuar diretamente sobre uma imagem que esteja sendo mostrada na tela, o que sempre causa um problema chamado "flicking".

var
  Buf: TBitmap; // Buffer de tela
...

//Inicia o processamento do jogo:
procedure TForm1.FormActivate(Sender: TObject);
begin
  Buf:= TBitmap.Create;       //Cria o buffer de tela
  Buf.Width:= Image1.Width;   //Largura
  Buf.Height:= Image1.Height; //Altura
  Buf.Canvas.Font.Color:= clLime;   //Parâmetros para a
  Buf.Canvas.Font.Name:= 'arial';   //impressão dos números
  Buf.Canvas.Font.Style:= [fsBold];
  Buf.Canvas.Font.Height:= 32;
end;

Vale lembrar que todas as TImage que conterão as demais figuras deverão estar com suas propriedades Visible em false, para que não sejam visualizadas pelo jogador.

Todo o conjunto de animação é controlado por um TTimer, que está ajustado para um interval de 60. O evento OnTimer é quem fará todo o controle do jogo e funcionará a partir de uma variável global chamada Stat (byte). Se Stat for zero, não há tanque andando, ou seja, o jogo ainda não começou. Por isso é que Stat é declarada como constante, com valor igual a zero, e não como variável simples.

Se Stat for igual a 1, então tem um tanque andando pela tela e portanto pode ser atingido pelo jogador. Se Stat for igual a 2, então o evento OnTimer deverá preparar um novo tanque para iniciar a sua corrida.

Faz parte do jogo deixar o tanque "furado como uma peneira", durante o seu percurso. Também quando a torre explode, a imagem final da animação deverá ser um tanque destroçado, que ainda assim anda até o final da tela. Sempre que um novo tanque for liberado, ele deverá estar 0 Km, impecável.

Resolvemos isso, sem maiores problemas de programação, trabalhando com duas TImage: uma (Image6) contém a figura do tanque em perfeitas condições e é usada como uma matriz para os novos tanques. A outra (Image2) não é inicializada e é quem receberá todas as alterações que o tanque sofrerá ao longo da sua jornada. É esta TImage que é usada para colocar o tanque no buffer de tela (Buf).

Outra diferença entre essas duas TImage é que a matriz do tanque (Image6) possui apenas 32 pixels que altura, que é a altura total do tanque, mas Image2 possui 64 pixels de altura, pois deverá conter as figuras da explosão da torre. Podíamos fazer de forma diferente, montando cada pedaço da animação no seu respectivo local dentro do Buf, mas como estamos fazendo iremos simplificar extraordinariamente a programação.


O tanque inicia sua trajetória quando Stat recebe o valor 2. Vejamos isso na programação (evento OnTimer):

  if Stat = 2 then begin
    //Apaga o buffer do tanque
    Image2.Canvas.Brush.Color:= clBlack;
    Image2.Canvas.Pen.Color:= clBlack;
    Image2.Canvas.Rectangle(0,0,128,64);
    //Transf a imagem de um tanque intacto para o buffer
    Matrz:= Image6.Canvas.Handle;
    BitBlt(Tank,0,32,128,32,Matrz,0,0,SRCCOPY);
    //Ajusta demais parâmetros
    Tx:= -128; Ty:= 90; Explod:= 0;
    inc(Tanques); Stat:= 1;
  end;

Primeiro apagamos todo o buffer do tanque, para eliminar qualquer resquício do tanque anterior. Depois copiamos a figura do tanque (Image6) para o buffer (usamos a função BitBlt, já abordada em inúmeras matérias do Club TILT).

Tx é uma variável que contém a coordenada X do canto superior esquerdo da área de impressão do tanque. Sendo negativo (a largura total do tanque é de 128 pixels), a imagem não será vista na tela, mesmo que a procedure comande a transferência, via BitBlt, da imagem. Está aqui uma das incríveis vantagens de se programar no Windows + Delphi. Em outro sistema teríamos que criar complexas rotinas para verificar a posição do tanque e enviar para a tela apenas o pedaço visível dele.

Ty é a variável que contém a coordenada Y do canto superior esquerdo da área de impressão do tanque e, a rigor, não é usada em nosso programa (não tem utilidade prática). Mas a usamos assim mesmo para o caso de mudar a altura do tanque, ou seja, usar um modelo diferente de veículo. Isto fica para uma futura implementação.

Explod é uma variável que controlará a explosão da torre do canhão. Se for zero, a torre está intacta. E finalmente Stat recebe o valor 1, que é para iniciar o movimento do tanque.

Ao iniciar a impressão de uma cena, do movimento do tanque, o buffer de tela é todo limpo (com a cor preta) e o chão é montado usando-se uma matriz TImage como padrão de fundo.

procedure LimpaBuf; // Apaga o buffer de tela
var
  Chao,HBuf: HBitmap;
begin
  HBuf:= Buf.Canvas.Handle;
  Chao:= Form1.Image3.Canvas.Handle;
  Buf.Canvas.Brush.Color:= clBlack;
  Buf.Canvas.Pen.Color:= clBlack;
  Buf.Canvas.Rectangle(0,0,524,154);
  BitBlt(HBuf,0,154,131,100,Chao,0,0,SRCCOPY);
  BitBlt(HBuf,131,154,131,100,Chao,0,0,SRCCOPY);
  BitBlt(HBuf,262,154,131,100,Chao,0,0,SRCCOPY);
  BitBlt(HBuf,393,154,131,100,Chao,0,0,SRCCOPY);
end;

O movimento do tanque se baseia na sua coordenada X, tendo como variável de controle Tx (dentro do evento OnTimer).

    BitBlt(HBuf,Tx,Ty,128,64,Tank,0,0,SRCCOPY);
    ...
    inc(Tx,2);
    if Tx > 530 then Stat:= 2;

A primeira linha transfere todo o conteúdo do buffer do tanque para o buffer da tela. A segunda linha faz com que o tanque "ande" dois pixels para a direita. Por que dois? Com um pixel ele anda muito devagar e com 3 pixels muito depressa. O cálculo efetivo, para decidir pelo valor dois é feito através do método nada científico "achei que assim ficava melhor".

Quando Tx passar de 530, significa então que o tanque desapareceu do lado direito da tela (lembra que ela só tem 524 pixels de largura. Daí, Stat recebe o valor 2 que é para liberar um novo tanque. Como no programa isso acontecerá ainda neste evento (OnTimer), tomamos a decisão de lançar um novo tanque 6 pixels (ou seja, 3 eventos OnTimer) depois do tanque já ter desaparecido. Isto dá tempo para a ordem de partida chegar até a guarnição e os motores do tanque serem ligados (brincadeira). Na verdade, essa diferença de tempo é para que um tanque não comece a aparecer imediatamente após o primeiro ter sumido. Assim o jogador respira um pouco e reposiciona sua arma, afinal o objetivo é acertar o tanque e não provocar um ataque do coração no jogador.

O tiro é disparado por um botão que fica fora da tela e é controlado por uma variável chamada (obviamente) Tiro. Se Tiro for zero, o jogador não disparou ainda. Se for 220, então o jogador acabou de apertar o gatilho. Por que 220? Porque usaremos esta variável para determinar também a coordenada Y da bala e 220 a coloca justamente na boca da arma do jogador. A coordenada X da bala e da arma é dada pela propriedade Position, do componente TScrollBar que movimenta a arma.

Por falar em arma, a sua impressão é feita em duas posições distintas: em modo normal (repouso), na linha 222 ou durante o recuo da arma, na linha 230. Quem define isso é o trecho:

    if Tiro = 220 then Acan:= 230 else Acan:= 222;
    ...
    BitBlt(HBuf,ScrollBar1.Position,Acan,24,32,
                                    Cano,24,0,SRCAND);
    BitBlt(HBuf,ScrollBar1.Position,Acan,24,32,
                                    Cano,0,0,SRCPAINT);

Aqui usamos o recurso de imprimir uma figura (o cano da arma) através de sua máscara de impressão. Este é outro assunto prá lá de manjado no Club TILT e dispensa maiores comentários.

O tiro é um perfeito exemplo de aproveitamento de bits. Obteremos seu desenho diretamente da máscara do cano da arma. Seu comprimento varia de 1 a 2 pixels, dependendo da distância que ele está do tanque.

Explico melhor: ao disparar, a bala faz uma trajetória em linha reta até o tanque. Nosso sistema de coordenadas é bidimensional, mas pretendemos dar a impressão de que a bala se distancia da arma, o que implicaria numa terceira dimensão inexistente.

Para obter este efeito, precisamos atuar de três formas distintas: na "velocidade" com a qual a bala se distancia da arma, no ângulo que ela deve fazer para atingir o alvo e no seu tamanho, que diminui a medida que afasta.

    if Tiro > 0 then begin
      if Tiro < 155 then Tmb:= 1 else Tmb:= 2;
      BitBlt(HBuf,Cx,Tiro,1,Tmb,Cano,24,0,SRCCOPY);

Para diminuir é fácil, basta começar com uma bala com 2 pixels de comprimento e, depois de uma certa coordenada (no meio do caminho mais ou menos) reduzí-la para um pixel. A variável Tmb irá guardar o tamanho da bala.

O ângulo e a velocidade são feitos em conjunto, diminuindo o tamanho do salto que a bala faz, em pixels, usando uma daquelas fórmulas que aprendemos na escola... o inverso do quadrado da distância... Bem, isso é apenas um jogo, então vamos frear a bala usando uma equação mais simples: a metade da distância percorrida no salto anterior.

      dec(Tiro,Vt); Vt:= ((Vt + 2) div 2) + 1;

Primeiro somamos dois a Vt para que nunca ocorra uma divisão por zero (senão o computador trava) e garantimos no final (+1) que pelo menos um pixel a bala vai andar.

Como complemento ao movimento do tiro, precisamos verificar se ele foi disparado neste evento e se foi, colocar o fogo na boca da arma:

      if Tiro = 220 then begin
        BitBlt(HBuf,Cx-4,216,10,13,Fogo,20,0,SRCAND);
        BitBlt(HBuf,Cx-4,216,10,13,Fogo,0,0,SRCPAINT);
      end;

Resta agora saber onde o tiro atingiu o tanque (se é que atingiu).

      if Tiro < 140 then begin
        Tiro:= 0; Mosca:= Cx-Tx;
        //Se acertou no tanque...
        if (Mosca < 120) and (Mosca > 10) then begin
          //Som da bala penetrando no metal
          Som.Sound[1].Replay;
          //Marca o tanque com o furo
          BitBlt(Tank,Mosca,51,2,2,Cano,20,0,SRCCOPY);
          //Se acertou na área da frente, grito do homem
          if (Mosca > 95) and (Mosca < 99) then
                                         Som.Sound[2].Replay;
          //Se acertou na área do meio, grito do whookie
          if (Mosca > 75) and (Mosca < 79) then
                                         Som.Sound[3].Replay;
          //Se acertou na traseira (painel da munição)
          if (Mosca > 45) and (Mosca < 48) and (Explod = 0)
            then begin
            //Explode o tanque
            Som.Sound[5].Replay;
            Explod:= 1; inc(Destrd); inc(Ammo,5);

Quando o tiro acerta no painel de munição, a variável Explod recebe o valor 1 e irá variar, de acordo com o frame da explosão que for impresso, até o valor 8 (temos 7 frames de explosão). Os frames são enviados para o buffer do tanque e seguem a mesma seqüência da animação geral.

    if (Explod > 0) and (Explod < 8) then begin
      Boom:= Image7.Canvas.Handle;
      //Transfere o frame da explosão para o tanque
      BitBlt(Tank,0,0,128,50,Boom,0,(Explod-1)*50,SRCCOPY);
      inc(Explod);
    end;

Agora só falta imprimir os parâmetros de munição, tanques enviados e tanques destruídos.

    //Imprime quantidade de tiros que ainda tem
    Buf.Canvas.Font.Color:= clLime;
    Num:= '00' + IntToStr(Ammo);
    Num:= copy(Num,length(Num)-2,3);
    Buf.Canvas.TextOut(5,0,Num);
    //Imprime quantos tanques já passaram
    Num:= '0' + IntToStr(Tanques);
    Num:= copy(Num,length(Num)-1,2);
    Buf.Canvas.TextOut(430,0,Num);
    //Imprime quantos tanques foram destruídos
    Buf.Canvas.Font.Color:= clRed;
    Num:= '0' + IntToStr(Destrd);
    Num:= copy(Num,length(Num)-1,2);
    Buf.Canvas.TextOut(480,0,Num);
    //Transfere o buffer para a tela
    BitBlt(Tela,0,0,524,254,HBuf,0,0,SRCCOPY);
    Image1.Repaint;

Está pronto o sistema principal de controle das animações. Agora é relaxar, estalar os dedos, fazer pontaria e detonar o maior número possível de tanques. Baixe o pacote dos fontes, no telefone ao lado, e acompanhe o que cada linha de programação faz. Dá para acrescentar um monte de implementações, recursos e novas etapas neste jogo (afinal, é para isso que ele é publicado com os fontes abertos).

Clique aqui e baixe o pacote zip contendo os fontes do jogo.

 
online