I always wanted to design a gaming console as I was always fascinated by how gaming consoles blend hardware, software, and graphics. As I had one ESP32 based LORA (long-range) development board lying around, I thought of using that. This whole circuit works at 5V. The console works with only one button that can do multiple tasks. The firmware is upgradable, but for my initial project, I have added three cool games that are both fun to play and easy to code!
About the projectThe Motive is simple:
- This is a simple gaming console running on ESP32 with a built-in 0.96-inch OLED Display.
- The game, navigation, and play are all done by a single button.
- The built-in display on the ESP32 module acts as the screen.
- LEDs light up in various animations to show start/win/lose, etc.
- The buzzer sounds with varying frequency to generate different tones.
- Easy customization and firmware upgrade.
- Hamba Match: In this game, an image moves continuously across the screen, and to win, you need to press at the correct time to embed the image in its outline.
- Hamba Run: I tried to recreate the dinosaur game you play on Chrome. Jump to avoid obstacles!
- Number Hold Game: In this game, a random number is shown on the display along with a continuously changing number. You need to press at the right time so that the continuous number exactly matches the random number.
- More Games: I will continue to add more simple and fun games to play on my GitHub. (link given below)
The hardware side is pretty simple. Here, I have used a breadboard to hold all the components. The circuit is built with around the ESP32 LoRa Module, which acts as the main controller. ESP32 sits perfectly on the breadboard, leaving all the pins on either side of the board.
The system is powered by 5V coming from the USB. The development board contains a 3.3V regulator, which I used for lights and button input.
The development board cannot output enough current, so I used generic PNP transistors (BC557) to switch the LEDs and the Buzzer. The LEDs are connected in parallel so they blink at the same time.
The input push button is connected to a 1k pull-up resistor (it reads HIGH when not pressed), which is a good practice to avoid ghost trigger.
The additional 100nF capacitor connected to with switch is very important to counter the debounce effect as we will use an interrupt to read the button inputs.
The casingDid you know you can CNC plastic? JLCCNC offers up to a 5-axis milling process! and all at a very low cost.
Big thanks to JLCCNC for supporting this project! They provide affordable and high-quality CNC machining services. Starting at just $1, click here to get the $70 coupons at JLCCNC: https://jlccnc.com/?from=alaminashik
The code:GitHub link: https://github.com/AlAminAshik/Mini-game-consol-using-lora-display-esp32.git
I tried to keep the code simple to allow everyone to understand. The code could be made neater by creating some custom libraries, but for now, I kept everything under main.cpp
# Important: To use the built-in OLED display of the development board, there are some adjustments to be made to the code. On the Heltec board, the OLED display can be powered on/off, so to turn it on, pull the GPIO 16 pin to HIGH at the initial setup. I have attached a screenshot of the code.
The code is divided into different sections, and I have discussed the different blocks of code below:
The menu is created by calling two functions:The drawMenu() shows the name and icons
and menuAction() executes the game selected
//show menu item name and cursor
void drawMenu() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
// Menu item 1
display.setCursor(20, 10);
display.print("Play Hamba Match");
// Menu item 2
display.setCursor(20, 30);
display.print("Play Hamba Run");
// Menu item 3
display.setCursor(20, 50);
display.print("Play Number Hold");
// Draw arrow
if (currentMenu == 0) {
display.setCursor(5, 10);
display.print(">");
}
else if(currentMenu == 1) {
display.setCursor(5, 30);
display.print(">");
}
else if(currentMenu == 2) {
display.setCursor(5, 50);
display.print(">");
}
display.display();
}
// Execute action based on selected menu item
void menuAction(int menuIndex) {
if (menuIndex == 0) {
runHambaMatch = true; // Set flag to run Hamba game
Serial.println("Function 1 executed");
} else if (menuIndex == 1) {
runHambaRun = true; // Set flag to run Hamba Run game
Serial.println("Function 2 executed");
} else if (menuIndex == 2) {
targetNumber = random(1, 11); // Set a initial random target number between 1 and 10
runNumberHold = true; // Set flag to run Number Hold game
Serial.println("Function 3 executed");
}
display.display();
}
The input button is coded as an interrupt so that the button function runs at a point of the game without any delay.You must add ICACHE_RAM_ATTR to avoid unexpected resets.
ICACHE_RAM_ATTR void Play_button_pressed() {
Serial.println("Play button pressed!");
if (digitalRead(playButton) == LOW) {
pressStartTime = millis();
buttonHeld = false;
buttonPressed = true;
}
}
The Hamba match game function: (watch the video)void play_hamba_match_game(){
//run filled cow image
for (cow_position = -100; cow_position < 100; cow_position=cow_position+5) {
display.clearDisplay(); // clear the display
display.setCursor(0,0); // set cursor to top left corner
display.setTextSize(1); // set text size to 1
display.setTextColor(SSD1306_WHITE); // set text color to white
display.print("Level: "); // show level text
display.print(diffLevel); // print the current difficulty level
display.drawBitmap(cow_position, 0, cowFilled, LOGO_WIDTH, LOGO_HEIGHT, SSD1306_WHITE);
//hold outline of the cow
display.drawBitmap(0, 0, cowOutline, LOGO_WIDTH, LOGO_HEIGHT, SSD1306_WHITE);
display.display(); // update the display
delay(40-(int)pow(diffLevel, 2)*1.5); // wait for difflevel milliseconds
//loop until button is pressed
if(buttonPressed == true) { //stop when button is pressed
break;
}
//flash the red and blue lights
unsigned long currentMillis = millis(); // Get the current time
if(currentMillis - previousMillis >= 500) { // If 500 milliseconds have passed
previousMillis = currentMillis; // Store the current time
ledState = !ledState; // Change the state of the LED
digitalWrite(RedLights, ledState); // Turn on RedLights
digitalWrite(GreenLights, !ledState); // Turn off GreenLights
//play buzzer sound
ledcWriteTone(Buzzer_pin, 1000);
}
else {
ledcWriteTone(Buzzer_pin, 0); // Turn off buzzer sound
}
}
if(buttonPressed){
delay(200); //wait to show the last cow position frame
// If the game is not running, turn off the lights
digitalWrite(RedLights, LOW); // Turn on RedLights
digitalWrite(GreenLights, LOW); // Turn on GreenLights
if(cow_position >= -10 && cow_position <= -6) {
playWinSound(); // Play the winning sound
digitalWrite(RedLights, HIGH); // Turn off RedLights
digitalWrite(GreenLights, HIGH); // Turn off GreenLights
switch (diffLevel) {
case 1:
diffLevel = diffLevel + 1; //increase difficulty level for next round
showextLevel(diffLevel);
break;
case 2:
diffLevel = diffLevel + 1; //increase difficulty level for next round
showextLevel(diffLevel);
break;
case 3:
diffLevel = diffLevel + 1; //increase difficulty level for next round
showextLevel(diffLevel);
break;
case 4:
diffLevel = diffLevel + 1; //increase difficulty level for next round
showextLevel(diffLevel);
break;
case 5:
celebration(); // Call the celebration function to show the celebration graphics
display.clearDisplay(); // Clear the display
display.setCursor(14, 15); // Set cursor to top left corner
display.setTextSize(2); // Set text size to 2
display.setTextColor(SSD1306_WHITE); // Set text color to white
display.print("Play"); // Print "Play" on the display
display.setCursor(14, 35); // Set cursor to next line
display.print("Again!!"); // Print "Again!!" on the display
display.display(); // Update the display
ledcWriteTone(Buzzer_pin, 0); // Play no sound while waiting
diffLevel = 1; //reset difficulty level
break;
}
//wait until the play button is pressed to play again
while (digitalRead(playButton) == HIGH) {
delay(50);
}
buttonPressed = false; //reset button pressed flag
}
else {
diffLevel = 1; //reset difficulty level
playLoseSound(); // Play the losing sound
while (digitalRead(playButton) == HIGH) {
//display win or loose
display.clearDisplay(); // Clear the display
display.setCursor(10, 20); // Set cursor to top left corner
display.setTextSize(2); // Set text size to 2
display.setTextColor(SSD1306_WHITE); // Set text color to white
display.print("You Lose!"); // Print "You Lose!" on the display
display.setCursor(12, 40); // Set cursor to top left corner
display.setTextSize(1); // Set text size to 1
display.setTextColor(SSD1306_WHITE); // Set text color to white
display.print("Try Again!"); // Print "Try Again!" on the display
display.display(); // Update the display
}
buttonPressed = false; //reset button pressed flag
}
}
}
Number hold game function: (watch the video)void play_number_hold_game(){
// while(buttonPressed == false) { //loop until button is pressed
currentNumber = (currentNumber % 10) + 1;
display.clearDisplay();
//show level at top left
display.setCursor(0,0); // set cursor to top left corner
display.setTextSize(1); // set text size to 1
display.setTextColor(SSD1306_WHITE); // set text color to white
display.print("L: "); // show level text
display.print(diffLevel); // print the current difficulty level
//Target number at top
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(16, 45);
display.print("Target:");
display.setCursor(100, 45);
display.print(targetNumber);
// Circulating number at bottom
display.setTextSize(4);
display.setCursor(50, 10);
display.print(currentNumber);
display.display();
//delay(150); // Small delay to allow display to update
delay(150-(int)pow(diffLevel, 3)); // wait for difflevel milliseconds
//flash the red and blue lights
unsigned long currentMillis = millis(); // Get the current time
if(currentMillis - previousMillis >= 500) { // If 500 milliseconds have passed
previousMillis = currentMillis; // Store the current time
ledState = !ledState; // Change the state of the LED
digitalWrite(RedLights, ledState); // Turn on RedLights
digitalWrite(GreenLights, !ledState); // Turn off GreenLights
//play buzzer sound
ledcWriteTone(Buzzer_pin, 1000);
}
else {
ledcWriteTone(Buzzer_pin, 0); // Turn off buzzer sound
}
// }
//check for win or lose condition
if(buttonPressed){
delay(200); //wait to show the last cow position frame
// If the game is not running, turn off the lights
digitalWrite(RedLights, LOW); // Turn on RedLights
digitalWrite(GreenLights, LOW); // Turn on GreenLights
if(currentNumber == targetNumber) {
playWinSound(); // Play the winning sound
digitalWrite(RedLights, HIGH); // Turn off RedLights
digitalWrite(GreenLights, HIGH); // Turn off GreenLights
switch (diffLevel) {
case 1:
diffLevel = diffLevel + 1; //increase difficulty level for next round
showextLevel(diffLevel);
break;
case 2:
diffLevel = diffLevel + 1; //increase difficulty level for next round
showextLevel(diffLevel);
break;
case 3:
diffLevel = diffLevel + 1; //increase difficulty level for next round
showextLevel(diffLevel);
break;
case 4:
diffLevel = diffLevel + 1; //increase difficulty level for next round
showextLevel(diffLevel);
break;
case 5:
celebration(); // Call the celebration function to show the celebration graphics
display.clearDisplay(); // Clear the display
display.setCursor(14, 15); // Set cursor to top left corner
display.setTextSize(2); // Set text size to 2
display.setTextColor(SSD1306_WHITE); // Set text color to white
display.print("Play"); // Print "Play" on the display
display.setCursor(14, 35); // Set cursor to next line
display.print("Again!!"); // Print "Again!!" on the display
display.display(); // Update the display
ledcWriteTone(Buzzer_pin, 0); // Play no sound while waiting
diffLevel = 1; //reset difficulty level
break;
}
//wait until the play button is pressed to play again
while (digitalRead(playButton) == HIGH) {
delay(50);
}
buttonPressed = false; //reset button pressed flag
}
else {
diffLevel = 1; //reset difficulty level
playLoseSound(); // Play the losing sound
while (digitalRead(playButton) == HIGH) {
//display win or loose
display.clearDisplay(); // Clear the display
display.setCursor(10, 20); // Set cursor to top left corner
display.setTextSize(2); // Set text size to 2
display.setTextColor(SSD1306_WHITE); // Set text color to white
display.print("You Lose!"); // Print "You Lose!" on the display
display.setCursor(12, 40); // Set cursor to top left corner
display.setTextSize(1); // Set text size to 1
display.setTextColor(SSD1306_WHITE); // Set text color to white
display.print("Try Again!"); // Print "Try Again!" on the display
display.display(); // Update the display
}
buttonPressed = false; //reset button pressed flag
}
targetNumber = random(1, 11); // reset random 1–10
currentNumber = 0; //reset current number to 0
}
}
Hamba Run Game function: (watch the video)void play_hamba_run_game(){
int8_t boxHeight = 0;
uint8_t numOfObstacles = 0;
bool cowJump = false;
unsigned long currentObstacleTime = millis();
if(currentObstacleTime - lastObstacleTime >= random(100,1000)) {
lastObstacleTime = currentObstacleTime; // Store the current time
//select obstacle number and height to send from right to left
numOfObstacles = random(0,3); //random number of obstacles between 0 and 2
boxHeight = random(5, 22); //random height of obstacles
}
//scan through the whole screen width
for(int i=display.width(); i>=-4; i-=4) {
//increment score
score += 0.2; // Increase score
int scoreInt = round(score); // Round score to nearest integer
//check if the button is pressed to bool jump the cow
if(buttonPressed == true) { //stop when button is pressed
cowJump = true;
buttonPressed = false; //reset button pressed flag
}
if (cowJump) {
cowPosition += 5; // Move the cow up
if (cowPosition >= 30) { // If the cow reaches the peak height
cowJump = false; // Start moving down
}
} else {
if (cowPosition > 0) {
cowPosition -= 2; // Move the cow down
}
}
//check if the cow hits the obstacle
//if the number of obstacles is more than 0 and the obstacle is in the cow's x range and the cow's y position is less than the obstacle height
//if the obstacle count is 2 then the obstacle must pass more to the left before hitting the cow
if (numOfObstacles > 0 && i <= 26 && i >= (numOfObstacles == 2 ? 2 : 16) && (cowPosition+10) <= boxHeight + 10) {
// Cow hits the obstacle
playLoseSound(); // Play the losing sound
delay(500); // Wait for a moment
showLose(); // Show lose message and update
//show score
display.setCursor(30, 50); // Set cursor below text
display.setTextSize(1); // Set text size to 1
display.setTextColor(SSD1306_WHITE); // Set text color to white
display.print("Score: "); // Print "Score: " on the display
display.print(scoreInt); // Print the score on the display
display.display(); // Update the display
cowPosition = 0; // Reset cow position
numOfObstacles = 0; // Reset obstacles
lastObstacleTime = millis(); // Reset obstacle timer
delay(2500); // Show the message for 2 seconds
score = 0; // Reset score
break; // Exit the for loop to restart the game
}
else{
//show game on the screen
display.clearDisplay(); // clear the display
//draw the cow horn
display.drawBitmap(20, 48-cowPosition, cow_horn, 12, 16, SSD1306_WHITE); // Draw cow with horn
//draw the moving obstacle
switch (numOfObstacles) {
case 0:
//no obstacle
break;
case 1:
display.fillRect(i, 64-boxHeight, 4, boxHeight, SSD1306_WHITE); // Draw rectangle
break;
case 2:
display.fillRect(i, 64-boxHeight, 4, boxHeight, SSD1306_WHITE); // Draw rectangle
display.fillRect(i+10, 64-boxHeight, 4, boxHeight, SSD1306_WHITE); // Draw rectangle
break;
}
//draw the score
display.setCursor(0,0); // set cursor to top left corner
display.setTextSize(1); // set text size to 1
display.setTextColor(SSD1306_WHITE); // set text color to white
display.print("Score: "); // show level text
display.print(scoreInt); // print the current difficulty level
display.display(); // Update screen with each newly-drawn rectangle
}
//flash the red and blue lights
unsigned long currentMillis = millis(); // Get the current time
if(currentMillis - previousMillis >= 500) { // If 500 milliseconds have passed
previousMillis = currentMillis; // Store the current time
ledState = !ledState; // Change the state of the LED
digitalWrite(RedLights, ledState); // Turn on RedLights
digitalWrite(GreenLights, !ledState); // Turn off GreenLights
//play buzzer sound
ledcWriteTone(Buzzer_pin, 1000);
}
else {
ledcWriteTone(Buzzer_pin, 0); // Turn off buzzer sound
}
}
}
The code under "void loop()" only checks the status of the button, which game to play, and when to stop a game, and shows the menu.void loop() {
if (endGame) drawMenu(); //show menu when no gameq is running
else if (runHambaRun == true && !endGame) play_hamba_run_game(); // Call the play_hamba_run_game function to run the game
else if (runHambaMatch == true && !endGame) play_hamba_match_game(); // Call the play_hamba_match_game function to run the game
else if (runNumberHold == true && !endGame) play_number_hold_game(); // Call the play_number_hold_game function to run the game
//exit from any game if button is held for more than 2 seconds
if (digitalRead(playButton) == LOW){
if(millis() - pressStartTime > 2000) {
if(digitalRead(playButton) == LOW) {
digitalWrite(RedLights, LOW); // Turn off RedLights
digitalWrite(GreenLights, LOW); // Turn off GreenLights
ledcWriteTone(Buzzer_pin, 0); // Turn off buzzer sound
runHambaRun = false;
runHambaMatch = false;
runNumberHold = false;
endGame = true; // Set flag to indicate game is over
diffLevel = 1; //reset difficulty level
}
}
}
//show menu until a game is selected
if (buttonPressed) {
buttonPressed = false;
// Debounce check
delay(50);
if (digitalRead(playButton) == LOW) {
unsigned long pressDuration = 0;
// Wait until button released
while (digitalRead(playButton) == LOW) {
pressDuration = millis() - pressStartTime;
if (pressDuration > 1000 && !buttonHeld) { // long press
buttonHeld = true;
endGame = false; // Exit menu mode
menuAction(currentMenu);
break;
}
}
// If short press (less than 1s)
if (!buttonHeld) {
currentMenu = (currentMenu + 1) % menuCount; // Move to next menu item
drawMenu();
}
}
}
}
All the comments are added to the code, so tweak the code as you wish!
Uploading the CodeTo upload the code, you can directly copy the main.cpp file, paste into the Arduino IDE, install the necessary libraries, and hit upload.
If you use Visual Studio Code (platformIO) like me, use the following platformio.ini file to upload code without any issues:
[env:heltec_wifi_loRa_32_v2] #using heltec_wifi_kit_v2 board will not work
platform = espressif32
board = heltec_wifi_loRa_32_v2
framework = arduino
monitor_speed = 115200
upload_speed = 115200
lib_deps =
adafruit/Adafruit SSD1306@^2.5.13
Generate Char Array From ImagesI have used the https://javl.github.io/image2cpp/ website to generate an image from PNG images.
Initially, I drew some images on Illustrator and exported them as PNG. Uploaded that on the website and tweaked accordingly.
The screenshot shows the process of generating the char code for a cow horn.
The resolution is not good, though.
The EndAnd you're done! The game is very fun to play! It's like a fidget toy to me. I look forward to adding more games and sharing them with you guys. Cheers!
Comments