Learn how to build a simple game with an embedded ESP32 board in this article by Agus Kurniawan, an independent technology consultant, author, and lecturer with experience for 18+ years in various software development projects, delivering materials in training and workshop and delivering technical writings.
This article will look at developing a game with an embedded ESP32 board and some embedded modules. Here, you will learn how to work with a joystick, buttons, sound, and an LCD. Before we begin, make sure you have the following things ready:
· A computer with an OS installed, such as Windows, Linux, or mac
· An ESP32 development board – it is recommended to use the ESP-WROVER-KIT v4 board from Espressif
Introducing game-embedded systems
You will be familiar with the Game Boy, an 8-bit handheld game console developed by Nintendo. This console includes a joystick, buttons, and an LCD. A joystick is used to move an object from one place to another, while buttons are usually used for actions such as firing and jumping.
A game embedded system is described in a general sense in figure 1. Along with the features mentioned, having a sound system increases a game's potential in terms of entertainment. For this purpose, we need a sound actuator such as a speaker to generate sounds for the game:
Next, we will review a joystick sensor module.
Introducing the joystick sensor module
If you have experience with playing games on consoles such as PlayStation or Xbox, you will be familiar with using joystick control to manage game movements and perform various actions. An analog joystick usually consists of two potentiometers. This sensor generates 2D direction points.
There are many cheap analog joystick sensors available in online electronics stores such as AliExpress and Alibaba. One of the analog joystick models is Thumb Joystick from SparkFun, found at https://www.sparkfun.com/products/9032. You can see this device in figure 2:
An analog joystick usually has five pins: VCC, GND, Vx, Vy, and SW. The Vx and Vy pins represent direction values from the device. Another option is to use a joystick module kit. This is a complete kit that you use directly on your board. You can find the SparkFun Joystick Shield Kit at https://www.sparkfun.com/products/9760. This form can be seen in figure 3:
Technically, an analog joystick can be defined as in figure 4. The movement of an analog joystick is 2D. If we move the joystick to the left, we will get Vx approaching zero. Otherwise, we can perform for Vy, as shown here:
Next, we'll implement the ESP32 program with the joystick sensor.
Working with the joystick sensor module
Connecting an analog joystick to the ESP32 is easy. You can just connect this device to the analog pins. The ESP32 has two ADC1 and ADC2 pins, with some channels. You can find the analog pins with their channels on the ESP32 in figure 5:
Wiring
In this section, we'll perform hardware wiring between the ESP32 and an analog joystick. For demo purposes, I will use the ESP-WROVER-KIT v4.1 board. We can connect our analog joystick with the ESP32 on the following wiring:
· Analog joystick 5V is connected to ESP32 5V
· Analog joystick GND is connected to ESP32 GND
· Analog joystick Vx is connected to ESP32 IO35 (ADC1 channel 7)
· Analog joystick Vy is connected to ESP32 IO15 (ADC2 channel 3)
You can see the hardware wiring in figure 6, as follows:
Next, we write a project for the analog joystick and ESP32.
Creating a project
In this section, we will create a project called joystickdemo. Figure 7 shows a sample project structure that we will use:
Our main program is the joystickdemo.c file. We can copy the file content from weather.c from https://github.com/PacktPublishing/Internet-of-Things-Projects-with-ESP32/tree/master/Chapter03/joystickdemo/main. Next, we'll write a program on the joystickdemo.c file.
Writing the program
Our program will read the analog joystick positions, x and y, through the ADC pins. Then, we display these positions on the LCD. Firstly, we modify the tft_demo() function. We call joystick_demo() in the tft_demo() function as follows:
void tft_demo() {...joystick_demo();}
We declare the joystick_demo() function to read the analog joystick via ADC. The result of the ADC measurement is displayed on the LCD. To retrieve analog data from ADC1, we perform the following steps:
· Set ADC bit length using the adc1_config_width() function
· Set ADC attenuation with the adc1_config_channel_atten() function
· To get ADC value, we can use the adc1_get_raw() function
For ADC2, we don't need to call the adc1_config_width() function. Instead, we call adc2_get_raw() with the ADC bit-length parameter.
The following are the complete codes for the joystick_demo() function:
static void joystick_demo(){int y;TFT_resetclipwin();adc1_config_width(ADC_WIDTH_12Bit);adc1_config_channel_atten(ADC1_CHANNEL_7, ADC_ATTEN_11db);adc2_config_channel_atten(ADC2_CHANNEL_3, ADC_ATTEN_11db);disp_header("JOYSTIK DEMO");update_header(NULL, "Move your joystick");char tmp_buff[64];int joyX, joyY;while(1){joyX = adc1_get_raw(ADC1_CHANNEL_7);adc2_get_raw(ADC2_CHANNEL_3,ADC_WIDTH_12Bit, &joyY);y = 4;sprintf(tmp_buff, "x: %d y: %d ", joyX,joyY);TFT_print(tmp_buff, 4, y);vTaskDelay(500 / portTICK_PERIOD_MS);// clear textTFT_clearStringRect(4,y,tmp_buff);tmp_buff[0]='\0';}}
Save the program. After doing this, we compile and flash this program onto the ESP32 board.
Running the program
You can compile and flash the joystickdemo program onto the ESP32. To do this, just type this command:
$ make flash
Make sure that the ESP32 serial port is correct.
After the screen is flashed, you can see the LCD display on the screen. Try to move the dot position of the screen by changing direction using the analog joystick. Figure 8 shows a sample of the program output on the LCD:
You can see this demo on my YouTube account at https://youtu.be/lIVEkXa16Fg. Next, we'll learn how to work with a sound buzzer on the ESP33 board.
Working with a sound buzzer
In this section, we'll work with sound. Most games usually use music to provide a sound background. For a simple sound device, we can use sound buzzer devices. We can use a PC-mountable mini-speaker - PC Mount (12mm, 2.048kHz) from SparkFun, found at https://www.sparkfun.com/products/7950. You can see this in Figure 9:
Next, we connect the sound buzzer to the ESP32 board.
Connecting the sound buzzer with the ESP32
A sound buzzer has two pins. One pin is connected to the GPIO and the other is connected to the GND. We connect a sound buzzer to the ESP32 IO27 as follows:
Next, we write a program to access the sound buzzer device.
Writing a program for the sound buzzer with the ESP32
To do this, we create a project called buzzer. You can see the project structure in figure 11. The buzzer.c file is our main program:
Firstly, we declare all required header files and define IO27 for the sound buzzer:
#include#include "freertos/FreeRTOS.h"#include "freertos/task.h"#include "driver/gpio.h"#include "sdkconfig.h"#define BUZZER 27
We also define the main entry on the app_main() function. This function will execute the buzzer_task()function:
void app_main(){xTaskCreate(&buzzer_task, "buzzer_task", configMINIMAL_STACK_SIZE, NULL, 5, NULL);}
Technically, we generate sound on the sensor by giving HIGH on IO27. We can use the gpio_set_level() function for this. The following is an implementation of the buzzer_task() function:
void buzzer_task(void *pvParameter){// set gpio and its directiongpio_pad_select_gpio(BUZZER);gpio_set_direction(BUZZER, GPIO_MODE_OUTPUT);int sounding = 1;while(1) {if(sounding==1){gpio_set_level(BUZZER, 1);sounding = 0;}else {gpio_set_level(BUZZER, 0);sounding = 1;}vTaskDelay(1000 / portTICK_PERIOD_MS);}}
Once you have saved this program, you can compile and flash the program onto the ESP32 board. When this has successfully completed, you should hear a sound from the buzzer device.
Demo – building a simple embedded game
In this section, we will develop a simple game. To do this, we integrate our previous experiences using the LCD, analog joystick, and sound buzzer. We will build a ball game here. For the implementation of this game project, we use the ESP-WROVER-KIT for ESP32 board.
Let's start!
The game scenario
Each game has a scenario. Some games also define some levels for users. In this project, we'll make a simple game scenario. We define our game flowchart in Figure 12, as follows:
We can build a game scenario as follows:
1. Initialize a game by populating a circle with a different radius. These circles are represented as sprites.
2. Set our ball sprite on a certain coordinate.
3. The user can move the ball sprite using the analog joystick.
4. If our ball is inside a circle, we turn on the sound buzzer for a few seconds.
5. If not, we don't do anything.
6. If all circles are deleted, the game will be completed. Game over will be displayed on the LCD.
Next, we perform wiring and writing a program for the game.
Developing the game program
After integrating the wiring, you can create a project called game. You can copy a project from https://github.com/PacktPublishing/Internet-of-Things-Projects-with-ESP32/tree/master/Chapter02/weather. Figure 13 shows our project structure:
To detect collision between our sprite and circle sprites, we can use a simple method. This involves calculating the distance between our position and circle center point. Then, we compare the point-to-circle distance and circle radius. If our distance is lower than the circle radius, this means that our sprite hits the circle. This is demonstrated in Figure 14:
Now, we modify the tft_demo() function codes on game.c. We call the game_demo() function on the end of the code lines from the tft_demo() function, as follows:
void tft_demo() {...game_demo();}
Our game scenario is implemented in the game_demo() function.
1. Firstly, we initialize our circle sprites, ADC, and sound buzzer, as follows:
static void game_demo(){int x, y, r, i, n;n = 10;Circle_Sprite circles[n];// initialize ADCadc1_config_width(ADC_WIDTH_12Bit);adc1_config_channel_atten(ADC1_CHANNEL_7, ADC_ATTEN_11db);adc2_config_channel_atten(ADC2_CHANNEL_3, ADC_ATTEN_11db);// set gpio and its direction for buzzergpio_pad_select_gpio(BUZZER);gpio_set_direction(BUZZER, GPIO_MODE_OUTPUT);// initialize screenTFT_resetclipwin();disp_header("Circle Game Demo");update_header(NULL, "Move your joystick to circle");
2. We generate a random position and radius for all circles and save all circle data in the circles[] variable, as follows:
// generate circlesfor(i=0;ix = rand_interval(16, dispWin.x2-16);y = rand_interval(32, dispWin.y2-32);r = rand_interval(8, 16);color_t c = random_color();TFT_fillCircle(x,y,r,c);circles[i].x = x;circles[i].y = y;circles[i].r = r;circles[i].deleted = 0;circles[i].color = c;}
3. We show our sprite with a current position of x=100 and y=100:
int joyX, joyY;int running = 1;r = 8;int curr_x = 100, curr_y = 100;int cx = 151, cy = 212;TFT_fillCircle(curr_x,curr_y,r,TFT_RED);vTaskDelay(1000 / portTICK_PERIOD_MS);TFT_drawCircle(curr_x,curr_y,r,TFT_BLACK);char tmp_buff[64];int sound = 0;
4. Now, we perform looping with while() syntax. For this, we read the analog joystick and change our sprite position based on the analog joystick input.
We also detect whether our sprite hits or not by calling the check_insideCircle() function. If it does, we delete our circle from the screen. If our sprite moves to out of the boundaries of the LCD, we put it on the end of the LCD position by setting curr_x and curr_y.
5. Lastly, if all circles are hit, we finish our game:
while(running){joyX = adc1_get_raw(ADC1_CHANNEL_7);adc2_get_raw(ADC2_CHANNEL_3,ADC_WIDTH_12Bit, &joyY);joyX = map_to_scren(joyX,0,4095,0,dispWin.x2);joyY = map_to_scren(joyY,0,4095,0,dispWin.y2);// validate point to screencurr_x = curr_x + joyX - cx;curr_y = curr_y + joyY - cy;
Check if the cursor is outside our screen and adjust the values to keep it on the screen.
if(curr_x<8)curr_x = 8;if(curr_x>(dispWin.x2-8))curr_x = dispWin.x2 - 8;if(curr_y<32)curr_y = 32;if(curr_y>(dispWin.y2-32))curr_y = dispWin.y2-32;
6. Looping over all circles that we keep in the circles[] array and skipping the ones that were previously deleted, we need to check if our ball is inside the target circle. This is done by the check_insideCircle() function. If that is the case, based on the stored coordinates, we will fill the circle with black (TFT_BLACK), the same color as our background, thus giving the impression that the circle has disappeared.
// check a ball inside a circlefor(i=0;iif(circles[i].deleted == 1)continue;if(check_insideCircle(curr_x,curr_y,circles[i])==1){gpio_set_level(BUZZER, 1);sound = 1;circles[i].deleted = 1;TFT_fillCircle(circles[i].x,circles[i].y,circles[i].r,TFT_BLACK);break;}}TFT_fillCircle(curr_x,curr_y,r,TFT_RED);
7. Change the color to yellow and then to black again. Then, play a short beep on buzzer.
_fg = TFT_YELLOW;sprintf(tmp_buff, "x: %d y: %d ", curr_x,curr_y);TFT_print(tmp_buff, 4, 4);vTaskDelay(200 / portTICK_PERIOD_MS);TFT_fillCircle(curr_x,curr_y,r,TFT_BLACK);_fg = TFT_BLACK;TFT_print(tmp_buff, 4, 4);if(sound==1){gpio_set_level(BUZZER, 0);sound = 0;}
If it is the last circle on the screen, then it is the end of the game since all the circles were hit.
// check if finishedint nn = 0;for(i=0;iif(circles[i].deleted == 1)nn++;}if(nn==n)break;}
8. At the finishing stage, we show Game Over on the LCD using the TFT_print() function, as follows:
TFT_resetclipwin();disp_header("ESP32 Game DEMO");TFT_setFont(COMIC24_FONT, NULL);int tempy = TFT_getfontheight() + 4;_fg = TFT_ORANGE;TFT_print("ESP32-", CENTER, (dispWin.y2-dispWin.y1)/2 - tempy);TFT_setFont(UBUNTU16_FONT, NULL);_fg = TFT_CYAN;TFT_print("Game Over", CENTER, LASTY+tempy);tempy = TFT_getfontheight() + 4;TFT_setFont(DEFAULT_FONT, NULL);while(1){}}
We can implement object collision in the check_insideCircle() function. For this, we will use a math formula from Figure 14. You can write codes for the check_insideCircle() function as follows:
int check_insideCircle(int x, int y, Circle_Sprite sp){int d = sqrt(pow(x-sp.x,2)+pow(y-sp.y,2));if(d<=sp.r)return 1;else{int rr = d - sp.r - 8;if(rr<=0)return 1;elsereturn 0;}}
9. Save this program.
You are now ready to compile and flash the program onto the ESP32 board.
Playing the game
Compile and flash our game project onto the ESP32.
To apply a game, you move your sprite to hit all circles. If all circles are hit, a game will finish. You can see a form of the game program in Figure 15:
I have recorded how to play this game on my YouTube account. You can see it at https://youtu.be/sXmZ1pJ_l1E.
You have now learned how to work with an analog joystick to control movement, as well as explore a simple sound device with a sound buzzer and develop a simple game. We hit all circles in order to finish the game.
If you found this article interesting, you can explore Internet of Things Projects with ESP32 to create and program the Internet of Things projects using the Espressif ESP32. Internet of Things Projects with ESP32 will serve as a fundamental guide for developing an ESP32 program.