projetos e jogos abertos
 
 
Os mísseis
Aprenda um truque útil, para mapear deslocamentos
 

Antes de prosseguir com nosso jogo, vou abordar um aspecto da programação que pode ser considerado como a "alma" do Guerra no Golfo. Para que você compreenda exatamente os pontos tratados aqui, vou contar como tudo se passou desde o princípio.

Um pouco antes da criação da versão CGA, do jogo Guerra no Golfo, eu havia publicado uma versão completa do editor gráfico GRAPHOS III para o PC. Rodando em configuração CGA, as rotinas gráficas tinham sido convertidas do Assembler Z80 para o Intel 8088.

Uma dessas rotinas já me chamava a atenção desde os tempos do MSX: o recurso de linha elástica. Se você não sabe o que é isso, explico: ao clicar sobre um determinado ponto, fixamo-os como coordenada inicial e arrastando o mouse, uma linha vai sendo "esticada" até o ponto e posição desejados. Quase todos os editores de desenho da atualidade dispõem de tal recurso.

A solução, para implementar tal recurso, pode ser obtida de duas formas diferentes: ou você mantém num buffer o conteúdo original da tela e, a cada movimento do mouse, restaura-o juntamente com uma nova linha, do ponto inicial até a posição do mouse, ou salva num buffer menor o conteúdo original de cada ponto da linha traçada e restaura-os sempre que o mouse se mover.

Na primeira solução, o processamento é simples mas demorado, uma vez que as dimensões das telas são cada vez maiores. Na segunda solução temos que lançar mão de uma rotina especial que trace uma linha e ao mesmo tempo mapeie cada ponto plotado, salvando o seu conteúdo. Não existe por aí, dando sopa, tal rotina e para adaptá-la de uma rotina clássica de line precisamos compreender como se dá esse processamento.

E onde entra o jogo nisso tudo? Bem, por ter que mapear cada ponto, sempre pensei que tal rotina poderia servir para, por exemplo, controlar o percurso de um navio em alto mar, num jogo de estratégia naval. Quando era adolescente, adorava criar esse tipo de jogo, mas era sempre complicado resolver os problemas de movimentação dos navios. Nunca foi possível usar um sistema preciso para tanto. Sempre que olhava a rotina de line pensava que um dia poderia juntar as duas coisas.

Foi assistindo às transmissões da Guerra no Golfo que me veio a idéia do jogo: mísseis, visão noturna (verde), monitor CGA, linhas tracejadas, etc. A velha rotina de linha elástica finalmente poderia servir para algo mais do que o seu uso original. Claro, ela precisaria de uns "macetes" para adequar-se ao seu novo trabalho e é isso que pretendo mostrar aqui.


Imagine uma linha a ser traçada da coordenada 0,0 até a coordenada 40,10. Régua e lápis na mão, no papel é fácil fazê-lo. Matematicamente calculamos o "tamanho" da linha (diagonal) apelando para o velho e preciso Pitágoras. No exemplo acima, algo em torno de 41,23105626.

Mas, no universo dos computadores e sua matemática de números compucabalísticos, a coisa não é bem assim. Veja na figura abaixo que, para 40 pontos por 10 pontos, a linha continua tendo os 40 pontos originais da coordenada X. A diferença fica por conta das 9 "descidas" que a plotagem deve dar, para atingir a coordenada final.


Podemos dizer que, 40 dividido por 10 é igual a 4, ou seja, para cada 4 pontos plotados na horizontal, temos que descer a plotagem em um ponto. Mas e se a coordenada final X for 35 e não 40?

Para cada ponto e meio plotado na horizontal... Mas não existe meio ponto no computador, então teremos que guardar um "meio" pois, com outro "meio" fazemos novamente um ponto. Assim também para outros "pedaços" de ponto. Só não podemos ir acumulando as frações para descontar tudo no final, pois isso nos daria uma linha com a ponta torta. Seria o caos.


O pessoal que lida com programação há muito tempo descobriu como contornar esse tipo de problema, lançando mão das operações com inteiros. Veja como, usando um contador especial, podemos determinar o desenho da linha com suas "descidas".

procedure ShowLine;
label
  Quad1,Quad2;
const
  X: integer = 0;
  Y: integer = 0;
  X2: integer = 40;
  Y2: integer = 10;
var
  L,S,C: integer;
begin
  L:=X2-X; S:=Y2-Y; C:=L div 2;
Quad1:
  Form1.Canvas.Pixels[X,Y]:= clBlue;
  C:= C+S; if C < L then goto Quad2;
  C:= C-L; Y:= Y+1;
Quad2:
  X:= X+1; if X<=X2 then goto Quad1;
end;

Na procedure acima, L é a diferença entre as coordenadas X, ou seja, a largura do retângulo formado a partir da linha/diagonal e S é a altura desse retângulo. Criamos um contador especial para as frações, ou seja, um contador que nos dirá quando "baixar" a plotagem do ponto. É uma espécie de divisão maluca (ao contrário) - somando a altura ao contador, sempre que o resultado for maior que a largura total é porque já acumulou um ponto inteiro.

Aqui temos um problema. Da forma como foi escrita, a procedure só funciona para valores de larguras maiores que as alturas, o que corresponde a inclinação de 90 a 135 graus do desenho da linha. O que faremos é simplesmente "rebater" o ponto quando a plotagem for em outra direção. 

Por exemplo, quando os pontos X2 e Y2 são menores que os pontos origens, isto é, a linha vai ser desenhada para trás, invertemos os valores para obter a diferença absoluta e, ao invés de incrementar a plotagem, decrementamos as coordenadas. No lugar de X:= X+1 usamos X:= X-1, ou ainda, X:= X+(-1).

Neste último exemplo, para facilitar a construção da procedure, podemos definir uma variável que corresponderá a +1 ou -1, dependendo da direção da plotagem da linha (variáveis Mx e My).

Veja, na rotina completa, que nos procedimentos iniciais estamos apenas estabelecendo a direção do movimento e a angulação. O resto se processa como na rotina anterior.

procedure ShowLine;
label
 Quad1,Quad2,Quad3,Quad4,Fim;
const
  X: integer = 0;
  Y: integer = 0;
  X2: integer = 100;
  Y2: integer = 150;
var
  L,S,C,Mx,My: integer;
begin
  Mx:= 1; My:= 1;
  L:= X2-X; if L < 0 then begin
    L:= X-X2; Mx:= -1; end;
  S:= Y2-Y; if S < 0 then begin
    S:= Y-Y2; My:= -1; end;
  if L >= S then C:= S div 2 else C:= L div 2;
  if (L < S) then goto Quad3;
Quad1:
  Form1.Canvas.Pixels[X,Y]:= clRed;
  C:= C+S; if C < L then goto Quad2;
  C:= C-L; Y:= Y+My;
Quad2:
  X:= X+Mx;
  if (Mx = 1) and (X <= X2) then goto Quad1;
  if (Mx = -1) and (X >= X2) then goto Quad1;
  goto Fim;
Quad3:
  Form1.Canvas.Pixels[X,Y]:= clRed;
  C:= C+L; if C < S then goto Quad4;
  C:= C-S; X:= X+Mx;
Quad4:
  Y:= Y+My;
  if (My = 1) and (Y <= Y2) then goto Quad3;
  if (My = -1) and (Y >= Y2) then goto Quad3;
Fim:
end;

Agora que já temos uma rotina de plotagem de linha, temos dividí-la em duas partes: a parte inicial, onde são calculadas as distâncias e angulações e a parte da plotagem de cada pixel propriamente dita. É nessa parte que poderemos incluir umas instruções para salvar o conteúdo original.

Vamos estabelecer, como variáveis globais, X1 / Y1 para os pontos de partida da linha, ou melhor da tragetória do míssil e X2 / Y2 para os pontos de chegada. Definiremos então três matrizes globais, para as seguintes finalidades:

Mis: array [0..29,0..8] of integer;

Esta matriz irá guardar as coordenadas e variáveis de 30 mísseis e cada elementos da linha corresponderá a:

Mis[Qms,0]:= X    Coordenadas X,Y do ponto
Mis[Qms,1]:= Y
Mis[Qms,2]:= X2   Coordenadas X2,Y2 do alvo
Mis[Qms,3]:= Y2
Mis[Qms,4]:= C    Contador
Mis[Qms,5]:= L    Largura
Mis[Qms,6]:= S    Altura
Mis[Qms,7]:= Mx   Flags (+1) ou (-1)
Mis[Qms,8]:= My

A segunda matriz (MCr) será usada para guardar a cor do pixel original, no local de plotagem do míssil.

MCr: array [0..29] of longint;
Tip: array [0..29,0..2] of byte;

A terceira matriz será usada para guardar o tipo de míssil que está sendo usado para aquelas coordenadas. Na linha, cada parâmetro significa:

Tip[Qms,0]:= 0   Tipo de míssil usado (1= patriot...)
Tip[Qms,1]:= 0   Se patriot, o alcance do míssil em pontos
Tip[Qms,2]:= 0   Se 255, patriot sem rumo / de 0 a 9 o scud alvo

Considere que estabelecemos controle para 30 mísseis simultâneos, dos quais os 10 primeiros serão scuds e os outros 20 serão mísseis do jogador - patriots, cruisers ou tomahawks.

Os mísseis do jogador possuem o controle Tip, que permite várias definições. Por exemplo, os patriots possuem uma alcance pequeno, ou seja, quando acaba o combustível eles caem, mesmo que o alvo esteja próximo.

Já os outros dois modelos, como são disparados contra possíveis alvos em terra, já teriam seu alcance calculado e definido antes do disparo e dispensam tal controle. O mesmo se dá com os Scuds.

Além disso, os patriots podem não ter um alvo definido, ou seja, o jogador não conseguiu enquadrar corretamente o scud no controle de disparo.

As variáveis globais e as procedures completas, para inicialização dos mísseis e para o seu deslocamento são dadas a seguir. Nesta parte do projeto coloquei um botão para disparar os scuds contra as bases, apenas para "conferir" o funcionamento das rotinas. Clique aqui e baixe o pacote com os fontes até esta etapa do jogo..

{---------- Início do programa / comentário -----------------}

TERCEIRA PARTE

Primeiro definimos as variáveis globais que usaremos para
o controle dos mísseis:

var
   X1,Y1: integer;       {Coordenadas do míssil}
   X2,Y2: integer;       {Coordenadas do alvo}
   Mpx,Mpy: integer;     {Flags de incremento/decremento}
   Cms: longint;         {Cor do píxel do míssil}
   Qms: byte;            {Número do míssil manipulado}
   Mis: array [0..29,0..8] of integer;
   MCr: array [0..29] of longint;
   Tip: array [0..29,0..2] of byte;

A rotina que controla o deslocamento do míssil é dividida
em duas etapas: primeiro a preparação das variáveis e
depois o controle do deslocamento propriamente dito.

{--- Rotina preparação da linha de deslocamento dos mísseis
   entra: X1,Y1  = posição de partida
          X2,Y2  = coordenadas do alvo
          Qms    = número do míssil
          Cms    = cor do míssil
}
procedure PreparaMissil;
begin
   {Handle do radar}
   TelRad:= Form1.Radar.Canvas.Handle;
   {Salva o conteúdo original da posição}
   MCr[Qms]:= GetPixel(TelRad,X1,Y1);
   {Coloca o míssil na tela}
   SetPixel(TelRad,X1,Y1,Cms);
   {Coordenada atual}
   Mis[Qms,0]:= X1; Mis[Qms,1]:= Y1;
   {Alvo}
   Mis[Qms,2]:= X2; Mis[Qms,3]:= Y2;
   {Flags}
   Mis[Qms,7]:= 1; Mis[Qms,8]:= 1;
   {Define as angulações (L, S e C)}
   Mis[Qms,5]:= X2-X1; if Mis[Qms,5] < 0 then begin
      Mis[Qms,5]:= X1-X2; Mis[Qms,7]:= -1; end;
   Mis[Qms,6]:= Y2-Y1; if Mis[Qms,6] < 0 then begin
      Mis[Qms,6]:= Y1-Y2; Mis[Qms,8]:= -1; end;
   if Mis[Qms,5] >= Mis[Qms,6] then
      Mis[Qms,4]:= Mis[Qms,6] div 2
      else Mis[Qms,4]:= Mis[Qms,5] div 2;
end;

{--- Movimenta míssil -------------------------------------
   entra:   Qms    = número do míssil
            Cms    = cor do míssil
}
procedure MoveMissil;
label
  Quad1,Quad2,Quad3,Quad4,Quad5,Fim;
begin
   TelRad:= Form1.Radar.Canvas.Handle;
   {Repõe o conteúdo}
   SetPixel(TelRad,Mis[Qms,0],Mis[Qms,1],Mcr[Qms]);
   if Mis[Qms,5] < Mis[Qms,6] then goto Quad3;
Quad1:
   Mis[Qms,4]:= Mis[Qms,4]+Mis[Qms,6];
   if Mis[Qms,4] < Mis[Qms,5] then goto Quad2;
   Mis[Qms,4]:= Mis[Qms,4]-Mis[Qms,5];
   Mis[Qms,1]:= Mis[Qms,1]+Mis[Qms,8];
Quad2:
   if Tip[Qms,1] <> 0 then begin
      Tip[Qms,1]:= Tip[Qms,1] - 1;
      if Tip[Qms,1] = 0 then goto Quad5; end;
   Mis[Qms,0]:= Mis[Qms,0]+Mis[Qms,7];
   MCr[Qms]:= GetPixel(TelRad,Mis[Qms,0],Mis[Qms,1]);
   if ((Mis[Qms,1] < 81) and (AvRad1 <> 1000)) or
      ((Mis[Qms,1] > 80 ) and (Mis[Qms,1] < 162) and
      (AvRad2 <> 1000)) or ((Mis[Qms,1] > 161 ) and
      (Mis[Qms,1] < 243) and (AvRad3 <> 1000)) or
      ((Mis[Qms,1] > 242) and (AvRad4 <> 1000))
      then SetPixel(TelRad,Mis[Qms,0],Mis[Qms,1],Cms);
   if (Mis[Qms,7] = 1) and (Mis[Qms,0] <= Mis[Qms,2]) then
      goto Fim;
   if (Mis[Qms,7] = -1) and (Mis[Qms,0] >= Mis[Qms,2]) then
      goto Fim;
   SetPixel(TelRad,Mis[Qms,0],Mis[Qms,1],Mcr[Qms]);
   Mis[Qms,0]:= 0;
   goto Fim;
Quad3:
   Mis[Qms,4]:= Mis[Qms,4]+Mis[Qms,5];
   if Mis[Qms,4] < Mis[Qms,6] then goto Quad4;
   Mis[Qms,4]:= Mis[Qms,4]-Mis[Qms,6];
   Mis[Qms,0]:= Mis[Qms,0]+Mis[Qms,7];
Quad4:
   if Tip[Qms,1] <> 0 then begin
      Tip[Qms,1]:= Tip[Qms,1] - 1;
      if Tip[Qms,1] = 0 then goto Quad5; end;
   Mis[Qms,1]:= Mis[Qms,1]+Mis[Qms,8];
   MCr[Qms]:= GetPixel(TelRad,Mis[Qms,0],Mis[Qms,1]);
   if ((Mis[Qms,1] < 81) and (AvRad1 <> 1000)) or
      ((Mis[Qms,1] > 80 ) and (Mis[Qms,1] < 162) and
      (AvRad2 <> 1000)) or((Mis[Qms,1] > 161 ) and
      (Mis[Qms,1] < 243) and (AvRad3 <> 1000)) or
      ((Mis[Qms,1] > 242) and (AvRad4 <> 1000))
      then SetPixel(TelRad,Mis[Qms,0],Mis[Qms,1],Cms);
   if (Mis[Qms,8] = 1) and (Mis[Qms,1] <= Mis[Qms,3]) then
      goto Fim;
   if (Mis[Qms,8] = -1) and (Mis[Qms,1] >= Mis[Qms,3]) then
      goto Fim;
Quad5:
   SetPixel(TelRad,Mis[Qms,0],Mis[Qms,1],Mcr[Qms]);
   Mis[Qms,0]:= 0;
Fim:
end;

Agora vamos fazer um botão de disparo, apenas para testar
o funcionamento das rotinas.

Crie um SpeedButton e coloque o seguinte procedimento no
evento OnClick:

label
   Fim;
var
   Tmp: integer;
begin
   Cms:= clRed; X1:= 200; Y1:= 30;
   for Qms:= 0 to 9 do if Mis[Qms,0] = 0 then begin
      Tmp:= Qms; if Tmp > 4 then Tmp:= Tmp - 5;
      Tmp:= Tmp * 2;
      X2:= BasCord[Tmp];
      Y2:= BasCord[Tmp+1];
      PreparaMissil; goto Fim;
      end;
   Fim:
end;

O míssil scud é disparado, mas ainda não se desloca. O
controle do deslocamento será feito no evento OnTimer que
comanda o vôo dos aviões AWACs. No final da procedure,
acrescente a seguinte linha (antes da instrução
Radar.Repaint;): 

      Cms:= clRed; for Qms:= 0 to 9 do
         if Mis[Qms,0] <> 0 then MoveMissil;

Pronto. Agora os scuds, uma a um, irão se dirigir para
as bases.

{------------ Fim do programa / comentário ------------------}

  online