Introduction
This guide will walk you through creating a multiplayer Pong game using two ESP32 boards. The boards communicate over Wi-Fi using UDP, chosen for its low latency, making it ideal for real-time applications like gaming. You’ll learn how to connect OLED displays, read input from buttons, and establish a communication protocol using UDP.
Components Required
To complete this project, you’ll need the following components:
- 2 x ESP32 Boards
- 2 x SH1106 OLED Displays (SSD1306 can also be used with a slight modification in the code)
- 4 x Push Buttons (2 for each board)
- Jumper Wires
- Breadboards
- USB Cables
1. Hardware Setup
Connecting the OLED Displays
The SH1106 OLED displays will connect to the ESP32 boards via I2C. The wiring is as follows:
- SDA (Data Line): Connect to ESP32 pin 21
- SCL (Clock Line): Connect to ESP32 pin 22
- VCC: Connect to 3.3V on ESP32
- GND: Connect to GND on ESP32
Connecting the Push Buttons
Each ESP32 will have two buttons to control the paddles. Connect them as follows:
- Button 1 (Up): Connect one leg to GPIO 2 and the other leg to GND.
- Button 2 (Down): Connect one leg to GPIO 4 and the other leg to GND.
Note: You can use pull-up resistors or enable the internal pull-ups in the code to ensure stable button inputs.
2. Software Setup
Installing Required Libraries
Ensure the following libraries are installed in your Arduino IDE:
- Adafruit_GFX: For graphics support on the OLED display.
- Adafruit_SH1106: For interfacing with the SH1106 OLED display. (Use Adafruit_SSD1306 if you're using the SSD1306 display instead).
- WiFi: For handling Wi-Fi connectivity.
- WiFiUdp: For UDP communication.
You can install these libraries by navigating to Sketch > Include Library > Manage Libraries in the Arduino IDE.
3. Writing the Code
You will create two separate programs: one for the server (ESP32 #1) and one for the client (ESP32 #2). The server will manage the game logic, while the client will send paddle movements and receive game state updates.
Program 1: Server (ESP32 #1)
The server listens for UDP packets from the client and updates its game state accordingly.
#include <WiFi.h> #include <WiFiUdp.h> #include <U8g2lib.h> #define BUTTON_UP 2 #define BUTTON_DOWN 4 const char* ssid = "PongGame"; // SSID of Wi-Fi network const char* password = "12345678"; // Password of Wi-Fi network WiFiUDP udp; const unsigned int localUdpPort = 4210; // Local UDP port for listening char incomingPacket[255]; // Buffer for incoming packets IPAddress clientIP; U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); int paddle1Y = 30, paddle2Y = 30; int ballX = 64, ballY = 32; int ballVelX = 1, ballVelY = 1; void setup() { Serial.begin(115200); pinMode(BUTTON_UP, INPUT_PULLUP); pinMode(BUTTON_DOWN, INPUT_PULLUP); u8g2.begin(); u8g2.clearBuffer(); // Start Wi-Fi as an access point WiFi.softAP(ssid, password); Serial.println("WiFi AP started"); udp.begin(localUdpPort); Serial.printf("Now listening on UDP port %d\n", localUdpPort); } void loop() { handleButtons(); updateGame(); drawGame(); sendGameState(); receivePaddlePosition(); delay(20); // Adjust delay for game speed } void handleButtons() { if (digitalRead(BUTTON_UP) == LOW) { paddle1Y -= 2; if (paddle1Y < 0) paddle1Y = 0; } if (digitalRead(BUTTON_DOWN) == LOW) { paddle1Y += 2; if (paddle1Y > 48) paddle1Y = 48; // Limit paddle position } } void updateGame() { ballX += ballVelX; ballY += ballVelY; // Ball collision with top/bottom if (ballY <= 0 || ballY >= 64) ballVelY = -ballVelY; // Ball collision with paddles if (ballX <= 4 && ballY >= paddle1Y && ballY <= paddle1Y + 16) ballVelX = -ballVelX; if (ballX >= 124 && ballY >= paddle2Y && ballY <= paddle2Y + 16) ballVelX = -ballVelX; // Ball out of bounds if (ballX <= 0 || ballX >= 128) { ballX = 64; ballY = 32; } } void drawGame() { u8g2.clearBuffer(); u8g2.drawBox(2, paddle1Y, 2, 16); // Draw paddle 1 u8g2.drawBox(124, paddle2Y, 2, 16); // Draw paddle 2 u8g2.drawDisc(ballX, ballY, 2); // Draw ball u8g2.sendBuffer(); } void sendGameState() { String gameState = String(paddle1Y) + "," + String(ballX) + "," + String(ballY); // Convert the string to a byte array const char* gameStateCStr = gameState.c_str(); udp.beginPacket(clientIP, localUdpPort); udp.write((const uint8_t*)gameStateCStr, strlen(gameStateCStr)); // Send game state to client udp.endPacket(); } void receivePaddlePosition() { int packetSize = udp.parsePacket(); if (packetSize) { int len = udp.read(incomingPacket, 255); if (len > 0) { incomingPacket[len] = 0; paddle2Y = atoi(incomingPacket); // Get paddle 2 position from client clientIP = udp.remoteIP(); // Get client IP address } } }
Program 2: Client (ESP32 #2)
The client sends its paddle position to the server and receives the game state to update its display.
#include <WiFi.h> #include <WiFiUdp.h> #include <U8g2lib.h> #define BUTTON_UP 2 #define BUTTON_DOWN 4 const char* ssid = "PongGame"; // SSID of Wi-Fi network const char* password = "12345678"; // Password of Wi-Fi network WiFiUDP udp; const char* serverIP = "192.168.4.1"; // IP of the server (Player 1) const unsigned int localUdpPort = 4210; // Local UDP port for listening char incomingPacket[255]; // Buffer for incoming packets U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); int paddle2Y = 30; int ballX, ballY, paddle1Y; void setup() { Serial.begin(115200); pinMode(BUTTON_UP, INPUT_PULLUP); pinMode(BUTTON_DOWN, INPUT_PULLUP); u8g2.begin(); u8g2.clearBuffer(); // Connect to Wi-Fi network WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi..."); } Serial.println("Connected to WiFi"); udp.begin(localUdpPort); } void loop() { handleButtons(); sendPaddlePosition(); receiveGameState(); drawGame(); delay(20); // Adjust delay for game speed } void handleButtons() { if (digitalRead(BUTTON_UP) == LOW) { paddle2Y -= 2; if (paddle2Y < 0) paddle2Y = 0; } if (digitalRead(BUTTON_DOWN) == LOW) { paddle2Y += 2; if (paddle2Y > 48) paddle2Y = 48; // Limit paddle position } } void sendPaddlePosition() { String paddlePos = String(paddle2Y); const char* paddlePosCStr = paddlePos.c_str(); udp.beginPacket(serverIP, localUdpPort); udp.write((const uint8_t*)paddlePosCStr, strlen(paddlePosCStr)); // Send paddle position to server udp.endPacket(); } void receiveGameState() { int packetSize = udp.parsePacket(); if (packetSize) { int len = udp.read(incomingPacket, 255); if (len > 0) { incomingPacket[len] = 0; sscanf(incomingPacket, "%d,%d,%d", &paddle1Y, &ballX, &ballY); // Update game state } } } void drawGame() { u8g2.clearBuffer(); u8g2.drawBox(2, paddle1Y, 2, 16); // Draw paddle 1 u8g2.drawBox(124, paddle2Y, 2, 16); // Draw paddle 2 u8g2.drawDisc(ballX, ballY, 2); // Draw ball u8g2.sendBuffer(); }
4. Testing and Troubleshooting
- Upload the Code: Upload the server code to one ESP32 and the client code to the other.
- Connect to Wi-Fi: Power on the ESP32 boards. The server will create a Wi-Fi access point, and the client will connect to it.
- Check Serial Monitor: Open the serial monitor on both boards to ensure they connect correctly.
-
Adjust Timing: If the game is too fast or too slow, adjust the
delay(20)
in both codes to fine-tune the game speed.
5. Conclusion
You’ve now built a basic multiplayer Pong game using two ESP32 boards. This project is a great way to explore the capabilities of the ESP32 for real-time applications and communication over Wi-Fi. You can expand on this project by adding features like scoring, sound effects, or even a more sophisticated graphical interface.
Enjoy your game!