I started a side hobby of building little electronics projects about a year ago. While I did graduate with a dual electrical engineering degree and computer science honours degree (wow, was it really so long ago in 2002?!), I never kept up the EE knowledge because I focused on software development as my passion instead. I forgot how much fun it is to build a physical circuit as opposed to the abstract software applications I wire together daily. The most fun is marrying the two — making something on the web change something in the “real” world. I literally giggled when I first got a lamp to turn on and off by clicking a button on a web page. I know it’s a stupidly simple little thing (and my husband rightly teased me for finally putting that EE degree to good use), but the whole IOT concept is fascinating, and I like that I can do both.
One of my favorite things to play with is a 16×16 NeoPixel matrix (I stumbled upon a cheap knock off from Amazon, but here’s the Adafruit alternative: Flexible 16×16 NeoPixel RGB Matrix). The resolution is high enough that you can create simple little games, but it only requires 2 pins on an Arduino to control. The Adafruit libraries also make it super easy to code.
And the colors are stunning.
Since it’s been a cold snowy weekend here in Saskatoon, I decided to hook up the matrix and write the classic Snake. First, the wiring (shown in the fritzing diagram below). Some things I learned while putting this together:
- 16×16 fritzing parts are hard to find, so I just used an 8×8 in the diagram.
- Use a breadboard power supply to power the matrix because the Arduino isn’t powerful enough to handle the current required by the matrix. I used the 5V side of my configurable power supply.
- Joystick callibration can be a bugger. It took a lot of trial and error to settle on the right numbers to detect a player’s movement accurately.
- Use math whenever possible to reduce memory requirements for the microcontroller. I initially used an Uno and mapped my game’s board array to the NeoPixel matrix numbers by using a mapping array, but this took up so much memory that it wouldn’t load on the Uno. I switched to a Mega fix this limitation before realizing I could remove the memory requirement entirely by using a function to calculate it instead.
There are a bunch of C++ Snake clones out there I could have copied, but for fun I wrote my own:
snake_with_rbg_matrix-NxN.ino:
#include <Adafruit_NeoPixel.h> #include <Adafruit_GFX.h> #include <gfxfont.h> #define PIN 10 #define UP 1 #define LEFT 2 #define RIGHT 3 #define DOWN 4 const int JOYSTICK_XPIN = A0; const int JOYSTICK_YPIN = A1; #define ROWS 16 #define COLS 16 Adafruit_NeoPixel matrix = Adafruit_NeoPixel(256, PIN, NEO_GRB + NEO_KHZ800); uint32_t RED = matrix.Color(255, 0, 0); uint32_t GREEN = matrix.Color(0, 255, 0); uint32_t BLUE = matrix.Color(0, 0, 255); uint32_t CLEAR = matrix.Color(0, 0, 0); uint32_t PURPLE = matrix.Color(255, 0, 255); uint32_t YELLOW = matrix.Color(255, 255, 0); struct point { int x; int y; }; point player[ROWS * COLS]; int playerDirection; int playerLength; point playerHead; point apple; int board[ROWS][COLS]; unsigned long lastClockTick; int gameRate; int numApplesEaten = 0; void setup() { matrix.begin(); matrix.setBrightness(15); matrix.show(); randomSeed(analogRead(0)); defineBoard(); startGame(); } void defineBoard() { // draw the outer board for(int i = 0; i < COLS; i++) { board[0][i] = 1; board[ROWS - 1][i] = 1; } generateRandomBoard(); } void generateRandomBoard() { // clear the existing board first for(int i = 1; i < ROWS - 1; i++) { for(int j = 0; j < COLS; j++) { if(j == 0 || j == (COLS - 1)) { board[i][j] = 1; } else { board[i][j] = 0; } } } // 20 random pixels set, but make sure that it won't cause the player to lose (diagonal pixels) for(int i = 0; i < 20; i++) { bool found = false; int x = random(1, ROWS - 1); int y = random(1, COLS - 1); while(!found) { if(!boardContainsCoordinates(x, y) && !boardContainsCoordinates(x - 1, y - 1) && !boardContainsCoordinates(x + 1, y - 1) && !boardContainsCoordinates(x - 1, y + 1) && !boardContainsCoordinates(x + 1, y + 1)){ found = true; } else { x = random(1, ROWS - 1); y = random(1, COLS - 1); } } board[x][y] = 1; } } void startGame() { matrix.clear(); resetGameVariables(); drawBoard(); drawPlayer(); drawApple(); matrix.show(); } void resetGameVariables() { generateRandomBoard(); bool found = false; // generate a random spot for the user to start, and a random direction while(!found) { // start the player in a random spot playerHead.x = random(1, ROWS - 1); playerHead.y = random(1, COLS - 1); int startDirection = random(0, 2); if(startDirection == 0) { if(playerHead.y < COLS / 2) { playerDirection = RIGHT; } else { playerDirection = LEFT; } } else { if(playerHead.x < ROWS / 2) { playerDirection = DOWN; } else { playerDirection = UP; } } if(playerHas5Moves()) { found = true; } } generateApple(); playerLength = 1; player[0].x = playerHead.x; player[0].y = playerHead.y; lastClockTick = millis(); gameRate = 300; numApplesEaten = 0; } // make sure with the random start that the player has a few moves to react bool playerHas5Moves() { for(int i = 0; i < 5; i++) { switch(playerDirection) { case RIGHT: if(board[playerHead.x][playerHead.y + i] == 1) { return false; } break; case LEFT: if(board[playerHead.x][playerHead.y - i] == 1) { return false; } break; case UP: if(board[playerHead.x - i][playerHead.y] == 1) { return false; } break; case DOWN: if(board[playerHead.x + i][playerHead.y] == 1) { return false; } break; } } return true; } void generateApple() { bool found = false; // make sure the apple doesn't end up on a board coordinate or on top of the player while(!found) { apple.x = random(1, ROWS - 1); apple.y = random(1, COLS - 1); if(!playerContainsCoordinates(apple.x, apple.y) && !boardContainsCoordinates(apple.x, apple.y)) { found = true; } } } bool playerContainsCoordinates(int x, int y) { for(int i = 0; i < playerLength; i++) { if(player[i].x == x && player[i].y == y) { return true; } } return false; } bool boardContainsCoordinates(int x, int y) { return board[x][y] == 1; } void drawPlayer() { for(int i = 0; i < playerLength; i++) { matrix.setPixelColor(convertToMatrixPoint(player[i].x, player[i].y), BLUE); } } void drawBoard() { for(int i = 0; i < ROWS; i++) { for(int j = 0; j < COLS; j++) { if(board[i][j] == 1) { matrix.setPixelColor(convertToMatrixPoint(i, j), GREEN); } else { matrix.setPixelColor(convertToMatrixPoint(i, j), CLEAR); } } } // identify the bottom left pixel by painting it blue matrix.setPixelColor(convertToMatrixPoint(0, 0), BLUE); // identify the matrix start pixels and direction matrix.setPixelColor(0, PURPLE); matrix.setPixelColor(1, YELLOW); } void drawApple() { matrix.setPixelColor(convertToMatrixPoint(apple.x, apple.y), RED); } void loop() { float x = analogRead(JOYSTICK_XPIN); float y = analogRead(JOYSTICK_YPIN); float deltax = abs(510 - x); float deltay = abs(505 - y); // detect if the player's movement is more likely x or y direction if(deltax > deltay) { if(x < 480) { playerDirection = DOWN; } else if(x > 540) { playerDirection = UP; } } else { if(y < 475) { playerDirection = RIGHT; } else if(y > 535) { playerDirection = LEFT; } } if(millis() - lastClockTick > gameRate) { advancePlayer(); detectCollision(); detectAppleEaten(); updateBoard(); lastClockTick = millis(); } } void advancePlayer() { if(playerDirection == LEFT) { playerHead.y -= 1; } else if(playerDirection == RIGHT) { playerHead.y += 1; } else if(playerDirection == UP) { playerHead.x -= 1; } else if(playerDirection == DOWN) { playerHead.x += 1; } // see if this point already exists in the player's matrix for(int i = 0; i < playerLength; i++) { if(player[i].x == playerHead.x && player[i].y == playerHead.y) { gameOver(); } } for(int i = playerLength - 1; i > 0; i--) { player[i] = player[i - 1]; } player[0].x = playerHead.x; player[0].y = playerHead.y; } void detectCollision() { if(board[playerHead.x][playerHead.y] == 1) { gameOver(); } } void detectAppleEaten() { if(playerHead.x == apple.x && playerHead.y == apple.y) { numApplesEaten++; playerLength += 1; player[playerLength - 1].x = playerHead.x; player[playerLength - 1].y = playerHead.y; if(numApplesEaten % 5 == 0 && gameRate > 100) { gameRate -= 20; } generateApple(); } } void updateBoard() { drawBoard(); drawPlayer(); drawApple(); matrix.show(); } void gameOver() { matrix.clear(); for(int i = 0; i < ROWS; i++) { for(int j = 0; j < COLS; j++) { matrix.setPixelColor(convertToMatrixPoint(i, j), GREEN); } } matrix.show(); delay(3000); startGame(); } int convertToMatrixPoint(int i, int j) { if(i % 2 == 0) { return (COLS * i) + (COLS - 1) - j; } else { return (COLS * i) + j; } }
The method convertToMatrixPoint is where I map the game board (which was a two dimensional matrix) to the NeoPixel matrix library (which identifies individual pixels with a single number). To make it fun, the matrix locations go in a long continuous strand from bottom to top, so the formula is different for even and odd rows. Thank you Mr. Long for teaching mapping functions in grade 9!
When I first wrote the code for the joystick, I just used an if/elseif statement instead of checking if x (left/right) or y (up/down) was the larger movement. That meant that if the user had any left/right movement that the game would detect that movement first, even if the up/down movement was what the user actually indicated. That meant a lot of crashing when I was certain I moved the joystick in the right direction. The check for the larger movement solved that problem — I like how it handles now much better.
I uploaded the project to GitHub: https://github.com/collene/arduino-snake-nxn and the video to YouTube:
Not bad for a weekend project! Apparently this month’s Hackerbox (which I have yet to receive, damn international shipping) also has an LED matrix with an ESP32 board. I might have to take Snake here to a cloud version…
Nice Work!! trying to do the same. But my Matrix will not light up. I have the same komponents. I wired all like u. Have u any Idea?
LikeLike
Are you also using a breadboard power supply, or are you trying to power the matrix off of the Arduino? I didn’t have much luck with running it off the Arduino, so now I always use a breadboard power supply. When you turn the breadboard power supply on, do a few of the pixels on the matrix appear to blink? Have you used the matrix in other projects?
LikeLike
Yeah i used the matrix in another project too. In other projects it works fine. But with that code no pixel will light up…
LikeLike
Do you have the Adafruit libraries installed (though I’m assuming the code actually compiles and uploads to the Arduino since you’ve used the matrix in other projects). Have you tried adding a few Serial.println statements and seeing where the program stops executing?
LikeLike
btw i used a breadboard power supply
LikeLike
I have a question,how can i prove the code with 8×8 matrix?
LikeLike
I tried to make the code flexible, so you should just be able to adjust lines 14 and 15 to be:
#define ROWS 8
#define COLS 8
“Should” is the key word there because I haven’t tested it 🙂 You might need to adjust some of the other methods. If you do find a bug when you change the rows and cols, let me know how you fixed it!
LikeLike
Hi Collene
I’m a italian student, I have done your project , it’s all ok but I’m using a power directly in the arduino module with a ac/ad adapter output 9v 1a
sometimes happen that dosn’t work properly, could be a problem by the power?
thanks in advance
LikeLike
Hi Luciano, Yeah it’s probably a power problem. That’s why I used a separate breadboard power supply and didn’t power it right off the arduino. If you have a lot of the neopixels turned on (or a lot of white/brighter pixels), it can draw a lot of current. Try using a separate power supply for the matrix. Or increasing the adapter to 2A. You can read a good explanation on the Adafruit product page: https://www.adafruit.com/product/1487
LikeLike