Jogo do Dinossauro do Chrome no Arduino

E aí, gente! Tudo certo? No post de hoje vamos fazer um projeto bem simples e bem interessante no Arduino: O JOGO DO DINOSSAURO DO GOOGLE CHROME!

É isso mesmo que vocês leram! E não, não é uma automação do jogo utilizando o Arduino. Nós iremos criar nós mesmos o jogo do dinossauro do chrome no Arduino!

Demonstração do jogo. Fonte: Autoria Própria (2021)

Para a criação desse projeto, nós precisaremos de:

  • 1 Arduino
  • 1 Display LCD 16x2
  • 1 Push Button
  • 1 Potenciômetro de 10kΩ
  • 1 Resistor de 10kΩ
  • 1 Resistor de 330Ω
  • 1 Buzzer (opcional)
  • Vários Jumpers
Para realizarmos a montagem, vamos utilizar o seguinte esquemático:
Esquema da montagem do jogo do chrome. Fonte: Autoria Própria (2021)

Para facilitar a visualização das conexões, vou deixar uma tabela aqui embaixo onde vocês poderão encontrar onde cada pino deverá ser ligado.

Pino - Componente Função Conectar em
1 - LCD Vss (alimentação negativa) GND
2 - LCD Vdd (alimentação positiva) VCC/5V
3 - LCD V0 (contraste) Pino central do potenciômetro
4 - LCD RS (seleção de comandos ou dados) Pino 12 do Arduino
5 - LCD RW (leitura ou escrita) GND
6 - LCD E (pino de ativação) Pino 11 do Arduino
7 - LCD DB0 (data bit 0) -
8 - LCD DB1 (data bit 1) -
9 - LCD DB2 (data bit 2) -
10 - LCD DB3 (data bit 3) -
11 - LCD DB4 (data bit 4) Pino 5 do Arduino
12 - LCD DB5 (data bit 5) Pino 4 do Arduino
13 - LCD DB6 (data bit 6) Pino 3 do Arduino
14 - LCD DB7 (data bit 7) Pino 2 do Arduino
15 - LCD A (ânodo do backlight) VCC com resistor de 330Ohm
16 - LCD K (cátodo do backlight) GND
Pino da Esquerda - Potenciômetro Alimentação negativa GND
Pino Central - Potenciômetro Controle de Contraste Pino 4 do LCD
Pino da Direita - Potenciômetro Alimentação positiva VCC
Pino 1 - Push Button* Alimentação positiva VCC
Pino 2 - Push Button* Envio do sinal Pino 9 do Arduino e GND com resistor de 10kOhm
Positivo - Buzzer Sinal de ativação e controle do som Pino 8 do Arduino
Negativo - Buzzer Alimentação negativa GND

*Perceba que, no push button, existem 4 pinos, porém quando se olha para eles em pares, você pode perceber que dois pinos estão mais pertos um do outro esses são os pinos 1 e 2, independente da ordem, pois os pinos que estão mais distantes são uma extensão desses pinos. Veja a imagem abaixo para entender melhor

Indicação dos pinos do push button. Fonte: Autoria Própria (2021)

Os pinos de DB4 a DB7 são utilizados tanto nas interfaces de 4 bits, que é a que está sendo utilizada, quanto nas de 8 bits, porém, nesta última configuração, todos os pinos de data bit são utilizados. Os pinos 15 e 16 do LCD estão relacionados com a alimentação do backlight, isto é, a luz de fundo, e o ânodo foi colocado com um resistor para limitar a corrente que passa na luz de fundo. 

Lembrando que o buzzer é completamente opcional, mas é um toque a mais bem interessante.

O código ficou um pouco grande, porém é fácil de entender! Vamos dar uma olhada:

#include <LiquidCrystal.h>
#define JUMP              9
#define BUZZER            8

#define MAX_JUMP_TIME     1200

#define POINTS_SPEED2     400
#define POINTS_SPEED3     900
#define POINTS_SPEED4     1500

#define CACTUS_GEN_CHANCE_THRESHOLD   60
#define CACTUS_GEN_TIME               800
#define CACTUS_STYLE                  3

LiquidCrystal lcd(12, 11, 5, 4, 3, 2); // RS, E, DB4, DB5, DB6, DB7

byte dino[] = {
  B01110,
  B01011,
  B01111,
  B00110,
  B10111,
  B11110,
  B01110,
  B01010
};
byte cactus1[] = {
  B00100,
  B10100,
  B10101,
  B10101,
  B11101,
  B00111,
  B00100,
  B00100
};
byte cactus2[] = {
  B00100,
  B10101,
  B10101,
  B10101,
  B11111,
  B00100,
  B00100,
  B00100
};

byte cactus3[] = {
  B00100,
  B00101,
  B10101,
  B10101,
  B10111,
  B11100,
  B00100,
  B00100
};

byte block[] = {
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111
};

bool jumping = false, started = false;
unsigned int playerY = 1;
unsigned long jumpTime = 0, lastDraw = 0, lastGenerated = 0, startTime = 0;
int cacti[16], cactiCount = 0;
int cactiSpeed = 500; //500ms (slow) -> 200ms (fast)

void clearPlayer(){
  lcd.setCursor(4,playerY);
  lcd.print(" ");
}

void gameOver(){
  started = false;
  clearCacti();
  lcd.clear();
  lcd.setCursor(3,0);
  lcd.print("GAMEOVER");
  lcd.setCursor(3,1);
  lcd.print((millis()-startTime)/100);
  tone(BUZZER, 415);
  delay(80);
  tone(BUZZER, 302);
  delay(50);
  noTone(BUZZER);
  delay(400);
}

void allBlocks(){
  for(int i = 0; i < 16; i++)
    for(int j = 0; j < 2; j++){
      lcd.setCursor(i,j);
      lcd.write(byte(4));
    } 
}

void checkSpeed(){
  if((millis()-startTime)/100 >= POINTS_SPEED2 && cactiSpeed > 400){
    cactiSpeed = 400;
    allBlocks();
  }else if((millis()-startTime)/100 >= POINTS_SPEED3 && cactiSpeed > 300){
    cactiSpeed = 300;
    allBlocks();
  }else if((millis()-startTime)/100 >= POINTS_SPEED4 && cactiSpeed > 200){
    cactiSpeed = 200;
    allBlocks();
  }
}

void drawPlayer(int y, bool clean){
  int comp[2] = {1,0};
  if(clean){
    lcd.setCursor(4,comp[y]);
    lcd.print(" ");
  }
  lcd.setCursor(4,y);
  lcd.write(byte(0));
}

void addCactus(){
  for(int i = 0; i < 16; i++){
    if(cacti[i] == -1){
      if(i>0 && cacti[i-1] == 14 && cactiSpeed == 500) break;
      cacti[i] = 15;
      cactiCount++;
      break;
    }
  }
}

void clearCacti(){
  for(int i = 0; i < 16; i++) cacti[i] = -1;
  cactiCount = 0;
}

void removeFirstCactus(){
  for(int i = 0; i < 16; i++){
    cacti[i] = cacti[i+1];
    if(i >= cactiCount){
      cacti[i] = -1;
      break;
    }
  }
  cactiCount--;
}

void drawCacti(){
  bool moved = false;
  for(int i = 0; i < cactiCount; i++){
    if(millis()-lastDraw >= cactiSpeed){
      cacti[i] = cacti[i] - 1;
      moved = true;
    }
    if(cacti[i] == 4 && playerY == 1){
      started = false;
      gameOver();
      break;
    }
    lcd.setCursor(cacti[i], 1);
    if(cacti[i]>-1){
      lcd.write(CACTUS_STYLE);
    }else{
      removeFirstCactus();
      i--;
    }
  }
  if(moved) lastDraw = millis();
}

void generateCactus(){
  if(millis()-lastGenerated < CACTUS_GEN_TIME) return;
  int chance = random(0,cactiSpeed);
  if(chance < CACTUS_GEN_CHANCE_THRESHOLD){
    addCactus();
    lastGenerated = millis();
  }
}

void setup() {
  lcd.begin(16, 2);
  lcd.createChar(0, dino);
  lcd.createChar(1, cactus1);
  lcd.createChar(2, cactus2);
  lcd.createChar(3, cactus3);
  lcd.createChar(4, block);
  lcd.setCursor(5,0);
  lcd.print("APERTE");
  lcd.setCursor(5,1);
  lcd.print("O PULO");
  pinMode(JUMP, INPUT);
  pinMode(BUZZER, OUTPUT);
  randomSeed(analogRead(0));
  clearCacti();
}

void loop() {
  if(started){
    lcd.clear();
    checkSpeed();
    if(millis()-jumpTime >= MAX_JUMP_TIME*0.1){
      noTone(BUZZER);
    }
    if(digitalRead(JUMP)){
      if(!jumping){
        jumpTime = millis();
        jumping = true;
        drawPlayer(--playerY, true);
        tone(BUZZER, 800);
      }
      if(jumping){
        if(millis()-jumpTime > MAX_JUMP_TIME){
          jumping = false;
          drawPlayer(++playerY, true);
          noTone(BUZZER);
        }
      }
    }else{
      if(jumping){
        jumping = false;
        drawPlayer(++playerY, true);
      }
    }
    generateCactus();
    drawPlayer(playerY, false);
    drawCacti();
    delay(150);
  }else{
    if(digitalRead(JUMP)){
      started = true;
      startTime = millis();
      tone(BUZZER, 1440);
      delay(200);
      noTone(BUZZER);
    }
  }
}

Vamos falar sobre algumas funções primeiro e depois vamos explicar como tudo funciona.

O comando:  
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
Define uma variável chamada 'lcd' como sendo um display LCD, e os parâmetros passados no seu construtor são, respectivamente, os pinos de RS, E, DB4, DB5, DB6 e DB7. 
Essa variável vai permitir que utilizemos funções como:
  • lcd.begin(colunas, linhas) - que inicializa o display LCD definindo o seu tamanho.
  • lcd.clear() - que limpa o display lcd
  • lcd.createChar(num, data) - que registra um caractere customizado ao LCD
  • lcd.setCursor(x,y) - que define o cursor na posição selecionada
  • lcd.print(valor) - que imprime no LCD o valor passado na posição atual do cursor
  • lcd.write(byte) - imprime no LCD um caractere customizado criado na posição atual do cursor
Inclusive, para esse projeto, foram utilizados caracteres customizados e eles poderão ser vistos mais pra frente.

Quanto ao Buzzer, os comandos utilizados são:
  • tone(pino, frequencia) - que toca uma determinada frequência num pino específico
  • noTone(pino) - que para de tocar o som no buzzer em determinado pino
Agora vamos falar um pouco sobre as funções que eu mesmo criei:
  • clearPlayer() - limpa o caractere do jogador na posição dele
  • gameOver() - limpa a tela e mostra o texto "Game Over" junto com a pontuação do jogador
  • allBlocks() - define todos os caracteres do display LCD como um bloco preenchido
  • checkSpeed() - verifica a pontuação atual do jogador para mudar ou não a velocidade do jogo
  • drawPlayer(y, clean) - desenha o jogador na posição y e limpa ou não a posição anterior
  • addCactus() - adiciona um novo cacto ao jogo
  • clearCacti() - remove todos os cactos existentes
  • removeFirstCactus() - remove apenas o primeiro cacto da lista
  • drawCacti() - desenha todos os cactos presentes
  • generateCactus() - gera um cacto aleatoriamente
Na função setup do Arduino, nós inicializamos o LCD, adicionamos os nossos caracteres customizados, que podem ser vistos na imagem abaixo, imprimimos uma tela inicial de jogo, definimos os pinos do botão e do buzzer como entrada e saída, respectivamente, definimos um valor de semente para o gerador aleatório de forma aleatória e limpamos nossa lista de cactos.

Adicionar imagens dos caracteres customizados
Dinossauro/Personagem
Cacto 1
Cacto 2
Cacto 3
Bloco cheio

Na função loop, verificamos se o jogo foi iniciado e, caso tenho sido, a tela será limpa em cada iteração para depois ser realizado a escrita novamente e verifica-se quando se pressiona o botão de pulo e executa as funções e desenhos para o pulo.

Como vimos, temos 3 tipos de cactos. Então fica a pergunta, como utilizar eles?
No nosso setup, nós definimos os Dinossauro como o caractere customizado 0, Cacto 1 como 1, Cacto 2 como 2, Cacto 3 como 3 e o bloco cheio como 4. E para fazer o uso de diferentes modelos de cactos, podemos usar as constantes encontradas no começo do código.

#define JUMP              9
#define BUZZER            8

#define MAX_JUMP_TIME     1200

#define POINTS_SPEED2     400
#define POINTS_SPEED3     900
#define POINTS_SPEED4     1500

#define CACTUS_GEN_CHANCE_THRESHOLD   60
#define CACTUS_GEN_TIME               800
#define CACTUS_STYLE                  3

Então o que faz cada coisa?

Constante Função
JUMP Define em qual pino está ligado o botão de pulo
BUZZER Define em qual pino está ligado o buzzer
MAX_JUMP_TIME Define o tempo máximo de pulo, pois o jogador não pode ficar voando se deixar o botão pressionado
POINTS_SPEED2 Define a quantidade de pontos necessários para iniciar a velocidade 2
POINTS_SPEED3 Define a quantidade de pontos necessários para iniciar a velocidade 3
POINTS_SPEED4 Define a quantidade de pontos necessários para iniciar a velocidade 4
CACTUS_GEN_CHANCE_THRESHOLD Define o limiar para a chance de geração de um novo cacto
CACTUS_GEN_TIME Define o tempo mínimo a ser passado para gerar um novo cacto
CACTUS_STYLE Define o estilo do cacto (Valores possíveis: 1, 2 ou 3)

Então essas são as configurações do nosso jogo! 

Abaixo vocês poderão encontrar o vídeo desse projeto no canal do Youtube. Se tiver alguma dúvida, pode perguntar! Até mais!




Comentários

Postagens mais visitadas deste blog

Medição de Frequência Cardíaca com Arduino + KY-039

Olá, Mundo!