Community project

LVGL 俄罗斯方块

Rockets_cn

Published July 21, 2026

ESP320 components2 assembly steps
Remix this project
Photo of LVGL 俄罗斯方块

This project brings the classic Tetris game to the UNIHIKER K10 development board using LVGL graphics library. The game features all seven standard Tetris pieces with rotation mechanics, line-clearing logic, and a colorful palette displayed on the board's integrated screen.

Builders will receive a complete firmware implementation with collision detection, input handling, and game state management. The guide includes the wiring diagram for the K10 board, a full parts list, and step-by-step assembly instructions to get the game running immediately.

Wiring diagram

Interactive · read-only

Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.

Assembly

2 steps
  1. 准备行空板 K10

    使用一条可传输数据的 USB-C 线连接 UNIHIKER K10 与电脑。此项目只使用板载屏幕、A/B 按键与加速度计,因此无需面包板或外接接线。

    • Tip: 将 K10 平放在桌面上并确保屏幕朝上;游戏会以此方向作为倾斜控制的基准。
    • 仅使用可靠的 5 V USB 供电;不要同时向扩展接口外接不明电源。
  2. 确认板载控制方式

    屏幕显示游戏后,轻微向左或右倾斜 K10 控制方块横向移动;向下倾斜可加速下落。按 A 键旋转方块,按 B 键暂停或继续;游戏结束后按 B 键重新开始。

    • Tip: 若左右方向与习惯相反,直接将整块板转向 180 度后使用,或告诉我以修改控制方向。
    • 避免大幅甩动设备,以免 USB-C 线或接口受力。

Firmware

ESP32
main.cppDeploy to device
#include <Arduino.h>
#include "unihiker_k10.h"


// Forward declarations
bool hasBlock(int8_t type, int8_t rotation, int8_t x, int8_t y);
bool collides(int8_t tx, int8_t ty, int8_t tr);
uint8_t visibleColor(uint8_t row, uint8_t col);
void drawText(const char *text, uint16_t x, uint16_t y, uint32_t color);
void renderGame();
void clearLines();
void spawnPiece();
void lockPiece();
void startGame();
void handleInput(uint32_t now);

UNIHIKER_K10 k10;

constexpr uint8_t COLS = 10;
constexpr uint8_t ROWS = 18;
constexpr uint8_t CELL = 11;
constexpr uint16_t BOARD_X = 8;
constexpr uint16_t BOARD_Y = 34;
constexpr uint32_t INPUT_REPEAT_MS = 170;
constexpr uint32_t BLACK = 0x101018;
constexpr uint32_t WHITE = 0xF5F5F5;

uint8_t board[ROWS][COLS] = {};
const uint32_t palette[] = {
  BLACK, 0x00D8FF, 0x3F6FFF, 0xFFE23B,
  0xFF8A3D, 0x39DB76, 0xB05CFF, 0xFF4D80
};

const uint8_t pieces[7][4][4][4] = {
  {{{0,0,0,0},{1,1,1,1},{0,0,0,0},{0,0,0,0}},{{0,0,1,0},{0,0,1,0},{0,0,1,0},{0,0,1,0}},{{0,0,0,0},{1,1,1,1},{0,0,0,0},{0,0,0,0}},{{0,1,0,0},{0,1,0,0},{0,1,0,0},{0,1,0,0}}},
  {{{1,0,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}},{{0,1,1,0},{0,1,0,0},{0,1,0,0},{0,0,0,0}},{{0,0,0,0},{1,1,1,0},{0,0,1,0},{0,0,0,0}},{{0,1,0,0},{0,1,0,0},{1,1,0,0},{0,0,0,0}}},
  {{{0,0,1,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}},{{0,1,0,0},{0,1,0,0},{0,1,1,0},{0,0,0,0}},{{0,0,0,0},{1,1,1,0},{1,0,0,0},{0,0,0,0}},{{1,1,0,0},{0,1,0,0},{0,1,0,0},{0,0,0,0}}},
  {{{0,1,1,0},{0,1,1,0},{0,0,0,0},{0,0,0,0}},{{0,1,1,0},{0,1,1,0},{0,0,0,0},{0,0,0,0}},{{0,1,1,0},{0,1,1,0},{0,0,0,0},{0,0,0,0}},{{0,1,1,0},{0,1,1,0},{0,0,0,0},{0,0,0,0}}},
  {{{0,1,1,0},{1,1,0,0},{0,0,0,0},{0,0,0,0}},{{0,1,0,0},{0,1,1,0},{0,0,1,0},{0,0,0,0}},{{0,0,0,0},{0,1,1,0},{1,1,0,0},{0,0,0,0}},{{1,0,0,0},{1,1,0,0},{0,1,0,0},{0,0,0,0}}},
  {{{0,1,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}},{{0,1,0,0},{0,1,1,0},{0,1,0,0},{0,0,0,0}},{{0,0,0,0},{1,1,1,0},{0,1,0,0},{0,0,0,0}},{{0,1,0,0},{1,1,0,0},{0,1,0,0},{0,0,0,0}}},
  {{{1,1,0,0},{0,1,1,0},{0,0,0,0},{0,0,0,0}},{{0,0,1,0},{0,1,1,0},{0,1,0,0},{0,0,0,0}},{{0,0,0,0},{1,1,0,0},{0,1,1,0},{0,0,0,0}},{{0,1,0,0},{1,1,0,0},{1,0,0,0},{0,0,0,0}}}
};

int8_t currentPiece = 0;
int8_t currentRotation = 0;
int8_t pieceX = 3;
int8_t pieceY = 0;
uint32_t score = 0;
uint16_t totalLines = 0;
uint32_t lastFall = 0;
uint32_t lastInput = 0;
bool paused = false;
bool gameOver = false;
bool lastA = false;
bool lastB = false;
bool screenDirty = true;

bool hasBlock(int8_t type, int8_t rotation, int8_t x, int8_t y) {
  return pieces[type][rotation & 3][y][x] != 0;
}

bool collides(int8_t tx, int8_t ty, int8_t tr) {
  for (uint8_t y = 0; y < 4; ++y) {
    for (uint8_t x = 0; x < 4; ++x) {
      if (!hasBlock(currentPiece, tr, x, y)) continue;
      const int8_t bx = tx + x;
      const int8_t by = ty + y;
      if (bx < 0 || bx >= COLS || by >= ROWS || (by >= 0 && board[by][bx])) return true;
    }
  }
  return false;
}

uint8_t visibleColor(uint8_t row, uint8_t col) {
  for (uint8_t y = 0; y < 4; ++y) {
    for (uint8_t x = 0; x < 4; ++x) {
      if (hasBlock(currentPiece, currentRotation, x, y) && pieceY + y == row && pieceX + x == col) {
        return currentPiece + 1;
      }
    }
  }
  return board[row][col];
}

void drawText(const char *text, uint16_t x, uint16_t y, uint32_t color) {
  k10.canvas->canvasText(text, x, y, color, k10.canvas->eCNAndENFont16, 16, true);
}

void renderGame() {
  // The K10 canvas is buffered: updateCanvas() is the required LCD transfer.
  // Redraw only the game board and the small status panel when state changes.
  for (uint8_t row = 0; row < ROWS; ++row) {
    for (uint8_t col = 0; col < COLS; ++col) {
      const uint16_t x = BOARD_X + col * CELL;
      const uint16_t y = BOARD_Y + row * CELL;
      const uint32_t color = palette[visibleColor(row, col)];
      k10.canvas->canvasRectangle(x, y, CELL - 1, CELL - 1, color, color, true);
    }
  }

  k10.canvas->canvasRectangle(120, 32, 116, 105, BLACK, BLACK, true);
  char scoreText[24];
  snprintf(scoreText, sizeof(scoreText), "Score: %lu", static_cast<unsigned long>(score));
  drawText(scoreText, 122, 43, WHITE);
  char linesText[24];
  snprintf(linesText, sizeof(linesText), "Lines: %u", totalLines);
  drawText(linesText, 122, 61, WHITE);
  if (gameOver) {
    drawText("GAME OVER", 122, 82, 0xFF6B6B);
    drawText("B: restart", 122, 100, WHITE);
  } else if (paused) {
    drawText("PAUSED", 122, 82, 0xFFE23B);
  } else {
    drawText("Playing", 122, 82, 0x39DB76);
  }
  k10.canvas->updateCanvas();
  screenDirty = false;
}

void clearLines() {
  // Compact every non-full row toward the bottom. This handles one to four
  // completed rows in the same lock operation without skipping adjacent rows.
  uint8_t cleared = 0;
  int8_t writeRow = ROWS - 1;
  for (int8_t readRow = ROWS - 1; readRow >= 0; --readRow) {
    bool full = true;
    for (uint8_t col = 0; col < COLS; ++col) {
      if (board[readRow][col] == 0) {
        full = false;
        break;
      }
    }
    if (full) {
      ++cleared;
      continue;
    }
    if (writeRow != readRow) {
      for (uint8_t col = 0; col < COLS; ++col) board[writeRow][col] = board[readRow][col];
    }
    --writeRow;
  }
  while (writeRow >= 0) {
    for (uint8_t col = 0; col < COLS; ++col) board[writeRow][col] = 0;
    --writeRow;
  }
  if (cleared > 0) {
    const uint16_t points[] = {0, 100, 300, 500, 800};
    score += points[cleared];
    totalLines += cleared;
  }
}

void spawnPiece() {
  currentPiece = random(0, 7);
  currentRotation = 0;
  pieceX = 3;
  pieceY = 0;
  if (collides(pieceX, pieceY, currentRotation)) gameOver = true;
}

void lockPiece() {
  for (uint8_t y = 0; y < 4; ++y) {
    for (uint8_t x = 0; x < 4; ++x) {
      if (!hasBlock(currentPiece, currentRotation, x, y)) continue;
      const int8_t bx = pieceX + x;
      const int8_t by = pieceY + y;
      if (by >= 0 && by < ROWS && bx >= 0 && bx < COLS) board[by][bx] = currentPiece + 1;
    }
  }
  clearLines();
  spawnPiece();
  screenDirty = true;
}

void startGame() {
  memset(board, 0, sizeof(board));
  score = 0;
  totalLines = 0;
  paused = false;
  gameOver = false;
  spawnPiece();
  lastFall = millis();
  screenDirty = true;
}

void handleInput(uint32_t now) {
  const bool a = k10.buttonA->isPressed();
  const bool b = k10.buttonB->isPressed();
  if (a && !lastA && !paused && !gameOver) {
    const int8_t next = (currentRotation + 1) & 3;
    if (!collides(pieceX, pieceY, next)) currentRotation = next;
    else if (!collides(pieceX - 1, pieceY, next)) { --pieceX; currentRotation = next; }
    else if (!collides(pieceX + 1, pieceY, next)) { ++pieceX; currentRotation = next; }
    screenDirty = true;
  }
  if (b && !lastB) {
    if (gameOver) startGame();
    else { paused = !paused; screenDirty = true; }
  }
  lastA = a;
  lastB = b;
  if (paused || gameOver || now - lastInput < INPUT_REPEAT_MS) return;

  const float x = k10.getAccelerometerX();
  const float y = k10.getAccelerometerY();
  if (x > 0.38f && !collides(pieceX + 1, pieceY, currentRotation)) { ++pieceX; lastInput = now; screenDirty = true; }
  else if (x < -0.38f && !collides(pieceX - 1, pieceY, currentRotation)) { --pieceX; lastInput = now; screenDirty = true; }
  else if (y > 0.52f && !collides(pieceX, pieceY + 1, currentRotation)) { ++pieceY; lastInput = now; screenDirty = true; }
}

void setup() {
  k10.begin();
  k10.initScreen(2);
  k10.creatCanvas();
  k10.setScreenBackground(BLACK);
  k10.canvas->canvasClear();
  drawText("Tetris", BOARD_X, 8, WHITE);
  drawText("A: rotate", 122, 108, WHITE);
  drawText("B: pause", 122, 126, WHITE);
  randomSeed(micros());
  startGame();
}

void loop() {
  const uint32_t now = millis();
  handleInput(now);
  if (!paused && !gameOver) {
    const uint32_t fallInterval = score < 1000 ? 650 : (score < 2500 ? 480 : 340);
    if (now - lastFall >= fallInterval) {
      lastFall = now;
      if (collides(pieceX, pieceY + 1, currentRotation)) lockPiece();
      else { ++pieceY; screenDirty = true; }
    }
  }
  if (screenDirty) renderGame();
  delay(1);
}

“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik