Hice una videoconsola en el instituto

21 de enero de 2024

Hace poco encontré un USB con archivos que formaron parte de la construcción a mano de una videoconsola que creé en el instituto. En este artículo muestro todo su contenido tal cual lo encontré.

Tabla de Contenidos

¿Cómo se me ocurrió la idea?

Cuando mi profesora me enseñó la Nokia 5110, una pantalla LCD monocroma para Arduino, me propuse desarrollar el juego “Pong” y cargarlo en ella, permitiéndome jugar con dos simples “mandos”.

Una Nokia 5110 LCD
Una Nokia 5110 LCD

Pensé en cortar un par de tablas de madera para crear una “caja” donde meter todos los circuitos eléctricos, con dos mandos y la pantalla LCD sobresaliendo de ella.

Al momento me di cuenta de que lo que estaba a punto de crear era una videoconsola: La Nokius 5110 (la originalidad no era prioridad).

El juego

Quería programar el juego “Pong”, pero no quería copiarlo exactamente, así que añadí un par de características para hacerlo más interesante:

La construcción

Primero, tuve que montar el circuito eléctrico, incluyendo un interruptor de encendido, la pantalla LCD y los dos mandos, cada uno compuesto por dos palancas y un botón. Aquí está el esquema del montaje que conseguí:

Una placa Arduino conectada a la Nokia 5110
Circuito eléctrico de la Nokius 5110

Como se puede ver, solo llegué a ilustrar las conexiones con la pantalla, que era la parte más compleja. Confío en que al menos eso lo hice correctamente, y se pueda recrear con los componentes necesarios.

El diseño de la interfaz

Quería recrear la típica pantalla de carga de las videoconsolas que aparecía durante unos segundos antes de cargar el juego, así que diseñé la siguiente:

Una pantalla de carga 84x48 donde pone 'nokius 5110'
Pantalla de carga de la Nokius 5110

El siguiente paso fue diseñar la interfaz del propio videojuego, así que tras muchos bocetos, me decanté por la siguiente:

Interfaz de juego parecida al 'Pong'
Interfaz del juego

El código

Finalmente, me enfrenté a la parte más complicada. Las placas Arduino se programan en C. Apenas conocía el lenguaje, más allá de crear circuitos eléctricos que encienden LEDs al pulsar un botón. Además, lo más parecido a programar un videojuego había sido Scratch.

Después de unos días de investigación y pruebas, programé el juego en unas 1000 líneas de código, donde alrededor de 500 eran imágenes en formato bmp, como la pantalla de carga o los números.

Importes

#include <Adafruit_GFX.h>
#include <Adafruit_PCD8544.h>

Pines de Arduino

Adafruit_PCD8544 display = Adafruit_PCD8544(7, 6, 5, 4, 3);

Variables

int puntuacionEstablecida=10; //puntuacion máxima de cada partida (max 10)

int Y1; //posición Y del jugador 1
int Y2; //posicion y del jugador 2

int x=random(40,44); //posicion x de la bola
int y=random(10,40); //posicion y de la bola

int direccionx=-1; //direcion horizontal de la bola
int direcciony=1; //direccion vertical de la bola
float avancex=1; //cuanto avanza en x la bola en el siguiente frame
float avancey=0.5; //cuanto avanza en y la bola en el siguiente frame
float xa; //posicion x de la bola exacta
float ya; //posicion y de la bola exacta

int direccionxa1=1; //direccion horizontal del ataque del jugador 1
int direccionxa2=1; //direccion horizontal del ataque del jugador 2

int ataque1; //ataque acumulado del jugador 1 (con el valor de 30 el ataque estará cargado y podrá dispararse)
int ataque2; //ataque acumulado del jugador 2 (con el valor de 30 el ataque estará cargado y podrá dispararse)

int reboteJugador; //variable que detecta qué tipo de rebote va a hacer la bola [hacia la derecha 1 o hacia la izquierda 2]
int rebotePared; //variable que detecta qué tipo de rebote va a hacer la bola [hacia ahajo 1 o hacia la arriba 2]

int puntoPara; //para qué jugador va el siguiente punto

int pendiente; //pendiente de la bola

int p1u=8; //pulsador 1 arriba
int p1d=9; //pulsador 1 abajo
int p2u=10; //pulsador 2 arriba
int p2d=11; //pulsador 2 abajo
int p1a=12; //pulsador 1 ataque
int p2a=13; //pulsador 2 ataque

int puntuacion1=0; //puntuación jugador 1
int puntuacion2=0; //puntuación jugador 2

int velocidad; //velocidad bola

int v;

int animacionAtaque;
int enviandoAtaque1; //variable que vale 1 cuando el ataque 1 se está efectuando (desde que se dispara hasta que muere)
int enviandoAtaque2; //variable que vale 1 cuando el ataque 2 se está efecuando (desde que se dispara hasta que muere)

int xa1; //valor de la posicion x del ataque 1
int ya1; //valor de la posicion y del ataque 1
int xa2; //valor de la posicion x del ataque 2
int ya2; //valor de la posicion y del ataque 2

Constantes

#define player_HEIGHT 2
#define player_WIDTH  8

static const unsigned char PROGMEM player_bmp[] ={
B11000000,
B11000000,
B11000000,
B11000000,
B11000000,
B11000000,
B11000000,
B11000000,};

#define puntuador_HEIGHT 18
#define puntuador_WIDTH 4

static const unsigned char PROGMEM puntuador_bmp[] ={
B11111111,B11111111,B11000000,
B11111111,B11111111,B11000000,
B11111111,B11111111,B11000000,
B11111111,B11111111,B11000000,};

#define bola_HEIGHT 2
#define bola_WIDTH  2

static const unsigned char PROGMEM bola_bmp[] ={
B11000000,
B11000000,
};

#define cero1_HEIGHT 5
#define cero1_WIDTH  3

static const unsigned char PROGMEM cero1_bmp[] ={
B11100000,
B10100000,
B10100000,
B10100000,
B11100000,};

#define uno1_HEIGHT 5
#define uno1_WIDTH  3

static const unsigned char PROGMEM uno1_bmp[] ={
B01000000,
B11000000,
B01000000,
B01000000,
B11100000,};

// ...

#define nueve1_HEIGHT 5
#define nueve1_WIDTH  3

static const unsigned char PROGMEM nueve1_bmp[] ={
B11100000,
B10100000,
B11100000,
B00100000,
B00100000,};

Funciones

Cuando Arduino se enciende, ejecuta la función ‘setup’, que inicializa los pines y la pantalla, y luego llama a la función ‘inicio’, que muestra la pantalla de carga y espera a que el usuario pulse un botón para empezar la partida.

void setup()   {
  pinMode(p1a, INPUT);
  pinMode(p2a, INPUT);
  pinMode(p1u, INPUT);
  pinMode(p1d, INPUT);
  pinMode(p2u, INPUT);
  pinMode(p2d, INPUT);
  
  Serial.begin(9600);

  display.begin();
  display.setContrast(50);
  display.clearDisplay();
  
 inicio();
}

La función ‘loop’ debería haber sido el bucle del juego, pero como he dicho, no sabía programar, así que acabé haciéndolo de una forma rudimentaria, aunque no demasiado ineficiente.

void loop() {
  
}

void inicio(){

  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(BLACK);
  display.setCursor(9,24);
  display.println("Nokius 5110");
  display.display();

  // Muestra la pantalla de carga durante 4 segundos
  delay(4000);

  juego();
 
}

Aquí tenemos la función juego(), que es la que contiene el bucle del juego. Se encarga de actualizar la posición de la bola y de los jugadores, comprobar si la bola colisiona con los jugadores, las paredes, o si un jugador marca un punto.

También comprueba si el jugador ha pulsado el botón de ataque, y si es así, comprueba si el ataque está cargado y si lo está, lo lanza.

void juego(){
  
  display.clearDisplay();

  if(puntuacion1==0 && puntuacion2==0){ //al empezar la partida
  
    Y1=24;
    Y2=24;
    ataque1=0;
    ataque2=28;
    xa1=40;
    ya1=-10;
    xa2=40;
    ya2=-10;
    velocidad=40;

  for(int a=3; a>=1;a--){
    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(BLACK);
    display.setCursor(42,24);
    display.println(a);

    display.drawBitmap(80, Y1,  player_bmp, 2, 8, BLACK);
    display.drawBitmap(3, Y2,  player_bmp, 2, 8, BLACK);
    display.drawBitmap(0, 0,  marco_bmp, 84, 48, BLACK); 
  
    display.display();
    delay(1000);
    }
  
  } else {
//nada
  }

  x=random(40,44); //sistema para situar la bola en un punto x aleatorio al principio de cada ronda
  y=random(10,40); //sistema para situar la bola en un punto y aleatorio al principio de cada ronda
  puntoPara=0; //resetear el valor de esta variable a 0

  xa=x;
  ya=y;

  for (int c=0; c<=0; c++){ //bucle que se encrga de ejecutar cada fotograma del juego para que todo aparezca y desapareza en funcion del valor de cada variable, el cual finaliza cuando se termine la partida o se marque un punto
  
    if (digitalRead(p1u) && Y1<=38){
    Y1++;
    }

    if (digitalRead(p1d) && Y1>=10){
    Y1--;
    }

    if (digitalRead(p2u) && Y2<=38){
    Y2++;
    }

    if (digitalRead(p2d) && Y2>=10){
    Y2--;
    }

    if (digitalRead(p1a) && ataque1>=30) {
    ataque1=0;
    enviandoAtaque1=1;
    ya1=Y1;
    xa1=81;
    direccionxa1=-1;
    }

    if (digitalRead(p2a) && ataque2>=30) {
    ataque2=0;
    enviandoAtaque2=1;
    ya2=Y2;
    xa2=6;
    direccionxa2=1;
    }

  if(x==79 && Y1<=y && Y1>=y-7){ //si la posicion de la bola está casi tocando la pared del jugador 1 y la posicion del jugador 1 está a la misma altura que la bola, entonces la está tocando

    ataque1=ataque1+2;
    reboteJugador=1;

    if(Y1<=y-2 && Y1>=y-5){ //parte del medio
  
      if(direcciony<<0){ //si está yendo la bola hacia arriba
      direcciony=-1;
      } else { //si está yendo la bola hacia abajo
      direcciony=1;
      }
      
      avancey=0.5;
      
      }
  
    if(Y1<=y && Y1>=y-1){ //parte arriba
      direcciony=-1;
      ataque1=ataque1+2;
      avancey=0.75;
    }
  
    if(Y1<=y-6 && Y1>=y-7){ //parte de abajo
      direcciony=1;
      ataque1=ataque1+2;
      avancey=0.75;
    }
  
  }

  if(x<=5 && Y2<=y && Y2>=y-7){ //y ahora lo mismo con el jugador 2

    ataque2=ataque2+2;
    reboteJugador=1;
  
    if(Y2<=y-2 && Y2>=y-5){
      if(direcciony<<0){
      direcciony=-1;
      } else {
      direcciony=1;
    }
    
    avancey=0.5;
    
    }

    if(Y2<=y && Y2>=y-1){ //parte arriba
    direcciony=-1;
    ataque2=ataque2+2;
    avancey=0.75;
    }
    
    if(Y2<=y-6 && Y2>=y-7){ //parte de abajo
    direcciony=1;
    ataque2=ataque2+2;
    avancey=0.75;
    }
  }
                                                                                                                                                                                                                  /*
  SISTEMA DE ATAQUE [DETECTOR DE POSICIÓN]
                                                                                                                                                                                                                                                                                                                                                                                                                                   */
  if(enviandoAtaque1){
  if(xa1<=5 && Y2<=ya1 && Y2>=ya1-7){ // si el jugador 2 consigue devolver el ataque del jugador 1
    direccionxa1=direccionxa1*(-1);
  }

  if(xa1<=3){ //si el jugador 2 no consigue devolver el ataque del jugador 1 [REVISAR EN UN FUTURO SI SE PUEDE COMPRIMIR ESTO CON EL if(x<=3) {} DE ABAJO]
    puntoPara=1; //punto para jugador 1
    enviandoAtaque1=0; //eliminar ataque 1
    c++;
  }

  if(xa1==79 && Y1<=ya1 && Y1>=ya1-7){ // si el jugador 1 consigue frenar el rebote de su ataque
   enviandoAtaque1=0; //eliminar ataque 1
  }

  if(xa1>=81){ //si el jugador 1 no consigue frenar el rebote de su ataque [REVISAR EN UN FUTURO SI SE PUEDE COMPRIMIR ESTO CON EL if(x<=3) {} DE ABAJO]
    puntoPara=2; //punto para jugador 2
    enviandoAtaque1=0; //eliminar ataque 1
    c++;
  }}

  if(enviandoAtaque2){

  if(xa2==79 && Y1<=ya2 && Y1>=ya2-7){ // si el jugador 1 consigue devolver el ataque del jugador 2
   direccionxa2=direccionxa2*(-1);
  }

  if(xa2>=81){ //si el jugador 1 no consigue devolver el ataque del jugador 2 [REVISAR EN UN FUTURO SI SE PUEDE COMPRIMIR ESTO CON EL if(x<=3) {} DE ABAJO]
    puntoPara=2; //punto para jugador 2
    enviandoAtaque2=0; //eliminar ataque 2
    c++;
  }

  if(xa2<=5 && Y2<=ya2 && Y2>=ya2-7){ // si el jugador 1 consigue frenar el rebote de su ataque
    enviandoAtaque2=0; //eliminar ataque 2
  }

    if(xa2<=3){ //si el jugador 2 no consigue frenar el rebote de su ataque [REVISAR EN UN FUTURO SI SE PUEDE COMPRIMIR ESTO CON EL if(x<=3) {} DE ABAJO]
    puntoPara=1; //punto para jugador 1
    enviandoAtaque2=0; //eliminar ataque 2
    c++;
  }}
                                                                                                                                                                                                                  /*
  SISTEMA DE REBOTES                                                                                                                                                                                              Si lees esto eres una maravillosa persona
                                                                                                                                                                                                                  */
  if(y<=9 || y>=46){
    rebotePared=1;
  }

  if(rebotePared==1){
    direcciony=direcciony*-1;
    rebotePared=0;
  }

  if(reboteJugador==1){
    direccionx=direccionx*-1;
    reboteJugador=0;

    if(velocidad>=10){
    velocidad--;
    }}
    
  if(x<=3){
    puntoPara=1; //punto para jugador 1
    c++;
  }

  if(x>=81){
    puntoPara=2; //punto para jugador 2
    c++;
  }



  if(ataque1>=30){ //sistema para igualar a 30 el valor del ataque de cada jugador cuando este sea superior a 30 y funcione correctamente la barra de ataque
    ataque1=30;
  }

  if(ataque2>=30){ //sistema para igualar a 30 el valor del ataque de cada jugador cuando este sea superior a 30 y funcione correctamente la barra de ataque
    ataque2=30;
  }

  if(animacionAtaque<=20){
    animacionAtaque++;
  } else {
    animacionAtaque=0;
  }
  
  c--;

  xa=xa+(avancex*direccionx);
  ya=ya+(avancey*direcciony);

  x=round(xa);
  y=round(ya);

  if(enviandoAtaque1){xa1=xa1+direccionxa1;}
  if(enviandoAtaque2){xa2=xa2+direccionxa2;}

  Serial.println(direccionxa2);
  Serial.println(xa2);
  Serial.println(enviandoAtaque2);
  Serial.println();

  display.clearDisplay(); //limpiar pantalla

  display.drawBitmap(80, Y1,  player_bmp, 2, 8, BLACK); //jugador 1 (derecha)
  display.drawBitmap(3, Y2,  player_bmp, 2, 8, BLACK); //jugador 2 (izquierda)
  display.drawBitmap(0, 0,  marco_bmp, 84, 48, BLACK); //interfaz
  display.drawBitmap(x, y,  bola_bmp, 2, 2, BLACK); //bola de juego

  if(enviandoAtaque1){
  display.drawBitmap(xa1, ya1, bola_bmp, 2, 2, BLACK); //proyectil del ataque del jugador 1, con el mismo script que el de la bola de juego
  } else { //si enviandoAtaque1=0
  xa1=40;
  ya1=-10;
  }
  
  if(enviandoAtaque2){
  display.drawBitmap(xa2, ya2, bola_bmp, 2, 2, BLACK); //proyectil del ataque del jugador 2, con el mismo script que el de la bola de juego
  } /*else { //si enviandoAtaque2=0
  xa2=40;
  ya2=-10;
  }*/
  
  for(int a=0; a<=2; a++){
    display.drawLine(2, 3+a, 2+ataque2, 3+a, BLACK); //x,y x2,y2 color
  }

  for(int a=0; a<=2;a++){
    display.drawLine(81, 3+a, 81-ataque1, 3+a, BLACK);
  }

  if(puntuacion1==0){
    display.drawBitmap(44, 2,  cero1_bmp, 3, 5, BLACK);
  }

  if(puntuacion1==1){
    display.drawBitmap(44, 2,  uno1_bmp, 3, 5, BLACK);
  }

  if(puntuacion1==2){
    display.drawBitmap(44, 2,  dos1_bmp, 3, 5, BLACK);
  }

  if(puntuacion1==3){
    display.drawBitmap(44, 2,  tres1_bmp, 3, 5, BLACK);
  }

  if(puntuacion1==4){
    display.drawBitmap(44, 2,  cuatro1_bmp, 3, 5, BLACK);
  }

  if(puntuacion1==5){
    display.drawBitmap(44, 2, cinco1_bmp, 3, 5, BLACK);
  }

  if(puntuacion1==6){
    display.drawBitmap(44, 2, seis1_bmp, 3, 5, BLACK);
  }

  if(puntuacion1==7){
    display.drawBitmap(44, 2, siete1_bmp, 3, 5, BLACK);
  }

  if(puntuacion1==8){
    display.drawBitmap(44, 2, ocho1_bmp, 3, 5, BLACK);
  }

  if(puntuacion1==9){
    display.drawBitmap(44, 2, nueve1_bmp, 3, 5, BLACK);
  }

  if(puntuacion2==0){
    display.drawBitmap(37, 2,  cero1_bmp, 3, 5, BLACK);
  }

  if(puntuacion2==1){
    display.drawBitmap(37, 2,  uno1_bmp, 3, 5, BLACK);
  }

  if(puntuacion2==2){
    display.drawBitmap(37, 2,  dos1_bmp, 3, 5, BLACK);
  }

  if(puntuacion2==3){
    display.drawBitmap(37, 2,  tres1_bmp, 3, 5, BLACK);
  }

  if(puntuacion2==4){
    display.drawBitmap(37, 2,  cuatro1_bmp, 3, 5, BLACK);
  }

  if(puntuacion2==5){
    display.drawBitmap(37, 2, cinco1_bmp, 3, 5, BLACK);
  }

  if(puntuacion2==6){
    display.drawBitmap(37, 2, seis1_bmp, 3, 5, BLACK);
  }

  if(puntuacion2==7){
    display.drawBitmap(37, 2, siete1_bmp, 3, 5, BLACK);
  }

  if(puntuacion2==8){
    display.drawBitmap(37, 2, ocho1_bmp, 3, 5, BLACK);
  }

  if(puntuacion2==9){
    display.drawBitmap(37, 2, nueve1_bmp, 3, 5, BLACK);
  }

  display.display(); //ejecutar todas las anteriores órdenes de la pantalla
  delay(velocidad); //tiempo entre cada "fotograma", que cada vez es más rápido para aumentar así la dificultad del juego conforme avance la partida

  }

  if(puntoPara==1){
    puntuacion1++;
    puntoPara=0;
  } 

  if(puntoPara==2){
    puntuacion2++;
    puntoPara=0;
  }

  if(puntuacion1>=puntuacionEstablecida || puntuacion2>=puntuacionEstablecida){

    juegoterminado();
  
  } else {

    juego();
  
  }

}

Y finalmente, la función juegoterminado(), que se encarga de mostrar el ganador y esperar 4 segundos antes de reiniciar el juego.

void juegoterminado(){

  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(BLACK);
  display.setCursor(9,24);
  display.println("Juego terminado");
  display.display();

  delay(4000);

  puntuacion1=0;
  puntuacion2=0;

  juego();
}

Resultados

Me las arreglé para montar con éxito la consola con un diseño bastante minimalista, con un acabado negro mate exterior y una tapa superior desmontable para reconectar los pines cuando estos se desconectaban eventualmente.

Me da pena no haber recuperado vídeos y fotos del resultado en vivo, pero al menos he conseguido encontrar los archivos y plasmarlos en este post para siempre, que es suficiente.

Conclusión

Aunque el código fuera horrible, lo cierto es que para ser mi primer programa en C no está nada mal haber incorporado un bucle de actualización y renderizado con limitación de FPS, teniendo en cuenta que no tenía ni idea de lo que estaba haciendo.

Fue un proyecto muy entretenido que me motivó a seguir estudiando programación y lograr lo que soy capaz de hacer hoy en día.

Gracias a Sol por ser una profesora increíble y ayudarme con el proyecto ❤️