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

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.

Hardware Used

Arduino Mega 2560 (Compatible) CH340G

 


Matrix LED Display (8x32 module)

 


PS2 Joystick Module (Dual-axis XY)

 


Jumper Wires

 


Breadboard

 


Power Supply Module

 


Software Used

Arduino IDE

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

Hardware Setup

Actual Setup

Wiring Diagram (Fritzing)

Pin Configuration

Arduino Mega 2560 Pins

PIN A0 (Analog In)   →       PS2 Joystick Module VRx (GREY)

PIN A1 (Analog In)   →       PS2 Joystick Module VRy (PURPLE)

PIN 7 (PWM)            →       PS2 Joystick Module SW (BLUE)

PIN 8 (PWM)            →       LED Matrix CLK (CYAN)

PIN 9 (PWM)            →       LED Matrix CS (PINK)

PIN 10 (PWM)          →       LED Matrix DIN (WHITE)

GND                         →       Connected to Breadboard's Ground Bus/Rails (BLACK)

VCC                          →       Connected to Breadboard's Power Bus/Rails (RED)

Software Setup

Arduino IDE

Arduino IDE is required to write, upload, and debug the program code. It is available for download at https://www.arduino.cc/en/software.

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.

Code


#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);
}


Code Breakdown

#include <LedControl.h>

  • Imports the LedControl library for controlling MAX7219-driven LED matrices.

Constants & Macros

  • #define statements assign fixed values (macros) used throughout the program to improve readability and simplify updates:
    • INITIAL_DELAY and MIN_DELAY — control game speed.
    • DIN_PIN, CS_PIN, CLK_PIN — define digital pins for matrix communication.
    • VRX_PIN, VRY_PIN, SW_PIN — define analog and digital pins for joystick input.
    • NUM_MODULES — total number of LED matrix modules used.
    • MAX_SNAKE_LENGTH — maximum snake length.

Utility Function: reverseByte(byte b)

  • Reverses the bit order of a byte, crucial for mapping character bitmaps to the correct orientation on the LED matrix.

Global Objects

·        Manages communication between the Arduino and the MAX7219-driven LED matrix modules. It handles sending commands to turn LEDs on and off, adjust brightness, clear displays, and update rows or individual LEDs.

·        This instance is declared globally (outside of any class or function), so that it can be accessed from anywhere in the program. This is necessary since both the setup(), Matrix class, and Game class need to access and control the LED matrices via this same LedControl instance.

 

DIN_PIN - Defines the data input pin on the Arduino. This pin sends serial data to the MAX7219.

CLK_PIN - Defines the clock pin on the Arduino. This pin provides timing signals to synchronize the data transfer.

CS_PIN - Defines the chip select (load) pin. This pin tells the MAX7219 when to latch (apply) the received data.

NUM_MODULES - Specifies how many MAX7219 modules (or LED matrix displays) are connected in a daisy chain.


Joystick Class

  • Purpose: Handles joystick input.

WaitForFirstAction()

  • Waits for joystick movement before starting or restarting the game.

GetDirection(String currentDirection)

  • Reads analog joystick values and determines the new direction.
  • Prevents reversing direction to avoid instant self-collision.

Matrix Class

  • Purpose: Manages snake behavior, LED matrix display, and game logic.

Matrix(Joystick* js, LedControl* ledCtrl)

  • Initializes joystick and LED control objects.

Reset()

  • Resets snake to random starting position.
  • Sets initial direction randomly.
  • Clears the display and places food.

Clear()

  • Clears all LED matrix modules.

DrawPixel(int x, int y, bool state)

  • Lights up or turns off a pixel at given coordinates based on state.

DrawPixelForScore(int x, int y, bool state)

  • Similar to DrawPixel(), but with a different module mapping for score display.

PlaceSnake()

  • Draws the snake on the matrix based on stored position arrays.

MoveSnake()

  • Updates snake position based on current direction.
  • Checks for wall wrap-around.
  • Handles food consumption, body growth, and food replacement.

hasEatOwnBody()

  • Detects self-collision by comparing head position with body segments.

PlaceFood()

  • Randomly places food on the matrix in a position not occupied by the snake.

ScrollScore(int finalScore)

  • Displays a scrolling message of the final score after game over.
  • Converts numbers and letters into bitmap patterns.
  • Scrolls the message smoothly across the display.

YouLoseScreen()

  • Shows "GAME OVER" animation.
  • Calls ScrollScore() to display final score.
  • Clears display afterward.

Game Class

  • Purpose: Manages game state and flow.

Game(LedControl* lc)

  • Initializes a new joystick and matrix object.

Display()

  • Main game loop:
    • If game over, waits for player action to restart.
    • If active, moves snake and checks for collision.
    • If idle, displays snake and food until joystick moves.

GetScore()

  • Returns current game score.

setup() Function

  • Runs once during power-up.
  • Initializes serial communication.
  • Sets up MAX7219 matrix modules.
  • Configures joystick button pin.
  • Seeds random number generator.
  • Resets the game state.

loop() Function

  • Continuously runs during game execution.
  • Calls Display() to update the game.
  • Monitors score and prints to serial if changed.
  • Adjusts game speed based on score.

Video Demonstration

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.

References

Arduino Mega 2560 Official Guide from the Arduino Website

Simple Arduino Snake Game from GitHub Website

Last Minute Engineers Website

Project Authors

  • Leizhelle Yvonne Brasileño
  • Aozey Caingles
  • Myrl Arcyl Diesto
32x8 led matrixAnalog joystickArduino megaGameGamesLed matrixMegaMega2560Retro gameSnake game

Leave a comment

All comments are moderated before being published