Projetos

Jogo de Nave na TV com módulo Joystick e Arduino

Eletrogate 25 de abril de 2024

Introdução

Este é um jogo de nave espacial para 1 jogador, onde você controla um OVNI que deve eliminar tudo o que estiver em seu caminho. Você pode movê-lo para cima, para baixo e atirar. Não pense que será uma tarefa fácil, pois diversas forças de defesa tentarão destruir você.

Neste post, você vai aprender como montar e programar este jogo, com saída de imagem para TV e utilizando apenas alguns componentes eletrônicos básicos: uma placa Arduino UNO e um Módulo Joystick para trabalhar como controle.


Biblioteca Gráfica TVout

O videogame construído neste projeto gera imagens para TV. Portanto, estaremos utilizando a biblioteca TVout para a plataforma Arduino.

A biblioteca TVout permite a geração de um sinal de imagem de vídeo composto no padrão NTSC ou PAL, a uma resolução de 128×96 píxeis. Esta resolução pode ser definida para um valor diferente, maior ou menor, dependendo dos recursos de memória da placa Arduino que for utilizada.

TVout pode ser utilizada em diferentes modelos de placas Arduino, mas, dependendo do modelo da placa, as conexões de saída de áudio e vídeo podem ser diferentes. Neste projeto, estaremos utilizando uma placa Arduino UNO, portanto as conexões demonstradas são válidas apenas para esta.

Instalação

Para instalar a biblioteca TVout, acesse o menu Sketch ► Incluir Biblioteca ► Gerenciar Bibliotecas…

Na tela do Gerenciador de Bibliotecas procure pelo nome “TVout” e clique no botão INSTALAR. A biblioteca será instalada dentro deste caminho: “\Documents\Arduino\libraries\TVout

IMPORTANTE: Assim que a instalação terminar, abra o Windows Explorer e acesse o local onde foi instalada a biblioteca. Como você pode observar, a pasta “TVoutfonts” se encontra dentro da pasta “TVout” e, desta forma, o Arduino IDE não vai conseguir encontrá-la para compilar o código. Para corrigir isto, você precisa recortar esta pasta “TVoutfonts” e colocá-la em um nível anterior, ou seja, dentro da pasta “libraries”, de forma que, quando você estiver dentro da pasta Libraries, você possa ver tanto a pasta “TVout” quanto a pasta “TVoutfonts”.


Materiais Necessários

Para este tutorial, você vai precisar dos seguintes componentes eletrônicos:

Para facilitar a conexão entre a mini protoboard e o conector jack RCA fêmea, recomenda-se o uso deste conector, que possui uma ponta com garra jacaré e a outra ponta um jumper macho:


Hardware

Para o desenvolvimento deste projeto siga o diagrama abaixo:

(Abra a imagem em uma nova guia para visualizar com maior resolução)

O hardware é composto de duas partes: a primeira é utilizada pela biblioteca TVout para a emissão do sinal de vídeo, a segunda é utilizada pelo jogo para receber os sinais do joystick utilizado pelo jogador.

Saída de Imagem para a TV

A biblioteca TVout utiliza dois resistores para a composição de seu sinal de vídeo, um resistor de 1k Ohms e outro de 470 Ohms.

Entrada de Dados

Para que o jogo saiba se a nave do jogador deve subir, descer ou atirar ele utiliza o pino direcional e o botão SET do módulo Joystick 5-direções, que conecta o pino COM ao pino GND, e os pinos UP, DWN e SET respectivamente às portas digitais 2, 3 e 4.


Funcionamento

O fornecimento de energia do videogame deve ser de 5V.
Você pode conectar a Arduino UNO a um cabo USB e ligar este cabo à porta USB do computador ou a um carregador de celular.

O jogo inicia mostrando a nave do jogador (OVNI) no canto esquerdo da tela. A quantidade de vidas e a pontuação do jogador são exibidas na parte inferior da tela.
O chão se mostra em movimento para passar a ideia de que a nave está constantemente seguindo para a direita.

Após alguns segundos um determinado “Alvo” irá surgir na parte direita da tela e começará a seguir para a esquerda. Dependendo do tipo de alvo este começará a atirar contra o jogador.
Neste jogo temos 7 tipos diferentes de alvos: Casa, Árvore, Balão, Tanque, Helicóptero, Teco-teco e Avião. Cada um destes alvos vale uma determinada pontuação para o score do jogador.

A casa, a árvore e o balão são alvos que não atiram contra o jogador.
O tanque, o helicóptero, o teco-teco e o avião são alvos hostis e vão atirar para tentar acertar a nave.

Você movimenta a sua nave empurrando o direcional do módulo joystick 5-direções para cima ou para baixo. Para atirar você deve apertar o botão “SET”. Se você preferir atirar utilizando o botão “RST” ou “MID” você deve trocar o jumper de lugar, no módulo joystick, colocando-o ou no primeiro pino ou no terceiro.

O jogo se encerra quando todas as vidas se esgotarem. A palavra “GAME-OVER” ficará escrita na tela até que o jogador aperte o botão de tiro novamente, fazendo com que o jogo seja reiniciado.

Vídeo demonstrando o funcionamento do Jogo:

 


Software

Para fazer o download do código do jogo, clique neste link: Eletrogate_SpaceJockey.zip

Instalação
  • Extraia o conteúdo do arquivo .ZIP e abra o arquivo “Eletrogate_SpaceJockey.ino” através do Arduino IDE,
  • Conecte a Arduino UNO no computador através de um cabo USB,
  • Acesse o menu Ferramentas ► Placa ► Arduino AVR Boards e escolha “Arduino UNO“,
  • Clique em Sketch ► Carregar ou use o atalho Ctrl + U.
Ligando o seu Jogo na TV

Assim que você já tiver montado o circuito e enviado o código do jogo para sua Arduino, você já pode começar a jogar:

  • Conecte a saída de vídeo no conector AV / RCA amarelo da TV,
  • Conecte a Arduino a um cabo USB e ligue a outra ponta deste cabo a um carregador de celular.
Entendendo a lógica de funcionamento…

O projeto Jogo de Nave na TV com módulo Joystick é composto por 2 arquivos:
“Eletrogate_SpaceJockey.ino” e “sprites.h”

A programação do jogo é simples de entender. Vamos conferir o funcionamento de cada uma das partes através da explicação que segue logo abaixo:

Arquivo “sprites.h”

Neste arquivo nós encontramos as referências de imagens que correspondem aos elementos visuais do jogo. Alguns sprites são formados apenas por uma única imagem, e por isso são objetos estáticos e sem animação.

Repare que todas estas imagens gráficas são declaradas como arrays com os modificadores const unsigned char <nome da variável> PROGMEM. Isso deve ser feito desta forma para que estes bytes das imagens sejam armazenados somente na memória de programa e não na memória RAM do Arduino, fazendo com que a memória RAM fique com mais espaço livre para as variáveis e para a memória gráfica que a biblioteca TVout aloca para o seu uso.

. . .
 
// Sprite da Árvore (id = 4)
const unsigned char imgArvore[] PROGMEM = {
  /* 1 imagem */
  8,12, 0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0x7F, 0x32, 0x1C, 0x58, 0x38, 0x18, 0xDB, 
};

// Sprite da Casa (id = 5)
const unsigned char imgCasa[] PROGMEM = {
  /* 1 imagem */
  10,11, 0x0C, 0xC0, 0x1E, 0xC0, 0x3F, 0x40, 0x7F, 0x80, 0xFF, 0xC0, 0x5E, 0x80, 0x52, 0x80, 0x73, 0x80, 0x00, 0x00, 0x7F, 0x80, 0xFF, 0xC0,
};
 
. . .

Outros sprites são formados por duas imagens ou mais, e estas ficam se alternando ao longo do jogo para causar um efeito de animação do objeto.

. . .
 
// Imagens referentes à animação do Helicóptero (id = 0)
const unsigned char imgHelicoptero[][11] PROGMEM = {
  /* 2 imagens */
  { 8,9, 0xF0, 0x10, 0x70, 0xF9, 0xFF, 0x78, 0x00, 0x28, 0xFC, },
  { 8,9, 0x1E, 0x10, 0x70, 0xF8, 0xFF, 0x79, 0x00, 0x28, 0xFC, }
};

// Imagens referentes à animação do Teco-Teco (id = 1)
const unsigned char imgTecoTeco[][8] PROGMEM = {
  /* 2 imagens */
  { 8,6, 0x18, 0x3D, 0xFF, 0xA7, 0x98, 0x1C, },
  { 8,6, 0x98, 0xBD, 0xFF, 0x27, 0x18, 0x1C, }
};
 
. . .

 

arquivo “Eletrogate_SpaceJockey.ino”
typedef struct {
  bool ativo;
  bool atira;
   int camada;
   int pontos;
   int tipo;
 float vel_x;
 float x;
   int y;
   int h;
} recAlvo;
recAlvo alvo[3];

Aqui nós definimos o corpo da estrutura que irá conter todas as características referentes aos Alvos.
Desta forma podemos definir características específicas para cada um dos Alvos que forem criados, de acordo com o seu tipo:
– no momento, ele está ativo ou inativo?
– ele é um Alvo que atira contra o jogador?
– em qual das três camadas ele pertence?
– quantos pontos o jogador vai ganhar se destruir um Alvo deste tipo?
– que tipo de Alvo é este? (avião, teco-teco, balão, helicóptero, tanque, casa ou árvore?)
– este Alvo se movimenta horizontalmente em qual velocidade?
– qual é a sua posição X e Y na tela?
– qual é a sua altura? (define o tamanho da área que o tiro do jogador pode acertar)

#include <TVout.h>
#include <fontALL.h>
#include "sprites.h"

TVout TV;

#define __PIN_UP      2
#define __PIN_DOWN    3
#define __PIN_FIRE    4

bool btnUP, btnDOWN, btnFIRE;

Aqui nós incluímos as bibliotecas TVout.h e fontALL.h, que já estão instaladas no Arduino.

Também incluímos o arquivo local sprites.h, que contém as imagens gráficas utilizadas no jogo.
Mantemos as imagens em um arquivo separado para simplificar a troca e inclusão de novas imagens e também para facilitar a leitura do código principal do jogo.

Instanciamos um objeto TV do tipo TVout para utilizar os recursos gráficos da biblioteca; definimos os pinos referentes às portas digitais em que o módulo Joystick 5-direções está conectado, e também definimos suas respectivas variáveis booleanas para identificarmos quando cada um dos botões estiver em estado Verdadeiro ou Falso.

  int frameId;
float frameId2;
float frameId_OVNI;
float frameId_Explosao;

float solo_vel;
float posX_espaco;

int alvoFire_X;
int alvoFire_Y;

int player_X;
int player_Y;

int playerFire_X;
int playerFire_Y;

bool player_Ativo;
 int player_Camada;
 int player_Vidas;

unsigned long player_Score;
unsigned long previousMillis = 0;

Neste ponto nós definimos as variáveis que serão utilizadas como índice para especificar qual imagem, do array de imagens, deverá ser utilizada durante as animações.

Definimos também as variáveis posX_espaco e solo_vel para determinarmos a posição e a velocidade de movimento do solo para a esquerda.

Depois colocamos algumas variáveis referentes à posição X,Y do tiro do Alvo, posição X,Y do jogador, posição X,Y do tiro do jogador, se o jogador está ativo ou não, qual a sua pontuação, em que camada ele está, e a quantidade de vidas que ele ainda possui.

void reset_game()
{
  randomSeed(analogRead(0));
  
  player_Score = 0;
  player_Ativo = true;
  player_Vidas = 2;
  player_X = 0;
  player_Y = 50;
  playerFire_X = 60;
  playerFire_Y = -10;
  btnFIRE = false;

  frameId = 0;
  frameId2 = 0;
  frameId_OVNI = 0;
  posX_espaco = 0;
  frameId_Explosao = 6; 
  solo_vel = 0.5;
  alvoFire_X = -10;
  
  for(int i=0; i<3; i++) {
    alvo[i].ativo = false;
  }
}

Esta é a função reset_game().
Nós chamamos esta função sempre que o jogo for se iniciar: no começo da função setup ou quando o jogo termina e o jogador aperta o botão de tiro para reiniciar a partida.

Ela serve para reiniciar algumas variáveis como zero e colocar outras variáveis com alguns valores inicias específicos para que o jogo mantenha sempre as mesmas características sempre que for iniciado.

void setup()
{
  reset_game();
  
  TV.begin(NTSC,120,96);
  TV.select_font(font8x8);

  pinMode(__PIN_UP, INPUT_PULLUP);
  pinMode(__PIN_DOWN, INPUT_PULLUP);
  pinMode(__PIN_FIRE, INPUT_PULLUP);
}

Esta é a função setup().
Esta função é executada sempre que o Arduino é iniciado. Aproveitamos para colocar aqui algumas coisas importantes que devem ser executadas logo no início:
– chamar a rotina que reseta o jogo (reseta os valores de algumas variáveis)
– iniciar o objeto TV com uma resolução de 120×96 pixels
– setar a fonte de texto como ‘font8x8’
– definir as portas digitais, utilizadas pelo módulo joystick 5-direcoes, como INPUT_PULLUP

void criarNovoAlvo()
{
  if((player_Vidas == -1) || (player_Ativo == false)) {
    return;
  }

  bool camada_1 = true;
  bool camada_2 = true;
  bool camada_3 = true;
  int camada = -1;
  int indice_livre = -1;
  
  for(int i=0; i<3; i++) {
    if( (alvo[i].ativo) && (alvo[i].camada == 1) ) camada_1 = false;
    if( (alvo[i].ativo) && (alvo[i].camada == 2) ) camada_2 = false;
    if( (alvo[i].ativo) && (alvo[i].camada == 3) ) camada_3 = false;

    if( (alvo[i].ativo == false) && (indice_livre == -1) ) indice_livre = i;
  }
  
  if(camada_1) {
    camada = 1;
  } else
  if(camada_2) {
    camada = 2;
  } else
  if(camada_3) {
    camada = 3;
  }

  if((camada == -1) || (indice_livre == -1)) {
    return;
  }

  int posY;
  
  int tipo;
  if(camada == 1) {
    tipo = random(4);
    posY = 2;
  } else
  if(camada == 2) {
    tipo = random(4);
    posY = 26;
  } else
  if(camada == 3) {
    tipo = random(7);
    posY = 51;
  };

  int i = indice_livre;
  int posX = 120 + 30;

  if(tipo == 0) {
    /* Helicóptero */
    alvo[i].pontos = 50;
    alvo[i].atira = true;
    alvo[i].vel_x = 0.35;
    alvo[i].x = posX;
    alvo[i].y = posY;
    alvo[i].h = 9;
  } else
  if(tipo == 1) {
    /* Teco-teco */
    alvo[i].pontos = 100;
    alvo[i].atira = true;
    alvo[i].vel_x = 0.5;
    alvo[i].x = posX;
    alvo[i].y = posY;
    alvo[i].h = 6;
  } else
  if(tipo == 2) {
    /* Avião */
    alvo[i].pontos = 100;
    alvo[i].atira = true;
    alvo[i].vel_x = 1;
    alvo[i].x = posX;
    alvo[i].y = posY;
    alvo[i].h = 6;
  } else
  if(tipo == 3) {
    /* Balão */
    alvo[i].pontos = 25;
    alvo[i].atira = false;
    alvo[i].vel_x = 0.25;
    alvo[i].x = posX;
    alvo[i].y = posY;
    alvo[i].h = 10;
  } else
  if(tipo == 4) {
    /* Árvore */
    alvo[i].pontos = 20;
    alvo[i].atira = false;
    alvo[i].vel_x = 0.5;
    alvo[i].x = posX;
    alvo[i].y = 63;
    alvo[i].h = 12;
  } else
  if(tipo == 5) {
    /* Casa */
    alvo[i].pontos = 20;
    alvo[i].atira = false;
    alvo[i].vel_x = 0.5;
    alvo[i].x = posX;
    alvo[i].y = 65;
    alvo[i].h = 10; // 12
  } else
  if(tipo == 6) {
    /* Tanque */
    alvo[i].pontos = 100;
    alvo[i].atira = true;
    alvo[i].vel_x = 0.6;
    alvo[i].x = posX;
    alvo[i].y = 65;
    alvo[i].h = 10;
  };
  
  alvo[i].tipo = tipo;
  alvo[i].ativo = true;
  alvo[i].camada = camada;
}

Esta é a função criarNovoAlvo().
Logo no início nós colocamos uma condição de que ela só vai prosseguir com o código se a nave do jogador estiver em modo ‘ativo’ e se a quantidade de vidas do jogador estiver maior ou igual a zero.

Iniciamos o código desta função procurando dentro de todos os alvos que estão ativos, para saber em quais camadas cada um deles está para podermos encontrar se há alguma camada que ainda não está sendo ocupada por um Alvo ativo.

Se houver alguma camada livre e também houver algum índice livre para criar um novo Alvo, então o código prossegue.

Ele seleciona randomicamente algum dos tipos de Alvos disponíveis, seta a sua posição horizontal um pouco mais à direita do limite da tela para que ele leve algum tempo para aparecer, e seta sua posição vertical de acordo com a camada a que ele pertence.

Agora nós especificamos as características que serão atribuídas ao novo Alvo de acordo com o tipo de Alvo que foi escolhido para.

void desenharAlvos()
{
  for(int i=0; i<3; i++)
  {
    if(alvo[i].ativo && (alvo[i].x < 120-10)) {
      int posX = alvo[i].x;
      int posY = alvo[i].y;
      int tipo = alvo[i].tipo;
      
      if(tipo == 0) { /* Helicóptero */
        TV.bitmap(posX,posY, imgHelicoptero[(int)frameId]);
      } else
      if(tipo == 1) { /* Teco-teco */
        TV.bitmap(posX,posY, imgTecoTeco[(int)frameId]);
      } else
      if(tipo == 2) { /* Avião */
        TV.bitmap(posX,posY, imgAviao);
      } else
      if(tipo == 3) { /* Balão */
        TV.bitmap(posX,posY, imgBalao);
      } else
      if(tipo == 4) { /* Árvore */
        TV.bitmap(posX,posY, imgArvore);
      } else
      if(tipo == 5) { /* Casa */
        TV.bitmap(posX,posY, imgCasa);
      } else
      if(tipo == 6) { /* Tanque */
        TV.bitmap(posX,posY, imgTanque[(int)frameId2]);
      };
    }
  }
}

Esta é a função desenharAlvos().
Esta função é chamada de dentro da função loop e serve para desenhar na tela todos os Alvos que estiverem ativos.

Ela acessa todos os três alvos e verifica quais deles estão ativos. À medida em que for encontrando um Alvo ativo ele desenha na tela a imagem correspondente a ele de acordo com o tipo a que ele pertence.

void movimentarAlvos()
{
  for(int i=0; i<3; i++) {
    if(alvo[i].ativo) {
      alvo[i].x -= alvo[i].vel_x;
      if(alvo[i].x < 0) alvo[i].ativo = false;
    }
  }
}

A função movimentarAlvos() é chamada de dentro da função loop e serve para movimentar pela tela os Alvos que estiverem ativos, de acordo com a velocidade de movimento atribuída a cada um deles.

void linha(int x,int y, int x2, int y2, int cor) {
  bool desenhar = true;
  if((x < 0 ) && (x2 < 0)) {
    desenhar = false;
  }
  if(x < 0) x = 0;
  if(x2 < 0) x2 = 0;

  if(x > 120) x = 120;
  if(x2 > 120) x2 = 120;

  if(desenhar) {
    TV.draw_line(x,y, x2,y2, cor);
  }
}

O processo de desenhar linhas, da biblioteca TVout, somente desenha uma linha se o seu ponto de início e o ponto final estiverem dentro da área visível da tela.
A função linha() é utilizada para corrigir este processo verificando se o ponto inicial e o ponto final estão dentro da área visível. Se algum deles estiver fora desta área ele corrige a posição horizontal fazendo com que o pondo inicial e o ponto final assumam valores que estejam dentro da área da tela.

int explosao_X;
int explosao_Y;
bool explosaoGrande;
void iniciarExplosao(int x, int y, bool expGrande = false) {
  explosao_X = x;
  explosao_Y = y;
  frameId_Explosao = 0;
  explosaoGrande = expGrande;
}

void desenharExplosao()
{
  if((frameId_Explosao >= 0) && (frameId_Explosao < 6)) {
    if(explosaoGrande) {
      TV.bitmap(explosao_X,explosao_Y, imgExplosaoOVNI[(int)frameId_Explosao]);
    } else {
      TV.bitmap(explosao_X,explosao_Y, imgExplosao[(int)frameId_Explosao]);
    }
  }
}

A função desenharExplosao() serve para desenhar a animação da explosão em uma  região específica da tela.

Ela desenha a imagem específica do array de imagens de explosão de acordo com o índice frameId_Explosao.
Ela desenha as imagens correspondentes ao tamanho normal ou ao tamanho grande, de acordo com o que foi especificado na chamada da função iniciarExplosao().

A função iniciarExplosao() serve para dar início ao processo da explosão. Ela especifica a posição da explosão e também se será uma explosão de tamanho normal ou grande. Após especificar estas informações a função coloca o frameId_Explosao igual a zero para que a animação da explosão seja iniciada.

void incrementarFrameIDs()
{
  frameId = 1-frameId;
  frameId_OVNI += 0.5;
  
  if(frameId_OVNI == 8) {
    frameId_OVNI = 0;
    criarNovoAlvo();
  }

  frameId2 += 0.125;
  if(frameId2 == 2) {
    frameId2 = 0;
  }

  if(frameId_Explosao < 6) {
    frameId_Explosao += 0.5;
  }
}

A função incrementarFrameIDs() é chamada de dentro da função loop e serve para incrementar os índices das variáveis que controlam os frames das animações.
Quando o ciclo de frameId_OVNI chegar ao fim a variável se reinicia e é feita uma tentativa de criar um novo Alvo na tela.

  • frameId: esta variável é alternada entre 0 e 1 em tempo normal, e é utilizada para as animações de quase todos os Alvos.
  • frameId_OVNI: é incrementada de 1 em 1 a cada 2 ciclos e é utilizada para a animação do OVNI. Ao chegar ao valor 8 a variável se inicia.
  • frameId_Explosao: é incrementada de 1 em 1 a cada 2 ciclos e é utilizada para a animação da explosão. Quando a variável chega ao valor 6 ela para de ser incrementada.
  • frameId2: esta variável é alternada entre 0 e 1 a cada 8 ciclos, e é utilizada para a animação do Alvo do tipo tanque.

 

void loop()
{
  if (millis() - previousMillis >= 10)
  {
    previousMillis = millis();
    incrementarFrameIDs();
  }

  TV.clear_screen();

  TV.print(0,96-9, "SCORE:");
  TV.print(50,96-9, player_Score);

  TV.draw_rect(0,76,119,9, WHITE,WHITE);
  for(int i=0; i<player_Vidas; i++) {
    TV.bitmap(2 + i*10,80, imgVida);
  }

  movimentarAlvos(); 
  posX_espaco -= solo_vel;
  if(posX_espaco <= -32) {
    posX_espaco = 0;
  }

  . . .

Função LOOP – parte 1
A cada 10 milissegundos chama a função incrementarFrameIDs.
Limpa a tela e escreve a pontuação do jogador no canto inferior esquerdo da tela. Um pouco acima do texto SCORE, desenha os ícones que representam a quantidade de vidas do jogador.

Chama a função movimentarAlvos, e também altera o valor da variável posX_espaco de acordo com a velocidade de movimento especificada em solo_vel. Quando posX_espaco chegar a -32 ela se reinicia para zero. Utilizamos solo_vel para determinar a velocidade de movimento do solo pois esta velocidade acelera quando o jogador é destruído. Quando todos os Alvos somem da tela esta velocidade volta ao seu valor normal.

. . .
 
btnUP   = !digitalRead(__PIN_UP);
btnDOWN = !digitalRead(__PIN_DOWN);
btnFIRE = !digitalRead(__PIN_FIRE);

if((player_Ativo == false) && (player_Vidas >= 0)) {
  btnUP = false;
  btnDOWN = false;
  btnFIRE = false;
}

if(btnUP) {
  player_Y -= 2;
  if(player_Y < 1) player_Y = 1;
}

if(btnDOWN) {
  player_Y += 2;
  if(player_Y > 66) player_Y = 66;
}

if(btnFIRE && (playerFire_X >= 120)) {
  playerFire_X = player_X + 16;
  playerFire_Y = player_Y+2;
}

. . .

Função LOOP – parte 2
Faz a leitura dos estados das porta digitais e os atribui às suas respectivas variáveis. Se a nave do jogador estiver inativa então trava todos os controles para que não possa se mover ou lançar tiros.

Se o jogador empurrar o direcional para cima ou empurrar para baixo ele vai alterar a posição vertical do OVNI para movê-lo para cima ou para baixo, respeitando sempre os seus limites superior e inferior.

Se o jogador apertar o botão SET ele coloca o tiro em uma posição X, Y logo em frente à nave, lembrando que esta ação só será possível se, neste momento, o tiro do OVNI estiver fora da área visível da tela, para que este possa realizar somente um único tiro de cada vez.

. . .
  
player_Camada = -1;
if(player_Y <= 12) {
  player_Camada = 1;
} else
if((player_Y >= 20) && (player_Y <= 36)) {
  player_Camada = 2;
} else
if(player_Y >= 45) {
  player_Camada = 3;
}

if((player_Camada >= 1) && (alvoFire_X <= 0) && (frameId_OVNI == 4) && (random(100) > 50) )
{
  for(int i=0; i<3; i++) {
    if(alvo[i].ativo && alvo[i].atira && (alvo[i].x < 110) && (alvo[i].camada == player_Camada)) {
      alvoFire_X = alvo[i].x;
      alvoFire_Y = alvo[i].y + alvo[i].h/2;
      if(alvo[i].tipo == 6) alvoFire_Y = alvo[i].y + 1;
      break;
    }
  }
}

if(alvoFire_X > 0) {
  alvoFire_X -= 4;
  TV.bitmap(alvoFire_X,alvoFire_Y, imgTiro);
}

if(player_Ativo && (alvoFire_X <= 16) && (alvoFire_X > 0) && (alvoFire_Y >= player_Y) && (alvoFire_Y <= player_Y+5))
{
  for(int i=0; i<3; i++) {
    alvo[i].vel_x += 0.5;
  }
  solo_vel = 1;
  player_Ativo = false;
  iniciarExplosao(player_X, player_Y-1, true);
}
 
. . .

Função LOOP – parte 3
Verifica a posição vertical do OVNI para determinar em qual região das três camadas ele se encontra.

Após encontrar a camada em que o OVNI está o programa verifica todos os Alvos ativos para ver se algum deles está na mesma camada que o OVNI. Se algum deles estiver na mesma camada o programa posiciona o tiro do Alvo logo à sua frente, lembrando que esta ação só é possível se o tiro do Alvo, neste momento, estiver fora da área visível da tela, para que o Alvo também possa atirar um único tiro de cada vez.

Verifica se o tiro do Alvo está dentro da área visível da tela e o movimenta da direita para a esquerda até que o tiro não esteja mais visível na tela.

Verifica constantemente se o tiro está dentro da área ocupada pelo OVNI para identificar se houve uma colisão do tiro com o OVNI.
Se sim, então executa as seguintes ações:

  • aumenta a velocidade de movimentação de todos os Alvos
  • aumenta a velocidade de movimentação do solo
  • coloca o OVNI como inativo
  • inicia o processo de animação de uma explosão de tamanho grande na mesma posição em que estava o OVNI na tela.

OBS: Lembrando que, quando o OVNI é destruído e está em estado inativo, ele só se tornará ativo novamente depois que todos os Alvos da tela saírem da área visível.

. . .
  
if((player_Ativo == false) && (player_Vidas >= 0))
{
  bool nenhumAlvoAtivo = true;
  for(int i=0; i<3; i++) {
    if(alvo[i].ativo) nenhumAlvoAtivo = false;
  }
  if(nenhumAlvoAtivo) {
    player_Ativo = true;
    solo_vel = 0.5;
  }
}

if((player_Vidas == -1) && (frameId_Explosao >= 5)) {
  TV.print(8*3,25-8, "GAME-OVER");

  if(btnFIRE) {
    reset_game();
  }
}
  
. . .

Função LOOP – parte 4
Quando o OVNI estiver inativo, mas o jogador ainda possuir vidas, então o programa verifica se ainda existem Alvos ativos na área visível da tela.
Quando não houver mais Alvos ativos o programa então executa as seguintes ações:

  • subtrai uma das vidas do jogador
  • coloca o OVNI como ativo (se ainda houver vidas)
  • volta ao normal a velocidade de movimentação do solo

A tela de GAME-OVER
A mensagem “GAME-OVER” ficará escrita no meio da tela quando o jogador não possuir mais nenhuma vida e todos os Alvos estiverem fora da área visível da tela. Durante este período o programa aguarda até que o jogador aperte o botão SET para que o jogo chame a função reset_game() e inicie uma nova partida.

. . .
  
  if(playerFire_X <= 120) {
    playerFire_X += 4;
    
    for(int i=0; i<3; i++)
    {
      if(alvo[i].ativo)
      if((playerFire_X-2 >= alvo[i].x) && (playerFire_Y >= alvo[i].y) && (playerFire_Y <= alvo[i].y+alvo[i].h)) {
        player_Score += alvo[i].pontos;
        alvo[i].ativo = false;
        playerFire_X = 200;
        iniciarExplosao(alvo[i].x, alvo[i].y);
        break;
      }
    }
  }
  
  if((playerFire_X < 120-2) && (playerFire_Y >= 0)) {
    TV.bitmap(playerFire_X,playerFire_Y, imgTiro);
  }

  desenharAlvos();

  if(player_Ativo) {
    TV.bitmap(player_X,player_Y, imgOVNI[(int)frameId_OVNI]);
  }

  desenharExplosao();
  TV.delay_frame(2);
}

Função LOOP – parte final

Chegamos à parte final do código interno da função LOOP. Nesta parte o programa movimenta constantemente o tiro do OVNI da esquerda para a direita até que ele não esteja mais visível na tela.

Verifica constantemente se o tiro está dentro da área visível, então confere se ele entrou em colisão com algum dos Alvos ativos. Se o tiro atingiu algum deles então o programa executa as seguintes ações:

  • inicia animação de explosão na mesma posição do Alvo que foi destruído.
  • incrementa a pontuação do jogador de acordo com os pontos fornecidos pelo tipo de Alvo que foi destruído.
  • coloca como inativo o Alvo que foi destruído.
  • reseta o tiro do OVNI colocando-o fora da área visível da tela, permitindo que o jogador possa atirar novamente a partir deste momento.

 

Após este processo o programa executa algumas outas funções para finalizar:

Desenha o tiro do OVNI na tela, se ele estiver dentro da área visível.
Chama a função desenharAlvos() para que todos os Alvos ativos sejam desenhados na tela.
Desenha o OVNI na tela, se ele estiver ativo.

Chama constantemente a função desenharExplosao() para que os frames de animação da explosão sejam desenhados. Lembrando que as imagens da explosão só serão exibidas se o frameId_Explosao for menor do que 6.

E, por fim, o programa faz uma chamada ao método delayframe do objeto TV, passando o valor 2 como parâmetro, para que a TVout aguarde 2 ciclos antes de renderizar a imagem. Isso diminui um pouco a velocidade do jogo, mas permite que todos os elementos da tela tenham tempo suficiente para serem desenhados sem que eles fiquem piscando na tela da TV.

 


Conclusão

Bom, chegamos ao final do post!

Aprendemos como criar uma versão simplificada do jogo Space Jockey para o Arduino utilizando a biblioteca TVout.

Os recursos de memória e a velocidade de processamento de uma placa Arduino UNO são bem limitados e isso nos impede de criar jogos mais elaborados com maior resolução gráfica e muitos elementos gráficos na tela mas, apesar disso, podemos usar nossa criatividade e exercitar nossa capacidade de programação para criar diversos tipos de jogos simples e divertidos com a biblioteca TVout.

Se você gostou desta matéria veja também os meus outros posts sobre programação de jogos aqui na Eletrogate:

Jogo da Velha na TV!
Criando um Videogame PONG com Arduino

Até a próxima !


Sobre o Autor


Etienne Gomide

Programador e Desenvolvedor de Sistemas Embarcados. Bacharel em Sistemas de Informação pela Faculdade Federal de Viçosa (FDV). Trabalhou nas empresas CIENTEC e ORIONTEC como programador. Atua como Técnico em TI na Universidade Federal de Viçosa (UFV). Tem como passatempo o desenvolvimento de jogos e projetos para Arduino/esp8266/esp32/stm32.


Eletrogate

25 de abril de 2024

A Eletrogate é uma loja virtual de componentes eletrônicos do Brasil e possui diversos produtos relacionados à Arduino, Automação, Robótica e Eletrônica em geral.

Tenha a Metodologia Eletrogate dentro da sua Escola! Conheça nosso Programa de Robótica nas Escolas!

Eletrogate Robô

Cadastre-se e fique por
dentro de novidades!