Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

Animando o Console - Efeito Matrix Parte II

Tags: hbuffer strip
Saída da 2ª animação em modo texto. Bem mais interessante.
No último post criamos um pequeno programa que imprimia zeros e uns em posições aleatórias da tela, criando um efeito muito simples, mas nada parecido com o efeito do console do matrix que estamos querendo escrever.

O objetivo deste primeiro programa era se familiarizar com as funções da API do windows e começar a conhecer o ambiente e suas limitações.

O leitor mais atento poderá ter percebido, entretanto, vários problemas com o programa escrito. O primeiro deles é que não estamos realmente gerando os quadros (frames) de uma animação, estamos simplesmente jogando caracteres na tela da maneira mais displiscente e ineficiente se pode imaginar.

Outro problema sério é que não temos nenhum mecanismo de buffer, estamos escrevendo direto na área visível da aplicação e, como vimos na série Animações em Tempo Real com a VCL, não dá pra ir muito longe assim (note que o ambiente é diferente, mas as técnicas que utilizaremos são exatamente as mesmas).

Vamos começar a pôr ordem na casa então! Mas antes, se você estiver curioso, baixe o programa compilado e veja o resultado do código que vamos construir.

Bem mais interessante, heim!

Preparando o ambiente


Primeiro, vamos definir os handlers que irão apontar para nossos buffers. Precisamos de dois buffers, um que aponte pra uma área invisível na memória e um que aponte para o buffer de saída do console. Declare as seguintes variáveis e escreva uma nova procedure para realizarmos as inicializações.

var
hBuffer1, hBuffer2, hBackBuffer : THandle;
consoleBounds : TCOORD;

(...)

procedure ConfigConsole;
var
lCursorInfo : TConsoleCursorInfo;
lSecAttributes : TSecurityAttributes;
begin
SetConsoleTitle('DelphiGames Blog | Matrix Effect - Part 2');
Write('Getting handlers... ');

lSecAttributes.nLength:= sizeOf(TSecurityAttributes);
lSecAttributes.lpSecurityDescriptor := nil;
lSecAttributes.bInheritHandle := false;

hBuffer1 := GetStdHandle(STD_OUTPUT_HANDLE);
hBuffer2 := CreateConsoleScreenBuffer( GENERIC_WRITE or GENERIC_READ,
FILE_SHARE_READ,
lSecAttributes,
CONSOLE_TEXTMODE_BUFFER, nil);

if (hBuffer1 = INVALID_HANDLE_VALUE) or (hBuffer2 = INVALID_HANDLE_VALUE) then
begin
WriteLn('ERROR');
Halt(1);
end
else
begin
WriteLn('OK!');

hBackBuffer := hBuffer1;
lcursorInfo.dwSize := 1;
lcursorInfo.bVisible := False;

SetConsoleCursorInfo(hBuffer1, lcursorInfo);
SetConsoleCursorInfo(hBuffer2, lcursorInfo);

consoleBounds.X:= 80;
consoleBounds.Y:= 25;

SetConsoleScreenBufferSize(hBuffer1, consoleBounds);
SetConsoleScreenBufferSize(hBuffer2, consoleBounds);
end;
end;

Ôpa! Tem coisa nova aí, mas não precisar se intimidar não.

Começamos setando o título da janela, apontamos o hBuffer1 para o buffer de saída do console e criamos um novo buffer usando a API CreateConsoleScreenBuffer armazenando seu handler em hBuffer2. Seguimos checando se os dois buffers foram inicializados corretamente testando seu valor contra a constante INVALID_HANDLE_VALUE e, se estiver tudo ok, ajustamos o hBackBuffer para apontar para hBuffer1,  escondemos o cursor (SetConsoleCursorInfo) e concluímos ajustando o tamanho dos buffes para 80x25 caracteres (SetConsoleScreenBufferSize).

Ok. Temos um procedimento pra ajustar o console e alguns ponteiros (o handler acaba sendo um tipo de ponteiro no final das contas) para os buffers. A idéia é realizar todos as saídas em  hBackBuffer e, quando tivermos um frame pronto, fazemos um swap dos ponteiros, exibindo o buffer em estávamos trabalhando e escondendo o atual. Esta técnica também é conhecida como page-flipping e vamos implementá-la agora com a ajuda de mais uma chamda à Win32 API (SetConsoleActiveScreenBuffer). Escreva o método SwapBuffers logo abaixo da implementação de  ConfigConsole.

procedure SwapBuffers;
begin
if hBackBuffer = hBuffer1 then
begin
SetConsoleActiveScreenBuffer(hBuffer1);
hBackBuffer:= hBuffer2;
end
else begin
SetConsoleActiveScreenBuffer(hBuffer2);
hBackBuffer:= hBuffer1;
end;
end;

Para concluir o setup do ambiente, implemente a função clear logo depois de SwapBuffers e e altere código de entrada do programa conforme exibido a seguir

procedure Clear;
var
tc :tcoord;
nw: DWORD;
cbi : TConsoleScreenBufferInfo;
begin
GetConsoleScreenBufferInfo(hBackBuffer, cbi);
tc.x := 0;
tc.y := 0;
FillConsoleOutputAttribute(hBackBuffer, Black,cbi.dwsize.x*cbi.dwsize.y, tc, nw);
FillConsoleOutputCharacter(hBackBuffer, ' ', cbi.dwsize.x*cbi.dwsize.y, tc, nw);
SetConsoleCursorPosition(hBackBuffer, tc);
end;

//ponto de entrada do programa
begin
randomize;
ConfigConsole;
while hi(GetKeyState(VK_ESCAPE)) = 0 do
begin
Clear;
SwapBuffers;
end;
end.

Pronto! Com estas alterações seu ambiente está preparado.

Compile e execute o programa e você vai uma incrível... tela preta! Exatamente igual à imagem ao lado.

Mas não se engane, esta tela, agora dotada de um buffer duplo e uma lógica de page flipping, é capaz de muito mais do que pode indicar sua aparência simplória.

E é isto que vamos começar a descorbrir na próxima seção deste texto.

De volta à Matrix

Podemos pensar no efeito como sequências de caracteres que descem na tela deixando um rastro que vai ficando mais fraco até sumir. Esses caracteres começam em uma posição aleatória no eixo x e com y = 0. Vamos chamar cada um desses rastros de Strip.

No espaço entre a cláusula uses e a declaração das variáveis globais, faça as alterações necessárias para que seu código fique igual o código a seguir:

const
Black = 0;
Green = 2;
LightGreen = 10;
White = 15;

STRIP_COUNT = 150; //quantos strips teremos em nosso array
STRIP_MAX_LEN = 25; //tamanho máximo do rastro deixado pelo strip
STRIP_MIN_LEN = 6; //tamanho mínimo deixado pelo rastro.

type
TStrip = record
Position : COORD; //posição de nosso strip na tela
Length : byte; //tamanho do rastro
Delay : integer; //tempo de espera até o strip começar a ser exibido
end;

var
hBuffer1, hBuffer2, hBackBuffer : THandle;
consoleBounds : TCOORD;
strips: array[0..STRIP_COUNT] of TStrip;

Tudo bem simples.

Declaramos as constantes de cor (lembra que temos uma paleta de 16 cores dispoíveis?) que nos interessam, algumas constantes de parametrização dos strips, um record TStrip com a estrutura de dados necessária e, no final, um array de TStrip para armazenar os dados.

Agora vamos iniciar os strips com valores  aleatórios.

procedure InitStrips;
var
i: integer;
begin
for i:=0 to STRIP_COUNT-1 do
begin
strips[i].Length := random(STRIP_MAX_LEN - STRIP_MIN_LEN) + STRIP_MIN_LEN;
strips[i].Position.y := 0;
strips[i].Position.x := random(consoleBounds.x);
strips[i].Delay := random(20);
end;
end;

E atualizá-los a cada iteração de nosso loop principal com a seguinte rotina.

procedure UpdateStrips;
var
i : integer;
begin
for i:=0 to STRIP_COUNT-1 do
begin
if strips[i].Delay > 0 then
strips[i].Delay := strips[i].Delay -1
else
begin
strips[i].Position.Y := strips[i].Position.Y + 1;
if ( strips[i].Position.Y - strips[i].Length > consoleBounds.Y ) then
begin
strips[i].Length := random(STRIP_MAX_LEN - STRIP_MIN_LEN) + STRIP_MIN_LEN;
strips[i].Position.y := 0;
strips[i].Position.x := random(consoleBounds.x);
strips[i].Delay := random(100);
end;
end;
end;
end;

Só restam mais dois passos. Desenhar os strips e ajustar o loop principal para incluir as rotinas criadas.

procedure DrawStrips;
var
i, j : integer;
lColor, lCharsWritten: DWORD;
lChar : char;
lPosition: COORD;
begin
lCharsWritten := 0;
for i:=0 to STRIP_COUNT-1 do
begin
if (strips[i].Delay <= 0) then
begin
if (strips[i].Position.Y <= consoleBounds.Y) then
begin
//desenhamos o primeiro caractere em branco
lColor:= White;
lChar := Char(random(255-33)+33);
WriteConsoleOutputAttribute(hBackBuffer, @lColor, 1, strips[i].Position, lCharsWritten);
WriteConsoleOutputCharacter(hBackBuffer, @lChar, 1, strips[i].Position, lCharsWritten);
end;
for j:=1 to strips[i].Length-1 do
if (strips[i].Position.Y + j <= consoleBounds.Y) then
begin
//os primeiros 35% do rastro serão desenhados em verde claro
if (j / strips[i].Length <= 0.35) then
lColor:= LightGreen
else
lColor:= Green;
lChar := Char(random(255-33)+33);
lPosition.X:= strips[i].Position.X;
lPosition.Y:= strips[i].Position.Y - j;
WriteConsoleOutputAttribute(hBackBuffer, @lColor, 1, lPosition, lCharsWritten);
WriteConsoleOutputCharacter(hBackBuffer, @lChar, 1, lPosition, lCharsWritten);
end;
end;
end;
end;

begin
randomize;
ConfigConsole;
InitStrips;
while hi(GetKeyState(VK_ESCAPE)) = 0 do
begin
Clear; //limpamos a tela
UpdateStrips; //calculamos a posição dos strips no quadro atual
DrawStrips; //desenhamos todos os strips visíveis
SwapBuffers; //exibimos o quado criado com um page-flip
Sleep(10); //aguardamos 10ms para
end;
end.

Compile o código e você verá o efeito fucionando. Bacana heim!

Deferenças de Ambiente

Fizemos tudo certo até aqui, mas o código não compila no Lazarus... porquê?

Porque há uma diferença na declaração da função CreateConsoleScreenBuffer no arquivos windows.pas que acompanha o Delphi e o windows.pas que acompanha o Lazarus. Enquanto um espera uma referência para uma estrutura TSecurityAttributes o outro espera um ponteiro não tipado. Nada complicado, se usarmos a diretiva de compilação condicional certa para resolver a questão.

Quando usamos o compilador do free pascal, como é o caso do Lazarus, a diretiva {$DEFINE FPC} sempre estará ligada, nos dando um teste seguro para isolar as eventuais diferenças entre os dois mundos portanto, vamos alterar a função ConfigConsole para podermos compatibilizar nosso código com esses dois compiladores.

procedure ConfigConsole;
var
lCursorInfo : TConsoleCursorInfo;
lSecAttributes : TSecurityAttributes;
begin
SetConsoleTitle('DelphiGames Blog | Matrix Effect - Part 2');
Write('Getting handlers... ');

lSecAttributes.nLength:= sizeOf(TSecurityAttributes);
lSecAttributes.lpSecurityDescriptor := nil;
lSecAttributes.bInheritHandle := false;

hBuffer1 := GetStdHandle(STD_OUTPUT_HANDLE);
{$IFDEF FPC}
hBuffer2 := CreateConsoleScreenBuffer( GENERIC_WRITE or GENERIC_READ,
FILE_SHARE_READ,
lSecAttributes,
CONSOLE_TEXTMODE_BUFFER, nil);

{$ELSE}
hBuffer2 := CreateConsoleScreenBuffer( GENERIC_WRITE or GENERIC_READ,
FILE_SHARE_READ,
@lSecAttributes,
CONSOLE_TEXTMODE_BUFFER, nil);
{$ENDIF}
if (hBuffer1 = INVALID_HANDLE_VALUE) or (hBuffer2 = INVALID_HANDLE_VALUE) then
begin
WriteLn('ERROR');
Halt(1);
end
else
begin
WriteLn('OK!');

hBackBuffer := hBuffer1;
lcursorInfo.dwSize := 1;
lcursorInfo.bVisible := False;

SetConsoleCursorInfo(hBuffer1, lcursorInfo);
SetConsoleCursorInfo(hBuffer2, lcursorInfo);

consoleBounds.X:= 80;
consoleBounds.Y:= 25;

SetConsoleScreenBufferSize(hBuffer1, consoleBounds);
SetConsoleScreenBufferSize(hBuffer2, consoleBounds);
end;
end;

Notas Finais

Apesar do resultado do código que construímos neste post ter bem mais interessante que o código anterior, ainda há (sempre há) espaço para várias melhorias. Vou enumerar algumas aqui e deixá-las como proposta de exercícios para o leitor.

Sugestões de melhoria:

  • A velocidade da animação está dependente da velocidade do processador em que está sendo executada. Com poucas modificações no código, é possível adquirir uma taxa de frames fixa. Será que você consegue implementá-la?.
  • Todos os strips descem com a mesma velocidade. Como ficaria o feito se eles caíssem com velocidades diferentes?
  • Que tal exibir a taxa de quadros por segundo em que o programa está operando na barra de títulos do terminal?
  • Há um "slowdown" no início da animação. O que está causando isto? Você consegue resolver?


É isso aí pessoal. Espero que tenham gostado e até o próximo texto.

Abraços.

Links

  • Código fonte
  • Executável


This post first appeared on Computação Gráfica E Jogos Em Pascal, please read the originial post: here

Share the post

Animando o Console - Efeito Matrix Parte II

×

Subscribe to Computação Gráfica E Jogos Em Pascal

Get updates delivered right to your inbox!

Thank you for your subscription

×