Community project
K10 贪吃蛇游戏
Generated with AIThis project transforms the UNIHIKER K10 into an interactive space shooter game. Players control an ally ship to dodge enemy fire, shoot down incoming enemies, and survive waves of increasingly challenging opponents across a starfield backdrop.
The guide provides a complete firmware implementation built with Arduino and LVGL graphics library, featuring collision detection, enemy AI with gunner variants, explosion effects, and a menu system. Assembly requires only connecting the K10 to a computer via USB, making this an excellent introduction to game development on embedded systems.
Wiring diagram
Interactive · read-onlyPan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Assembly
2 steps准备 UNIHIKER K10
把 UNIHIKER K10 放在平整、干燥而且不导电的桌面上,让屏幕朝上。这个游戏使用板子自带的彩色屏幕和 A、B 两个按键,所以不需要添加元件或接线。
- Tip: 先找到板子正面的 A 和 B 按键:A 让战机向左移动,B 让战机向右移动。
- ⚠ 不要把螺丝、硬币或其他金属物放在板子背面或接口上,它们可能让板子短路。
连接 USB 数据线
用一根能够传数据的 USB 线把 UNIHIKER K10 接到电脑。USB 线会给板子供电,也会把游戏传到板子上。
- Tip: 游戏开始后,战机会自动连续发射激光;按住 A 或 B 可以左右躲避敌机。敌机被击落会加分,敌机撞到战机或飞到底部会损失一格护盾。护盾用完后,同时按住 A 和 B 可重新开始。
- ⚠ 插拔 USB 线时握住插头,不要拉扯线身或左右掰动板上的接口。
Firmware
ESP32#include <Arduino.h>
#include "unihiker_k10.h"
#include <lvgl.h>
// Hoisted type definitions
enum ScreenState { HOME_SCREEN, MODE_SCREEN, PLAYING };
struct Enemy { lv_obj_t *sprite; int16_t x, y; uint8_t speed; bool active, gunner; uint32_t nextShotAt; };
struct Bullet { lv_obj_t *sprite; int16_t x, y; bool active; };
struct EnemyBullet { lv_obj_t *sprite; int16_t x, y; bool active; };
struct Explosion { lv_obj_t *sprite; uint32_t hideAt; bool active; };
struct Star { lv_obj_t *dot; int16_t x, y; uint8_t speed; };
struct Ally { lv_obj_t *sprite; int16_t x, y; int8_t direction; uint32_t nextShotAt; };
// Forward declarations
bool overlaps(int16_t ax, int16_t ay, int16_t aw, int16_t ah, int16_t bx, int16_t by, int16_t bw, int16_t bh);
void setVisible(lv_obj_t *object, bool visible);
void updateHud();
void updateMenu();
void setGameObjectsVisible(bool visible);
void showHome();
void showModeMenu();
void placeEnemy(uint8_t index, bool fromTop);
void hideBullet(uint8_t index);
void hideEnemyBullet(uint8_t index);
void showExplosion(int16_t x, int16_t y);
void updateExplosions();
void fireFrom(int16_t x, int16_t y);
void fireEnemyShot(uint8_t enemyIndex);
void startGame(bool invincible);
void takeHit();
void buildScreen();
void updateStars();
void updateBullets();
void updateEnemyBullets();
void updateEnemies();
void updateMenuInput(bool aPressed, bool bPressed);
void updateGame();
constexpr int16_t SCREEN_W = 320;
constexpr int16_t SCREEN_H = 240;
constexpr int16_t HUD_H = 28;
constexpr int16_t PLAYER_W = 22;
constexpr int16_t PLAYER_H = 18;
constexpr int16_t PLAYER_Y = 207;
constexpr int16_t ALLY_W = 18;
constexpr int16_t ALLY_H = 14;
constexpr uint8_t ALLY_COUNT = 3;
constexpr uint8_t ENEMY_COUNT = 8;
constexpr uint8_t BULLET_COUNT = 24;
constexpr uint8_t ENEMY_BULLET_COUNT = 16;
constexpr uint8_t EXPLOSION_COUNT = 8;
constexpr uint8_t STAR_COUNT = 18;
constexpr uint32_t FRAME_MS = 25;
UNIHIKER_K10 k10;
lv_obj_t *playerSprite, *scoreLabel, *livesLabel, *messageLabel;
lv_obj_t *titleLabel, *menuLabel, *helpLabel;
Ally allies[ALLY_COUNT];
Enemy enemies[ENEMY_COUNT];
Bullet bullets[BULLET_COUNT];
EnemyBullet enemyBullets[ENEMY_BULLET_COUNT];
Explosion explosions[EXPLOSION_COUNT];
Star stars[STAR_COUNT];
ScreenState screenState = HOME_SCREEN;
bool invincibleMode = false;
bool previousA = false, previousB = false;
bool homeStartSelected = true;
bool normalSelected = true;
int16_t playerX;
uint16_t score;
uint8_t lives;
uint32_t lastFrame, nextShotAt;
lv_obj_t *makeBlock(lv_obj_t *parent, int16_t x, int16_t y, int16_t w, int16_t h, uint32_t color, uint8_t radius = 0) {
lv_obj_t *block = lv_obj_create(parent);
lv_obj_clear_flag(block, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_pos(block, x, y); lv_obj_set_size(block, w, h);
lv_obj_set_style_bg_color(block, lv_color_hex(color), LV_PART_MAIN);
lv_obj_set_style_bg_opa(block, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_border_width(block, 0, LV_PART_MAIN);
lv_obj_set_style_pad_all(block, 0, LV_PART_MAIN);
lv_obj_set_style_radius(block, radius, LV_PART_MAIN);
return block;
}
bool overlaps(int16_t ax, int16_t ay, int16_t aw, int16_t ah, int16_t bx, int16_t by, int16_t bw, int16_t bh) {
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
}
void setVisible(lv_obj_t *object, bool visible) {
if (visible) lv_obj_clear_flag(object, LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(object, LV_OBJ_FLAG_HIDDEN);
}
void updateHud() {
char scoreText[24], livesText[24];
snprintf(scoreText, sizeof(scoreText), "SCORE %u", score);
snprintf(livesText, sizeof(livesText), invincibleMode ? "LIVES INF" : "LIVES %u", lives);
lv_label_set_text(scoreLabel, scoreText);
lv_label_set_text(livesLabel, livesText);
}
void updateMenu() {
if (screenState == HOME_SCREEN) {
lv_label_set_text(titleLabel, "STAR FLEET");
lv_label_set_text(menuLabel, homeStartSelected ? "> START GAME <" : " START GAME");
lv_label_set_text(helpLabel, "A: SELECT B: CONFIRM");
} else {
lv_label_set_text(titleLabel, "CHOOSE MODE");
lv_label_set_text(menuLabel, normalSelected ? "> NORMAL: 5 LIVES <\n\n INVINCIBLE: INF LIVES" : " NORMAL: 5 LIVES\n\n> INVINCIBLE: INF LIVES <");
lv_label_set_text(helpLabel, "A: CHANGE B: START");
}
}
void setGameObjectsVisible(bool visible) {
setVisible(scoreLabel, visible); setVisible(livesLabel, visible); setVisible(messageLabel, visible);
setVisible(playerSprite, visible);
for (uint8_t i = 0; i < STAR_COUNT; ++i) setVisible(stars[i].dot, visible);
for (uint8_t i = 0; i < ALLY_COUNT; ++i) setVisible(allies[i].sprite, visible);
for (uint8_t i = 0; i < ENEMY_COUNT; ++i) setVisible(enemies[i].sprite, visible);
for (uint8_t i = 0; i < BULLET_COUNT; ++i) setVisible(bullets[i].sprite, false);
for (uint8_t i = 0; i < ENEMY_BULLET_COUNT; ++i) setVisible(enemyBullets[i].sprite, false);
for (uint8_t i = 0; i < EXPLOSION_COUNT; ++i) setVisible(explosions[i].sprite, false);
}
void showHome() {
screenState = HOME_SCREEN;
homeStartSelected = true;
setGameObjectsVisible(false);
setVisible(titleLabel, true); setVisible(menuLabel, true); setVisible(helpLabel, true);
updateMenu();
}
void showModeMenu() {
screenState = MODE_SCREEN;
normalSelected = true;
setVisible(titleLabel, true); setVisible(menuLabel, true); setVisible(helpLabel, true);
updateMenu();
}
void placeEnemy(uint8_t index, bool fromTop) {
enemies[index].x = random(8, SCREEN_W - 22);
enemies[index].y = fromTop ? -random(18, 115) : random(40, 150);
enemies[index].speed = random(1, 4);
enemies[index].gunner = random(0, 5) == 0;
enemies[index].nextShotAt = millis() + random(650, 1400);
enemies[index].active = true;
lv_obj_set_style_bg_color(enemies[index].sprite, lv_color_hex(enemies[index].gunner ? 0xA855F7 : 0xF43F5E), LV_PART_MAIN);
lv_obj_set_pos(enemies[index].sprite, enemies[index].x, enemies[index].y);
setVisible(enemies[index].sprite, true);
}
void hideBullet(uint8_t index) { bullets[index].active = false; setVisible(bullets[index].sprite, false); }
void hideEnemyBullet(uint8_t index) { enemyBullets[index].active = false; setVisible(enemyBullets[index].sprite, false); }
void showExplosion(int16_t x, int16_t y) {
for (uint8_t i = 0; i < EXPLOSION_COUNT; ++i) {
if (!explosions[i].active) {
explosions[i].active = true; explosions[i].hideAt = millis() + 120;
lv_obj_set_pos(explosions[i].sprite, x - 5, y - 5); setVisible(explosions[i].sprite, true); return;
}
}
}
void updateExplosions() {
uint32_t now = millis();
for (uint8_t i = 0; i < EXPLOSION_COUNT; ++i) if (explosions[i].active && now >= explosions[i].hideAt) {
explosions[i].active = false; setVisible(explosions[i].sprite, false);
}
}
void fireFrom(int16_t x, int16_t y) {
for (uint8_t i = 0; i < BULLET_COUNT; ++i) if (!bullets[i].active) {
bullets[i].active = true; bullets[i].x = x; bullets[i].y = y;
lv_obj_set_pos(bullets[i].sprite, x, y); setVisible(bullets[i].sprite, true); return;
}
}
void fireEnemyShot(uint8_t enemyIndex) {
for (uint8_t i = 0; i < ENEMY_BULLET_COUNT; ++i) if (!enemyBullets[i].active) {
enemyBullets[i].active = true; enemyBullets[i].x = enemies[enemyIndex].x + 7; enemyBullets[i].y = enemies[enemyIndex].y + 14;
lv_obj_set_pos(enemyBullets[i].sprite, enemyBullets[i].x, enemyBullets[i].y); setVisible(enemyBullets[i].sprite, true); return;
}
}
void startGame(bool invincible) {
invincibleMode = invincible; screenState = PLAYING; score = 0; lives = invincible ? 0 : 5;
playerX = (SCREEN_W - PLAYER_W) / 2;
setVisible(titleLabel, false); setVisible(menuLabel, false); setVisible(helpLabel, false);
setGameObjectsVisible(true);
lv_label_set_text(messageLabel, "A LEFT B RIGHT AUTO LASER"); updateHud();
lv_obj_set_pos(playerSprite, playerX, PLAYER_Y);
for (uint8_t i = 0; i < ALLY_COUNT; ++i) {
allies[i].x = 28 + i * 94; allies[i].y = 150 + i * 14; allies[i].direction = i == 1 ? -1 : 1;
allies[i].nextShotAt = millis() + 120 + i * 60; lv_obj_set_pos(allies[i].sprite, allies[i].x, allies[i].y);
}
for (uint8_t i = 0; i < BULLET_COUNT; ++i) hideBullet(i);
for (uint8_t i = 0; i < ENEMY_BULLET_COUNT; ++i) hideEnemyBullet(i);
for (uint8_t i = 0; i < EXPLOSION_COUNT; ++i) { explosions[i].active = false; setVisible(explosions[i].sprite, false); }
for (uint8_t i = 0; i < ENEMY_COUNT; ++i) {
placeEnemy(i, false); enemies[i].y = 38 + (i * 17) % 126; enemies[i].x = 10 + (i * 41) % (SCREEN_W - 30);
lv_obj_set_pos(enemies[i].sprite, enemies[i].x, enemies[i].y);
}
nextShotAt = millis() + 120;
}
void takeHit() {
showExplosion(playerX + PLAYER_W / 2, PLAYER_Y + PLAYER_H / 2);
if (invincibleMode) return;
if (lives > 0) --lives;
updateHud();
if (lives == 0) showHome();
}
void buildScreen() {
k10.setScreenBackground(0x020617); lv_obj_t *screen = lv_scr_act();
scoreLabel = lv_label_create(screen); lv_obj_set_pos(scoreLabel, 8, 7);
livesLabel = lv_label_create(screen); lv_obj_set_pos(livesLabel, 122, 7);
messageLabel = lv_label_create(screen); lv_obj_set_pos(messageLabel, 8, 224);
for (lv_obj_t *label : {scoreLabel, livesLabel, messageLabel}) {
lv_obj_set_style_text_font(label, &lv_font_montserrat_14, LV_PART_MAIN);
lv_obj_set_style_text_color(label, lv_color_hex(0xE2E8F0), LV_PART_MAIN);
}
lv_obj_set_style_text_color(livesLabel, lv_color_hex(0x67E8F9), LV_PART_MAIN);
lv_obj_set_style_text_color(messageLabel, lv_color_hex(0x94A3B8), LV_PART_MAIN);
titleLabel = lv_label_create(screen); menuLabel = lv_label_create(screen); helpLabel = lv_label_create(screen);
lv_obj_set_pos(titleLabel, 72, 48); lv_obj_set_pos(menuLabel, 48, 108); lv_obj_set_pos(helpLabel, 38, 205);
lv_obj_set_style_text_font(titleLabel, &lv_font_montserrat_24, LV_PART_MAIN);
lv_obj_set_style_text_font(menuLabel, &lv_font_montserrat_16, LV_PART_MAIN);
lv_obj_set_style_text_font(helpLabel, &lv_font_montserrat_14, LV_PART_MAIN);
lv_obj_set_style_text_color(titleLabel, lv_color_hex(0x67E8F9), LV_PART_MAIN);
lv_obj_set_style_text_color(menuLabel, lv_color_hex(0xFDE047), LV_PART_MAIN);
lv_obj_set_style_text_color(helpLabel, lv_color_hex(0x94A3B8), LV_PART_MAIN);
for (uint8_t i = 0; i < STAR_COUNT; ++i) { stars[i].x=random(0,SCREEN_W); stars[i].y=random(HUD_H+2,202); stars[i].speed=random(1,3); stars[i].dot=makeBlock(screen,stars[i].x,stars[i].y,i%3==0?2:1,i%3==0?2:1,0x94A3B8,1); }
playerSprite = makeBlock(screen,0,PLAYER_Y,PLAYER_W,PLAYER_H,0x22D3EE,2); makeBlock(playerSprite,8,2,6,7,0xE0F2FE,2); makeBlock(playerSprite,2,14,5,4,0x2563EB,1); makeBlock(playerSprite,15,14,5,4,0x2563EB,1);
for (uint8_t i=0;i<ALLY_COUNT;++i) { allies[i].sprite=makeBlock(screen,0,0,ALLY_W,ALLY_H,0x60A5FA,2); makeBlock(allies[i].sprite,6,2,6,5,0xDBEAFE,2); makeBlock(allies[i].sprite,1,11,4,3,0x1D4ED8,1); makeBlock(allies[i].sprite,13,11,4,3,0x1D4ED8,1); }
for (uint8_t i=0;i<ENEMY_COUNT;++i) { enemies[i].sprite=makeBlock(screen,0,0,18,14,0xF43F5E,3); makeBlock(enemies[i].sprite,6,3,6,5,0xFDE047,1); }
for (uint8_t i=0;i<BULLET_COUNT;++i) bullets[i].sprite=makeBlock(screen,0,0,4,9,0xA7F3D0,2);
for (uint8_t i=0;i<ENEMY_BULLET_COUNT;++i) enemyBullets[i].sprite=makeBlock(screen,0,0,4,8,0xFB923C,2);
for (uint8_t i=0;i<EXPLOSION_COUNT;++i) { explosions[i].sprite=makeBlock(screen,0,0,14,14,0xFDE047,7); lv_obj_set_style_bg_opa(explosions[i].sprite,LV_OPA_80,LV_PART_MAIN); }
}
void updateStars() { for (uint8_t i=0;i<STAR_COUNT;++i) { stars[i].y+=stars[i].speed; if(stars[i].y>218){stars[i].y=HUD_H+1;stars[i].x=random(0,SCREEN_W);} lv_obj_set_pos(stars[i].dot,stars[i].x,stars[i].y); } }
void updateBullets() {
for(uint8_t i=0;i<BULLET_COUNT;++i) if(bullets[i].active) {
bullets[i].y-=8; if(bullets[i].y<HUD_H){hideBullet(i);continue;} lv_obj_set_pos(bullets[i].sprite,bullets[i].x,bullets[i].y);
bool stopped=false;
for(uint8_t eb=0;eb<ENEMY_BULLET_COUNT;++eb) if(enemyBullets[eb].active && overlaps(bullets[i].x,bullets[i].y,4,9,enemyBullets[eb].x,enemyBullets[eb].y,4,8)) { showExplosion(bullets[i].x,bullets[i].y);hideBullet(i);hideEnemyBullet(eb);stopped=true;break; }
if(stopped) continue;
for(uint8_t e=0;e<ENEMY_COUNT;++e) if(enemies[e].active && overlaps(bullets[i].x,bullets[i].y,4,9,enemies[e].x,enemies[e].y,18,14)) { showExplosion(bullets[i].x,bullets[i].y);hideBullet(i);score+=10;updateHud();placeEnemy(e,true);break; }
}
}
void updateEnemyBullets() {
for(uint8_t i=0;i<ENEMY_BULLET_COUNT;++i) if(enemyBullets[i].active) {
enemyBullets[i].y+=5; if(enemyBullets[i].y>220){hideEnemyBullet(i);continue;} lv_obj_set_pos(enemyBullets[i].sprite,enemyBullets[i].x,enemyBullets[i].y);
bool stopped=false;
for(uint8_t e=0;e<ENEMY_COUNT;++e) if(enemies[e].active && overlaps(enemyBullets[i].x,enemyBullets[i].y,4,8,enemies[e].x,enemies[e].y,18,14)){showExplosion(enemyBullets[i].x,enemyBullets[i].y);hideEnemyBullet(i);stopped=true;break;}
if(stopped) continue;
for(uint8_t a=0;a<ALLY_COUNT;++a) if(overlaps(allies[a].x,allies[a].y,ALLY_W,ALLY_H,enemyBullets[i].x,enemyBullets[i].y,4,8)){showExplosion(enemyBullets[i].x,enemyBullets[i].y);hideEnemyBullet(i);stopped=true;break;}
if(stopped) continue;
if(overlaps(playerX,PLAYER_Y,PLAYER_W,PLAYER_H,enemyBullets[i].x,enemyBullets[i].y,4,8)){hideEnemyBullet(i);takeHit();if(screenState!=PLAYING)return;}
}
}
void updateEnemies() {
uint32_t now=millis();
for(uint8_t i=0;i<ENEMY_COUNT;++i) if(enemies[i].active) {
enemies[i].y+=enemies[i].speed; if(enemies[i].y>220){placeEnemy(i,true);continue;}
if(enemies[i].y>HUD_H && now>=enemies[i].nextShotAt){fireEnemyShot(i);enemies[i].nextShotAt=now+random(900,1600);}
if(overlaps(playerX,PLAYER_Y,PLAYER_W,PLAYER_H,enemies[i].x,enemies[i].y,18,14)){placeEnemy(i,true);takeHit();if(screenState!=PLAYING)return;}
lv_obj_set_pos(enemies[i].sprite,enemies[i].x,enemies[i].y);
}
}
void updateMenuInput(bool aPressed, bool bPressed) {
if (screenState == HOME_SCREEN) { if (bPressed && !previousB) showModeMenu(); }
else { if (aPressed && !previousA) { normalSelected=!normalSelected; updateMenu(); } if (bPressed && !previousB) startGame(!normalSelected); }
}
void updateGame() {
bool aPressed=k10.buttonA->isPressed(), bPressed=k10.buttonB->isPressed(); uint32_t now=millis();
if(screenState!=PLAYING){updateMenuInput(aPressed,bPressed);previousA=aPressed;previousB=bPressed;return;}
if(aPressed&&!bPressed) playerX-=3; if(bPressed&&!aPressed) playerX+=3; playerX=constrain(playerX,2,SCREEN_W-PLAYER_W-2); lv_obj_set_pos(playerSprite,playerX,PLAYER_Y);
if(now>=nextShotAt){fireFrom(playerX+PLAYER_W/2-2,PLAYER_Y-8);nextShotAt=now+110;}
for(uint8_t i=0;i<ALLY_COUNT;++i) if(now>=allies[i].nextShotAt){fireFrom(allies[i].x+ALLY_W/2-2,allies[i].y-8);allies[i].nextShotAt=now+120+i*10;}
updateStars();
for(uint8_t i=0;i<ALLY_COUNT;++i){allies[i].x+=allies[i].direction*(i==1?3:2);if(allies[i].x<=2){allies[i].x=2;allies[i].direction=1;}else if(allies[i].x>=SCREEN_W-ALLY_W-2){allies[i].x=SCREEN_W-ALLY_W-2;allies[i].direction=-1;}lv_obj_set_pos(allies[i].sprite,allies[i].x,allies[i].y);}
updateExplosions();updateBullets();updateEnemyBullets();if(screenState==PLAYING)updateEnemies();previousA=aPressed;previousB=bPressed;
}
void setup(){k10.begin();k10.initScreen(2);randomSeed(micros());buildScreen();showHome();lastFrame=millis();}
void loop(){uint32_t now=millis();if(now-lastFrame>=FRAME_MS){lastFrame=now;updateGame();}lv_timer_handler();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.