Construa um Clone do Flappy Bird com Arduino: Projeto de Jogo Passo a Passo com Tela Touch TFT
Neste Projeto Arduino, faremos um jogo legal com Arduino, na verdade uma réplica do popular jogo Flappy Bird para smartphones, usando um Arduino e uma Tela Touch TFT. Você pode aprender como funciona assistindo ao vídeo a seguir ou lendo o texto escrito abaixo.
Visão Geral
O jogo é bastante simples, mas interessante e viciante. Usando a tela touch, controlamos o pássaro e tentamos evitar os pilares móveis que aumentam a velocidade à medida que progredimos. Além disso, o jogo pode armazenar sua maior pontuação mesmo se você desconectar a alimentação.

No tutorial anterior (Tutorial Arduino TFT), aprendemos em detalhes como usar Telas Touch TFT com um Arduino e deixamos o exemplo do jogo para ser explicado neste tutorial. Então agora, assim como no tutorial anterior, explicaremos passo a passo o código por trás deste Jogo Arduino.
Código Fonte
Como o código é um pouco longo e para melhor compreensão, postarei o código fonte do programa em seções com descrição para cada seção. E no final deste artigo, postarei o código fonte completo.
Usaremos as bibliotecas UTFT e URTouch feitas por Henning Karlsen. Você pode baixar estas bibliotecas do site dele, www.RinkyDinkElectronics.com. Também usaremos a biblioteca EEPROM para armazenar a maior pontuação na EEPROM. A EEPROM é uma memória que pode armazenar dados mesmo quando a placa está desligada.
Após incluirmos as bibliotecas, precisamos criar os objetos UTFT e URTouch, bem como definir as variáveis necessárias para o jogo. Na seção setup, precisamos iniciar o display e o touch, ler a maior pontuação da EEPROM e iniciar o jogo usando a função personalizada initiateGame().
#include <UTFT.h>
#include <URTouch.h>
#include <EEPROM.h>
//==== Criando Objetos
UTFT myGLCD(SSD1289,38,39,40,41); //Parâmetros devem ser ajustados ao seu modelo de Display/Shield
URTouch myTouch( 6, 5, 4, 3, 2);
//==== Definindo Fontes
extern uint8_t SmallFont[];
extern uint8_t BigFont[];
extern uint8_t SevenSegNumFont[];
extern unsigned int bird01[0x41A]; // Bitmap do Pássaro
int x, y; // Variáveis para as coordenadas onde o display foi pressionado
// Floppy Bird
int xP = 319;
int yP = 100;
int yB = 50;
int movingRate = 3;
int fallRateInt = 0;
float fallRate = 0;
int score = 0;
int lastSpeedUpScore = 0;
int highestScore;
boolean screenPressed = false;
boolean gameStarted = false;
void setup() {
// Iniciar display
myGLCD.InitLCD();
myGLCD.clrScr();
myTouch.InitTouch();
myTouch.setPrecision(PREC_MEDIUM);
highestScore = EEPROM.read(0); // Ler a maior pontuação da EEPROM
initiateGame(); // Iniciar o jogo
}
Então, com a função personalizada initiateGame(), desenharemos o estado inicial do jogo e aqui está como faremos isso. Primeiro, precisamos limpar a tela, depois desenhar o fundo azul, desenhar a seção inferior, adicionar o texto e chamar a função personalizada drawBird() para desenhar o pássaro. Após isso, precisamos de um loop while que impedirá o jogo de começar até tocarmos a tela. Então, enquanto estivermos neste estado, se pressionarmos o canto superior direito, podemos redefinir a maior pontuação para zero e, se pressionarmos em qualquer outro lugar na tela, sairemos do loop while e entraremos no loop principal do código que iniciará o jogo.

// ===== initiateGame - Função Personalizada
void initiateGame() {
myGLCD.clrScr();
// Fundo azul
myGLCD.setColor(114, 198, 206);
myGLCD.fillRect(0,0,319,239);
// Chão
myGLCD.setColor(221,216,148);
myGLCD.fillRect(0, 215, 319, 239);
myGLCD.setColor(47,175,68);
myGLCD.fillRect(0, 205, 319, 214);
// Texto
myGLCD.setColor(0, 0, 0);
myGLCD.setBackColor(221, 216, 148);
myGLCD.setFont(BigFont);
myGLCD.print("Score:",5,220);
myGLCD.setFont(SmallFont);
myGLCD.print("HowToMechatronics.com", 140, 220);
myGLCD.setColor(0, 0, 0);
myGLCD.setBackColor(114, 198, 206);
myGLCD.print("Highest Score: ",5,5);
myGLCD.printNumI(highestScore, 120, 6);
myGLCD.print(">RESET<",255,5);
myGLCD.drawLine(0,23,319,23);
myGLCD.print("TAP TO START",CENTER,100);
drawBird(yB); // Desenha o pássaro
// Espera até tocarmos a tela
while (!gameStarted) {
if (myTouch.dataAvailable()) {
myTouch.read();
x=myTouch.getX();
y=myTouch.getY();
// Resetar maior pontuação
if ((x>=250) && (x<=319) &&(y>=0) && (y<=28)) {
highestScore = 0;
myGLCD.setColor(114, 198, 206);
myGLCD.fillRect(120, 0, 150, 22);
myGLCD.setColor(0, 0, 0);
myGLCD.printNumI(highestScore, 120, 5);
}
if ((x>=0) && (x<=319) &&(y>=30) && (y<=239)) {
gameStarted = true;
myGLCD.setColor(114, 198, 206);
myGLCD.fillRect(0, 0, 319, 32);
}
}
}
// Limpa o texto "TAP TO START" antes do jogo começar
myGLCD.setColor(114, 198, 206);
myGLCD.fillRect(85, 100, 235, 116);
}
Na seção loop principal, temos a variável xP que é usada para desenhar os pilares, bem como a variável yP. No início, a variável xP tem o valor 319 como o tamanho da tela e a variável yP tem o valor 100 que é a altura do primeiro pilar. A cada iteração, o valor da variável xP é diminuído pelo valor da variável movingRate que, no início, tem o valor 3 e, à medida que progredimos no jogo, aumenta.
// A Seção Loop Principal
void loop() {
xP=xP-movingRate; // xP - coordenada x dos pilares; intervalo: 319 - (-51)
drawPilars(xP, yP); // Desenha os pilares
// yB - coordenada y do pássaro que depende do valor da variável fallingRate
yB+=fallRateInt;
fallRate=fallRate+0.4; // A cada iteração a taxa de queda aumenta para que possamos ter o efeito de aceleração/gravidade
fallRateInt= int(fallRate);
// Verifica colisão
if(yB>=180 || yB<=0){ // topo e fundo
gameOver();
}
if((xP<=85) && (xP>=5) && (yB<=yP-2)){ // pilar superior
gameOver();
}
if((xP<=85) && (xP>=5) && (yB>=yP+60)){ // pilar inferior
gameOver();
}
// Desenha o pássaro
drawBird(yB);
// Após o pilar ter passado pela tela
if (xP<=-51){
xP=319; // Reseta xP para 319
yP = rand() % 100+20; // Número aleatório para a altura dos pilares
score++; // Aumenta a pontuação em um
}
//==== Controlando o pássaro
if (myTouch.dataAvailable()&& !screenPressed) {
fallRate=-6; // Definir fallRate como negativo fará o pássaro pular
screenPressed = true;
}
// Não permite segurar a tela / você deve tocá-la
else if ( !myTouch.dataAvailable() && screenPressed){
screenPressed = false;
}
// A cada cinco pontos, aumenta a taxa de movimento dos pilares
if ((score - lastSpeedUpScore) == 5) {
lastSpeedUpScore = score;
movingRate++;
}
}
Aqui está o princípio de funcionamento do jogo: temos pilares de 50 pixels de largura que se movem da direita para a esquerda e cada próximo pilar tem uma altura aleatória diferente. Para fazê-los mover, logicamente, após cada iteração, precisaríamos limpar a tela e redesenhar o gráfico com os pilares em sua nova posição. No entanto, não podemos fazer isso devido à baixa taxa de atualização da tela, o que causaria cintilação dos gráficos. Para ativar todos os seus pixels, a tela precisa de um pouco mais de tempo, então teremos que improvisar e redesenhar apenas aquelas coisas que estão se movendo.

Vamos dar uma olhada em como a função personalizada drawPilars() fará isso. Ela recebe as variáveis xP e yP e, usando-as e a função fillRect(), desenhará os pilares. Então, a cada iteração, ela desenha os pilares em sua nova localização com retângulos azuis adicionais de seus lados esquerdo e direito que apagam o pilar desenhado anteriormente e, dessa forma, realmente fazemos a improvisação de apenas redesenhar os pilares móveis. As declarações if aqui são uma improvisação adicional porque, por algum motivo, a função fillRect() não funcionava se seu parâmetro 'x2' tivesse um valor fora do tamanho da tela. Além disso, no final desta função personalizada, precisamos imprimir o valor da pontuação alcançada.
// ===== drawPilars - Função Personalizada
void drawPilars(int x, int y) {
if (x>=270){
myGLCD.setColor(0, 200, 20);
myGLCD.fillRect(318, 0, x, y-1);
myGLCD.setColor(0, 0, 0);
myGLCD.drawRect(319, 0, x-1, y);
myGLCD.setColor(0, 200, 20);
myGLCD.fillRect(318, y+81, x, 203);
myGLCD.setColor(0, 0, 0);
myGLCD.drawRect(319, y+80, x-1, 204);
} else if( x<=268) {
// Desenha retângulo azul à direita do pilar
myGLCD.setColor(114, 198, 206);
myGLCD.fillRect(x+51, 0, x+60, y);
// Desenha o pilar
myGLCD.setColor(0, 200, 20);
myGLCD.fillRect(x+49, 1, x+1, y-1);
// Desenha a moldura preta do pilar
myGLCD.setColor(0, 0, 0);
myGLCD.drawRect(x+50, 0, x, y);
// Desenha o retângulo azul à esquerda do pilar
myGLCD.setColor(114, 198, 206);
myGLCD.fillRect(x-1, 0, x-3, y);
// O pilar inferior
myGLCD.setColor(114, 198, 206);
myGLCD.fillRect(x+51, y+80, x+60, 204);
myGLCD.setColor(0, 200, 20);
myGLCD.fillRect(x+49, y+81, x+1, 203);
myGLCD.setColor(0, 0, 0);
myGLCD.drawRect(x+50, y+80, x, 204);
myGLCD.setColor(114, 198, 206);
myGLCD.fillRect(x-1, y+80, x-3, 204);
}
// Desenha a pontuação
myGLCD.setColor(0, 0, 0);
myGLCD.setBackColor(221, 216, 148);
myGLCD.setFont(BigFont);
myGLCD.printNumI(score, 100, 220);
}
De volta à seção loop, temos a variável yB que é a posição y do pássaro e depende da taxa de queda que, após cada iteração, é aumentada e, dessa forma, obtemos o efeito de aceleração ou gravidade. Também aqui verificamos colisões e usamos as declarações if para confinar o pássaro para que, se ele atingir o topo, o chão ou os pilares, o jogo acaba.

Em seguida, está a função personalizada drawBird() e vamos ver como ela funciona. O pássaro é na verdade uma foto que é convertida em um bitmap usando a ferramenta ImageConverter565 feita por Henning Karlsen. O arquivo ".c" criado usando a ferramenta precisa ser incluído no diretório para que carregue ao iniciar o sketch. Também temos que definir o bitmap assim e, usando a função drawBitmap(), desenharemos a foto na tela. O pássaro tem uma coordenada X fixa e a variável yB como coordenada Y. Semelhante aos pilares, apagaremos o estado anterior do pássaro desenhando dois retângulos azuis acima e abaixo do pássaro.
//====== drawBird() - Função Personalizada
void drawBird(int y) {
// Desenha o pássaro - bitmap
myGLCD.drawBitmap (50, y, 35, 30, bird01);
// Desenha retângulos azuis acima e abaixo do pássaro para limpar seu estado anterior
myGLCD.setColor(114, 198, 206);
myGLCD.fillRoundRect(50,y,85,y-6);
myGLCD.fillRoundRect(50,y+30,85,y+36);
}
De volta ao loop, podemos ver que, após o pilar ter passado pela tela, a variável xP será resetada para 319, yP obterá um novo valor aleatório de 20 a 100 para a altura dos pilares e a pontuação aumentará em um. Com a próxima declaração if, controlamos o pássaro. Se tocarmos a tela, definiremos a taxa de queda como negativa, o que fará o pássaro pular e a declaração else if não permitirá que isso aconteça se apenas segurarmos a tela. A última declaração if é para a dificuldade do jogo e aumenta a taxa de movimento dos pilares a cada cinco pontos.
Ok, o que resta agora é ver como a função personalizada gameOver() funciona. Após um atraso de um segundo, ela limpará a tela, imprimirá a pontuação e algum texto, se a pontuação for maior que a maior pontuação, a gravará na EEPROM, resetará todas as variáveis para seus valores de posição inicial e, no final, chamará a função personalizada initiateGame() para reiniciar o jogo.

//======== gameOver() - Função Personalizada
void gameOver() {
delay(1000); // 1 segundo
// Limpa a tela e imprime o texto
myGLCD.clrScr();
myGLCD.setColor(255, 255, 255);
myGLCD.setBackColor(0, 0, 0);
myGLCD.setFont(BigFont);
myGLCD.print("GAME OVER", CENTER, 40);
myGLCD.print("Score:", 100, 80);
myGLCD.printNumI(score,200, 80);
myGLCD.print("Restarting...", CENTER, 120);
myGLCD.setFont(SevenSegNumFont);
myGLCD.printNumI(2,CENTER, 150);
delay(1000);
myGLCD.printNumI(1,CENTER, 150);
delay(1000);
// Grava a maior pontuação na EEPROM
if (score > highestScore) {
highestScore = score;
EEPROM.write(0,highestScore);
}
// Reseta as variáveis para valores de posição inicial
xP=319;
yB=50;
fallRate=0;
score = 0;
lastSpeedUpScore = 0;
movingRate = 3;
gameStarted = false;
// Reiniciar jogo
initiateGame();
}
É isso e espero que a explicação do código tenha sido clara o suficiente. Se você tiver alguma dúvida, sinta-se à vontade para perguntar na seção de comentários abaixo.
Aqui está o código completo do jogo:
/* Projeto Jogo Arduino
* Programa feito por Dejan Nedelkovski,
* www.HowToMechatronics.com
*/
/* Este programa usa as bibliotecas UTFT e URTouch
* feitas por Henning Karlsen.
* Você pode encontrá-las e baixá-las em:
* www.RinkyDinkElectronics.com
*/
#include <UTFT.h>
#include <URTouch.h>
#include <EEPROM.h>
//==== Criando Objetos
UTFT myGLCD(SSD1289,38,39,40,41); //Parâmetros devem ser ajustados ao seu modelo de Display/Shield
URTouch myTouch( 6, 5, 4, 3, 2);
//==== Definindo Fontes
extern uint8_t SmallFont[];
extern uint8_t BigFont[];
extern uint8_t SevenSegNumFont[];
extern unsigned int bird01[0x41A]; // Bitmap do Pássaro
int x, y; // Variáveis para as coordenadas onde o display foi pressionado
// Floppy Bird
int xP = 319;
int yP = 100;
int yB = 50;
int movingRate = 3;
int fallRateInt = 0;
float fallRate = 0;
int score = 0;
int lastSpeedUpScore = 0;
int highestScore;
boolean screenPressed = false;
boolean gameStarted = false;
void setup() {
// Iniciar display
myGLCD.InitLCD();
myGLCD.clrScr();
myTouch.InitTouch();
myTouch.setPrecision(PREC_MEDIUM);
highestScore = EEPROM.read(0); // Ler a maior pontuação da EEPROM
initiateGame(); // Iniciar o jogo
}
void loop() {
xP=xP-movingRate; // xP - coordenada x dos pilares; intervalo: 319 - (-51)
drawPilars(xP, yP); // Desenha os pilares
// yB - coordenada y do pássaro que depende do valor da variável fallingRate
yB+=fallRateInt;
fallRate=fallRate+0.4; // A cada iteração a taxa de queda aumenta para que possamos ter o efeito de aceleração/gravidade
fallRateInt= int(fallRate);
// Verifica colisão
if(yB>=180 || yB<=0){ // topo e fundo
gameOver();
}
if((xP<=85) && (xP>=5) && (yB<=yP-2)){ // pilar superior
gameOver();
}
if((xP<=85) && (xP>=5) && (yB>=yP+60)){ // pilar inferior
gameOver();
}
// Desenha o pássaro
drawBird(yB);
// Após o pilar ter passado pela tela
if (xP<=-51){
xP=319; // Reseta xP para 319
yP = rand() % 100+20; // Número aleatório para a altura dos pilares
score++; // Aumenta a pontuação em um
}
//==== Controlando o pássaro
if (myTouch.dataAvailable()&& !screenPressed) {
fallRate=-6; // Definir fallRate como negativo fará o pássaro pular
screenPressed = true;
}
// Não permite segurar a tela / você deve tocá-la
else if ( !myTouch.dataAvailable() && screenPressed){
screenPressed = false;
}
// A cada cinco pontos, aumenta a taxa de movimento dos pilares
if ((score - lastSpeedUpScore) == 5) {
lastSpeedUpScore = score;
movingRate++;
}
}
// ===== initiateGame - Função Personalizada
void initiateGame() {
myGLCD.clrScr();
// Fundo azul
myGLCD.setColor(114, 198, 206);
myGLCD.fillRect(0,0,319,239);
// Chão
myGLCD.setColor(221,216,148);
myGLCD.fillRect(0, 215, 319, 239);
myGLCD.setColor(47,175,68);
myGLCD.fillRect(0, 205, 319, 214);
// Texto
myGLCD.setColor(0, 0, 0);
myGLCD.setBackColor(221, 216, 148);
myGLCD.setFont(BigFont);
myGLCD.print("Score:",5,220);
myGLCD.setFont(SmallFont);
myGLCD.print("HowToMechatronics.com", 140, 220);
myGLCD.setColor(0, 0, 0);
myGLCD.setBackColor(114, 198, 206);
myGLCD.print("Highest Score: ",5,5);
myGLCD.printNumI(highestScore, 120, 6);
myGLCD.print(">RESET<",255,5);
myGLCD.drawLine(0,23,319,23);
myGLCD.print("TAP TO START",CENTER,100);
drawBird(yB); // Desenha o pássaro
// Espera até tocarmos a tela
while (!gameStarted) {
if (myTouch.dataAvailable()) {
myTouch.read();
x=myTouch.getX();
y=myTouch.getY();
// Resetar maior pontuação
if ((x>=250) && (x<=319) &&(y>=0) && (y<=28)) {
highestScore = 0;
myGLCD.setColor(114, 198, 206);
myGLCD.fillRect(120, 0, 150, 22);
myGLCD.setColor(0, 0, 0);
myGLCD.printNumI(highestScore, 120, 5);
}
if ((x>=0) && (x<=319) &&(y>=30) && (y<=239)) {
gameStarted = true;
myGLCD.setColor(114, 198, 206);
myGLCD.fillRect(0, 0, 319, 32);
}
}
}
// Limpa o texto "TAP TO START" antes do jogo começar
myGLCD.setColor(114, 198, 206);
myGLCD.fillRect(85, 100, 235, 116);
}
// ===== drawPilars - Função Personalizada
void drawPilars(int x, int y) {
if (x>=270){
myGLCD.setColor(0, 200, 20);
myGLCD.fillRect(318, 0, x, y-1);
myGLCD.setColor(0, 0, 0);
myGLCD.drawRect(319, 0, x-1, y);
myGLCD.setColor(0, 200, 20);
myGLCD.fillRect(318, y+81, x, 203);
myGLCD.setColor(0, 0, 0);
myGLCD.drawRect(319, y+80, x-1, 204);
} else if( x<=268) {
// Desenha retângulo azul à direita do pilar
myGLCD.setColor(114, 198, 206);
myGLCD.fillRect(x+51, 0, x+60, y);
// Desenha o pilar
myGLCD.setColor(0, 200, 20);
myGLCD.fillRect(x+49, 1, x+1, y-1);
// Desenha a moldura preta do pilar
myGLCD.setColor(0, 0, 0);
myGLCD.drawRect(x+50, 0, x, y);
// Desenha o retângulo azul à esquerda do pilar
myGLCD.setColor(114, 198, 206);
myGLCD.fillRect(x-1, 0, x-3, y);
// O pilar inferior
myGLCD.setColor(114, 198, 206);
myGLCD.fillRect(x+51, y+80, x+60, 204);
myGLCD.setColor(0, 200, 20);
myGLCD.fillRect(x+49, y+81, x+1, 203);
myGLCD.setColor(0, 0, 0);
myGLCD.drawRect(x+50, y+80, x, 204);
myGLCD.setColor(114, 198, 206);
myGLCD.fillRect(x-1, y+80, x-3, 204);
}
// Desenha a pontuação
myGLCD.setColor(0, 0, 0);
myGLCD.setBackColor(221, 216, 148);
myGLCD.setFont(BigFont);
myGLCD.printNumI(score, 100, 220);
}
//====== drawBird() - Função Personalizada
void drawBird(int y) {
// Desenha o pássaro - bitmap
myGLCD.drawBitmap (50, y, 35, 30, bird01);
// Desenha retângulos azuis acima e abaixo do pássaro para limpar seu estado anterior
myGLCD.setColor(114, 198, 206);
myGLCD.fillRoundRect(50,y,85,y-6);
myGLCD.fillRoundRect(50,y+30,85,y+36);
}
//======== gameOver() - Função Personalizada
void gameOver() {
delay(3000); // 1 segundo
// Limpa a tela e imprime o texto
myGLCD.clrScr();
myGLCD.setColor(255, 255, 255);
myGLCD.setBackColor(0, 0, 0);
myGLCD.setFont(BigFont);
myGLCD.print("GAME OVER", CENTER, 40);
myGLCD.print("Score:", 100, 80);
myGLCD.printNumI(score,200, 80);
myGLCD.print("Restarting...", CENTER, 120);
myGLCD.setFont(SevenSegNumFont);
myGLCD.printNumI(2,CENTER, 150);
delay(1000);
myGLCD.printNumI(1,CENTER, 150);
delay(1000);
// Grava a maior pontuação na EEPROM
if (score > highestScore) {
highestScore = score;
EEPROM.write(0,highestScore);
}
// Reseta as variáveis para valores de posição inicial
xP=319;
yB=50;
fallRate=0;
score = 0;
lastSpeedUpScore = 0;
movingRate = 3;
gameStarted = false;
// Reiniciar jogo
initiateGame();
}
Aqui está um arquivo para download com o sketch do Arduino, a imagem do pássaro e o arquivo bitmap do pássaro.
Processo de manufatura
- Crie um Sistema Ambilight Impressionante para Monitor com Arduino
- Construa o Clássico Jogo Pong no Arduino UNO com Display OLED – Tutorial Passo a Passo
- Controle USB para Jogos DIY com Arduino – Construa Seu Próprio Gamepad de Alto Desempenho
- Jogo Breakout Touch com Arduino: Arcade Clássico em LCD TFT 2,4″
- Construa um Jogo Operação Animatrônico de Minifigura Lego Gigante com Arduino
- Detector de Frequência de Áudio com Arduino – Meça Picos de Som com Módulo de Alta Sensibilidade
- Jogo da Velha com Arduino, Matriz LED MAX7219 e Caixa de Papelão
- Construa um Temporizador Regressivo Confiável com Arduino e Display de 7 Segmentos SparkFun
- Pixel Chaser: Jogo LED Interativo de Um Toque com Arduino Nano
- Construa Seu Próprio Carro RC Porsche com Arduino: Guia Passo a Passo