Muitos devem lembrar do clássico jogo Flappy Bird que fez sucesso entre os usuários de iOS e Android, mas em pouco tempo foi retirado das app stores oficiais. Posteriormente começaram a surgir diversos clones do jogo, com versões para PC, Web e etc.
Pra quem não conhece o jogo, ele consiste em mover um passarinho entre tubos evitando a colisão. A cada passada pelos tubos o placar é incrementado. Se o passarinho bater nos tubos ou cair no chão o jogo é encerrado. Game Over.

Neste tutorial iremos mostrar como instalar uma versão de Flappy Bird para Arduino e como montar um circuito com botão e display para controlar o jogo.
Lista de Componentes
Para começar a jogar o Flappy Bird com Arduino você vai precisar dos seguintes componentes:
Montagem do Circuito
O Display TFT de 1.8″ tem resolução de 160×128 pixels e é capaz de mostrar 262144 cores (18-bit). Possui também um leitor de cartão SD podendo armazenar imagens. Seu controlador é um ST7735 (datasheet). Para comunicação com o display, usa-se o protocolo SPI utilizando o Hardware SPI do Arduino e alguns pinos adicionais.
O display tem a seguinte pinagem:

A conexão do display ao Arduino deve ser feita da seguinte forma:

Conecte também uma chave Push Button ao pino 2 do Arduino e um resistor pull-up de 1Kohm e o circuito ficará completo da seguinte forma:

Instalando as Bibliotecas Necessárias
Instale as bibliotecas GFX e ST7735 da Adafruit utilizando o library manager. Clique em Sketch -> Incluir Biblioteca -> Gerenciar Bibliotecas…

No campo de busca procure pela biblioteca gfx, selecione a biblioteca Adafruit GFX Library e clique em Instalar.

Procure também pela biblioteca st7735, selecione a biblioteca Adafruit ST7735 Library e clique em Instalar.

Programa Flappy Bird com Arduino
O programa é baseado no programa original encontrado no Github escrito por Themistokle Benetatos. Foi apenas alterado o valor da constante SKIP_TICKS de 20 para 22 para tirar alguns erros quando os canos verdes são desenhados na tela. Experimente alterar outras constantes, assim você pode mudar o tamanho dos canos ou diminuir a força do pulo do passarinho por exemplo (parâmetro JUMP_FORCE).
// =============================================================================
//
// Arduino - Flappy Bird clone
// ---------------------------
// by Themistokle "mrt-prodz" Benetatos
//
// This is a Flappy Bird clone made for the ATMEGA328 and a Sainsmart 1.8" TFT
// screen (ST7735). It features an intro screen, a game over screen containing
// the player score and a similar gameplay from the original game.
//
// Developed and tested with an Arduino UNO and a Sainsmart 1.8" TFT screen.
//
// TODO: - debounce button ?
//
// Dependencies:
// - https://github.com/adafruit/Adafruit-GFX-Library
// - https://github.com/adafruit/Adafruit-ST7735-Library
//
// References:
// - http://www.tweaking4all.com/hardware/arduino/sainsmart-arduino-color-display/
// - http://www.koonsolo.com/news/dewitters-gameloop/
// - http://www.arduino.cc/en/Reference/PortManipulation
//
// --------------------
// http://mrt-prodz.com
// http://github.com/mrt-prodz/ATmega328-Flappy-Bird-Clone
//
// =============================================================================
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <SPI.h>
// initialize Sainsmart 1.8" TFT screen
// (connect pins accordingly or change these values)
#define TFT_DC 9 // Sainsmart RS/DC
#define TFT_RST 8 // Sainsmart RES
#define TFT_CS 10 // Sainsmart CS
// initialize screen with pins
static Adafruit_ST7735 TFT = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);
// instead of using TFT.width() and TFT.height() set constant values
// (we can change the size of the game easily that way)
#define TFTW 128 // screen width
#define TFTH 160 // screen height
#define TFTW2 64 // half screen width
#define TFTH2 80 // half screen height
// game constant
#define SPEED 1
#define GRAVITY 9.8
#define JUMP_FORCE 2.15
#define SKIP_TICKS 22.0 // 1000 / 50fps // foi mudado de 20 para 22, linhas não sobram mais na tela
#define MAX_FRAMESKIP 5
// bird size
#define BIRDW 8 // bird width
#define BIRDH 8 // bird height
#define BIRDW2 4 // half width
#define BIRDH2 4 // half height
// pipe size
#define PIPEW 12 // pipe width
#define GAPHEIGHT 36 // pipe gap height
// floor size
#define FLOORH 20 // floor height (from bottom of the screen)
// grass size
#define GRASSH 4 // grass height (inside floor, starts at floor y)
// background
const unsigned int BCKGRDCOL = TFT.Color565(138,235,244);
// bird
const unsigned int BIRDCOL = TFT.Color565(255,254,174);
// pipe
const unsigned int PIPECOL = TFT.Color565(99,255,78);
// pipe highlight
const unsigned int PIPEHIGHCOL = TFT.Color565(250,255,250);
// pipe seam
const unsigned int PIPESEAMCOL = TFT.Color565(0,0,0);
// floor
const unsigned int FLOORCOL = TFT.Color565(246,240,163);
// grass (col2 is the stripe color)
const unsigned int GRASSCOL = TFT.Color565(141,225,87);
const unsigned int GRASSCOL2 = TFT.Color565(156,239,88);
// bird sprite
// bird sprite colors (Cx name for values to keep the array readable)
#define C0 BCKGRDCOL
#define C1 TFT.Color565(195,165,75)
#define C2 BIRDCOL
#define C3 ST7735_WHITE
#define C4 ST7735_RED
#define C5 TFT.Color565(251,216,114)
static unsigned int birdcol[] =
{ C0, C0, C1, C1, C1, C1, C1, C0,
C0, C1, C2, C2, C2, C1, C3, C1,
C0, C2, C2, C2, C2, C1, C3, C1,
C1, C1, C1, C2, C2, C3, C1, C1,
C1, C2, C2, C2, C2, C2, C4, C4,
C1, C2, C2, C2, C1, C5, C4, C0,
C0, C1, C2, C1, C5, C5, C5, C0,
C0, C0, C1, C5, C5, C5, C0, C0};
// bird structure
static struct BIRD {
unsigned char x, y, old_y;
unsigned int col;
float vel_y;
} bird;
// pipe structure
static struct PIPE {
char x, gap_y;
unsigned int col;
} pipe;
// score
static short score;
// temporary x and y var
static short tmpx, tmpy;
// ---------------
// draw pixel
// ---------------
// faster drawPixel method by inlining calls and using setAddrWindow and pushColor
// using macro to force inlining
#define drawPixel(a, b, c) TFT.setAddrWindow(a, b, a, b); TFT.pushColor(c)
// ---------------
// initial setup
// ---------------
void setup() {
// initialize the push button on pin 2 as an input
DDRD &= ~(1<<PD2);
// initialize a ST7735S chip, black tab
TFT.initR(INITR_BLACKTAB);
}
// ---------------
// main loop
// ---------------
void loop() {
game_start();
game_loop();
game_over();
}
// ---------------
// game loop
// ---------------
void game_loop() {
// ===============
// prepare game variables
// draw floor
// ===============
// instead of calculating the distance of the floor from the screen height each time store it in a variable
unsigned char GAMEH = TFTH - FLOORH;
// draw the floor once, we will not overwrite on this area in-game
// black line
TFT.drawFastHLine(0, GAMEH, TFTW, ST7735_BLACK);
// grass and stripe
TFT.fillRect(0, GAMEH+1, TFTW2, GRASSH, GRASSCOL);
TFT.fillRect(TFTW2, GAMEH+1, TFTW2, GRASSH, GRASSCOL2);
// black line
TFT.drawFastHLine(0, GAMEH+GRASSH, TFTW, ST7735_BLACK);
// mud
TFT.fillRect(0, GAMEH+GRASSH+1, TFTW, FLOORH-GRASSH, FLOORCOL);
// grass x position (for stripe animation)
char grassx = TFTW;
// game loop time variables
double delta, old_time, next_game_tick, current_time;
next_game_tick = current_time = millis();
int loops;
// passed pipe flag to count score
bool passed_pipe = false;
// temp var for setAddrWindow
unsigned char px;
while (1) {
loops = 0;
while( millis() > next_game_tick && loops < MAX_FRAMESKIP) {
// ===============
// input
// ===============
if ( !(PIND & (1<<PD2)) ) {
// if the bird is not too close to the top of the screen apply jump force
if (bird.y > BIRDH2*0.5) bird.vel_y = -JUMP_FORCE;
// else zero velocity
else bird.vel_y = 0;
}
// ===============
// update
// ===============
// calculate delta time
// ---------------
old_time = current_time;
current_time = millis();
delta = (current_time-old_time)/1000;
// bird
// ---------------
bird.vel_y += GRAVITY * delta;
bird.y += bird.vel_y;
// pipe
// ---------------
pipe.x -= SPEED;
// if pipe reached edge of the screen reset its position and gap
if (pipe.x < -PIPEW) {
pipe.x = TFTW;
pipe.gap_y = random(10, GAMEH-(10+GAPHEIGHT));
}
// ---------------
next_game_tick += SKIP_TICKS;
loops++;
}
// ===============
// draw
// ===============
// pipe
// ---------------
// we save cycles if we avoid drawing the pipe when outside the screen
if (pipe.x >= 0 && pipe.x < TFTW) {
// pipe color
TFT.drawFastVLine(pipe.x+3, 0, pipe.gap_y, PIPECOL);
TFT.drawFastVLine(pipe.x+3, pipe.gap_y+GAPHEIGHT+1, GAMEH-(pipe.gap_y+GAPHEIGHT+1), PIPECOL);
// highlight
TFT.drawFastVLine(pipe.x, 0, pipe.gap_y, PIPEHIGHCOL);
TFT.drawFastVLine(pipe.x, pipe.gap_y+GAPHEIGHT+1, GAMEH-(pipe.gap_y+GAPHEIGHT+1), PIPEHIGHCOL);
// bottom and top border of pipe
drawPixel(pipe.x, pipe.gap_y, PIPESEAMCOL);
drawPixel(pipe.x, pipe.gap_y+GAPHEIGHT, PIPESEAMCOL);
// pipe seam
drawPixel(pipe.x, pipe.gap_y-6, PIPESEAMCOL);
drawPixel(pipe.x, pipe.gap_y+GAPHEIGHT+6, PIPESEAMCOL);
drawPixel(pipe.x+3, pipe.gap_y-6, PIPESEAMCOL);
drawPixel(pipe.x+3, pipe.gap_y+GAPHEIGHT+6, PIPESEAMCOL);
}
// erase behind pipe
if (pipe.x <= TFTW) TFT.drawFastVLine(pipe.x+PIPEW, 0, GAMEH, BCKGRDCOL);
// bird
// ---------------
tmpx = BIRDW-1;
do {
px = bird.x+tmpx+BIRDW;
// clear bird at previous position stored in old_y
// we can't just erase the pixels before and after current position
// because of the non-linear bird movement (it would leave 'dirty' pixels)
tmpy = BIRDH - 1;
do {
drawPixel(px, bird.old_y + tmpy, BCKGRDCOL);
} while (tmpy--);
// draw bird sprite at new position
tmpy = BIRDH - 1;
do {
drawPixel(px, bird.y + tmpy, birdcol[tmpx + (tmpy * BIRDW)]);
} while (tmpy--);
} while (tmpx--);
// save position to erase bird on next draw
bird.old_y = bird.y;
// grass stripes
// ---------------
grassx -= SPEED;
if (grassx < 0) grassx = TFTW;
TFT.drawFastVLine( grassx %TFTW, GAMEH+1, GRASSH-1, GRASSCOL);
TFT.drawFastVLine((grassx+64)%TFTW, GAMEH+1, GRASSH-1, GRASSCOL2);
// ===============
// collision
// ===============
// if the bird hit the ground game over
if (bird.y > GAMEH-BIRDH) break;
// checking for bird collision with pipe
if (bird.x+BIRDW >= pipe.x-BIRDW2 && bird.x <= pipe.x+PIPEW-BIRDW) {
// bird entered a pipe, check for collision
if (bird.y < pipe.gap_y || bird.y+BIRDH > pipe.gap_y+GAPHEIGHT) break;
else passed_pipe = true;
}
// if bird has passed the pipe increase score
else if (bird.x > pipe.x+PIPEW-BIRDW && passed_pipe) {
passed_pipe = false;
// erase score with background color
TFT.setTextColor(BCKGRDCOL);
TFT.setCursor( TFTW2, 4);
TFT.print(score);
// set text color back to white for new score
TFT.setTextColor(ST7735_WHITE);
// increase score since we successfully passed a pipe
score++;
}
// update score
// ---------------
TFT.setCursor( TFTW2, 4);
TFT.print(score);
}
// add a small delay to show how the player lost
delay(1200);
}
// ---------------
// game start
// ---------------
void game_start() {
TFT.fillScreen(ST7735_BLACK);
TFT.fillRect(10, TFTH2 - 20, TFTW-20, 1, ST7735_WHITE);
TFT.fillRect(10, TFTH2 + 32, TFTW-20, 1, ST7735_WHITE);
TFT.setTextColor(ST7735_WHITE);
TFT.setTextSize(3);
// half width - num char * char width in pixels
TFT.setCursor( TFTW2-(6*9), TFTH2 - 16);
TFT.println("FLAPPY");
TFT.setTextSize(3);
TFT.setCursor( TFTW2-(6*9), TFTH2 + 8);
TFT.println("-BIRD-");
TFT.setTextSize(0);
TFT.setCursor( 10, TFTH2 - 28);
TFT.println("ATMEGA328");
TFT.setCursor( TFTW2 - (12*3) - 1, TFTH2 + 34);
TFT.println("press button");
while (1) {
// wait for push button
if ( !(PIND & (1<<PD2)) ) break;
}
// init game settings
game_init();
}
// ---------------
// game init
// ---------------
void game_init() {
// clear screen
TFT.fillScreen(BCKGRDCOL);
// reset score
score = 0;
// init bird
bird.x = 20;
bird.y = bird.old_y = TFTH2 - BIRDH;
bird.vel_y = -JUMP_FORCE;
tmpx = tmpy = 0;
// generate new random seed for the pipe gape
randomSeed(analogRead(0));
// init pipe
pipe.x = TFTW;
pipe.gap_y = random(20, TFTH-60);
}
// ---------------
// game over
// ---------------
void game_over() {
TFT.fillScreen(ST7735_BLACK);
TFT.setTextColor(ST7735_WHITE);
TFT.setTextSize(2);
// half width - num char * char width in pixels
TFT.setCursor( TFTW2 - (9*6), TFTH2 - 4);
TFT.println("GAME OVER");
TFT.setTextSize(0);
TFT.setCursor( 10, TFTH2 - 14);
TFT.print("score: ");
TFT.print(score);
TFT.setCursor( TFTW2 - (12*3), TFTH2 + 12);
TFT.println("press button");
while (1) {
// wait for push button
if ( !(PIND & (1<<PD2)) ) break;
}
}
Mais detalhes sobre o projeto original podem ser encontrados no Blog MRT-PRODZ.
Quer aprender ainda mais sobre Arduino? Acesse nossos outros conteúdos aqui :)







