I made a video console during high school
January 21, 2024Recently, 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.
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 player can charge an attack that, when released, will be launched towards the opponent’s wall, and if it hits it, it will bounce back towards the player who launched it. The “charge” is represented by a bar that fills up as the player hits the ball.
- The game speed increases as the score increases, making it more difficult to hit the ball, like in “Tetris”.
- The game ends when one of the players reaches 10 points.
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:
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:
The next step was to design the interface of the video game itself, so after many sketches, it settled on the following:
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 ❤️