Dissecando o Flappy Bird, parte 1

Após libertá-lo, agora é vez de dissecá-lo — mais ou menos como foi feito com o Retromania! A ideia aqui é (tentar) explicar um pouco do funcionamento das principais rotinas do jogo. Mas como sei muito bem que código em assembly não é das coisas mais simples, agradáveis e legíveis irei me focar na explicação do funcionamento das rotinas (o algoritmo) ao invés de passar instrução por instrução.

capa dissecção
Os caracteres de 128 a 255, o jogo está aqui!

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.

“The pipes, the pipes, are calling”

Como dito na breve entrevista feita pelo Émerson Renato (o BrancoRP) no blog OLD PLAYERS as rotinas que cuidam do desenho e rotação da tela consistem em 70% (da ação) do jogo e é feito pelas rotinas DRAWPIPE, ROTPIPE e UPDATEFB. Uma curiosidade é que (quase) tudo acontece em RAM, não diretamente na VRAM.

O jogo trabalha com três buffers distintos: o primeiro (FRMBUF1) contém uma cópia do que está na tela (na verdade é o contrário, a VRAM é que tem uma cópia do que está nele), o segundo (FRMBUF2) tem o desenho dos canos e o terceiro (FRMBUF3) é usado para coisas menores. Quando tudo fica pronto é hora de atualizar a VRAM.

DRAWPIPE (2º)

FlappyBird_pipes1
caracteres que compõe o cano

Para entender como funciona é necessário saber como eles são desenhados. A imagem ao lado é um padrão de 7×8 caracteres contendo a parte de baixo do cano — tenha em mente que o jogo roda em SCREEN 1, portanto faz mais sentido eu falar caracteres ao invés de pixels.

A primeira coisa a se fazer é desenhar em FRMBUF2 (área de 7×20) a parte comum — equivalendo a um padrão contínuo da 4ª linha (ver PIPEDATA). Isto só produz um cano intransponível que vai de uma ponta a outra da tela.

Aí entra a variável PIPESIZE que indica onde irei “carimbar” o padrão que é justamente o espaço por onde o pássaro passa (ver HOLEDATA) e etsá pronto! Pronto? Não! É preciso jogar este desenho na tela.

UPDATEFB (3º)

No início até pensei em partir para a solução que aparentemente parece mais óbvia,, ou seja, criar um grande buffer de 40×20 e usar uma janela deslizando para copiar apenas a área de 32×20 (a SCREEN 1) mas rapidamente percebi da dificuldade que seria tentar encaixar a rotação da tela com a geração de um novo cano com meus conhecimentos de assembly

No final entendi que esticar a tela só iria ficar postergando o meu problema e não resolvê-lo. Assim a alternativa escolhida foi a mais simples, no caso, mais simples de programar, separando a atualização da tela em duas etapas, a rotação feita pelo UPDATEFB que pode ser escrita em BASIC como algo assim:

(...)
100 FOR J%=2 TO 21:K%=FRMBUF1+32*J%
110 FOR I%=0 TO 30:K$=K%+1
120 POKE K%,PEEK(K%+1)
130 NEXT I%,J%
(...)

E seguida ele copia uma coluna específica em FRMBUF2, de acordo com PIPEFRAM, que é responsabilidade de ROTPIPE. Inclusive fazer a tela se deslocar para o outro lado (da esquerda para a direita), bastaria mexer apenas aqui.

ROTPIPE (1º)

FlappyBird_pipes2
O padrão é copiado em colunas

Este cara é executado antes de UPDATEFB e é o responsável por chamar DRAWPIPE. Mas as coisas ficariam sem sentido se eu não as explicasse antes. Dai, mudei a ordem (o número que deixei entre parênteses). A rotina ROTPIPE é executada antes de UPDATEDB, é ela que se encarrega de atualizar a variável PIPEFRAM (que varia de 0 até 6 e indica uma coluna específica de FRMBUF2, veja a figura).

Na primeira vez que é executada, ela chamará DRAWPIPE e desenhará um novo padrão de cano e colocará PIPEFRAM como 0. Ao ser executada UPDATEFB pegará a coluna 0 para cobrir o espaço liberado pelo deslocamento de FRMBUF1. Na vez seguinte mudará PIPEFRAM para 1 e UPDATEDB pegará esta coluna. E depois para 2 e assim até chegar na coluna 6. Aí a rotina se comportará de forma diferente e ficará insistindo para UPDATEDB repetir a 6ª coluna até que as repetições alcancem o número indicado em PIPEGAP. Daí um novo cano será sorteado e tudo começa novamente.

Esta mesma estratégia pode ser utilizada para a geração de cenários aleatórios, ou mesmo para produzir cenários mais complexos (“montanha”, “escada”, “gramado”, “rio” etc) em mapas que guardam valores como 1, 2, 3, 4, etc ao invés de padrões completos e que consomem mais memória.

Primeiro epílogo

Por enquanto acabou, na semana que vem tem mais.

0 pensou em “Dissecando o Flappy Bird, parte 1

  1. Hey, excelente artigo, adorei! Por acaso voce nao tem uma versao em Ingles dele, tem? Eu adoraria reproduzi-lo em Vintage Is The New Old, com sua permissao e’ claro. Se voce quiser, posso tambem tentar uma traducao macarronica eu mesmo.

      1. Combinado. Qual o melhor jeito de te passar o texto? Se quiser, estou online no IRC server irc.abime.net #vitno

      2. Hey, I escrevi a primeira versao traduzida. O texto esta’ em google docs, entao talvez eu poderia compartilhar o texto pra facilitar revisoes e mudancas. Fico no aguardo!

  2. fantástico!!! adoro estes artigos, estimulam a gente a criar coisas novas nos ensinando COMO pode ser feito… acho que já passou pela cabeça de muitos a idéia de fazer jogos, mas para quem nunca desenvolveu algo assim acaba sendo intimidador….
    parabéns!