This tutorial shows how to build a Stacker game on an 8x8 LED matrix using the LedControl library and a single button.
You will learn:
LedControl library installed from Arduino Library Manager - Install InstructionsSketch → Include Library → Manage Libraries....LedControl by Eberhard Fahle.Explanation:
LedControl gives you functions to talk to the MAX7219 chip and draw pixels on the matrix.Try this:
LedControl library examples and compare setLed() with setRow().VCC → 5VGND → GNDDIN → Arduino pin 10CLK → Arduino pin 13CS / LOAD → Arduino pin 78INPUT_PULLUP in code so the button reads HIGH when not pressed and LOW when pressedExplanation:
DIN, CLK, and CS to receive commands from the Arduino.Try this:
LOW when pressed.The sketch has three main parts:
setup() initializes the display, button, and game state.loop() moves the current block, checks for button presses, and ends or resets the game.Explanation:
setup() runs once at startup.loop() repeats forever, so the game is always updating.Try this:
placeBlock() does in one sentence.Use these pin assignments in your sketch:
int DIN = 10;
int CLK = 13;
int CS = 7;
const int buttonPin = 8;
LedControl lc = LedControl(DIN, CLK, CS, 1);
Game variables:
int currentRow = 7; // Start at the bottom
int currentWidth = 3; // Start with 3 blocks
int currentPos = 0; // Leftmost position of the moving block
int direction = 1; // 1 for Right, -1 for Left
bool stack[8][8]; // Memory of placed blocks
unsigned long lastMoveTime = 0;
int moveInterval = 200; // Speed of the moving block
bool gameOver = false;
Explanation:
currentRow is the row where the moving block currently travels.currentWidth is how many consecutive LEDs are lit for the moving block.currentPos is the leftmost column position of that moving block.direction controls whether the block moves right or left.stack[8][8] remembers which cells on the board are already stacked.moveInterval controls how fast the moving block updates.gameOver stops the normal game loop after a win or loss.Try this:
currentWidth to 4 and imagine how the first moving block looks.score variable that grows each time a block is placed successfully.void setup() {
lc.shutdown(0, false);
lc.setIntensity(0, 5);
lc.clearDisplay(0);
pinMode(buttonPin, INPUT_PULLUP);
// Initialize stack as empty
for (int r = 0; r < 8; r++) {
for (int c = 0; c < 8; c++) stack[r][c] = false;
}
}
Explanation:
shutdown(0, false) powers on the display.setIntensity(0, 5) sets a medium brightness so the block is visible.clearDisplay(0) turns off every LED.pinMode(buttonPin, INPUT_PULLUP) makes the button stable with internal pull-up.stack to false, clearing the game memory.Try this:
Serial.begin(9600); and print a message from setup() to confirm the board started.unsigned long currentTime = millis();
if (currentTime - lastMoveTime >= moveInterval) {
lastMoveTime = currentTime;
currentPos += direction;
// Bounce off walls
if (currentPos + currentWidth > 8 || currentPos < 0) {
direction *= -1;
currentPos += (direction * 2);
}
render();
}
Explanation:
millis() gives the elapsed time since the Arduino started.moveInterval.currentPos += direction slides the block left or right.render() updates the display after the block moves.Try this:
moveInterval to 150 and see how much harder the game becomes.if (digitalRead(buttonPin) == LOW) {
delay(200); // Debounce
placeBlock();
while(digitalRead(buttonPin) == LOW); // Wait for release
}
Explanation:
digitalRead(buttonPin) == LOW detects the button press because the pin pulls down to ground when pressed.delay(200) debounces the button so one press doesn’t register multiple times.placeBlock() attempts to lock the moving block into the stack.while loop waits until the button is released before continuing.Try this:
delay(200) with delay(100) and see if the button becomes more sensitive.void placeBlock() {
int placedCount = 0;
for (int i = 0; i < currentWidth; i++) {
int col = currentPos + i;
// If it's the bottom row, everything stays.
// Otherwise, check if there is a block underneath.
if (currentRow == 7 || stack[currentRow + 1][col]) {
stack[currentRow][col] = true;
placedCount++;
}
}
// If no blocks landed on top of others, you lose
if (placedCount == 0) {
handleEnd(false);
return;
}
currentWidth = placedCount; // Next row will be thinner
currentRow--; // Move up
moveInterval -= 15; // Speed up slightly
if (currentRow < 0) {
handleEnd(true); // You reached the top!
}
}
Explanation:
placedCount counts how many ledges from the moving block successfully land.currentRow == 7), every block is valid.currentWidth = placedCount makes the next row narrower.currentRow-- moves the next block up one row.moveInterval -= 15 makes the game faster after each successful placement.currentRow < 0, the player has stacked to the top and wins.Try this:
moveInterval -= 15 to -10.missedBlocks counter and display it via serial output.void render() {
lc.clearDisplay(0);
// Draw the established stack
for (int r = 0; r < 8; r++) {
for (int c = 0; c < 8; c++) {
if (stack[r][c]) lc.setLed(0, r, c, true);
}
}
// Draw the currently moving block
for (int i = 0; i < currentWidth; i++) {
lc.setLed(0, currentRow, currentPos + i, true);
}
}
Explanation:
clearDisplay(0) removes the old frame before drawing the new one.stack.Try this:
render() so the moving block is shown with different brightness than the stack.void handleEnd(bool win) {
gameOver = true;
for (int i = 0; i < 3; i++) {
lc.clearDisplay(0);
delay(200);
if (win) {
// Flash full screen for win
for(int r=0; r<8; r++) lc.setRow(0, r, 0xFF);
} else {
// Simple X for loss
lc.setRow(0, 0, 0x81); lc.setRow(0, 7, 0x81);
}
delay(200);
}
}
Explanation:
handleEnd(true) is called when the player reaches the top row.handleEnd(false) is called when the player misses the stack.gameOver = true stops the normal loop() behavior.Try this:
void resetGame() {
currentRow = 7;
currentWidth = 3;
currentPos = 0;
moveInterval = 200;
gameOver = false;
for (int r = 0; r < 8; r++) {
for (int c = 0; c < 8; c++) stack[r][c] = false;
}
}
Explanation:
resetGame() returns all game variables to their starting values.Try this:
Serial.println("Game reset"); so you can see when the game restarts.resetGame() is called after a win.#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.const int buttonPin = 8; sets the button input pin.LedControl lc = LedControl(DIN, CLK, CS, 1); creates the display object for one matrix.int currentRow = 7; starts the moving block at the bottom row.int currentWidth = 3; starts the block three LEDs wide.int currentPos = 0; begins the moving block at the left edge.int direction = 1; makes the block move right initially.bool stack[8][8]; stores which cells are already stacked.unsigned long lastMoveTime = 0; keeps track of the last movement update.int moveInterval = 200; sets how often the block moves.bool gameOver = false; tracks whether the game is paused.
void setup() initializes the display, button input, and clears the stack memory.void loop() repeats forever and either updates the game or waits for a reset.millis() is used for timing so the game remains responsive.digitalRead(buttonPin) == LOW detects a 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.Use the complete code below after you have verified wiring and library installation.
#include <LedControl.h>
// Pin Definitions
int DIN = 10;
int CLK = 13;
int CS = 7;
const int buttonPin = 8;
LedControl lc = LedControl(DIN, CLK, CS, 1);
// Game Variables
int currentRow = 7; // Start at the bottom
int currentWidth = 3; // Start with 3 blocks
int currentPos = 0; // Leftmost position of the moving block
int direction = 1; // 1 for Right, -1 for Left
bool stack[8][8]; // Memory of placed blocks
unsigned long lastMoveTime = 0;
int moveInterval = 200; // Speed of the moving block
bool gameOver = false;
void setup() {
lc.shutdown(0, false);
lc.setIntensity(0, 5);
lc.clearDisplay(0);
pinMode(buttonPin, INPUT_PULLUP);
// Initialize stack as empty
for (int r = 0; r < 8; r++) {
for (int c = 0; c < 8; c++) stack[r][c] = false;
}
}
void loop() {
if (gameOver) {
if (digitalRead(buttonPin) == LOW) resetGame();
return;
}
// 1. Handle Movement
unsigned long currentTime = millis();
if (currentTime - lastMoveTime >= moveInterval) {
lastMoveTime = currentTime;
currentPos += direction;
// Bounce off walls
if (currentPos + currentWidth > 8 || currentPos < 0) {
direction *= -1;
currentPos += (direction * 2);
}
render();
}
// 2. Handle Button Press (Stacking)
if (digitalRead(buttonPin) == LOW) {
delay(200); // Debounce
placeBlock();
while(digitalRead(buttonPin) == LOW); // Wait for release
}
}
void placeBlock() {
int placedCount = 0;
for (int i = 0; i < currentWidth; i++) {
int col = currentPos + i;
// If it's the bottom row, everything stays.
// Otherwise, check if there is a block underneath.
if (currentRow == 7 || stack[currentRow + 1][col]) {
stack[currentRow][col] = true;
placedCount++;
}
}
// If no blocks landed on top of others, you lose
if (placedCount == 0) {
handleEnd(false);
return;
}
currentWidth = placedCount; // Next row will be thinner
currentRow--; // Move up
moveInterval -= 15; // Speed up slightly
if (currentRow < 0) {
handleEnd(true); // You reached the top!
}
}
void render() {
lc.clearDisplay(0);
// Draw the established stack
for (int r = 0; r < 8; r++) {
for (int c = 0; c < 8; c++) {
if (stack[r][c]) lc.setLed(0, r, c, true);
}
}
// Draw the currently moving block
for (int i = 0; i < currentWidth; i++) {
lc.setLed(0, currentRow, currentPos + i, true);
}
}
void handleEnd(bool win) {
gameOver = true;
for (int i = 0; i < 3; i++) {
lc.clearDisplay(0);
delay(200);
if (win) {
// Flash full screen for win
for(int r=0; r<8; r++) lc.setRow(0, r, 0xFF);
} else {
// Simple X for loss
lc.setRow(0, 0, 0x81); lc.setRow(0, 7, 0x81);
}
delay(200);
}
}
void resetGame() {
currentRow = 7;
currentWidth = 3;
currentPos = 0;
moveInterval = 200;
gameOver = false;
for (int r = 0; r < 8; r++) {
for (int c = 0; c < 8; c++) stack[r][c] = false;
}
}
DIN, CLK, and CS.pinMode(buttonPin, INPUT_PULLUP); is set correctly.gameOver is stuck at true.currentPos + currentWidth > 8 and currentPos < 0.currentWidth is too large, the block may go out of bounds.8 to GND when pressed.INPUT_PULLDOWN if your board supports it.stack[currentRow][col] = true; only runs when the block is above another block or on the bottom row.moveInterval or start with a larger currentWidth.setLed().Serial.println(currentPos); and Serial.println(currentWidth); to debug block position.moveInterval or start with a larger currentWidth.Back to index