Páginas

sexta-feira, 11 de março de 2011

Criando jogo Genius...Arduino

Neste post, irei falar sobre como iniciar o desenvolvimento de objetos interativos com Arduino. Agora, vamos ver como adicionar dispositivos de entrada e saída, sejam eles analógicos ou digitais. A plataforma Arduino possui 14 entradas e saídas digitais, das quais 6 podem ser usadas para saída analógica emulada via PWM, e 6 entradas (entradas apenas) analógicas. As funções para entrada digital e para entrada e saída analógica são:
  • digitalRead(num_da_porta) — função que lê e retorna o valor digital (0 ou 1) da porta
  • analogWrite(num_da_porta, valor) — função que escreve um valor analógico na porta. Este valor pode variar de 0 a 255 e representa uma tensão média de saída entre 0V (0) e 5V (255). Para maiores informações, ver a explicação da modulação PWM no site do Arduino.
  • analogRead(num_da_porta) — função que lê e retorna um valor de tensão analógico presente na porta especificada, convertido para um número entre 0 e 1023, sendo o valor 0 correspondendo a 0V e 1023 correspondendo a 5V.
Agora iremos usar as entradas e saídas do arduino para fazer um jogo no estilo do Genius (Simon). O jogo consiste em quatro botões grandes que acendem em sequência e o jogador precisa memorizar esta sequência e repetí-la. A cada rodada a sequência aumenta em 1 elemento. O jogo termina quando o jogador erra a sequência apresentada ou, no nosso caso, quando a sequência chega a 100 elementos. :)

Montando os circuitos

Para implementar este jogo, utilizaremos 4 sensores de pressão para representar os botões, quatro LEDs coloridos para indicar qual botão da sequência deve-se pressionar, um LED para indicar que o jogador deve começar a pressionar os botões, e um auto-falante para emitir o som de cada botão.
Primeiro vamos conectar os LEDs às saídas digitais do Arduino. Escolhemos conectá-los às saídas digitais PWM, pois desta forma poderemos incrementar o nosso jogo posteriormente, adicionando efeitos nos LEDs, com escrita analógica nestas portas. Assim, os LEDs vermelho, verde, azul e amarelo serão conectados às portas 5, 6, 9 e 10, respectivamente. O LED que indica a vez do jogador repetir a sequência será conectado à porta 2. Adicionamos as seguintes diretivas de pré-processamento para facilitar a leitura do código:
// LEDs em portas de saída PWM digital
#define LED_RED      5
#define LED_GREEN    6
#define LED_BLUE     9
#define LED_YELLOW   10O circuito para ligar os LEDs às portas é composto do LED propriamente dito e um resistor de 220Ω em série para limitar a corrente no LED, como pode ser visto na figura abaixo:

Os sensores de pressão que utilizamos são resistores que variam sua resistência  de acordo com a força aplicada sobre ele. Sem nenhuma força exercida sobre ele, sua resistência é acima de 1MΩ, e com 10Kg sua resistência é próxima de 0Ω. Assim, ligaremos o sensor de pressão com um resistor de 10kΩ para formar um divisor de tensão, que vai variar a tensão de entrada na porta analógica de acordo com a força aplicada ao sensor de pressão. Ligaremos cada sensor de pressão (que chamaremos de PAD no código) a uma das portas analógicas de acordo com a definição abaixo:
// PADs nas portas de entrada analógica
#define PAD_RED      0
#define PAD_GREEN    1
#define PAD_BLUE     2
#define PAD_YELLOW   3
#define MAX_PADS     4
O circuito para os sensores de pressão está mostrado na figura abaixo:

Para emitir os sons do jogo, utilizaremos um pequeno auto-falante, destes encontrados em headphones. Este auto-falante será conectado à porta 12 do Arduino. Abaixo segue a diretiva de pré-processamento com a definição desta porta:
// Speaker on digital out port 12
#define SPEAKER        12
Caso seja necessário, podemos adicionar um resistor de baixo valor (100Ω ou menos) para reduzir o volume do auto-falante. O Genius utiliza notas musicais para indicar o acionamento de cada botão. Sabemos que o som de uma nota musical é a variação do deslocamento do ar em uma única frequência. Assim, se ligarmos e desligarmos a porta em que o auto-falante está conectado, na frequência desejada, o auto-falante emitira o som da nota correspondente. As notas que utilizaremos para cada um dos botões são: Lá para o vermelho, Lá, uma oitava acima (o dobro da frequência) para o verde, Ré para o azul e Sol para o amarelo. Para ligar e desligar o auto-falante na frequência desejada, precisamos calcular o período, que é o inverso da frequência. O auto-falante deverá permanecer meio período ligado e meio período desligado, tantas vezes quanto for a duração da nota. Assim, temos as seguintes definições para o meio período de cada nota a ser tocada:
// Notas desempenhado por cada PAD
// O valor de cada constante representa o período de cada
// Note que será tocado quando um bloco é pressionado.
// A relação de cada almofada com uma nota é o seguinte:
#define NOTE_HPERIOD_RED      1136    // Uma nota  - 440Hz
#define NOTE_HPERIOD_GREEN    568     // Uma nota e uma oitava acima do RED - 880Hz
#define NOTE_HPERIOD_BLUE     1700    // D-note - 294Hz
#define NOTE_HPERIOD_YELLOW   1275    // G-note - 392Hz
#define NOTE_HPERIOD_ERROR    5000    // 100Hz
Abaixo segue a função para tocar uma nota, bem como o circuito para ligar o auto-falante à porta.
 
// Plays a note according to its half periodvoid playNote(long hperiod, long duration)
{
    long elapsed_time = 0;

    //  Jogou menos tempo do que 'duração' 
    while (elapsed_time < duration) 
    {
        // turno para a metade do período 
        digitalWrite(SPEAKER, HIGH);
        delayMicroseconds(hperiod);

        // turno fora por metade do período 
        digitalWrite(SPEAKER, LOW);
        delayMicroseconds(hperiod);

        // Adiciona um período de tempo decorrido
        elapsed_time += (hperiod*2);
    }
}
Ao juntar todos estes circuitos e conectá-los ao Arduino, temos a nossa configuração de hardware pronta. A figura abaixo mostra como ficou este circuito montado em uma protoboard.

Funções do Jogo

A logica do jogo é bastante simples. Primeiro temos que sortear um elemento (pad) e adicioná-lo à sequência de pads. Depois executamos toda a sequência para que o jogador a memorize, e depois esperamos pelo pressionamento dos pads e comparamos os pads pressionado com a sequência salva. Caso o jogador acerte a sequência, começamos este ciclo novamente. Se não, sinalizamos o erro e o jogo recomeça.
Para o nosso jogo, definimos um conjunto de constantes, que representam o tempo que cada LED permanece aceso, o intervalo entre o acendimento dos LEDs, a sensibilidade dos PADs, etc. Além disto, definimos duas tabelas para converter um valor de PAD para a porta do respectivo LED e o valor do PAD em um periodo da nota a ser tocada. Também temos variáveis para representar o fim do jogo e para armazenar a sequência de pads a serem pressionados. Abaixo seguem estas definições e as funções setup() e loop() do nosso jogo:
////////////////////////////////////////////////////////////
// Definição de constantes
////////////////////////////////////////////////////////////
// Notas desempenhado por cada PAD 
// O valor de cada constante representa o período de cada 
// Note que será tocado quando um bloco é pressionado. 
// A relação de cada almofada com uma nota é o seguinte: 
#define NOTE_HPERIOD_RED      1136    // Uma nota  - 440Hz
#define NOTE_HPERIOD_GREEN    568     // Uma nota e uma oitava acima do RED - 880Hz
#define NOTE_HPERIOD_BLUE     1700    // D-note - 294Hz
#define NOTE_HPERIOD_YELLOW   1275    // G-note - 392Hz

#define NOTE_HPERIOD_ERROR    5000    // 100Hz


// Número máximo de toques na PAD, para terminar o jogo 
#define MAX_NUM_TOUCHES      100
// Duração máxima de cada toque em milisegundos 
#define MAX_TOUCH_DURATION   500
// Intervalo máximo de cada toque em milisegundos 
#define MAX_TOUCH_INTERVAL   250
// Sensibilidade do sensor de toque
#define TOUCH_SENSIBILITY    1000
// Porta analógica para a leitura do inicializador randomSeed 
#define RANDOM_SEED_PORT     5


////////////////////////////////////////////////////////////
// Definição de variáveis globais 
////////////////////////////////////////////////////////////

// Tabela de pesquisa para converter PAD para um LED
unsigned char PAD_TO_LED[MAX_PADS];
// Tabela de pesquisa para converter PAD para um período NOTA 
unsigned long PAD_TO_NOTE_HPERIOD[MAX_PADS];
// Jogo mais variável 
boolean gameOver;
// Seqüência de pastilhas de ser tocado 
unsigned char padSequence[MAX_NUM_TOUCHES];

// O número atual de usuários toca deve executar 
int currentNumTouches;
// Duração de cada toque (irá diminuir ao longo do tempo) 
long currentTouchDuration;
// Intervalo de tempo entre os toques (vai diminuir ao longo do tempo) 
int currentTouchInterval;

////////////////////////////////////////////////////////////
// Declarações de função 
////////////////////////////////////////////////////////////
// Função para repor o estado do jogo 
void restartGame();
// Função para preencher nex aleatório para ser pressionado e inseri-lo em seqüência pad 
void nextRound();
// Função que reproduz a seqüência de teclado atual 
void playSequence();
// Função que verifica a entrada do jogador contra a seqüência salvo 
void verifySequence();

// Função para aguardar uma entrada de teclado 
int readPads();
// Função para transformar o bloco em correspondig levou, 
// E jogar bloco de notas correspondentes 
void turnPadOn(int pad);
// Função que reproduz o som da vitória 
void playVictorySound();
// Função que reproduz o som de erro 
void playErrorSound();
// Função para tocar uma nota com base no seu período de meia 
void playNote(long period, long duration);
// Coloque todos os leds no teclado 
void turnAllLedsOn();
// Coloque todos os leds apagados pad 
void turnAllLedsOff();

////////////////////////////////////////////////////////////
// Configuração do Arduino () função 
////////////////////////////////////////////////////////////
void setup()
{
    // Definindo portas LED como SAÍDAS 
    pinMode(LED_PLAYING, OUTPUT);
    
    pinMode(LED_BLUE, OUTPUT);
    pinMode(LED_GREEN, OUTPUT);
    pinMode(LED_YELLOW, OUTPUT);
    pinMode(LED_RED, OUTPUT);
    
    // Definindo como porta SPEAKER OUTPUT
    pinMode(SPEAKER, OUTPUT);
    
    // Não há necessidade de usar pinMode para definir as entradas PADs 
    // Porque eles estão ligados as entradas analógicas 
    // (Na verdade você não pode fazer isso porque pinMode 
    // Afecta apenas as portas digitais) RS Rs se liga mané!

    // Inicializando a PAD para LED tabela de pesquisa 
    PAD_TO_LED[PAD_RED] = LED_RED;
    PAD_TO_LED[PAD_GREEN] = LED_GREEN;
    PAD_TO_LED[PAD_BLUE] = LED_BLUE;
    PAD_TO_LED[PAD_YELLOW] = LED_YELLOW;

    // Inicializando a almofada NOTA tabela de pesquisa 
    PAD_TO_NOTE_HPERIOD[PAD_RED] = NOTE_HPERIOD_RED;
    PAD_TO_NOTE_HPERIOD[PAD_GREEN] = NOTE_HPERIOD_GREEN;
    PAD_TO_NOTE_HPERIOD[PAD_BLUE] = NOTE_HPERIOD_BLUE;
    PAD_TO_NOTE_HPERIOD[PAD_YELLOW] = NOTE_HPERIOD_YELLOW;
    
    restartGame(); 
}

////////////////////////////////////////////////////////////
// Loop Arduino () função 
////////////////////////////////////////////////////////////
void loop()
{
    // Gera almofada próxima aleatória e acrescenta que a seqüência pad 
    nextRound();
    // Executar a seqüência de teclado atual 
    playSequence();
    delay(currentTouchInterval);
    // Aguarda pela entrada do jogador e verificar se a entrada do jogador
    // Corresponde a seqüência de teclado 
    verifySequence();
    
    // Se o jogo tiver terminado, reiniciar o jogo 
    if(gameOver)
    {
        restartGame();     
    }    
}
Para a geração de números aleatórios, utilizamos as seguintes funções do Arduino: randomSeed(), que inicializa o gerador de números aleatórios; e random() que retorna um numero aleatório dentro de um intervalo. Para inicializar a função randomSeed() usamos o valor lido de uma porta analógica não utilizada (conectada), que vai flutuar (variar) de acordo com as ondas eletromagnéticas do ambiente. Esta inicialização e a inicialização as variáveis do jogo é feita na função restartGame(), apresentada abaixo:
void restartGame()
{
    // Reset global variables
    gameOver             = false;
    currentNumTouches    = 0;
    currentTouchDuration = MAX_TOUCH_DURATION;
    currentTouchInterval = MAX_TOUCH_INTERVAL;

    // turn off all leds
    turnAllLedsOff();

    // Restart random number generation
    randomSeed(analogRead(RANDOM_SEED_PORT));
}
Abaixo seguem as funções para calcular o próximo pad a ser pressionado, executar a sequência de pads para que o usuário memorize e verificar se os pads pressionados pelo usuário estão na sequência correta.
////////////////////////////////////////////////////////////
// Game functions
////////////////////////////////////////////////////////////
void nextRound()
{
    // Increments the number of pad touches for this round
    currentNumTouches++;
    // If pad touches reaches the maximum, the player has finished the game
    // So, play the victory sounds
    if (currentNumTouches >= MAX_NUM_TOUCHES)
    {
        playVictorySound();
    }
    // Else, adds a new random pad to the sequence
    else
    {
        padSequence[currentNumTouches - 1] = random(0, MAX_PADS);
    }
}

void playSequence()
{
    // go through the stored PAD sequence turn on corresponding LED and
    // playing corresponding note
    for (int i=0; i < currentNumTouches; i++)
    {
        turnPadOn(padSequence[i]);
    }
}

void verifySequence()
{
    int pad = 0;

    // Turn the playing LED on
    // This notifies the player that it is time to press the pads
    digitalWrite(LED_PLAYING, HIGH);

    // Run though pad sequence waiting for player input and
    // verifying the input against the sequence
    for(int i=0; i< currentNumTouches; i++)
    {
        // Waits for pad input
        pad = readPads();
        // Verify if the correct pad was pressed
        if (pad == padSequence[i])
        {
            // If the pad is correct, turn the corresponding LED
            // on and play its note
            turnPadOn(pad);
        }
        else
        {
             // if pressed pad is a wrong pad, the player looses the game
             // so, play the Error sound
             playErrorSound();
             gameOver = true;
             break;
        }

    }
    // Turn the playing LED off
    digitalWrite(LED_PLAYING, LOW);
}
Quando o jogador pressiona um pad, sua resistência diminui, levando o valor de tensão na porta correspondente do arduino para baixo também. Assim, para verificar os pads pressionados, devemos ler cada uma das portas dos pads em sequência. Caso o valor lido seja menor que o nosso valor de sensibilidade (no nosso caso 1000, para uma sensibilidade bem alta), consideramos este pad pressionado. A função que faz esta leitura está descrita abaixo:
int readPads()
{
    boolean padPressed = false;
    int pad = 0;
    
    do{
        // Verifica a porta de cada bloco, verificando se 
        // Foi pressionado 
        for(int i=0; i
        {
            // Lê a porta de almofada 
            int pressure = analogRead(i);
            // Se a pressão atravessa o threashold, bloco atual é pressionado 
            if(pressure < TOUCH_SENSIBILITY)
            {
                padPressed = true;
                pad = i;
                break;
            }
        }
    }while (!padPressed);

    return pad;
}
As outras funções do código são funções auxiliares bastante simples, por isso não serão discutidas aqui. Você pode fazer o download do código completo para execução no seu arduino neste link: CESAR_simon_game.zip.
Abaixo segue um vídeo do protótipo do jogo funcionando. Nos próximos posts da serie abordaremos outras funcionalidades do arduino mais avançadas, fiquem ligados!

Um comentário:

teste comentario