8x32 LED Matrix Snake Game with Joystick Control using Arduino Mega

Snake Game using Arduino Mega and 8x32 LED Matrix

01 Overview

This project features a classic Snake Game recreated using an 8x32 LED matrix display, a joystick module, and an Arduino Mega. Inspired by the retro game, this version brings the nostalgia of pixel-based gameplay to life through a minimal yet dynamic LED interface. The player controls the snake using the joystick, guiding it to eat food and grow longer while avoiding collisions with its body.

The Arduino Mega serves as the system's brain, handling game logic, display updates, and input from the joystick. The 8x32 LED matrix visually represents the snake's movement and game elements in real-time, offering a simple yet captivating interface. This project demonstrates the potential of basic hardware components in game development and is a fun way to learn about matrix addressing, input handling, and embedded programming with Arduino.

02 Hardware and Software Components

Component Brief Description
Arduino Mega 2560 ATmega2560-based development board with many digital and analog pins, suitable for controlling multiple matrix modules and joystick inputs.
8x32 LED Matrix Display Four daisy-chained 8x8 LED matrix modules driven by MAX7219 controllers for displaying the snake, food, score, and game animations.
PS2 Joystick Module Dual-axis analog joystick with a push button that provides directional input and game-control actions.
Dupont Jumper Wires Flexible female-to-female, male-to-male, and male-to-female wires for connecting the Arduino, joystick, matrix modules, and breadboard.
830-point Solderless Breadboard Solderless prototyping board with terminal and power-strip tie points for assembling the circuit without soldering.
MB102 Power Supply Module Breadboard power module that supplies selectable 3.3V or 5V output rails for the connected components.

Software Used

Software Purpose
Arduino IDE Used to write, compile, upload, and debug the Arduino Mega Snake Game program.

03 Application Discussion

Arduino Mega 2560

The Arduino Mega acts as the central controller of the project. It is based on the ATmega2560 microcontroller and includes a CH340G USB interface, which keeps the board affordable without sacrificing functionality. With its large number of input/output pins and strong processing capabilities, the board is well-suited for complex projects requiring multiple components, such as sensors, displays, and input devices.

 

Application in the project:

  • Act as the Main Controller.
  • Offers ample digital and analog pins and memory to handle input from the joystick and control the LED matrix.

Arduino Mega 2560 - CreateLabz

Pinout:

Arduino Mega Pinout

8x32 LED Matrix Display

The LED matrix display visually shows graphics, scrolling text, and simple animations. It consists of a grid of small lights (LEDs) driven by a built-in chip that handles the communication between the display and the microcontroller. This chip makes it easier to control the matrix using only a few wires, which is especially helpful for beginners. It's an ideal choice for simple games or visual effects projects.

 

Application in the project:

  • Displays the snake, food, and movement.
  • The snake is visually represented by lighting up specific LEDs as it moves.

8x32 LED Matrix - CreateLabz

Pinout:

8x32 LED Matrix Pinout

PS2 Joystick Module

A PS2 joystick module is used for directional input. It detects movement along horizontal and vertical axes using two built-in potentiometers. It also features a push-button function when pressed down. The joystick sends signals to the Arduino, allowing it to control movement or trigger actions within a project, such as steering a character in a game or navigating a menu.

 

Application in the project:

  • Provides analog input for directional control (up, down, left, right).
  • Used to steer the snake during gameplay.

PS2 Joystick Module - CreateLabz

Pinout:

PS2 Joystick Module Pinout

04 Hardware Setup

Actual Setup

Wiring Diagram (Fritzing)

Pin Configuration

Arduino Mega 2560 Pins

Arduino Mega Pin Signal / Function Connected Component
A0 Analog input (VRx) PS2 Joystick Module, horizontal axis (GREY)
A1 Analog input (VRy) PS2 Joystick Module, vertical axis (PURPLE)
7 PWM / digital input (SW) PS2 Joystick Module, push button (BLUE)
8 PWM / clock (CLK) LED Matrix, MAX7219 clock line (CYAN)
9 PWM / chip select (CS) LED Matrix, MAX7219 chip-select line (PINK)
10 PWM / data input (DIN) LED Matrix, MAX7219 data line (WHITE)
GND Ground Breadboard ground bus/rail (BLACK)
VCC Power Breadboard power bus/rail (RED)

05 Software Setup

Arduino IDE

Arduino IDE is required to write, upload, and debug the program code.

LedControl Library

The LedControl library is also necessary for controlling MAX7219-based 8×8 LED matrix modules. This library can be installed through the Arduino IDE’s Library Manager by selecting Sketch > Include Library > Manage Libraries… and searching for LedControl.

06 Code

C++

#include <LedControl.h>

// -------------------- Constants & Macros --------------------
#define INITIAL_DELAY 200
#define MIN_DELAY 50

#define DIN_PIN 10
#define CS_PIN  9
#define CLK_PIN 8

#define VRX_PIN A0
#define VRY_PIN A1
#define SW_PIN  7

#define NUM_MODULES 4
#define MAX_SNAKE_LENGTH 128

// -------------------- Utility Functions --------------------
byte reverseByte(byte b) {
  b = (b & 0xF0) >> 4 | (b & 0x0F) << 4;
  b = (b & 0xCC) >> 2 | (b & 0x33) << 2;
  b = (b & 0xAA) >> 1 | (b & 0x55) << 1;
  return b;
}

// -------------------- Global Objects --------------------
LedControl lc = LedControl(DIN_PIN, CLK_PIN, CS_PIN, NUM_MODULES);

// -------------------- Joystick Class --------------------
class Joystick {
  public:
    bool WaitForFirstAction() {
      int x = analogRead(VRX_PIN);
      int y = analogRead(VRY_PIN);
      return (x < 400  x > 600  y < 400 || y > 600);
    }

    String GetDirection(String currentDirection) {
      int x = analogRead(VRX_PIN);
      int y = analogRead(VRY_PIN);
      if (x < 400 && currentDirection != "LEFT") return "RIGHT";
      if (x > 600 && currentDirection != "RIGHT") return "LEFT";
      if (y < 400 && currentDirection != "UP") return "DOWN";
      if (y > 600 && currentDirection != "DOWN") return "UP";
      return currentDirection;
    }
};

// -------------------- Matrix Class --------------------
class Matrix {
  public:
    Joystick* joystick;
    LedControl* lc;
    int snakeX[MAX_SNAKE_LENGTH], snakeY[MAX_SNAKE_LENGTH], snakeLength;
    String direction;
    int foodX, foodY;
    bool foodAvailable = false;
    int score = 0;

    Matrix(Joystick* js, LedControl* ledCtrl) {
      joystick = js;
      lc = ledCtrl;
    }

    void Reset() {
      snakeLength = 3;
      snakeX[0] = random(1, 31); snakeY[0] = random(1, 7);
      int dir = random(0, 4);
      direction = (dir == 0 ? "LEFT" : dir == 1 ? "RIGHT" : dir == 2 ? "UP" : "DOWN");
      for (int i = 1; i < snakeLength; i++) {
        snakeX[i] = snakeX[i-1] + (direction == "LEFT" ? 1 : direction == "RIGHT" ? -1 : 0);
        snakeY[i] = snakeY[i-1] + (direction == "UP" ? 1 : direction == "DOWN" ? -1 : 0);
      }
      score = 0;
      foodAvailable = false;
      Clear();
      PlaceFood();
    }

    void Clear() {
      for (int i = 0; i < NUM_MODULES; i++) lc->clearDisplay(i);
    }

    void DrawPixel(int x, int y, bool state) {
      int moduleMap[4] = {0, 3, 2, 1};
      int module = moduleMap[x / 8];
      int col = x % 8;
      lc->setLed(module, y, col, state);
    }

    void DrawPixelForScore(int x, int y, bool state) {
      int moduleMap[4] = {3, 2, 1, 0};
      int module = moduleMap[x / 8];
      int col = x % 8;
      lc->setLed(module, y, col, state);
    }

    void PlaceSnake() {
      for (int i = 0; i < snakeLength; i++)
        DrawPixel(snakeX[i], snakeY[i], true);
    }

    void MoveSnake() {
      direction = joystick->GetDirection(direction);
      int dx = (direction == "LEFT" ? -1 : direction == "RIGHT" ? 1 : 0);
      int dy = (direction == "UP" ? -1 : direction == "DOWN" ? 1 : 0);

      // Remove tail from display
      DrawPixel(snakeX[snakeLength-1], snakeY[snakeLength-1], false);

      // Move body
      for (int i = snakeLength-1; i > 0; i--) {
        snakeX[i] = snakeX[i-1];
        snakeY[i] = snakeY[i-1];
      }
      snakeX[0] += dx; snakeY[0] += dy;

      // Wrap around
      if (snakeX[0] < 0) snakeX[0] = 31;
      if (snakeX[0] > 31) snakeX[0] = 0;
      if (snakeY[0] < 0) snakeY[0] = 7;
      if (snakeY[0] > 7) snakeY[0] = 0;

      DrawPixel(snakeX[0], snakeY[0], true);

      // Eat food
      if (snakeX[0] == foodX && snakeY[0] == foodY) {
        snakeX[snakeLength] = snakeX[snakeLength-1];
        snakeY[snakeLength] = snakeY[snakeLength-1];
        snakeLength++; score++; foodAvailable = false;
        if (snakeLength >= MAX_SNAKE_LENGTH) snakeLength = MAX_SNAKE_LENGTH;
      }
      if (!foodAvailable) PlaceFood();
      DrawPixel(foodX, foodY, true);
    }

bool hasEatOwnBody() {
      for (int i = 1; i < snakeLength; i++)
        if (snakeX[0] == snakeX[i] && snakeY[0] == snakeY[i]) return true;
      return false;
    }

    void PlaceFood() {
      bool conflict;
      do {
        conflict = false;
        foodX = random(0, 32); foodY = random(0, 8);
        for (int i = 0; i < snakeLength; i++)
          if (snakeX[i] == foodX && snakeY[i] == foodY) { conflict = true; break; }
      } while (conflict);
      foodAvailable = true;
    }

    void ScrollScore(int finalScore) {
      // Define bitmaps for S, C, O, R, E, and ':'
      byte letters[6][8] = {
        {62,  99,  96,  62,  3,  99,  62,  0},      // S
        {60, 102, 96, 96, 96, 102, 60, 0},          // C
        {60, 102, 102, 102, 102, 102, 60, 0},       // O
        {124, 102, 102, 124, 108, 102, 102, 0},     // R
        {126, 98, 96, 124, 96, 98, 126, 0},         // E
        {24, 24, 0, 24, 24, 0, 0, 0}                // : (colon)
      };
      byte digits[10][8] = {
        {60,102,110,118,102,102,102,60},{24,56,24,24,24,24,24,60},
        {60,102,6,12,24,48,102,126},{60,102,6,28,6,6,102,60},
        {12,28,44,76,126,12,12,12},{126,96,124,6,6,6,102,60},
        {60,102,96,124,102,102,102,60},{126,102,6,12,24,24,24,24},
        {60,102,102,60,102,102,102,60},{60,102,102,62,6,6,102,60}
      };
      String str = String(finalScore);
      // Reverse the score string
      String rev_str = "";
      for (int i = str.length() - 1; i >= 0; i--) rev_str += str[i];

      // The order will be: [reversed digits][colon][:][E][R][O][C][S]
      int width = (rev_str.length() + 6) * 8 + 32;

      for (int s = -32; s <= width; s++) {
        Clear();
        int pos = 0;
        // Draw reversed digits
        for (int n = 0; n < rev_str.length(); n++) {
          int d = rev_str[n] - '0';
          for (int r = 0; r < 8; r++) {
            byte row = reverseByte(digits[d][r]);
            for (int c = 0; c < 8; c++) {
              if (row & (1 << (7 - c))) {
                int x = s + pos * 8 + c;
                if (x >= 0 && x < 32)
                  DrawPixelForScore(x, 7 - r, true);
              }
            }
          }
          pos++;
        }
        // Draw colon one pixel lower
        for (int r = 0; r < 8; r++) {
          byte row = reverseByte(letters[5][r]);  // colon is at index 5
          for (int c = 0; c < 8; c++) {
            if (row & (1 << (7 - c))) {
              int x = s + pos * 8 + c;
              int y = 7 - r - 1; // shift down by 1 pixel
              if (x >= 0 && x < 32 && y >= 0 && y < 8)
                DrawPixelForScore(x, y, true);
            }
          }
        }
        pos++;
        // Draw letters in reverse (E R O C S)
        for (int l = 4; l >= 0; l--) {
          for (int r = 0; r < 8; r++) {
            byte row = reverseByte(letters[l][r]);
            for (int c = 0; c < 8; c++) {
              if (row & (1 << (7 - c))) {
                int x = s + pos * 8 + c;
                if (x >= 0 && x < 32)
                  DrawPixelForScore(x, 7 - r, true);
              }
            }
          }
          pos++;
        }
        delay(1);
      }
    }

    void YouLoseScreen() {
      Clear();
      byte GAME[8][4]= {
        {126,66,24,126},{64,102,36,66},{64,90,66,64},{126,66,126,78},
        {64,66,66,66},{64,66,66,66},{64,66,66,66},{126,66,66,126}
      };
      for (int r = 0; r < 8; r++)
        for (int m = 0; m < 4; m++)
          lc->setRow(m,7-r,reverseByte(GAME[r][3-m]));
      delay(2000); Clear(); delay(300);

      byte OVER[8][4]= {
        {126,126,66,126},{66,64,66,66},{66,64,66,66},{126,126,66,66},
        {80,64,66,66},{72,64,66,66},{68,64,36,66},{66,126,24,126}
      };
      for (int r = 0; r < 8; r++)
        for (int m = 0; m < 4; m++)
          lc->setRow(m,7-r,reverseByte(OVER[r][3-m]));
      delay(2000); Clear(); delay(500);
      ScrollScore(score); delay(1000); Clear();
    }
};

// -------------------- Game Class --------------------
class Game {
  public:
    Joystick* joystick;
    Matrix* matrix;
    bool gameStarted = false, gameOver = false;

    Game() {}
    Game(LedControl* lc) {
      joystick = new Joystick();
      matrix = new Matrix(joystick, lc);
    }

    void Display() {
      if (gameOver) {
        if (joystick->WaitForFirstAction()) {
          matrix->Reset();
          gameOver = false;
          gameStarted = false;
        }
        return;
      }
      if (gameStarted) {
        matrix->MoveSnake();
        if (matrix->hasEatOwnBody()) {
          gameOver = true;
          matrix->YouLoseScreen();
        }
      } else {
        matrix->PlaceSnake();
        matrix->DrawPixel(matrix->foodX, matrix->foodY, true);
        if (joystick->WaitForFirstAction()) gameStarted = true;
      }
    }

    int GetScore() { return matrix->score; }
};

// -------------------- Main Program --------------------
Game snakeGame(&lc);
int lastScore = -1;

void setup() {
  Serial.begin(9600);
  for (int i = 0; i < NUM_MODULES; i++) {
    lc.shutdown(i, false);
    lc.setIntensity(i, 8);
    lc.clearDisplay(i);
  }
  pinMode(SW_PIN, INPUT_PULLUP);
  randomSeed(analogRead(A3));
  snakeGame.matrix->Reset();
}

void loop() {
  snakeGame.Display();
  int score = snakeGame.GetScore();
  if (score != lastScore) {
    Serial.print("Score: ");
    Serial.println(score);
    lastScore = score;
  }
  int d = INITIAL_DELAY - (score * 10);
  if (d < MIN_DELAY) d = MIN_DELAY;
  delay(d);
}

07 Code Breakdown

Here is what each part of the code does.

Library Used

Arduino / C++
#include <LedControl.h>
  • LedControl.h: Provides the functions needed to communicate with the MAX7219 chip and control multiple 8x8 LED matrix modules from a single Arduino.

Constants & Macros

Arduino / C++
#define INITIAL_DELAY  200
#define MIN_DELAY      60
#define DIN_PIN        12
#define CS_PIN         11
#define CLK_PIN        10
#define VRX_PIN        A0
#define VRY_PIN        A1
#define SW_PIN         2
#define NUM_MODULES    4
#define MAX_SNAKE_LENGTH 64
  • INITIAL_DELAY / MIN_DELAY: Set the initial and minimum game speed, so the snake moves faster as the score increases.
  • DIN_PIN, CS_PIN, CLK_PIN: Define the Arduino pins used to send data to the MAX7219 matrix modules.
  • VRX_PIN, VRY_PIN, SW_PIN: Define the joystick analog and switch inputs used to control the snake.
  • NUM_MODULES: Tells the program how many 8x8 LED matrix units are connected in series.
  • MAX_SNAKE_LENGTH: Prevents the snake from growing beyond the maximum allowed length.

Global Objects & Variables

Arduino / C++
LedControl lc(DIN_PIN, CLK_PIN, CS_PIN, NUM_MODULES);
Joystick joystick(VRX_PIN, VRY_PIN, SW_PIN);
Game snakeGame(&lc);
int lastScore = 0;
  • lc: Creates the controller object for the MAX7219 LED matrix chain.
  • joystick: Stores the joystick input configuration and methods for reading movement and button state.
  • snakeGame: Instantiates the main game logic object that coordinates display updates, movement, and collision checking.
  • lastScore: Keeps track of the previous score to detect when the score changes and update the serial monitor.

Utility Function

Arduino / C++
byte reverseByte(byte b) {
  b = (b & 0xF0) >> 4 | (b & 0x0F) << 4;
  b = (b & 0xCC) >> 2 | (b & 0x33) << 2;
  b = (b & 0xAA) >> 1 | (b & 0x55) << 1;
  return b;
}
  • reverseByte(): Reverses the bits of a byte. This is useful when mapping font data or custom bitmaps to the LED matrix so the output appears in the correct orientation.

Joystick Class

Arduino / C++
String Joystick::GetDirection(String currentDirection) {
  int x = analogRead(VRX_PIN);
  int y = analogRead(VRY_PIN);

  if (abs(x - 512) > abs(y - 512)) {
    return (x > 512) ? "RIGHT" : "LEFT";
  } else {
    return (y > 512) ? "DOWN" : "UP";
  }
}
  • GetDirection(): Reads the joystick’s X and Y values and decides which direction the snake should move.
  • Direction lock: The code checks the current direction before changing it, which prevents the player from instantly reversing into the snake’s own body.
  • WaitForFirstAction(): Pauses the game until the joystick moves, allowing the player to begin the round intentionally.

Matrix Class

Arduino / C++
void Matrix::MoveSnake() {
  int nextX = headX + dx;
  int nextY = headY + dy;

  if (nextX < 0) nextX = GRID_WIDTH - 1;
  if (nextX >= GRID_WIDTH) nextX = 0;
  if (nextY < 0) nextY = GRID_HEIGHT - 1;
  if (nextY >= GRID_HEIGHT) nextY = 0;

  if (nextX == foodX && nextY == foodY) {
    score++;
    PlaceFood();
  }
}
  • MoveSnake(): Updates the snake head position, wraps the snake around the edges, and checks whether the snake has eaten food.
  • PlaceFood(): Randomly chooses a new food position that is not currently occupied by the snake body.
  • PlaceSnake() / DrawPixel(): Updates the display so the head, body, and food are drawn correctly across the LED matrix chain.
  • ScrollScore(): After a game-over event, it converts the final score into a scrolling message across the display for a more polished result.

Game Class

Arduino / C++
void Game::Display() {
  if (gameOver) {
    matrix->YouLoseScreen();
    return;
  }

  matrix->MoveSnake();
  matrix->PlaceSnake();
}
  • Display(): This is the main game loop controller. It decides whether the game is active, waiting for input, or finished.
  • GetScore(): Returns the current number of food items collected, which is also used to control difficulty and serial output.

setup() Function

Arduino / C++
void setup() {
  Serial.begin(9600);
  for (int i = 0; i < NUM_MODULES; i++) {
    lc.shutdown(i, false);
    lc.setIntensity(i, 8);
    lc.clearDisplay(i);
  }
  pinMode(SW_PIN, INPUT_PULLUP);
  randomSeed(analogRead(A3));
  snakeGame.matrix->Reset();
}
  • Serial.begin(): Starts serial communication so debugging and score output can be viewed on the monitor.
  • Matrix initialization: Turns on each MAX7219 module, sets brightness, and clears all displays before the game starts.
  • Joystick setup: Configures the button pin with the internal pull-up resistor so the switch can be read cleanly.
  • randomSeed(): Seeds the random function so food appears in different places each round.
  • Reset(): Places the snake in a valid starting state and prepares the board for gameplay.

loop() Function

Arduino / C++
void loop() {
  snakeGame.Display();
  int score = snakeGame.GetScore();
  if (score != lastScore) {
    Serial.print("Score: ");
    Serial.println(score);
    lastScore = score;
  }
  int d = INITIAL_DELAY - (score * 10);
  if (d < MIN_DELAY) d = MIN_DELAY;
  delay(d);
}
  • Display(): Updates the game board continuously.
  • Score detection: Checks whether the score has changed and prints the new value to the serial monitor.
  • Difficulty scaling: As the player scores more points, the delay gets shorter, making the game faster and more challenging.

08 Video Demonstration

09 Conclusion

In this project, we successfully developed a Snake Game using an Arduino Mega 2560, a MAX7219-driven 32×8 LED matrix display, and a joystick module for input control. The game allowed the player to control the snake’s movement, collect food, increase score, and avoid collisions, with real-time updates shown on the LED matrix. Features such as wall wrap-around movement and a scrolling score display after game over helped enhance the gameplay experience despite the hardware’s limited display size. This project demonstrated how to manage real-time input, output, and game logic on a microcontroller while effectively using libraries like LedControl to handle LED matrix modules.

For future improvements, several enhancements can be considered. These include adding sound effects using a buzzer, allowing users to select a difficulty level, and introducing obstacles to make the game more challenging. Upgrading to an OLED or TFT display could also improve graphics and readability. Additionally, a two-player mode, high score saving using EEPROM, and wireless control through Bluetooth or Wi-Fi modules would expand the game’s functionality and interactivity. These ideas offer great opportunities to further explore embedded systems and interactive game development.

10 References

Arduino Mega 2560 Official Guide from the Arduino Website

Simple Arduino Snake Game from GitHub Website

Last Minute Engineers Website

11 Project Authors

  • Leizhelle Yvonne Brasileño
  • Aozey Caingles
  • Myrl Arcyl Diesto

Quality Checked By:

  • Kyla Jade Entorum
  • Lemuel Jess Azaria
32x8 led matrix, Analog joystick, Arduino mega, Game, Games, Led matrix, Mega, Mega2560, Retro game, Snake game

Leave a comment

All comments are moderated before being published