|
|
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 ------------------}
|
|