This tutorial shows how to build a Snake game on an 8x8 LED matrix using the LedControl library and an analog joystick.
You will learn:
void mean?In Arduino C++, void is a function return type that means “nothing.” When a function is declared as void, it does not return any value back to the caller.
In this sketch, void setup() and void loop() are special Arduino functions that the board calls automatically:
void setup() runs once at startup and does not return anything.void loop() runs repeatedly and also does not return a value.Using void is normal for functions that perform actions like initializing hardware or updating the game state without producing a result.
In Arduino programming, a library is a collection of pre-written code that provides functions and tools to simplify common tasks. For example, the LedControl library handles communication with the MAX7219 LED driver chip, allowing you to control the matrix without writing low-level SPI code from scratch.
Libraries are installed via the Arduino IDE’s Library Manager and included in your sketch with #include <LibraryName.h>. They extend Arduino’s capabilities, such as controlling displays, sensors, or motors, making projects like this Snake game easier to build.
Explanation:
LedControl abstracts the matrix control, so you can focus on game logic.Try this:
Servo or Wire.LedControl source code online and identify the setLed() function to understand how it works internally.LedControl library installed from Arduino Library Manager - Install InstructionsSketch → Include Library → Manage Libraries....LedControl by Eberhard Fahle.Explanation:
LedControl library provides the functions needed to control the MAX7219 driver chip and draw pixels on the 8x8 LED matrix.Try this:
LedControl examples in the Arduino IDE and compare how setRow() and setLed() are used in this tutorial.VCC → 5VGND → GNDDIN → Arduino pin 10CLK → Arduino pin 13CS / LOAD → Arduino pin 7Explanation:
DIN, CLK, and CS are the three signals that talk to the MAX7219 driver.VCC and GND power the LED matrix, while CS tells the MAX7219 when a complete command is ready.Try this:
DIN and CLK wires and see what error message or behavior the matrix shows.VRx → A0VRy → A1SW → digital pin 8GND → GNDVCC → 5VledPin → digital pin 9Use this quick test sketch to confirm the MAX7219 matrix is wired correctly:
#include <LedControl.h>
int DIN = 10;
int CLK = 13;
int CS = 7;
LedControl lc = LedControl(DIN, CLK, CS, 1);
void setup() {
lc.shutdown(0, false);
lc.setIntensity(0, 5);
lc.clearDisplay(0);
for (int r = 0; r < 8; r++) {
lc.setRow(0, r, 0xFF);
}
}
void loop() {}
If the matrix lights up fully, the wiring is good. If not, check power, ground, and the DIN/CLK/CS pin connections.
The sketch is divided into these sections:
#include <LedControl.h> – load the matrix driver librarysetup() to initialize the matrix and joystick pinsloop() to read the joystick and move the snakeExplanation:
setup(), and the game repeats in loop().Try this:
Use these pin assignments in your sketch:
int DIN = 10;
int CLK = 13;
int CS = 7;
int xPin = A0;
int yPin = A1;
const int buttonPin = 8;
int ledPin = 9;
LedControl lc = LedControl(DIN, CLK, CS, 1);
Game state variables:
Explanation:
DIN, CLK, and CS define the connection to the LED matrix.xPin and yPin read the joystick angles, while buttonPin detects restart presses.snakeRow[] and snakeCol[] store the snake body positions as arrays of coordinates.direction determines which way the snake will move on each update.Try this:
int score = 0; variable and think where it should be updated when food is eaten.const int maxLength = 64;
int snakeLength = 3;
int snakeRow[maxLength] = {3, 3, 3};
int snakeCol[maxLength] = {3, 2, 1};
int row = 3;
int col = 3;
String direction = "right";
int foodRow = 0;
int foodCol = 0;
byte endgame[8] = {
0x81, 0x42, 0x24, 0x18,
0x18, 0x24, 0x42, 0x81
};
unsigned long lastMoveTime = 0;
int moveInterval = 300;
bool gameOver = false;
In setup(), wake the matrix and configure the joystick pins.
void setup() {
lc.shutdown(0, false);
lc.setIntensity(0, 1);
lc.clearDisplay(0);
pinMode(xPin, INPUT);
pinMode(yPin, INPUT);
pinMode(buttonPin, INPUT_PULLUP);
randomSeed(analogRead(A2));
spawnFood();
}
Explanation:
shutdown(0, false) turns on the MAX7219 display.setIntensity(0, 1) sets a low brightness so the matrix is easy on the eyes.INPUT_PULLUP on the button pin makes the button read HIGH when idle and LOW when pressed.randomSeed(analogRead(A2)) creates a better random pattern by reading a floating analog pin.Try this:
Serial.begin(9600); to setup() and print xValue and yValue from loop() to verify your joystick readings.setIntensity() and see how it affects the display.Notes:
INPUT_PULLUP is used for the joystick button so it reads HIGH when not pressed.randomSeed(analogRead(A2)) adds randomness to food placement.The joystick values are read with analogRead(). The sketch uses thresholds to decide direction.
int xValue = analogRead(xPin);
int yValue = analogRead(yPin);
if (xValue > 700 && direction != "left") direction = "right";
else if (xValue < 300 && direction != "right") direction = "left";
else if (yValue > 700 && direction != "up") direction = "down";
else if (yValue < 300 && direction != "down") direction = "up";
Explanation:
direction != ... rules prevent the snake from reversing into itself immediately.Try this:
xValue and yValue while you move the joystick; use it to confirm which range means left, right, up, or down.Tip: if your joystick values are different, adjust 700 and 300 to match the center and edge readings.
Use this simple debug sketch to see the actual analogRead() values from your joystick:
int xPin = A0;
int yPin = A1;
void setup() {
Serial.begin(9600);
}
void loop() {
int xValue = analogRead(xPin);
int yValue = analogRead(yPin);
Serial.print("X=");
Serial.print(xValue);
Serial.print(" Y=");
Serial.println(yValue);
delay(200);
}
Move the joystick left, right, up, and down, then use the printed values to fine-tune the threshold numbers.
The sketch uses millis() to move the snake every moveInterval milliseconds.
unsigned long currentTime = millis();
if (currentTime - lastMoveTime >= moveInterval) {
lastMoveTime = currentTime;
// update head position here
}
Explanation:
millis() returns the number of milliseconds since the Arduino started.delay() keeps the program able to read the joystick continuously.Try this:
moveInterval to 500 and 200 and observe how the game speed changes.This keeps the sketch responsive while reading joystick input continuously.
To make the snake faster or slower, adjust moveInterval:
int moveInterval = 500; // slower
int moveInterval = 200; // faster
A larger value means slower movement, and a smaller value means faster movement.
Before drawing, shift the body positions down the arrays so the new head can be inserted.
for (int i = snakeLength - 1; i > 0; i--) {
snakeRow[i] = snakeRow[i - 1];
snakeCol[i] = snakeCol[i - 1];
}
snakeRow[0] = row;
snakeCol[0] = col;
Then draw the snake and food:
Explanation:
0 makes the snake appear to move forward.Try this:
[(3,3),(3,2),(3,1)] and moves right, what should the arrays become?lc.clearDisplay(0);
for (int i = 0; i < snakeLength; i++) {
lc.setLed(0, snakeRow[i], snakeCol[i], true);
}
lc.setLed(0, foodRow, foodCol, true);
If the head goes outside 0..7, the game ends.
if (col > 7 || col < 0 || row > 7 || row < 0) {
showEndgame();
return;
}
After the head moves, check if it hits any existing body segment.
for (int i = 1; i < snakeLength; i++) {
if (snakeRow[i] == row && snakeCol[i] == col) {
showEndgame();
return;
}
}
If the head hits the food, grow the snake and place a new food item.
if (row == foodRow && col == foodCol) {
spawnFood();
if (snakeLength < maxLength) snakeLength++;
}
Explanation:
Try this:
The food is placed in a random free cell. If the snake already fills the board, the game ends.
void spawnFood() {
bool valid = false;
int attempts = 0;
while (!valid && attempts < 100) {
foodRow = random(0, 8);
foodCol = random(0, 8);
valid = true;
for (int i = 0; i < snakeLength; i++) {
if (snakeRow[i] == foodRow && snakeCol[i] == foodCol) {
valid = false;
break;
}
}
attempts++;
}
if (!valid) showEndgame();
}
Explanation:
attempts limit prevents an infinite loop if the board is almost full.Try this:
spawnFood() that first collects all free cells into a list and then picks one at random.When the player loses, the display shows an end pattern and waits for the button press.
void showEndgame() {
lc.clearDisplay(0);
for (int i = 0; i < 8; i++) {
lc.setRow(0, i, endgame[i]);
delay(10);
}
delay(3000);
lc.clearDisplay(0);
gameOver = true;
}
void checkRestart() {
if (digitalRead(buttonPin) == LOW) {
delay(200);
while (digitalRead(buttonPin) == LOW);
resetGame();
gameOver = false;
}
}
Explanation:
showEndgame() clears the display, draws a pattern, then pauses briefly before setting gameOver.checkRestart() waits for the button to be pressed, debounces it, and then calls resetGame().Try this:
score reset inside resetGame() and print the score to the serial monitor when the game restarts.showEndgame() blink the display three times before showing the pattern.Then reset the snake state:
void resetGame() {
snakeLength = 3;
row = 3;
col = 3;
snakeRow[0] = 3; snakeCol[0] = 3;
snakeRow[1] = 3; snakeCol[1] = 2;
snakeRow[2] = 3; snakeCol[2] = 1;
direction = "right";
lastMoveTime = millis();
spawnFood();
}
Use the complete sketch below after you have verified wiring and library installation.
#include <LedControl.h> imports the LED matrix library.int DIN = 10; sets the data pin.int CLK = 13; sets the clock pin.int CS = 7; sets the chip select pin.int xPin = A0; sets the joystick X axis pin.int yPin = A1; sets the joystick Y axis pin.const int buttonPin = 8; sets the button input pin.LedControl lc = LedControl(DIN, CLK, CS, 1); creates the display object for one matrix.const int maxLength = 64; reserves space for every cell on the 8×8 board.int snakeLength = 3; starts the snake with three blocks.int snakeRow[maxLength] = {3, 3, 3}; stores the row position for each snake segment.int snakeCol[maxLength] = {3, 2, 1}; stores the column position for each snake segment.int row = 3; starts the head row at 3.int col = 3; starts the head column at 3.String direction = "right"; sets the initial movement direction.int foodRow = 0; stores the food row coordinate.int foodCol = 0; stores the food column coordinate.byte endgame[8] = { ... }; stores the game over pattern.unsigned long lastMoveTime = 0; keeps track of the last move time.int moveInterval = 300; sets how often the snake moves.bool gameOver = false; tracks whether the game is paused.
void setup() initializes the display, joystick input, and starting food.void loop() repeats forever, updating the game or waiting for restart.millis() is used for timing so the game stays responsive.digitalRead(buttonPin) == LOW detects the button press because the pin is pulled low when pressed.placeBlock() tries to lock the moving block into the stack and checks if the move is valid.render() redraws the board each frame.handleEnd() displays win or loss feedback and freezes the game.resetGame() restores the initial state for another playthrough.
lc.shutdown(0, false); turns on the MAX7219 display.lc.setIntensity(0, 1); sets the display brightness.lc.clearDisplay(0); clears all LEDs.pinMode(xPin, INPUT); and pinMode(yPin, INPUT); prepare the joystick pins.pinMode(buttonPin, INPUT_PULLUP); sets the button pin with internal pull-up.randomSeed(analogRead(A2)); seeds random using analog noise.spawnFood(); places the first food item.
if (gameOver) { checkRestart(); return; } stops movement after a loss.analogRead(xPin) and analogRead(yPin) read joystick direction values.currentTime - lastMoveTime >= moveInterval checks if it is time to move again.row++, row--, col++, col-- move the snake head.0..7.spawnFood() picks a new food position when the snake eats food.The draw loop lights the snake and food pixels.
spawnFood() chooses a random free cell and avoids placing food on the snake.showEndgame() flashes the end pattern and sets gameOver.checkRestart() waits for a button press and resets the game.resetGame() restores the starting snake state and spawns new food.DIN, CLK, CS wiring.LedControl is installed.A0 and A1.Serial.println(xValue) / Serial.println(yValue) to see values.512.buttonPin must be INPUT_PULLUP.8 to GND when pressed.spawnFood() loop checks all snake segments before accepting the position.showEndgame() sets gameOver = true and waits for the button.delay(200) helps.LedControl example first.millis() timing instead of delay() in the main game loop.moveInterval to make the game faster or slower.delay() inside loop() except for endgame or debouncing.numDevices incorrectly if you have only one matrix.row and col or rotate the matrix pattern.Serial.println() to calibrate joystick edges.moveInterval.moveInterval.Back to index