DEV Community

Cover image for I Made the Snake Game on the Arduino UNO R4 LED Matrix with a Joystick Controller
Jaiyank S.
Jaiyank S.

Posted on • Originally published at siphyshu.Medium

I Made the Snake Game on the Arduino UNO R4 LED Matrix with a Joystick Controller

My friend (Eeman Majumder) impulsively bought the new Arduino UNO R4 WIFI while we were hosting him at our place for a 2-week dev sprint. The day it came, we were thinking of fun things to do with it, and luckily we had a dual-axis joystick controller lying around the house. I put two and two together and built the popular retro snake game on the LED matrix! 🐍


da setup 🏗️

Let’s get some setup out of the way so we can get to the fun stuff ASAP!

First we get the environment and make the electrical connections, for this:

  1. Either install the Arduino IDE OR use the cloud editor.
  2. Get a dual-axis joystick module and connect the Arduino's 5V, GND, A0, A1, and D13 to the module's 5V, GND, URX, URY, and SW respectively.


SW is connected to D2 here, but I prefer the D13 so it doesn't obstruct the LED matrix screen

In the code, we define some constants, variables and objects that will set up the game. This includes some library imports, our input pins, the LED matrix grid and its size, the food object, the snake object and its speed, length, and direction, and finally the current and high score.

Setup Jar

main game logic 🧠

Now that the setup is done, it’s time to implement the core game logic. Yay!

We have to do a couple of things here — handle the joystick, move the snake, check for any collisions, update the screen, generate new food and add a small delay. The flowchart explains this whole process.

Process flowchart

Take input from joystick

We constantly input the analog values (0 to 1024) from the joystick, normalize them to -512 to 512 in both axes (this is optional, but it just makes things clearer), then map the value to its corresponding direction.

We also disallow any move from the joystick that is directly opposite of the current direction while the game is running. This is because the snake can’t go backwards — which will be considered an illegal move and end the game.

void handleJoystick() {
  xValue = analogRead(joystickXPin);
  yValue = analogRead(joystickYPin);
  swState = not digitalRead(joystickSwPin);

  xMap = map(xValue, 0, 1023, -512, 512);
  yMap = map(yValue, 0, 1023, 512, -512);

  // disallow moving in the opposite direction of current direction while the game is running
  if (xMap <= 512 && xMap >= 10 && yMap <= 511 && yMap >= -511) {
    if ((!isGameOver && directionPrev != 3) || isGameOver) { direction = 1; } // Right 
  } else if (xMap >= -512 && xMap <= -10 && yMap <= 511 && yMap >= -511) {
    if ((!isGameOver && directionPrev != 1) || isGameOver) { direction = 3; } // Left
  } else if (yMap <= 512 && yMap >= 10 && xMap <= 511 && xMap >= -511) {
    if ((!isGameOver && directionPrev != 4) || isGameOver) { direction = 2; } // Up
  } else if (yMap >= -512 && yMap <= -10 && xMap <= 511 && xMap >= -511) {
    if ((!isGameOver && directionPrev != 2) || isGameOver) { direction = 4; } // Down
  }

  if (!isGameOver) {
    directionPrev = direction;
  }
}
Enter fullscreen mode Exit fullscreen mode

Move the snake

We have to actually make the snake move, it won’t move on its own. We shift the body of the snake (which is essentially just an array of points having XY coordinates) forward by updating the coordinates of all the points, starting from the end (tail), to the coordinates of the next point.

Given the current direction, we shift the head +1 in that direction. In case it is at the wall, we wrap it around to the other side. We could disable this to make the game harder but for a pleasurable playing experience, I kept it.

void moveSnake() {
  // Move the body of the snake
  for (int i = snakeLength - 1; i > 0; i--) {
    snake[i] = snake[i - 1];
  }

  // Move the head of the snake
  switch (direction) {
    case 1:  // Right
      snake[0].x = (snake[0].x + 1) % matrixSizeX;
      break;
    case 2:  // Up
      snake[0].y = (snake[0].y - 1 + matrixSizeY) % matrixSizeY;
      break;
    case 3:  // Left
      snake[0].x = (snake[0].x - 1 + matrixSizeX) % matrixSizeX;
      break;
    case 4:  // Down
      snake[0].y = (snake[0].y + 1) % matrixSizeY;
      break;
  }
}
Enter fullscreen mode Exit fullscreen mode

Check if a collision occurs

We have to check two collisions — one with self and one with the food.

If the snake collides with itself, we end the game. We do this by checking if the head’s position is equal to any of the points in the body’s position.

If it collides with the food, we increment the snake’s length and the score and decrement the current speed by 5 if it’s still above the max speed.

void checkCollisions() {
  // Check for collision with self
  for (int i = 1; i < snakeLength; i++) {
    if (snake[0].x == snake[i].x && snake[0].y == snake[i].y) {
      // Game over, restart
      gameOver();
    }
  }

  // Check for collision with food
  if (snake[0].x == food.x && snake[0].y == food.y) {
    snakeLength++;
    score++;

    generateFood();

    if (currentSpeed > maxSpeed) {
      currentSpeed -= 5;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Generate new food

If the snake eats the food, we will have to put new food on the grid, right? This is simple — we update the food’s position to a random point in the grid, make sure that it doesn’t overlap with the snake, and update the matrix.

void generateFood() {
  food.x = random(matrixSizeX);
  food.y = random(matrixSizeY);

  // Make sure the food does not overlap with the snake
  for (int i = 0; i < snakeLength; i++) {
    if (food.x == snake[i].x && food.y == snake[i].y) {
      generateFood();
      return;
    }
  }

  grid[food.y][food.x] = 1;
  matrix.renderBitmap(grid, matrixSizeY, matrixSizeX);
}
Enter fullscreen mode Exit fullscreen mode

Update the LED matrix

To show all these changes above, we reset our grid (meaning switch of all LEDs on the matrix), then update the grid based on the snake and food positions, and then render the new grid on the LED matrix.

void updateMatrix() {
  resetGrid();

  for (int i = 0; i < snakeLength; i++) {
    grid[snake[i].y][snake[i].x] = 1;
  }
  grid[food.y][food.x] = 1;
  matrix.renderBitmap(grid, matrixSizeY, matrixSizeX);
}
Enter fullscreen mode Exit fullscreen mode

Game Over

Finally, when the snake bites itself, the game gets over.

Game Over

Here, we check if we made the high score, play the “Game Over” text on the LED matrix, display the score for a few seconds, and then ask the player if they want to continue playing the game.

void gameOver() {
  isGameOver = true;
  resetGrid();

  if (score >= highScore) {
    highScore = score;
  }

  printText("    Game Over    ", 35);
  for (int i = 0; i < 4; i++) {
    displayScore(true, false);
  }

  continuePlaying();
  initializeGame();
}
Enter fullscreen mode Exit fullscreen mode

finishing touches 🪄

At this point, our game is functional. We could stop here if we want and call it a day. Or, we could go the extra mile and add some finishing touches for a polished look and an overall satisfying playing experience. For this, I added some aesthetic additions and some qualify-of-life improvements.

Firstly, an ascii art intro animation when the game loads. It says SNAKE-R4.

Intro animation

I ended up designing a numbers font in Arduino’s LED Matrix editor for displaying the score, because the fonts in the ArduinoGraphics library did not quite align in the center of the LED matrix and it really bugged me.

Custom number font

Then, I added the option to continue playing by selecting Y/N with the joystick. Again, the frames for the animation were designed in the LED Matrix editor and included in the code using the .h files provided by it.

Continue playing

All we have to do in the code is to render the appropriate frame based on the joystick direction (left or right) and play the yes_option or no_option animation sequence when one of them gets selected.

void continuePlaying() {
  matrix.loadSequence(continue_playing);
  matrix.renderFrame(2);
  String selectedOption = "yes";

  while (!swState) {
    handleJoystick();
    if (direction == 3) {
      matrix.renderFrame(0);
      selectedOption = "yes";
    } else if (direction == 1) {
      matrix.renderFrame(1);
      selectedOption = "no";
    }
    delay(100);
  }

  if (selectedOption == "no") {
    matrix.loadSequence(no_option);
    matrix.play();
    delay(1500);
    printText("    thx for playing!        made by siphyshu    ", 35);
    while (true) {
      displayScore(false, true);
    }
  } else if (selectedOption == "yes") {
    matrix.loadSequence(yes_option);
    matrix.play();
    delay(1500);
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, if the player chooses no, the game ends with a credit sequence and the high-score is shown on the screen permanently in a never-ending loop.


Tada! 🎉 With these final touches, we are done with Snake-R4. I had a lot of fun building (and playing! :D) this project, and if you did too while reading about it, a follow would be amazing!

View the repository: https://github.com/siphyshu/snake-R4

Follow me on Twitter/X for behind-the-scenes:

Stay safe, have fun, and always be building! 🫂️

Top comments (2)

Collapse
 
keyurparalkar profile image
Keyur Paralkar

Awesome work 💯 @siphyshu

Collapse
 
siphyshu profile image
Jaiyank S.

Thankyou! 😄