Saída da 2ª animação em modo texto. Bem mais interessante. |
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 seguirprocedure 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 eixox
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:
É isso aí pessoal. Espero que tenham gostado e até o próximo texto.
Abraços.
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.