En esta tercera parte del proyecto «Ping Pong» vamos a implementar toda la lógica de los botones.

Sketch

Aquí tienes el sketch final que obtendremos tras completar esta tercera parte del proyecto:

/*********************
    DECLARACIONES
**********************/
//LEDS:
const int LEDS [] = {0,1,4,5,6,7,8,9,10,11};
int time_led =1000;
//BOTONES:
const byte BUTTON_1 = 3;
const byte BUTTON_2 = 2;
const byte BUTTON_START = 12;
bool btn1_pushed = false;
bool btn2_pushed = false;
//CONTROL DEL JUEGO:
byte turn = 1;
byte last_led = 0;
bool end_play = false;


/*********************
    CONFIGURACIÓN
**********************/
void setup() {
  Serial.begin(9600);
  //LEDS:
  for(int i=0; i<10; i++){
  	pinMode(LEDS[i], OUTPUT);
  }
  //BOTONES:
  pinMode(BUTTON_1, INPUT);
  pinMode(BUTTON_2, INPUT);
  pinMode(BUTTON_START, INPUT);
  attachInterrupt(digitalPinToInterrupt(BUTTON_1), interrBtn1, RISING);
  attachInterrupt(digitalPinToInterrupt(BUTTON_2), interrBtn2, RISING);
  //CONTROL DEL JUEGO
  randomSeed(analogRead(0));
}


/*********************
    FUNCIONES
**********************/
void clean (){
  //LEDS:
  for(int i=0; i<10; i++){
    digitalWrite(LEDS[i], LOW);
  }
  //BOTONES:
  btn1_pushed = false;
  btn2_pushed = false;
}

//LEDS
void led(int i){
  digitalWrite(LEDS[i], HIGH);
  delay(time_led);
  digitalWrite(LEDS[i], LOW);
}

void sequence(){
  clean ();
  if(turn == 1){
    for(int i=last_led; i<10 && !btn1_pushed; i++){
      led(i);
      last_led = i;
    }
    turn = 2;
  }else if(turn == 2){
    for(int i=last_led; i>=0 && !btn2_pushed; i--){
      led(i);
      last_led = i;
    }
    turn = 1;
  }
}


//BOTONES
void interrBtn1 (){
  Serial.println("Btn1 pulsado");
  btn1_pushed = true;
}

void interrBtn2 (){
  Serial.println("Btn2 pulsado");
  btn2_pushed = true;
}

//CONTROL DEL JUEGO
void start(){
  clean ();
  Serial.println("Pulsa START");
  while(digitalRead(BUTTON_START)==LOW);
  
  turn = random(2)+1;
  if(turn==1){
    last_led = 2;
  }else{
    last_led = 7;
  }
  end_play = false;
}


/*********************
    FUNCIÓN LOOP
**********************/
void loop() {
  start();
  while(!end_play){
  	sequence();
  }
}

Voy a comentar solo el código nuevo, recuerda que la lógica de los leds la implementamos en la parte 2.

Para que te resulte más fácil identificar el nuevo código he añadido algunos comentarios.

Declaraciones

Tenemos que declarar unas cuantas cosas nuevas, por un lado relacionadas con el funcionamiento de los botones y por otro lado con el control del juego.

Las constantes y variables relacionadas con los botones son estas:

const byte BUTTON_1 = 3;
const byte BUTTON_2 = 2;
const byte BUTTON_START = 12;
bool btn1_pushed = false;
bool btn2_pushed = false;
  • BUTTON_1: Es el pin digital en el que está conectado el botón que utilizará el jugador 1. En este caso el 3 (si estás usando otro pin cambia este valor). (B)
  • BUTTON_2: Lo mismo que el anterior pero para el jugador 2. (C)
  • BUTTON_START: Lo mismo que antes, pero para el botón de inicio de juego. (A)
  • btn1_pushed: Variable bool que nos servirá para saber si el botón del jugador 1 fue presionado. Tomará valor true cuando haya sido pulsado y false cuando no.
  • btn2_pushed: Lo mismo para el botón del jugador 2.
botones ping pong

En cuanto a las variables relacionadas con el control del juego he añadido estas tres:

byte turn = 1;
byte last_led = 0;
bool end_play = false;
  • turn: Sirve para controlar los turnos. Cuando sea el turno del jugador 1 tomará valor 1 y cuando sea el turno del jugador 2 tomará valor 2.
  • last_led: Tomará el valor del último led iluminado dentro del turno de un jugador. De esta forma sabremos si el led es el naranja u otro.
  • end_play: Tendrá valor false mientras no termine el juego. Cuando se ilumine alguno de los leds rojos cambiará su valor a true y terminará el juego.

Configuración

Dentro de la función setup() vamos a inicializar el puerto serie para poder mostrar mensajes en el monitor serie:

Serial.begin(9600);

También debemos configurar como entrada (INPUT) los pines digitales en los que están los pulsadores conectados utilizando pinMode() y las constantes anteriores:

pinMode(BUTTON_1, INPUT);
pinMode(BUTTON_2, INPUT);
pinMode(BUTTON_START, INPUT);

Y sus correspondientes interrupciones:

attachInterrupt(digitalPinToInterrupt(BUTTON_1), interrBtn1, RISING);
attachInterrupt(digitalPinToInterrupt(BUTTON_2), interrBtn2, RISING);

Como vimos en la parte 1 del proyecto, los pines digitales 2 y 3 del Arduino Uno soportan interrupciones, por eso conectamos ahí los pulsadores de los jugadores (BUTTON_1 y BUTTON_2).

Con la función attachInterrupt() podemos capturar una interrupción e indicarle a nuestro Arduino qué es lo que queremos que haga cuando se produzca, en este caso, cuando se presione el pulsador.

Para ello vamos a definir 2 funciones: interrBtn1() e interrBtn2() (las veremos en detalle en el apartado Funciones). La primera se ejecutará cuando se presione el botón 1 y la segunda cuando se presione el botón 2. Para conseguir esto debemos pasar el nombre de las funciones a attachInterrupt() como segundo parámetro.

En cuanto al tercer parámetro, RISING le indica a attachInterrupt() que debe capturar la interrupción cuando el pin asociado pase de LOW a HIGH, es decir, cuando se presione el pulsador.

Por último vamos a incluir una semilla para generar números aleatorios:

randomSeed(analogRead(0));

Nos será útil para decidir, de forma aleatoria, qué jugador empieza.

Funciones

Primero vamos a analizar las funciones que debemos implementar nuevas y luego revisaremos los cambios necesarios en las funciones que ya teníamos de la parte 2 del proyecto.

Las nuevas funciones son interrBtn1(), interrBtn2() y start().

Función interrBtn1()

La función interrBtn1() ya la mencionamos antes. Es la función que se ejecutará cada vez que se presione el pulsador del jugador 1:

void interrBtn1 (){
  Serial.println("Btn1 pulsado");
  btn1_pushed = true;
}

El código de esta función no tiene mucho misterio.

La primera instrucción:

Serial.println("Btn1 pulsado"); 

Muestra en el monitor serie «Btn1 pulsado«, así podremos saber que se está capturando correctamente la interrupción.

La segunda instrucción:

btn1_pushed = true;

Cambia el valor de la variable btn1_pushed a false. De esta forma sabremos si se ha pulsado el botón 1 de forma asíncrona, es decir, no tendremos que estar escuchando el pin digital todo el tiempo para saberlo.

Función interrBtn2()

Es similar a interrBtn1(). Hace exactamente lo mismo, pero para el botón del jugador 2:

void interrBtn2 (){
  Serial.println("Btn2 pulsado");
  btn2_pushed = true;
}

Cambia la variable btn2_pushed a true cuando se pulsa el botón.

Función start()

La función start() inicializa el juego y asigna el turno al jugador que empieza:

void start(){
  clean ();
  Serial.println("Pulsa START");
  while(digitalRead(BUTTON_START)==LOW);
  
  turn = random(2)+1;
  if(turn==1){
    last_led = 2;
  }else{
    last_led = 7;
  }
  end_play = false;
}

La primera instrucción de esta función ya la vimos anteriormente. Apaga todos los leds:

clean ();

La siguiente instrucción simplemente muestra el mensaje «Pulsa START» en el monitor serie:

Serial.println("Pulsa START");

A continuación se ejecuta el bucle while:

while(digitalRead(BUTTON_START)==LOW);

Esta instrucción la hemos usando montones de veces en otros tutoriales de pulsadores.

El bucle while se repite mientras el pin digital del botón START no esté presionado. Como el while termina en punto y coma, la ejecución de las siguientes líneas no avanzará hasta que no se presione el pulsador y cambie su valor a HIGH.

Una vez se presione el botón, se asigna el turno del jugador que debe empezar. Para que sea aleatorio y no empiece siempre el mismo, utilizaremos la función random():

turn = random(2)+1;

Como puedes ver le tenemos que sumar 1. Si no hacemos esa suma y simplemente ejecutamos random(2), el resultado será 0 o 1. Como queremos que sea 1 o 2, para que quede más claro el jugador, le debemos sumar 1 al resultado. La variable turn almacenará este dato e irá cambiando de valor a lo largo del juego.

Lo siguiente es comprobar si es el turno del jugador 1 o del 2:

if(turn==1){
  last_led = 2;
}else{
  last_led = 7;
}

Si es turno del 1 se cumple la condición del if, por lo que la variable last_led toma valor 2. Si es el turno del jugador 2, no se cumple la condición del if y se ejecuta el else, por lo que la variable last_led toma valor 7.

¿Por qué hacemos esto? La secuencia de luces siempre debe empezar en la última encendida, pero cuando empieza el juego ese dato no existe, por lo que tenemos que asignar a last_led el indice del primer led verde que debe encenderse (depende de cada jugador).

Para el jugador 1 el primer led debe ser el que está en la posición 2 del array y para el jugador 2 es el que está en la posición 7:

led inicial de cada secuencia del juego

Usaremos la variable last_led dentro de la función sequence(). Más abajo lo veremos y entenderás mejor su finalidad.

Y ya para terminar con la función start(), nos queda una última instrucción:

end_play = false;

Inicializar la variable end_play a false. Así conseguimos que tenga este valor cada vez que empiece un juego nuevo.

Hemos terminado con las funciones nuevas ¡Perfecto! Vamos ahora a repasar las que ya teníamos, puesto que algunas cosas han cambiado.

Función clean()

La función clean() tiene dos nuevas instrucciones:

btn1_pushed = false;
btn2_pushed = false;

Recuerda que la función clean() se ejecuta antes de cada secuencia para apagar los leds. Parece un buen sitio para reiniciar las variables de los pulsadores dándoles de nuevo valor false. De esta forma quedan las variables preparadas para cuando se capture una nueva interrupción de pulsador.

Función sequence()

La función sequence() ha cambiado bastante desde la parte 3 del proyecto:

void sequence(){
  clean ();
  if(turn == 1){
    for(int i=last_led; i<10 && !btn1_pushed; i++){
      led(i);
      last_led = i;
    }
   	turn = 2;
  }else if(turn == 2){
    for(int i=last_led; i>=0 && !btn2_pushed; i--){
      led(i);
      last_led = i;
    }
    turn = 1;
  }
}

Vamos a comentarla entera para que te quede claro.

Como ahora conocemos el jugador que está jugando, nos los dice la variable turn, no vamos a ejecutar las dos secuencias, ejecutaremos solo la que correspondiente al jugador, es decir, la que le acerca la «pelota».

Por eso llamamos a la función clean() una sola vez al inicio, en vez de dos como antes.

Apagará todos los leds y dejará con valor false las variables btn1_pushed y btn2_pushed. Así estarán preparadas por si se produce una interrupción mientras generamos la secuencia.

Si te fijas, he incluido una instrucción if else. Si recuerdas, en el código de la parte 2 del proyecto no estaba:

if(turn == 1){
    for(int i=last_led; i<10 && !btn1_pushed; i++){
      led(i);
    }
   	last_led = i-1;
   	turn = 2;
}else if(turn == 2){
    for(int i=last_led; i>=0 && !btn2_pushed; i--){
      led(i);
    }
    last_led = i+1;
    turn = 1;
}

Gracias a esta condición sobremos qué jugador está jugando y podremos generar la secuencia correcta.

Si el jugador es el 1, la condición del primer if es verdadera y se ejecuta su código:

if(turn == 1){
    for(int i=last_led; i<10 && !btn1_pushed; i++){
      led(i);
      last_led = i;
    }
   	turn = 2;
}

En el código anterior, antes de hacer este cambio, teníamos un for como este:

for(int i=2; i<10; i++){
   led(i);
}

Como habrás podido comprobar tenemos que añadir algunas cosas nuevas.

Primero, tenemos que cambiar la inicialización del indice i. Ahora el bucle empieza el last_led porque debe generar la secuencia desde el último led encendido por la secuencia anterior:

int i=last_led

Es decir, si el jugador anterior pulsó en el led naranja, la nueva secuencia empezará desde ese led en sentido contrario. Si el anterior jugador pulsó en un led verde, la nueva secuencia empezará en ese led, pero en sentido contrario.

El otro cambio significativo del for es la condición. Ahora, además de ser el indice menor que 10 también debe cumplirse otra condición, que la variable btn1_pushed tenga valor false:

i<10 && !btn1_pushed

Cuando el jugador presiona el botón el bucle debe parar y quedarse en el último led encendido, por eso es necesaria esta condición.

Otro detalle importante del for es que debemos incluir otra instrucción extra en su cuerpo:

last_led = i;

Que se ejecute el cuerpo del for implica que el led se ilumine, puesto que se ejecuta la función led(). Por lo tanto, el último led iluminado será el de la posición i del array, por eso en la variable last_led debemos guardar ese dato, para saber desde donde tendremos que hacer la secuencia inversa.

Cuando el primer if termina su ejecución significa que el jugador 1 a terminado su turno y ahora le toca al jugador 2. Por eso antes de terminar el if del primer jugador debemos incluir esta instrucción que cambia el valor de la variable turn por 2:

turn = 2;

Una vez completado el primer if la función también termina saltando todo el código correspondiente a la instrucción else.

¿Y qué pasa si el primer if no es cumple? Pues se ejecutará directamente el else, donde se vuelve a comprobar el turno, en este caso si se trata del jugador 2:

else if(turn == 2){
    for(i=last_led; i>=0 && !btn2_pushed; i--){
      led(i);
      last_led = i;
    }
    turn = 1;
}

Si te fijas, el código es prácticamente igual al del if anterior. La principal diferencia es la condición del for:

i>=0 && !btn2_pushed

Al igual que antes, este for viene de la versión anterior del proyecto y debemos añadir algo más en la condición. Además de tener que cumplirse que i >= 0, también debe cumplirse que el botón del jugador 2 no haya sido presionado, es decir, que btn2_pushed sea false. El bucle se repetirá mientras se cumplan estas dos condiciones.

El resto del código es idéntico al del anterior if, por lo que no me detendré mucho más.

En resumen, la función sequence() se ejecuta por cada turno y genera la secuencia de leds en función del jugador y del último led encendido. Cuando un jugador presiona su botón, si es su turno, detiene la secuencia de leds porque detiene el bucle for. Esto hace que en la variable last_led quede almacenado el valor de i y que la variable turn tome el valor del siguiente jugador.

Espero que lo hayas entendido. Te prometo que es la parte más compleja del proyecto.

Función loop

Tenemos que modificar la función loop para que ejecute la nueva función start() que hemos definido al inicio y ejecute la función sequence() en bucle:

void loop() {
  start();
  while(!end_play){
  	sequence();
  }
}

Como puedes ver, la condición del while es que el juego no haya llegado a su fin. Mientras la variable end_play tenga valor false, la función sequence() se ejecutará múltiples veces consecutivas, saltando de un jugador a otro en cada iteración.

Y con esto completamos la tercera parte del proyecto. Carga el sketch en tu Arduino y comprueba que funcionan los pulsadores correctamente.