Dissecando o Flappy Bird, parte 2

E aqui voltamos para a segunda parte da dissecção do passarinho retardado que decidiu dar uma revoada por algum mundo perdido onde mora aquele encanador italiano daquela empresa japonesa. Nesta parte temos o desenho do pássaro, e a coisa mais importante em um jogo, a temporização.

capa dissecção

Não custa lembrar que o código fonte do jogo está disponível no GitHub, e sendo assim não irei replicá-lo, apenas citá-lo aqui.

“Little birdie why do you fly upside down?”

FlappyBird_birdies
pássaros decompostos

De todos os gráficos do jogo, o pássaro foi a parte mais simples do jogo já que o desenho foi copiado descaradamente do jogo original. Ou quase copiado, já que precisei adaptar alguns detalhes com conta das limitações que os sprites tem de tamanho (16×16 e não reclame) e cor (monocromáticos mas empilhando até 4 eu conseguiria algumas cores). A rotina que cuida do desenho é a DRAWBIRD (criativo, não?) e ela cuida de duas coisas.

DRAWBIRD

O que ele faz é pegar o número do quadro do pássaro, multiplicar por 4 e daí fazer um equivalente a quatro “PUT SPRITE…” seguidos. Duas outras coisas são também realizadas: a primeira é corrigir um bug besta no VDP da Texas Instruments. Você posiciona um sprite na linha N e ele sempre o desenhará em N+1. Duvida?

10 COLOR 15,0,0:SCREEN 2,2:SPRITE$(0)=STRING$(32,255)
20 LINE (128,32)-STEP(15,0),8:K$=INPUT$(1)
30 PUT SPRITE 0,(128,16),1,0' E CADE A LINHA VERMELHA?
40 GOTO 40

A outra coisa que faz é atualizar automaticamente a variável BIRDFRAM, assim a cada nova execução da rotina ela desenhará um quadro diferente da animação. Aliás este é o motivo da animação ter quatro quadros ao invés dos três do jogo original — preferi simplificar a programação já que eu podia desperdiçar VRAM cin padrões de sprites.

A BIRDFRAM é inicializada no começo do jogo e eu simplesmente vou esquecer que ela existe (sempre que chega en 3 ela volta para 0)! Obviamente a única exceção disto no jogo está na rotina GAMEOVER onde eu forço constantemente BIRDFRAM com o valor 4 (ou seja, o pássaro caindo).

“Where do I start; Where do I begin”

Sincronização é uma coisa importante nos jogos, principalmente os de ação que precisam de um processamento fluído, caso contrário a jogabilidade vai para o nível do insuportável. Sendo assim eu preciso fazer com que cada laço do jogo seja executado na mesma quantidade de tempo para que a sensação seja de fluidez seja convincente. Basicamente é algo que eu usei e expliquei na segunda parte do desmonte do Retromania (mais precisamente na linha 190).

A lógica é a mesma, executar o laço e esperar dar o tempo necessário. Mas acontece que defini que não faria versões distintas do jogo para modelos de MSX europeus ou japoneses (sim, havia um escopo de projeto na minha cabeça). O jogo deveria sozinho dar seu jeito de resolver o problema. E como resolvê-lo? Simples, o MSX foi projetado para ser ligado em aparelhos e T, portanto há uma necessidade do Z80 e o VDP viverem em harmonia.

De forma BEM resumida o MSX gera uma interrupção para o Z80 a cada ciclo do VDP e estes ciclos estão ligados com frequência de operação do dispositivo de vídeo do sistema, a TV. Os modelos brasileiros, coreanos e japoneses operam a 60Hz enquanto que os demais a 50Hz. Daí basta fazer as contas corretamente para saber que a cada segundo o VDP do HOTBIT HB-8000 produz 60 interrupções enquanto que o do PHILIPS VG-8020 apenas 50. E a BIOS do MSX já faz a tarefa para você, faça um teste:

TIME=0:FOR I=0 TO 9999:NEXT I:PRINT TIME

No exemplo acima um computador rodando a 60Hz imprimirá 95 enquanto que o outro, a 50Hz, informará 115! Aliás está aqui a razão pela qual os jogos japoneses sempre rodaram de forma arrastada para os europeus e sobre o quanto achávamos a música dos jogos europeus vindos do ZX Spectrum estranhamente aceleradas (se bem que eu prefiro a trilha do Astro Marine Corps em 60Hz!).

INITENV

Lembram que no 1º grau aprendemos sobre uma coisa chamada máximo divisor comum? Então este negócio, se aplica aqui! O maior número que posso usar para dividir 50Hz e 60Hz e continuar obtendo um número inteiro é 10! Assim eu tenho para modelos PAL o valor 5 e para os NTSC o 6 representando o mesmo intervalo de tempo 0,1s (sim, você pode pensar assim, o jogo inteiro roda a 10fps).

E para facilitar a minha vida eu trabalho com duas constantes chamadas (quase erroneamente) de VDPCICLE1 e VDPCICLE5, a primeira contendo o valor 5 (ou 6) e a segunda 50 (ou 60) para indicar, respectivamente, 0,1s e 1s e assim temporizar todos os meus laços e pausas (olhando o código fonte você verá um uso extensivo delas, até mesmo há uma rotina chamada WAITASEC, específica para fazer o MSX esperar uns instantes).

A detecção sobre quais valores utilizar está na função INITENV, a ROM do jogo vem pronto para NTSC e lá eu me encarrego de alterar os valores caso tenha descoberto que o código roda em uma máquina PAL (a informação está no 8º bit do byte 0x002B da BIOS do MSX).

GAME

O jogo propriamente dito está na rotina GAME, ele é razoavelmente simples de ser explicada mas precisava de toda a teoria acima como prólogo:

;
; * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
; *
; * o jogo propriamente dito está aqui (ela é quase críptica!)
; *
; * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
;
GAME:  call SNDBEEP ; emito um beep
       call WRITEFB
       (...)
GAME0: ld hl,JIFFY
       ld (hl),0 ; zero o temporizador
       (...)
       ld hl,VDPCICLE1
       ld b,(hl)
       call WAITASEC ; aguardo 1/10s
       jp GAME0

Eu zero o contador de tempo (variável de ambiente JIFFY, que equivale ao TIME do MSX-BASIC), executo o que preciso ser executado referente ao jogo e depois espero completar 1/10s, daí começo tudo novamente.

Segundo epílogo

E aqui se encerra este pacote de dicas, creio que deixei o melhor para o final 🙂