The purpose of this project is to be able to play a snake game with a joystick. The game is displayed on the JDMO.96A-1 display and can be played using a joystick to set the direction in which the snake will move. If the snake eats himself a “GAME OVER” text will be displayed, otherwise, the snake grows after each food that it ate.
I used the following components for this project:
This function was used to start the display and clear it, and after that to initialize the snake and set its color to white.
void setup() { Serial.begin(115200); if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) { Serial.println(F("SSD1306 allocation failed")); for (;;) ; } display.clearDisplay(); // Clear the display buffer // Initialize the snake for (int i = snakeSize - 1; i >= 0; i--) { snakeX[i] = snakeSize - 1 - i; snakeY[i] = 0; } // Set text color and size display.setTextColor(SSD1306_WHITE); display.setTextSize(1); // Start the game loop gameLoop(); }
Here I update the snake position then I draw it. After the snake is on the screen, I spawn a pixel random on the screen that can be eaten by the snake.
void gameLoop() { while (true) { // Update the snake position updateSnake(); // Draw the snake drawSnake(); // Check for joystick input checkJoystick(); // Check if the snake eats the pixel if (snakeX[0] == pixelX && snakeY[0] == pixelY) { snakeSize++; spawnPixel(); // Spawn a new pixel after eating the current one } delay(200); } }
On this function I update the entire snake positions (each of the pixels that make the body of the snake). Then I move the head on the direction of the pixel, I get this direction from a function called: checkJoystick. After that, I check if the snake hit the boundaries of the screen to teleport on the other side of the screen.
void updateSnake() { // Update the entire snake's position for (int i = snakeSize - 1; i > 0; i--) { snakeX[i] = snakeX[i - 1]; snakeY[i] = snakeY[i - 1]; } // Move the head of the snake based on the direction switch (direction) { case 0: // Right snakeX[0]++; break; case 1: // Down snakeY[0]++; break; case 2: // Left snakeX[0]--; break; case 3: // Up snakeY[0]--; break; } // Check for collisions with the screen boundaries if (snakeX[0] >= SCREEN_WIDTH / 8) { snakeX[0] = 0; } else if (snakeX[0] < 0) { snakeX[0] = SCREEN_WIDTH / 8 - 1; } if (snakeY[0] >= SCREEN_HEIGHT / 8) { snakeY[0] = 0; } else if (snakeY[0] < 0) { snakeY[0] = SCREEN_HEIGHT / 8 - 1; } for (int i = 0; i < snakeSize; i++) { Serial.println("Before collision (" + String(snakeX[i]) + "," + String(snakeY[i]) + ")"); } // Check for collisions with the snake's body, excluding the head for (int i = 1; i < snakeSize; i++) { if (snakeX[0] == snakeX[i] && snakeY[0] == snakeY[i]) { // Snake head collided with its body, end the game endGame(); } } }
Used to draw the snake on the screen.
void drawSnake() { // Clear the display display.clearDisplay(); // Draw the snake for (int i = 0; i < snakeSize; i++) { display.fillRect(snakeX[i] * 8, snakeY[i] * 8, 8, 8, SSD1306_WHITE); } // Draw the pixel display.fillRect(pixelX * 8, pixelY * 8, 8, 8, SSD1306_WHITE); // Display the buffer display.display(); }
I used this function to check the position of the joystick so I can give the snake the direction that I want it to move to. In this code I also made impossible for the snake to go in the opposite direction of the current one
void checkJoystick() { int xValue = analogRead(JOYSTICK_X); int yValue = analogRead(JOYSTICK_Y); int OK = 0; if (xValue < JOYSTICK_THRESHOLD && lastXValue <= JOYSTICK_THRESHOLD) { // Move right if (direction != 0 && direction != 2) { direction = 0; OK = 1; } } else if (xValue > JOYSTICK_THRESHOLD2 && lastXValue >= -JOYSTICK_THRESHOLD) { // Move left if (direction != 2 && direction != 0) { direction = 2; OK = 1; } } if (yValue < JOYSTICK_THRESHOLD && lastYValue <= JOYSTICK_THRESHOLD && OK == 0) { // Move up if (direction != 3 && direction != 1) { direction = 3; } } else if (yValue > JOYSTICK_THRESHOLD2 && lastYValue >= -JOYSTICK_THRESHOLD && OK == 0) { // Move down if (direction != 1 && direction != 3) { direction = 1; } } lastXValue = xValue; lastYValue = yValue; }
I spawn the food of the snake as a pixel random on the screen.
void spawnPixel() { // Generate random coordinates for the pixel pixelX = random(SCREEN_WIDTH / 8); pixelY = random(SCREEN_HEIGHT / 8); // Ensure the pixel does not spawn on the snake for (int i = 0; i < snakeSize; i++) { while (pixelX == snakeX[i] && pixelY == snakeY[i]) { pixelX = random(SCREEN_WIDTH / 8); pixelY = random(SCREEN_HEIGHT / 8); } } // Draw the pixel on the display display.fillRect(pixelX * 8, pixelY * 8, 8, 8, SSD1306_WHITE); display.display(); }
Print on the screen the GAME OVER message if the snake hit himself.
void endGame() { // Add your game-over logic here display.clearDisplay(); display.setCursor(8,8); display.print("GAME OVER!"); display.display(); delay(5000); // Display "You Lose" for 5 seconds // Print snake coordinates for debugging Serial.println("Snake Coordinates:"); for (int i = 0; i < snakeSize; i++) { Serial.println("(" + String(snakeX[i]) + "," + String(snakeY[i]) + ")"); } while (true) { // Do nothing } }
Here I declared the screen and the pins that I used. I also declared 2 THRESHOLDS because when the joystick was on the middle, I constantly get values between 1850 and 1900 and I don’t want that those values to influent the movement. On the end I declared snake characteristics.
#define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 32 // Define the I2C address for the display #define OLED_ADDR 0x3C Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_ADDR); #define JOYSTICK_X A5 #define JOYSTICK_Y A6 #define JOYSTICK_THRESHOLD 1800 #define JOYSTICK_THRESHOLD2 2000 int snakeSize = 3; int snakeX[100], snakeY[100]; int direction = 0; // 0: right, 1: down, 2: left, 3: up
The library that I used are:
#include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h>
And mostly I used the for the LCD.
As a conclusion for the project, I realized a snake game that can be controlled using a joystick. The purpose of the game is to eat as many food as possible and to not touch the rest of the body.
https://www.optimusdigital.ro/ro/senzori-senzori-de-atingere/742-modul-joystick-ps2-biaxial-negru-cu-5-pini.html?search_query=joystick&results=42
https://cleste.ro/ecra-oled-0-96-inch.html
https://cleste.ro/placa-dezvoltare-nodemcu-wifi-bluetooth-esp32.html
https://randomnerdtutorials.com/esp32-pinout-reference-gpios/
https://esp32io.com/tutorials/esp32-joystick