Hice una videoconsola en el instituto
21 de enero de 2024Hace 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”.
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:
- El jugador puede cargar un ataque que, al soltarlo, será lanzado hacia la pared del oponente, y si este lo esquiva, rebotará volviendo hacia el jugador que la lanzó. La “carga de ataque” está representada por una barra que se llena a medida que el jugador devuelve la bola.
- La velocidad del juego aumenta a medida que la puntuación aumenta, haciendo más difícil darle a la bola, como en “Tetris”.
- El juego termina cuando uno de los jugadores llega a 10 puntos.
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í:
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:
El siguiente paso fue diseñar la interfaz del propio videojuego, así que tras muchos bocetos, me decanté por la siguiente:
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 ❤️