Community project
Rabbit Drawing Display
This project turns an ESP32-based display board into an interactive snake game. The built-in buttons control the snake's direction as it moves across the grid, eating food and growing longer while avoiding collisions. The guide includes the complete firmware, wiring setup via USB, and step-by-step assembly instructions to get the game running.
Makers will receive a fully functional game implementation using LVGL graphics library, a parts list, and instructions for connecting the ESP32 board. The project demonstrates real-time game logic, input handling, and display rendering on a compact embedded system.
Wiring diagram
Assemble it in 3 steps
1. Use the built-in controls
Leave the UNIHIKER K10 as it is. This game uses the color screen and the two buttons already built into the board, so there are no loose wires or extra parts to connect.
- The left built-in button is A and the right built-in button is B.
- Do not connect wires to the board for this project; the game uses hardware already inside the K10.
2. Connect the board by USB
Plug a USB data cable into the UNIHIKER K10 and your computer. The cable powers the board and lets Schematik send the game to it.
- Use a USB data cable, not a charge-only cable.
- Do not force the USB plug; it should slide in with gentle pressure.
3. Play the game
After deploying, the green snake starts moving by itself. Press button A to turn it left and button B to turn it right. Eat the red square, avoid the edges and your own body, then press either button to start a new game after it ends.
- You cannot reverse direction; use several left or right turns to steer around the board.
- Keep fingers clear of the screen while playing so you can see the snake and the red food square.
Deploy the firmware
#include <Arduino.h>
#include <unihiker_k10.h>
#include <lvgl.h>
// Hoisted type definitions
struct Cell {
int8_t x;
int8_t y;
};
// Forward declarations
uint32_t nextRandom();
bool sameCell(Cell a, Cell b);
bool snakeContains(Cell candidate, uint16_t count);
void placeFood();
void setMessage(const char *text, uint32_t color);
void drawChangedCells();
void updateScore();
void startGame();
void turnLeft();
void turnRight();
void moveSnake();
void createInterface();
UNIHIKER_K10 k10;
static const uint8_t COLS = 15;
static const uint8_t ROWS = 18;
static const uint8_t CELL = 14;
static const uint8_t BOARD_X = 15;
static const uint8_t BOARD_Y = 52;
static const uint16_t MAX_CELLS = COLS * ROWS;
lv_obj_t *tiles[ROWS][COLS];
lv_obj_t *scoreLabel;
lv_obj_t *messageLabel;
Cell snake[MAX_CELLS];
uint16_t snakeLength;
Cell food;
int8_t directionX;
int8_t directionY;
uint16_t score;
uint32_t nextMoveAt;
uint32_t randomState = 0x4B10u;
bool gameOver;
bool previousA;
bool previousB;
uint32_t previousBoard[ROWS];
uint32_t nextRandom() {
randomState = randomState * 1103515245u + 12345u;
return randomState;
}
bool sameCell(Cell a, Cell b) {
return a.x == b.x && a.y == b.y;
}
bool snakeContains(Cell candidate, uint16_t count) {
for (uint16_t i = 0; i < count; ++i) {
if (sameCell(snake[i], candidate)) return true;
}
return false;
}
void placeFood() {
do {
food.x = nextRandom() % COLS;
food.y = nextRandom() % ROWS;
} while (snakeContains(food, snakeLength));
}
void setMessage(const char *text, uint32_t color) {
lv_label_set_text(messageLabel, text);
lv_obj_set_style_text_color(messageLabel, lv_color_hex(color), LV_PART_MAIN);
}
void drawChangedCells() {
for (uint8_t row = 0; row < ROWS; ++row) {
uint32_t newBoard = 0;
for (uint8_t col = 0; col < COLS; ++col) {
uint32_t color = 0x102A43;
Cell here = {(int8_t)col, (int8_t)row};
if (sameCell(here, food)) color = 0xF94144;
for (uint16_t segment = 0; segment < snakeLength; ++segment) {
if (sameCell(here, snake[segment])) {
color = (segment == 0) ? 0x8CE99A : 0x38B764;
break;
}
}
uint32_t nibble = (color == 0x102A43) ? 0 : (color == 0xF94144 ? 1 : (color == 0x8CE99A ? 2 : 3));
newBoard |= nibble << (col * 2);
uint32_t oldNibble = (previousBoard[row] >> (col * 2)) & 0x3;
if (nibble != oldNibble) {
lv_obj_set_style_bg_color(tiles[row][col], lv_color_hex(color), LV_PART_MAIN);
}
}
previousBoard[row] = newBoard;
}
}
void updateScore() {
char text[24];
snprintf(text, sizeof(text), "SNAKE %u", score);
lv_label_set_text(scoreLabel, text);
}
void startGame() {
snakeLength = 3;
snake[0] = {(int8_t)(COLS / 2), (int8_t)(ROWS / 2)};
snake[1] = {(int8_t)(COLS / 2 - 1), (int8_t)(ROWS / 2)};
snake[2] = {(int8_t)(COLS / 2 - 2), (int8_t)(ROWS / 2)};
directionX = 1;
directionY = 0;
score = 0;
gameOver = false;
memset(previousBoard, 0xFF, sizeof(previousBoard));
placeFood();
updateScore();
setMessage("A: LEFT B: RIGHT", 0xD9EAF7);
drawChangedCells();
nextMoveAt = millis() + 450;
}
void turnLeft() {
int8_t oldX = directionX;
directionX = directionY;
directionY = -oldX;
}
void turnRight() {
int8_t oldX = directionX;
directionX = -directionY;
directionY = oldX;
}
void moveSnake() {
Cell next = {(int8_t)(snake[0].x + directionX), (int8_t)(snake[0].y + directionY)};
bool eating = sameCell(next, food);
uint16_t collisionCount = eating ? snakeLength : snakeLength - 1;
if (next.x < 0 || next.x >= COLS || next.y < 0 || next.y >= ROWS || snakeContains(next, collisionCount)) {
gameOver = true;
setMessage("GAME OVER - press A or B", 0xFFD166);
return;
}
for (uint16_t i = snakeLength; i > 0; --i) snake[i] = snake[i - 1];
snake[0] = next;
if (eating) {
++snakeLength;
++score;
updateScore();
if (snakeLength == MAX_CELLS) {
gameOver = true;
setMessage("YOU WIN!", 0x8CE99A);
} else {
placeFood();
}
}
drawChangedCells();
}
void createInterface() {
lv_obj_t *screen = lv_scr_act();
lv_obj_set_style_bg_color(screen, lv_color_hex(0x071B2B), LV_PART_MAIN);
lv_obj_set_style_border_width(screen, 0, LV_PART_MAIN);
scoreLabel = lv_label_create(screen);
lv_obj_set_pos(scoreLabel, 15, 12);
lv_obj_set_style_text_color(scoreLabel, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
lv_obj_set_style_text_font(scoreLabel, &lv_font_montserrat_14, LV_PART_MAIN);
messageLabel = lv_label_create(screen);
lv_obj_set_pos(messageLabel, 15, 31);
lv_obj_set_style_text_font(messageLabel, &lv_font_montserrat_14, LV_PART_MAIN);
for (uint8_t row = 0; row < ROWS; ++row) {
for (uint8_t col = 0; col < COLS; ++col) {
tiles[row][col] = lv_obj_create(screen);
lv_obj_set_pos(tiles[row][col], BOARD_X + col * CELL, BOARD_Y + row * CELL);
lv_obj_set_size(tiles[row][col], CELL - 1, CELL - 1);
lv_obj_set_style_border_width(tiles[row][col], 0, LV_PART_MAIN);
lv_obj_set_style_radius(tiles[row][col], 2, LV_PART_MAIN);
}
}
}
void setup() {
k10.begin();
k10.initScreen();
createInterface();
startGame();
}
void loop() {
bool aPressed = k10.buttonA->isPressed();
bool bPressed = k10.buttonB->isPressed();
if (gameOver) {
if ((aPressed && !previousA) || (bPressed && !previousB)) startGame();
} else {
if (aPressed && !previousA) turnLeft();
if (bPressed && !previousB) turnRight();
if ((int32_t)(millis() - nextMoveAt) >= 0) {
moveSnake();
nextMoveAt = millis() + 230;
}
}
previousA = aPressed;
previousB = bPressed;
lv_timer_handler();
delay(5);
}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.




