Criando um joguinho com Arduino usando orientação a objetos

{lang: 'pt-BR'}

Recentemente resolvi fazer um joguinho simples com Arduino e surgiu a necessidade de separar melhor o código do jogo do código que fazia a interface com o usuário, que neste caso eram apenas alguns leds e botões. Ainda que simples, separar em módulos distintos iria facilitar a organização e o entendimento do código. Resolvi programar orientado a objeto e o resultado foi bem positivo.

O jogo é bem similar a um jogo que na minha infância se chamava Genius. Consiste num conjunto de 4 luzes coloridas que se acendem em uma sequência aleatória. O jogador precisa reproduzir a sequência pressionando os botões correspondentes às luzes para avançar para a próxima etapa.

Genius

O código deste projeto está em https://github.com/werneckpaiva/arduino/tree/master/genius-game

Modelo eletrônico do jogo

model

Classe LedOutput

Criei uma classe que facilita acender e apagar uma fileira de leds. Separei este código em um arquivo chamado LedOutput.h

#ifndef LedOutput_h
#define LedOutput_h

class LedOutput{
  private:
    short qnt;
    short basePin;

  public:
    LedOutput(short q, short p){
      qnt = q;
      basePin = p;
    }

    void initialize(){
      for (int i=0; i<qnt; i++) pinMode(basePin + i, OUTPUT);
    }

    void turnLedOn(short i){
      digitalWrite(basePin + i, HIGH);
    }

    void turnLedOff(short i){
      digitalWrite(basePin + i, LOW);
    }

    void turnOnlyLedOn(short i){
      turnAllLedsOff();
      turnLedOn(i);
    }
    void turnAllLedsOn(){
      for (int i=0; i<qnt; i++) turnLedOn(i);
    }

    void turnAllLedsOff(){
      for (int i=0; i<qnt; i++) turnLedOff(i);
    }

    void blinkAll(){
      turnAllLedsOn();
      delay(200);
      turnAllLedsOff();
    }

    void blinkLed(short led){
      turnOnlyLedOn(led);
      delay(400);
      turnAllLedsOff();
    }
};
#endif

O arquivo começa com a declaração do código da minha classe, somente se a classe não foi declarada anteriormente.

Para instanciar essa classe você precisa passar 2 parâmetros: a quantidade de leds e o número da primeira saída digital que controlará o led:

LedOutput ledOutput(4, 2);

Classe ButtonInput

A segunda classe criada lê o botão pressionado pelo usuário. Neste caso havia um problema adicional, pois a leitura do botão deveria acontecer apenas quando o usuário deixou de pressionar o botão e não quando começou a pressioná-lo. Para garantir isso eu guardo o identificador do botão pressionado e retorno um valor inválido enquanto o botão permanecer pressionado. Quando o usuário solta o botão, eu retorno o valor que foi guardado.

#ifndef ButtonInput_h
#define ButtonInput_h

class ButtonInput{
  private:
    short qnt;
    short basePin;

    short lastIn = -1;
    LedOutput *ledOutput;

  public:
    ButtonInput(LedOutput *out, short q, short p){
      qnt = q;
      basePin = p;
      ledOutput = out;
    }

    void initialize(){
      for (int i=0; i<qnt; i++) { 
         pinMode(basePin + i, INPUT); digitalWrite(basePin + i, HIGH); 
      } 
    } 

    short read(){ 
      short in = readPushedBtn(); 
      if (in >= 0){
        lastIn = in;
        ledOutput->turnLedOn(in);
      } else if (lastIn >= 0){
        ledOutput->turnAllLedsOff();
        in = lastIn;
        lastIn = -1;
        return in;
      }
      return -1;
    }

    short readPushedBtn(){
      for (int i=0; i<qnt; i++) if(digitalRead(basePin + i)==HIGH) return i;
      return -1;
    }
};

#endif

GeniusGame

O módulo do jogo usa as classes de input e output como parâmetro do construtor. A dinâmica do jogo acontece por meio de dois modos: Replay Mode e Play Mode.

Replay Mode

O modo replay acontece quando o jogo está exibindo a sequência de luzes que deve ser reproduzida pelo usuário. Ao iniciar o modo Replay, o jogo faz o sorteio de mais uma luz, salva no final do buffer e exibe toda a sequência já sorteada.

Play Mode

O modo play é aquele em que o usuário reproduz a sequência de luzes pressionando os botões. A cada botão pressionado o código valida se aquele item da sequência está correta e permite que o usuário avance para a próxima etapa.

#define START_MODE 0
#define REPLAY_MODE 1
#define PLAY_MODE 2

#ifndef GeniusGame_h
#define GeniusGame_h

class GeniusGame{
  private:
    ButtonInput *input;
    LedOutput *ledOutput;

    short qnt = 0;

    short gameOrder[100];
    short gamePos = -1;
    short playPos = -1;

    short currentMode = START_MODE;

  public:
    GeniusGame(ButtonInput *in, LedOutput *out, short q){
      input = in;
      ledOutput = out;
      qnt = q;
    }

    void start(){
      randomSeed(analogRead(0));
      gamePos = -1;
      playPos = -1;
      for(int i=0; iblinkAll();
        delay(100);
      }
      currentMode = REPLAY_MODE;
    }

    void replay(){
      gamePos++;
      gameOrder[gamePos] = random(qnt);
      for (int i=0; i<=gamePos; i++){
        ledOutput->blinkLed(gameOrder[i]);
        delay(200);
      }
      currentMode = PLAY_MODE;
    }

    void finish(){
      for(int i=0; i<5; i++){
        ledOutput->blinkAll();
        delay(50);
      }
    }

    void validateInput(short in){
      playPos++;
      if (in == gameOrder[playPos]){
        if (playPos == gamePos){
          playPos = -1;
          currentMode=REPLAY_MODE;
        } else {
          currentMode=PLAY_MODE;
        }
      } else {
        finish();
        start();
      }
    }

    void process(){
       if (currentMode==REPLAY_MODE){
        replay();
      } else {
        int in = input->read();
        if (in >= 0){
          validateInput(in);
        }
      }
    }
};
#endif

O código principal do Arduino é bastante simples e consiste em instanciar as classes e chamar o código de processamento do game no loop.

#include "LedOutput.h"
#include "ButtonInput.h"
#include "GeniusGame.h"

LedOutput ledOutput(4, 2);
ButtonInput btnInput(&ledOutput, 4, 8);
GeniusGame game(&btnInput, &ledOutput);

void setup() {
  ledOutput.initialize();
  btnInput.initialize();
  game.start();
}

void loop() {
  game.process();
  delay(100);
}

Espero que seja útil.

{lang: 'pt-BR'}