The classic game Snake has been with us for a long time on various devices – on PCs, Texas Instruments calculators, or even the venerable Nokia 3210. Thanks to its simple mechanics, the game can be implemented on almost any device with some processing power. So why not also on an Arduino?
In this project, you’ll build a version of Snake on an Arduino UNO R4. You’ll use an OLED display with 128×64 pixels, and control the game with a small joystick. Although the game runs on the Arduino UNO, it might be a bit slow for skilled players. Therefore, an ESP32 will also be used later, which has more power and provides a smoother gaming experience.
For this project, you need:
- Arduino UNO R4 and/or ESP32
- OLED Display with 128×64 pixels
- Joystick
Playing Snake on the Arduino UNO
First, we have the version for the Arduino UNO. The “new” UNO R4 is recommended here because it has significantly more power than its predecessor, the R3. It doesn’t matter whether you use the WiFi or Minima version for this project. However, if you’re considering getting an R4, I definitely recommend the WiFi version since it’s only slightly more expensive and allows you to connect your projects to the Internet.
Let’s get to connecting the components: Connect the OLED display and joystick as follows to the Arduino UNO:
Depending on the joystick you use, the pin labeling might vary. For example, instead of VERT, it could be labeled as VRy. Once everything is wired up, you can proceed to the sketch.
Required Libraries
To use your OLED display with the Arduino, you need two libraries. Open the Library Manager in the Arduino IDE, search for, and install these two Adafruit libraries:
- Adafruit SSD1306
- Adafruit GFX
In the sketch, you will also see a third library, Wire.h – this one is pre-installed, so you don’t need to worry about it.
___STEADY_PAYWALL___
The Complete Sketch
Here is the complete sketch for Snake. Copy the following code, create a new sketch, and upload it to your Arduino.
// Playing Snake on Arduino
// Pollux Labs
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Wire.h>
// OLED Display Configuration
#define SCREEN_WIDTH 128 // Width of the OLED display
#define SCREEN_HEIGHT 64 // Height of the OLED display
#define OLED_RESET -1 // OLED reset pin (not used)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Joystick Configuration
#define VRX_PIN A0 // Joystick X-axis pin (analog pin A0 on Arduino Uno)
#define VRY_PIN A1 // Joystick Y-axis pin (analog pin A1 on Arduino Uno)
#define SW_PIN 7 // Joystick button pin (digital pin 7 on Arduino Uno)
// Snake Properties
#define SNAKE_SIZE 4 // Size of each segment of the snake
#define MAX_SNAKE_LENGTH 50 // Maximum length of the snake (reduced for Arduino Uno to save memory)
int snakeX[MAX_SNAKE_LENGTH], snakeY[MAX_SNAKE_LENGTH]; // Arrays to store snake segment positions
int snakeLength = 5; // Initial length of the snake
int directionX = 1, directionY = 0; // Initial movement direction (right)
// Food Properties
int foodX = random(2, (SCREEN_WIDTH - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE; // X-coordinate of the food (not directly at the edge)
int foodY = random(12 / SNAKE_SIZE, (SCREEN_HEIGHT - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE; // Y-coordinate of the food (game area below the frame, not directly at the edge)
// Score
int score = 0;
void setup() {
Serial.begin(9600); // Initialize serial communication with lower baud rate for Arduino Uno
// Initialize display
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Initialize the OLED display with I2C address 0x3C
Serial.println(F("SSD1306 allocation failed"));
for (;;); // Stop if display initialization fails
}
display.clearDisplay(); // Clear display buffer
display.display(); // Show cleared buffer
// Initialize joystick
pinMode(VRX_PIN, INPUT); // Set joystick X-axis pin as input
pinMode(VRY_PIN, INPUT); // Set joystick Y-axis pin as input
pinMode(SW_PIN, INPUT_PULLUP); // Set joystick button pin as input with internal pull-up resistor
// Initialize snake position
for (int i = 0; i < snakeLength; i++) {
snakeX[i] = SCREEN_WIDTH / 2 - (i * SNAKE_SIZE); // Set initial X-coordinates of the snake segments
snakeY[i] = SCREEN_HEIGHT / 2; // Set initial Y-coordinate of the snake segments
}
Serial.println("Setup completed");
}
void loop() {
// Read joystick values
int xValue = analogRead(VRX_PIN); // Read X-axis value from joystick
int yValue = analogRead(VRY_PIN); // Read Y-axis value from joystick
Serial.print("Joystick X: ");
Serial.print(xValue);
Serial.print(" Y: ");
Serial.println(yValue);
// Set direction based on joystick inputs, prevent diagonal movement
if (xValue < 300 && directionX == 0) { // Move left if not currently moving horizontally
directionX = -1;
directionY = 0;
Serial.println("Direction: Left");
} else if (xValue > 700 && directionX == 0) { // Move right if not currently moving horizontally
directionX = 1;
directionY = 0;
Serial.println("Direction: Right");
} else if (yValue < 300 && directionY == 0) { // Move up if not currently moving vertically
directionX = 0;
directionY = -1;
Serial.println("Direction: Up");
} else if (yValue > 700 && directionY == 0) { // Move down if not currently moving vertically
directionX = 0;
directionY = 1;
Serial.println("Direction: Down");
}
// Update snake position
for (int i = snakeLength - 1; i > 0; i--) { // Move each segment to the position of the previous segment
snakeX[i] = snakeX[i - 1];
snakeY[i] = snakeY[i - 1];
}
snakeX[0] += directionX * SNAKE_SIZE; // Update head position in X-direction
snakeY[0] += directionY * SNAKE_SIZE; // Update head position in Y-direction
Serial.print("Snake head X: ");
Serial.print(snakeX[0]);
Serial.print(" Y: ");
Serial.println(snakeY[0]);
// Check for collision with food
if (snakeX[0] == foodX && snakeY[0] == foodY) { // If the snake's head reaches the food
if (snakeLength < MAX_SNAKE_LENGTH) {
snakeLength++; // Increase snake length
score++; // Increase score
Serial.println("Food eaten, snake length: " + String(snakeLength));
}
// Generate new food position (not directly at the edge)
foodX = random(2, (SCREEN_WIDTH - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
foodY = random(12 / SNAKE_SIZE, (SCREEN_HEIGHT - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
Serial.print("New food at X: ");
Serial.print(foodX);
Serial.print(" Y: ");
Serial.println(foodY);
}
// Check for collision with walls
if (snakeX[0] < SNAKE_SIZE || snakeX[0] >= SCREEN_WIDTH - SNAKE_SIZE || snakeY[0] < 10 || snakeY[0] >= SCREEN_HEIGHT - SNAKE_SIZE) {
Serial.println("Collision with wall, game resetting");
resetGame(); // Reset the game if the snake hits a wall
}
// Check for collision with itself
for (int i = 1; i < snakeLength; i++) {
if (snakeX[0] == snakeX[i] && snakeY[0] == snakeY[i]) { // If the snake's head collides with its own body
Serial.println("Collision with self, game resetting");
resetGame(); // Reset the game
}
}
// Draw everything
display.clearDisplay(); // Clear display buffer
// Draw score
display.setTextSize(1); // Set text size
display.setTextColor(SSD1306_WHITE); // Set text color
display.setCursor(0, 0); // Set cursor for score
display.print("Score: ");
display.print(score); // Show current score
// Draw border around playing field
display.drawRect(0, 10, SCREEN_WIDTH, SCREEN_HEIGHT - 10, SSD1306_WHITE); // Draw a white frame around the playing field
// Draw snake
for (int i = 0; i < snakeLength; i++) {
display.fillRect(snakeX[i], snakeY[i], SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE); // Draw each segment of the snake
}
// Draw food
display.fillRect(foodX, foodY, SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE); // Draw food
display.display(); // Show updated buffer
delay(150); // Delay to control snake speed
}
void resetGame() {
// Reset snake properties
snakeLength = 5; // Reset snake length
directionX = 1; // Reset direction to right
directionY = 0;
score = 0; // Reset score
for (int i = 0; i < snakeLength; i++) {
snakeX[i] = SCREEN_WIDTH / 2 - (i * SNAKE_SIZE); // Reset snake position to center
snakeY[i] = SCREEN_HEIGHT / 2;
}
// Generate new food position (not directly at the edge)
foodX = random(2, (SCREEN_WIDTH - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
foodY = random(12 / SNAKE_SIZE, (SCREEN_HEIGHT - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
Serial.println("Game reset");
Serial.print("New food at X: ");
Serial.print(foodX);
Serial.print(" Y: ");
Serial.println(foodY);
}
After uploading, the game should start right away: In the middle of the display, you’ll see the initially 5-pixel long snake, which starts moving. A piece of food appears randomly placed on the playing field. As you probably know, your goal is to eat as much as possible without colliding with the playing field boundary or yourself.
Your score is displayed in the upper left corner. For each extension of your snake, your score increases by 1.
How the Sketch Works
Let’s take a look at some important parts of the sketch.
Setup
The setup()
function performs all the necessary initializations:
- Serial communication is started to output debugging information.
- The OLED display is initialized. If initialization fails, an error message is displayed and the program stops.
void setup() {
Serial.begin(9600);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for (;;);
}
display.clearDisplay();
display.display();
}
- The joystick is initialized by configuring its pins as inputs.
- The snake’s position is initially set to the center of the display.
Loop
The loop()
function runs continuously and handles all aspects of the game, such as control, movement, collision detection, and display output.
Joystick Control
The joystick is used to change the movement direction of the snake. The analog inputs of the joystick provide values that determine which direction the snake moves:
- If the joystick is moved left, the direction is set to left, as long as the snake is not already moving right.
- The same applies to the other directions.
if (xValue < 300 && directionX == 0) {
directionX = -1;
directionY = 0;
Serial.println("Direction: Left");
}
Snake Movement
The snake’s position is updated by looping through each segment of the snake and moving it to the position of the previous segment. The head of the snake is moved in the chosen direction.
for (int i = snakeLength - 1; i > 0; i--) {
snakeX[i] = snakeX[i - 1];
snakeY[i] = snakeY[i - 1];
}
snakeX[0] += directionX * SNAKE_SIZE;
snakeY[0] += directionY * SNAKE_SIZE;
Eating Food
When the snake reaches the food, its length increases, and a new score is calculated. The food is then generated at a new position that is not directly at the edge to make the game easier.
if (snakeX[0] == foodX && snakeY[0] == foodY) {
if (snakeLength < MAX_SNAKE_LENGTH) {
snakeLength++;
score++;
}
foodX = random(2, (SCREEN_WIDTH - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
foodY = random(12 / SNAKE_SIZE, (SCREEN_HEIGHT - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
}
Collision Detection
The loop()
function also checks if the snake collides with the boundaries of the playing field or with itself:
- If the snake collides with a wall, the game is reset.
- If the snake touches its own body, the game is also reset.
if (snakeX[0] < SNAKE_SIZE || snakeX[0] >= SCREEN_WIDTH - SNAKE_SIZE || snakeY[0] < 10 || snakeY[0] >= SCREEN_HEIGHT - SNAKE_SIZE) {
resetGame();
}
OLED Display Output
The OLED display is updated during each loop:
- The score is displayed in the upper left corner.
- A white frame is drawn around the playing field to indicate the boundaries.
- The snake and the food are drawn on the display.
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("Score: ");
display.print(score);
display.drawRect(0, 10, SCREEN_WIDTH, SCREEN_HEIGHT - 10, SSD1306_WHITE);
for (int i = 0; i < snakeLength; i++) {
display.fillRect(snakeX[i], snakeY[i], SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE);
}
display.fillRect(foodX, foodY, SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE);
display.display();
Resetting the Game
The resetGame()
function resets the snake to its initial position and length if a collision is detected. The score is also reset, and a new position for the food is generated.
void resetGame() {
snakeLength = 5;
directionX = 1;
directionY = 0;
score = 0;
for (int i = 0; i < snakeLength; i++) {
snakeX[i] = SCREEN_WIDTH / 2 - (i * SNAKE_SIZE);
snakeY[i] = SCREEN_HEIGHT / 2;
}
}
Playing Snake on ESP32
If you’ve already played Snake on the Arduino UNO, you may have noticed that the game runs quite slowly – even “choppy.” This is due to the rather limited power, which is not sufficient for all the calculations in the loop function to be done quickly enough.
If you have an ESP32, you can easily move the game over and enjoy faster gameplay. Connect the OLED display and joystick as follows to the ESP32:
The Sketch
To run Snake on the ESP32, only a few minor adjustments are needed. First, you’ll assign different pins in the sketch. Additionally, you can increase the maximum snake length – from 50 on the Arduino to 100 on the ESP32:
#define MAX_SNAKE_LENGTH 100 // Maximum length of the snake
Also, adjust the threshold values for the joystick, as the ESP32 has a higher resolution for analog inputs than the Arduino UNO. The Arduino Uno uses a 10-bit resolution, meaning the analog inputs return values from 0 to 1023. In contrast, the ESP32 uses a 12-bit resolution, which gives a range of 0 to 4095.
For movement to the left, for example, a value of 1000 is used. In the sketch for the Arduino UNO, this value was set at 300:
if (xValue < 1000 && directionX == 0) { // Threshold for ESP32
To control the snake’s speed, you use a delay. Since the ESP32 has more power, you set the value in line 147 of the sketch to 100 (instead of 150 for the Arduino UNO).
Below is the complete sketch for the ESP32.
// Playing Snake on ESP32
// Pollux Labs
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Wire.h>
// OLED Display Configuration
#define SCREEN_WIDTH 128 // Width of the OLED display
#define SCREEN_HEIGHT 64 // Height of the OLED display
#define OLED_RESET -1 // OLED reset pin (not used)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Joystick Configuration
#define VRX_PIN 34 // Joystick X-axis pin
#define VRY_PIN 35 // Joystick Y-axis pin
#define SW_PIN 32 // Joystick button pin
// Snake Properties
#define SNAKE_SIZE 4 // Size of each segment of the snake
#define MAX_SNAKE_LENGTH 100 // Maximum length of the snake
int snakeX[MAX_SNAKE_LENGTH], snakeY[MAX_SNAKE_LENGTH]; // Arrays to store snake segment positions
int snakeLength = 5; // Initial length of the snake
int directionX = 1, directionY = 0; // Initial movement direction (right)
// Food Properties
int foodX = random(2, (SCREEN_WIDTH - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE; // X-coordinate of the food (not directly at the edge)
int foodY = random(12 / SNAKE_SIZE, (SCREEN_HEIGHT - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE; // Y-coordinate of the food (game area below the frame, not directly at the edge)
// Score
int score = 0;
void setup() {
Serial.begin(115200); // Initialize serial communication
// Initialize display
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Initialize the OLED display with I2C address 0x3C
Serial.println(F("SSD1306 allocation failed"));
for (;;); // Stop if display initialization fails
}
display.clearDisplay(); // Clear display buffer
display.display(); // Show cleared buffer
// Initialize joystick
pinMode(VRX_PIN, INPUT); // Set joystick X-axis pin as input
pinMode(VRY_PIN, INPUT); // Set joystick Y-axis pin as input
pinMode(SW_PIN, INPUT_PULLUP); // Set joystick button pin as input with internal pull-up resistor
// Initialize snake position
for (int i = 0; i < snakeLength; i++) {
snakeX[i] = SCREEN_WIDTH / 2 - (i * SNAKE_SIZE); // Set initial X-coordinates of the snake segments
snakeY[i] = SCREEN_HEIGHT / 2; // Set initial Y-coordinate of the snake segments
}
Serial.println("Setup completed");
}
void loop() {
// Read joystick values
int xValue = analogRead(VRX_PIN); // Read X-axis value from joystick
int yValue = analogRead(VRY_PIN); // Read Y-axis value from joystick
Serial.print("Joystick X: ");
Serial.print(xValue);
Serial.print(" Y: ");
Serial.println(yValue);
// Set direction based on joystick inputs, prevent diagonal movement
if (xValue < 1000 && directionX == 0) { // Move left if not currently moving horizontally
directionX = -1;
directionY = 0;
Serial.println("Direction: Left");
} else if (xValue > 3000 && directionX == 0) { // Move right if not currently moving horizontally
directionX = 1;
directionY = 0;
Serial.println("Direction: Right");
} else if (yValue < 1000 && directionY == 0) { // Move up if not currently moving vertically
directionX = 0;
directionY = -1;
Serial.println("Direction: Up");
} else if (yValue > 3000 && directionY == 0) { // Move down if not currently moving vertically
directionX = 0;
directionY = 1;
Serial.println("Direction: Down");
}
// Update snake position
for (int i = snakeLength - 1; i > 0; i--) { // Move each segment to the position of the previous segment
snakeX[i] = snakeX[i - 1];
snakeY[i] = snakeY[i - 1];
}
snakeX[0] += directionX * SNAKE_SIZE; // Update head position in X-direction
snakeY[0] += directionY * SNAKE_SIZE; // Update head position in Y-direction
Serial.print("Snake head X: ");
Serial.print(snakeX[0]);
Serial.print(" Y: ");
Serial.println(snakeY[0]);
// Check for collision with food
if (snakeX[0] == foodX && snakeY[0] == foodY) { // If the snake's head reaches the food
if (snakeLength < MAX_SNAKE_LENGTH) {
snakeLength++; // Increase snake length
score++; // Increase score
Serial.println("Food eaten, snake length: " + String(snakeLength));
}
// Generate new food position (not directly at the edge)
foodX = random(2, (SCREEN_WIDTH - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
foodY = random(12 / SNAKE_SIZE, (SCREEN_HEIGHT - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
Serial.print("New food at X: ");
Serial.print(foodX);
Serial.print(" Y: ");
Serial.println(foodY);
}
// Check for collision with walls
if (snakeX[0] < SNAKE_SIZE || snakeX[0] >= SCREEN_WIDTH - SNAKE_SIZE || snakeY[0] < 10 || snakeY[0] >= SCREEN_HEIGHT - SNAKE_SIZE) {
Serial.println("Collision with wall, game resetting");
resetGame(); // Reset the game if the snake hits a wall
}
// Check for collision with itself
for (int i = 1; i < snakeLength; i++) {
if (snakeX[0] == snakeX[i] && snakeY[0] == snakeY[i]) { // If the snake's head collides with its own body
Serial.println("Collision with self, game resetting");
resetGame(); // Reset the game
}
}
// Draw everything
display.clearDisplay(); // Clear display buffer
// Draw score
display.setTextSize(1); // Set text size
display.setTextColor(SSD1306_WHITE); // Set text color
display.setCursor(0, 0); // Set cursor for score
display.print("Score: ");
display.print(score); // Show current score
// Draw border around playing field
display.drawRect(0, 10, SCREEN_WIDTH, SCREEN_HEIGHT - 10, SSD1306_WHITE); // Draw a white frame around the playing field
// Draw snake
for (int i = 0; i < snakeLength; i++) {
display.fillRect(snakeX[i], snakeY[i], SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE); // Draw each segment of the snake
}
// Draw food
display.fillRect(foodX, foodY, SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE); // Draw food
display.display(); // Show updated buffer
delay(100); // Delay to control snake speed
}
void resetGame() {
// Reset snake properties
snakeLength = 5; // Reset snake length
directionX = 1; // Reset direction to right
directionY = 0;
score = 0; // Reset score
for (int i = 0; i < snakeLength; i++) {
snakeX[i] = SCREEN_WIDTH / 2 - (i * SNAKE_SIZE); // Reset snake position to center
snakeY[i] = SCREEN_HEIGHT / 2;
}
// Generate new food position (not directly at the edge)
foodX = random(2, (SCREEN_WIDTH - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
foodY = random(12 / SNAKE_SIZE, (SCREEN_HEIGHT - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
Serial.println("Game reset");
Serial.print("New food at X: ");
Serial.print(foodX);
Serial.print(" Y: ");
Serial.println(foodY);
}
How to Improve the Game?
You now have a playable version of Snake with a score display. How could you further expand or improve the game? One idea could be a high score. The current holder of this high score could use the joystick to enter their initials, which would be saved alongside the score in the ESP32’s file system and displayed on the screen – a great motivation for another round of Snake!