I made a video console during high school

January 21, 2024

Recently, I found a USB drive that contained the remnants of what once was part of a handmade video console I created during high school. In this post, I showcase everything exactly as I found it.

Table of Contents

How did the idea come about?

After my teacher introduced me to the Nokia 5110, a monochrome LCD screen for Arduino, I set out to develop the “Pong” game and load it onto it, allowing me to play with two simple switches.

A Nokia 5110 LCD
A Nokia 5110 LCD

I thought about cutting a couple of wooden boards to create a “box” to house all the elecric circuits, with two controllers and the LCD screen protruding from it, realizing that what I was about to create was a game console: The Nokius 5110 (originally enough).

The game

I wanted to program the “Pong” game, but I didn’t want to copy it exactly, so I added a couple of features to make it more interesting:

The build

First, I had to assemble the electrical circuit, including a power switch, the LCD screen, and the two controllers, each comprised of two levers and one button. Here is a schematic of the setup I achieved:

An arduino board connected to a Nokia 5110
Nokius 5110 circuit

As I can see, I only focused on illustrating the connections with the screen, which was the most complex part. I trust that at least I did that correctly, and it can be recreated with the necessary components.

The UI design

I wanted to recreate the typical splash screen of video game consoles that appeared for a few seconds before loading the game, so I designed the following:

A 84x48 screen with the text 'nokius 5110'
Nokius 5110 splash screen

The next step was to design the interface of the video game itself, so after many sketches, it settled on the following:

Interface similar to 'Pong'
Nokius 5110 game UI

The code

Finally, I tackled the most challenging part. Arduino boards are programmed in C. I barely knew the language, beyond creating electric circuits that light up LEDs by pressing a button. Also, the closest thing to programming a video game had been Scratch.

After a few days of research and testing, I programmed the game in less than 1000 lines of code, where around 500 were images in bmp format, like the splash screen or the numbers.

Imports

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

Arduino pins

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

Constants

#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,};

Functions

When arduino starts, it runs the setup() function, which initializes the pins and the screen, and then calls the inicio() function, which displays the splash screen and waits for the user to press a button to start the game.

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();
}

The loop function would be the game loop, but as I said, I didn’t know how to program video games, so I had to do it in a rudimentary but not too inefficient way.

// empty loop function LOL

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 4 segundos
  delay(4000);

  juego();
 
}

Here we have the juego() function, which is the one that contains the game loop. It is responsible for updating the position of the ball and the players, checking if the ball collides with the players, the walls, or if a player scores a point. It also checks if the player has pressed the attack button, and if so, it checks if the attack is loaded and if it is, it launches it.

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();
  
  }

}

And finally, the juegoterminado() function, which is responsible for displaying the winner and waiting 4 seconds before restarting the game.

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();
}

The result

I managed to assemble the console with a quite minimalist and successful design, featuring a matte black exterior and a removable lid on the top to reconnect the pins that would occasionally come loose.

I regret not having recorded videos and captured the result live, but at least I saved the images and the code included in this post, so that’s something.

Conclusion

Even though the code is clearly subpar, the truth is that implementing an update and rendering loop with FPS limitation is quite an achievement considering I had no idea about what I was doing.

It was a very entertaining project that encouraged me to continue studying programming and achieve what I am capable of today.

Thanks to Sol for being a incredible teacher and helping me with the project ❤️