Introducción
La idea de este juego que realizaremos con nuestro robot mBot es intentar simular el famoso juego del “escondite inglés” pero lógicamente con algunas variantes para que nuestro robot lo pueda hacer.
Los jugadores tendrán que colocarse a cierta distancia del robot y ganará el jugador que logre pulsar el botón que se encuentra en la parte superior sin ser detectado.
Operativa del juego:
-
- Al activar el interruptor de encendido se mostrará en la matriz de LEDs el texto “WAIT!” seguido de una cuenta atrás qué irá del 30 al 1 con el objetivo de esperar un tiempo hasta que el sensor de movimiento se inicialice correctamente. Durante este tiempo los LEDs de la parte superior estarán de color rojo. Una vez pasado ese tiempo se mostrará el texto “READY!” y los LEDS de la parte superior pasarán a estar de color verde señalando que ya se puede activar el juego.
- Mientras no se active el juego mBot mostrará en la matriz de LEDs una secuencia de cara de “dormido”.
- Para comenzar el juego se deberá mantener pulsado el botón de la parte superior durante más de 1 segundo momento en el cual se iniciará una secuencia de inicio de juego con sonido mostrándose en la matriz de LEDs una cara de “despierto” seguido de cuenta atrás del 5 al 1 que finalizará mostrándose el texto “GO!”.
- Una vez iniciado el juego los LEDS situados en la parte superior del robot se pondrán aleatoriamente de color rojo o verde y se mostrará en la matriz de LEDs una secuencia de cara de “espera”.
- Si se ponen los LEDS de la parte superior de color verde te podrás acercar hacia el robot e intentar apretar el botón antes de que se pongan de color rojo. Cuando están en verde nunca podrás ser detectado. Si logras apretar el botón mientras se encuentra el color verde activo “habrás ganado” y el robot realizará una secuencia de fin de juego con sonido mostrando en la matriz de LEDs una cara de “alegría” y el texto “YOU WIN!”.
- Si se ponen los LEDs de color rojo tendrás que quedarte quieto porque ahora el robot sí que te podrá detectar. Si has sido detectado el robot se abalanzará hacia adelante mostrando una cara de “sorpresa” y haciendo que “el brazo” acoplado al servomotor baje creando el efecto de querer atraparte. Si has sido detectado más de 3 veces “habrás perdido” y el robot emitirá una secuencia de fin de juego con sonido mostrando en la matriz de LEDs una cara de “tristeza” y el texto: “GAME OVER!”. Si todavía te quedan “vidas” se mostrará en la matriz de LEDs una cuenta atrás del 3 al 1 para señalar que el juego continúa.
Para ver el funcionamiento de lo anteriormente descrito aquí os dejo el video demostrativo del juego:
Listado de componentes
-
- Robot mBot.
- Módulo sensor PIR.
- Módulo adaptador RJ25.
- Matriz de LEDs.
- Pack de estructuras para mBot.
- 3 x Cable RJ25 20cm.
- Micro Servo 9g.
- Velcro con adhesivo.
Montaje de componentes
Para poder realizar el juego necesitaremos instalar los módulos de sensor de movimiento y el adaptador RJ25 mediante el pack de estructuras para mBot.
En la siguiente imagen se puede ver estos dos módulos ya instalados en la parte superior del robot junto con el módulo de sensor de sonido que para este juego no utilizaremos.
También instalaremos la matriz de LEDs en la parte frontal del robot.
La conexiones de los diferentes módulos a los puertos de la placa mCore serán:
-
- Módulo sensor de movimiento PIR: Puerto 1.
- Módulo adaptador RJ25: Puerto 2.
- Matriz de LEDs: Puerto 4.
Para la creación del brazo que hará el efecto de “atrapar” al jugador cuando este sea detectado necesitaremos acoplar una de las vigas que viene incluida en el pack de estructuras al servomotor. Para realizar la mano bastará coger una cartulina, dibujar una y recortarla (en mi caso dejé esta tarea a mi hijo de 6 años ya que yo para esto soy un desastre).
Primero pondremos una de las partes del velcro en la base de robot donde fijaremos el servomotor y la otra parte del velcro en una de las caras del servomotor.
Posteriormente fijaremos la viga al servomotor y con cinta transparente pegaremos la mano en un extremo de la viga:
Finalmente conectaremos el servomotor en el slot 1 del adaptador RJ25:
Programación
Antes de mostrar el sketch con toda la programación del juego hay que tener en cuenta las siguientes observaciones:
Observación 1:
Nuestro sensor de movimiento lo configuraremos en modo “non-retriggering”. Para ello pondremos en la parte de setup de nuestro sketch la instrucción:
pir.SetPirMotionMode(0);
En el siguiente diagrama se puede apreciar el funcionamiento del sensor en modo “non-retriggering”:
El pulso generado al detectarse un movimiento consta de dos tiempos:
-
- Tx. Tiempo de duración que se mantiene la salida en “high” cuando se detecta un movimiento.
- Ti. Tiempo durante el cual el sensor no detectará movimiento.
Como se puede apreciar en la imagen anterior cuando se activa la salida al detectarse movimiento se genera un pulso de duración “Tx” más un tiempo Ti donde el sensor no puede detectar movimiento. Aunque durante el tiempo “Tx” se detecte otro movimiento el sensor no vuelve a generar nuevamente un pulso y solo al terminar el periodo Tx + Ti del pulso activo el sensor podrá generar un nuevo pulso al detectar un nuevo movimiento.
Lo ideal es que la suma de estos dos tiempos sea lo más baja posible y por suerte en nuestro sensor la suma de esos dos tiempos es muy pequeño (menos de 1 segundo).
Observación 2:
Otro tema a tener en cuenta es que si utilizamos la librería Servo de Arduino que viene ya incluida en la librería MeMCore observaremos que el servomotor realiza pequeños movimientos haciendo como si el brazo “temblara”. Esto sucede por utilizar la librería Servo junto con la librería MeRGBLed que controla los LEDs RGB de la placa.
La solución y explicación a este problema lo podéis ver en el siguiente enlace de la página Web de Adafruit (en este caso hace referencia al problema de utilizar la librería Servo con la librería NEoPixel):
https://learn.adafruit.com/neopixels-and-servos/overview
La solución está en utilizar otra librería, la Adafruit_TiCoServo que funcionará perfectamente en nuestro mBot y nos quitará este problema.
Para poder utilizarla tendremos que quitar la librería Servo que se incluye automáticamente al utilizar el #include <MeMCore.h>. Para ello tendremos que ir al fichero “MeConfig.h” y comentar la línea #include <utility/Servo.h> para que de esta manera no cargue la librería.
Por último os dejo el sketch, basado en parte en el sketch que hice para el robot Mario.
// --------------------------------------------------------------------------- // mBot: Juego del escondite inglés - v1.0 - 01/04/2018 // // AUTOR/LICENCIA: // Creado por Angel Villanueva - @avilmaru // Copyright 2018 Licencia: GNU GPL v3 http://www.gnu.org/licenses/gpl-3.0.html // // LINKS: // Blog: https://mecatronicalab.es // // // HISTORICO: // 01/04/2018 v1.0 - Release inicial. // // --------------------------------------------------------------------------- /* * IMPORTANTE: Comentar en el fichero MeConfig.h la linea #include <utility/Servo.h> para que no cargue la libreria Servo ya * que utilizaremos la libreria Adafruit_TiCoServo.h y si no hacemos esto habrá conflicto entre ambas librerías. * Utilizaremos esta librería en vez de la libreria Servo por problemas al utilizarla junto con la libreria MeRGBLed */ // Notas #include "pitches.h" // Utilizaremos esta librería en vez de la libreria Servo por problemas al utilizarla junto con la libreria MeRGBLed #include <Adafruit_TiCoServo.h> #include <MeMCore.h> MeDCMotor motor1(M1); MeDCMotor motor2(M2); MePIRMotionSensor pir(PORT_1); MePort port(PORT_2); MeLEDMatrix ledMx(PORT_4); MeRGBLed led(PORT_7,2); MeBuzzer buzzer; Adafruit_TiCoServo servo; int16_t servopin = port.pin1(); // SLOT1 const int pinPulsador = A7; const int TIEMPO_VERIFICACION = 10; const int MAX_FALLOS = 3; const int MAS_DE_1S = 1000; const int MAX_COLOR_TIME = 5000; const int melodia_inicio_notas[7] = {NOTE_F5, NOTE_FS5, NOTE_G5, NOTE_GS5, 0, NOTE_F5, NOTE_GS5}; const int melodia_inicio_duracion[] = {8, 8, 8, 8, 8, 8, 2}; // duracion de las notas: 2 = 1/2, 4 = 1/4, 8 = 1/8, etc const int melodia_inicio_total_notas = 7; const int melodia_fin_notas[] = {NOTE_C4, NOTE_G3, NOTE_G3, NOTE_A3, NOTE_G3, 0, NOTE_B3, NOTE_C4}; const int melodia_fin_duracion[] = {4, 8, 8, 4, 4, 4, 4, 4}; const int melodia_fin_total_notas = 8; const int melodia_perder_notas[] = {NOTE_E3, NOTE_DS3, NOTE_D3, NOTE_CS3}; const int melodia_perder_duracion[] = {4, 4, 4, 1}; const int melodia_perder_total_notas = 4; const int melodia_ganar_notas[] = {NOTE_F5, NOTE_G5, NOTE_A5, NOTE_B6, NOTE_C6}; const int melodia_ganar_duracion[] = {8, 8, 8, 8, 4}; const int melodia_ganar_total_notas = 5; unsigned long tiempoAcumuladoMismoColor = 0; unsigned long startTime = 0; unsigned long endTime = 0; unsigned long loopTime = 0; unsigned long SwitchPressedTime = 0; unsigned long t0; int fin_de_juego = 0; int total_veces_persona_detectada = 0; int activa_aviso_nueva_deteccion = 0; int secuencia =0; String colorActivo = ""; String ultimoColorActivo = ""; String modo = "SIN DETERMINAR"; String ultimoTipo= ""; double factor_delay=0.3; unsigned char *cara; void setup() { // PULSADOR pinMode(pinPulsador, INPUT); // PIR: modo funcionamiento: non-repeatable trigger pir.SetPirMotionMode(0); // MATRIZ LEDS ledMx.setBrightness(6); ledMx.setColorIndex(1); ledMx.clearScreen(); // SERVO servo.attach(servopin); compruebaServo(); // Para que la secuencia cambie cada vez que se inicie el programa --> Se conecta a una entrada analógica que no tenga nada conectado randomSeed(analogRead(A3)); activarLed("ROJO"); // Texto wait! for(int i=0; i < 45; i++) { ledMx.drawStr(0-i,8," WAIT! "); delay(50); } // Esperamos un tiempo para que el PIR se inicialice correctamente! for(int i=0; i < 30; i++) { ledMx.showNum(30-i,1); delay(1000); } // Texto ready! for(int i=0; i < 50; i++) { ledMx.drawStr(0-i,8," READY! "); delay(50); } } void loop() { if (modo == "JUEGO") { if (fin_de_juego == 0) { /* * Seleccionar aleatoriamente el color que se mostrará */ int randColor = random(0, 101); // max es max -1 if (randColor & 0x11) // Impar --> color rojo colorActivo = "ROJO"; else // Par --> color verde colorActivo = "VERDE"; /* * Corrección del color para evitar que un mismo color se repita demasido tiempo de manera seguida */ if (ultimoColorActivo == colorActivo) { if (tiempoAcumuladoMismoColor > MAX_COLOR_TIME) { if (colorActivo == "VERDE") colorActivo = "ROJO"; else if (colorActivo == "ROJO") colorActivo = "VERDE"; tiempoAcumuladoMismoColor = 0; } }else tiempoAcumuladoMismoColor = 0; /* * Duracion aleatoria que estará el color anterior activo */ loopTime = random(2, 5) * 1000; // Entre 2s y 4 s (el valor máximo especificado es max -1 ) /* * Ponemos el LED azul y esperamos 1,5s antes de volver a detectar (es un modo de avisar que estamos a punto de volver a detectar) * de esta manera damos tiempo de que el jugador no se mueva. * * En el sensor de makeblock aproximadamente los tiempos medidos son: * Tx: 0,4s Ti: 0,3s */ if (activa_aviso_nueva_deteccion) { activa_aviso_nueva_deteccion = 0; avisoDeteccion(); } /* * NOTA: Es posible que mientras está el color ROJO activo estemos dentro de un periodo Ti (Block Time), en este caso * el PIR no podrá detectar este movimiento hasta que termine el Ti. */ startTime = millis(); endTime = startTime; do { if (colorActivo == "VERDE") { activarLed("VERDE"); int lecturaPULSADOR = lecturaPin(pinPulsador); // Luz VERDE y has pulsado --> Has ganado --> Fin de juego if (lecturaPULSADOR == LOW) // PULL-UP { fin_de_juego = 1; //Cara de ganar cara = new unsigned char[16]{0,16,32,64,68,34,19,3,3,19,34,68,64,32,16,0}; mostrarCara(cara); //Melodia de ganar melodia(melodia_ganar_notas,melodia_ganar_duracion,melodia_ganar_total_notas); // Texto you win! for(int i=0; i < 65; i++) { ledMx.drawStr(0-i,8," YOU WIN! "); delay(50); } break; } }else if (colorActivo == "ROJO") { activarLed("ROJO"); // Luz ROJA y has sido detectado... if (pir.isHumanDetected()) { ++total_veces_persona_detectada; //Has superado el numero de veces que podias ser detectado --> Has perdido --> Fin de juego if (total_veces_persona_detectada > MAX_FALLOS) { fin_de_juego = 1; //Cara de perder cara = new unsigned char[16]{0,32,16,16,16,17,33,1,1,33,17,16,16,16,32,0}; mostrarCara(cara); //Melodias de perder melodia(melodia_perder_notas,melodia_perder_duracion,melodia_perder_total_notas); melodia(melodia_fin_notas,melodia_fin_duracion,melodia_fin_total_notas); //Texto game over! for(int i=0; i < 70; i++) { ledMx.drawStr(0-i,8," GAME OVER! "); delay(50); } }else{ //secuencia de pillado secuenciaAlerta(); activa_aviso_nueva_deteccion = 1; } break; } } secuenciaCara(1000,"ESPERANDO"); endTime = millis(); } while (endTime - startTime < loopTime); // Hasta que no se supere los segundos generados aleatoriamente // Vamos acumulando el tiempo en que esta activo el mismo color tiempoAcumuladoMismoColor = tiempoAcumuladoMismoColor + (endTime - startTime); ultimoColorActivo = colorActivo; } else modo = "SIN DETERMINAR"; }else { // Determinar el modo de juego mirando el tiempo en que se ha mantenido el botón pulsado activarLed("VERDE"); secuenciaCara(1000,"DORMIDO"); startTime = millis(); endTime = startTime; while (lecturaPin(pinPulsador) == LOW) // PULL-UP endTime = millis(); SwitchPressedTime = endTime - startTime; if ( SwitchPressedTime > MAS_DE_1S) { modo = "JUEGO"; cara = new unsigned char[16]{0,60,66,74,74,66,60,0,0,60,66,74,74,66,60,0}; mostrarCara(cara); senalAcusticaConIndicador(2, 250, NOTE_C5, "AZUL"); secuenciaInicio(); } else modo = "SIN DETERMINAR"; } } /* * FUNCIONES */ void Avance() { /* Debido a no tenemos un enconder y cada motor no va a la misma velocidad aquí ajustaremos las velocidades de ambos (si aplica) para conseguir que vaya lo mas recto posible */ motor1.run(-255); motor2.run(255); } void Retroceso() { /* Debido a no tenemos un enconder y cada motor no va a la misma velocidad aquí ajustaremos las velocidades de ambos (si aplica) para conseguir que vaya lo mas recto posible */ motor1.run(255); motor2.run(-255); } void Parar() { motor1.run(0); motor2.run(0); } void desactivarLed() { led.setColor(0, 0, 0, 0); led.show(); } void activarLed(String color) { if (color == "VERDE") led.setColor(0, 0, 255, 0); else if (color == "ROJO") led.setColor(0, 255, 0, 0); else if (color == "AZUL") led.setColor(0, 0, 0, 255); led.show(); } void secuenciaInicio() { // Inicializar las variables fin_de_juego = 0; total_veces_persona_detectada = 0; tiempoAcumuladoMismoColor = 0; ultimoColorActivo = ""; desactivarLed(); melodia(melodia_inicio_notas,melodia_inicio_duracion,melodia_inicio_total_notas); for(int i=0; i < 5; i++) { activarLed("ROJO"); ledMx.showNum(5-i,1); buzzer.tone(NOTE_C5, 500); desactivarLed(); buzzer.noTone(); delay(500); } // Texto go! for(int i=0; i < 30; i++) { ledMx.drawStr(0-i,8," GO! "); delay(50); } } void avisoDeteccion() { for(int i=0; i < 3; i++) { activarLed("AZUL"); ledMx.showNum(3-i,1); buzzer.tone(NOTE_C5, 500); desactivarLed(); buzzer.noTone(); delay(500); } } void melodia(const int notas_melodia[], const int duracion_notas[], const int total_notas) { for (int nota = 0; nota < total_notas; nota++) { int duracionNota = 1000 / (duracion_notas[nota]); buzzer.tone(notas_melodia[nota], duracionNota); /* La función "buzzer.tone" ya introduce el delay de la duración de la nota a diferencia de la librería "tone" --> con lo cual solo hay que añaidr el 30% ojo! que si la nota es 0 no se introduce el delay de la duración de la nota y solo en este caso si que hay que añadir la duración de la nota + 30% en el delay */ factor_delay=0.3; if (notas_melodia[nota] == 0) factor_delay=1.3; int pausaEntreNotas = duracionNota * factor_delay; delay(pausaEntreNotas); buzzer.noTone(); } } void senalAcusticaConIndicador(int n_veces, int duracion_nota, int nota, String color) { for(int i=0; i < n_veces; i++) { activarLed(color); buzzer.tone(nota, duracion_nota); desactivarLed(); buzzer.noTone(); delay(duracion_nota); } } boolean lecturaPin(int pin) { boolean estado; boolean estadoAnterior; estadoAnterior = analogRead(pin); for(int i=0; i < TIEMPO_VERIFICACION; i++) { delay(1); // esperar 1 milisegundo estado = analogRead(pin); if( estado != estadoAnterior) // si el estado cambia -> resetear el contador y guardar el estado { i = 0; estadoAnterior = estado; } } // si hemos llegado aqui el estado se ha mantenido estable (no ha cambiado) por un periodo de tiempo y damos por válida la lectura return estado; } /* En esta función no podemos utilizar instrucciones como el delay o permanecer en un bucle ya que pararía la ejecución en un punto concreto. Para ello se ha utilizadola instrucción millis() (Ej:. if (millis() - t > intervalo) ) que nos permite ejecutar cada x tiempo ciertas instrucciones sin detenerse en un punto. De esta manera no paramos la ejecución en ningún punto concreto y el hilo de ejecución podrá detectar si se produce un sonido mientras se está ejecutando una secuencia de la cara y por tanto iniciar la secuencia de alerta lo antes posible */ void secuenciaCara(unsigned long intervalo, String tipo) { if (millis() - t0 > intervalo) { t0 = millis(); if (ultimoTipo != tipo) secuencia = 0; secuencia++; if (tipo == "ESPERANDO") { switch (secuencia) { case 1: cara = new unsigned char[16]{0,60,66,74,74,66,60,0,0,60,66,74,74,66,60,0}; break; case 2: cara = new unsigned char[16]{0,60,66,66,66,90,60,0,0,60,66,66,66,90,60,0}; break; case 3: cara = new unsigned char[16]{0,60,90,66,66,66,60,0,0,60,90,66,66,66,60,0}; secuencia=0; break; } }else if (tipo == "DORMIDO") { switch (secuencia) { case 1: cara = new unsigned char[16]{0,16,8,8,8,16,0,0,0,0,16,8,8,8,16,0}; break; case 2: cara = new unsigned char[16]{0,0,16,8,8,8,16,0,0,0,0,16,8,8,8,16}; secuencia=0; break; } } mostrarCara(cara); ultimoTipo = tipo; } } /* En esta función si que utilizaremos delays ya que nos interesa que se ejecute entera y no tenga en cuenta si existe o no un sonido mientras se ejecuta a diferencia de la función anterior. */ void secuenciaAlerta() { activarLed("ROJO"); cara = new unsigned char[16]{60,66,129,153,153,129,66,60,60,66,129,153,153,129,66,60}; mostrarCara(cara); Retroceso(); delay(150); Avance(); delay(500); servo.write(70); Parar(); buzzer.tone(784, 500); // esto ya produce delay servo.write(170); buzzer.tone(784, 500); // esto ya produce delay desactivarLed(); delay(300); } void mostrarCara(unsigned char *cara) { unsigned char drawBuffer[16]; memcpy(drawBuffer,cara,16); free(cara); ledMx.drawBitmap(0,0,16,drawBuffer); } void compruebaServo() { // Si el servo no se encuentra en su posicion inicial lo ponemos if (servo.read() != 170) { servo.write(170); // Ponemos el servo en la posición inicial delay(2000); // Damos 2s para que de tiempo a volver a la posicion } }