基于 WS63 + ST7789 240×240 彩屏的经典俄罗斯方块:支持7种方块、踢墙旋转、按键连发、等级加速与消行音效。
俄罗斯方块(Tetris)
1. 项目简介
本项目在小鸿 AI 的 240×240 彩屏上实现了经典俄罗斯方块游戏。棋盘 10×20 格,支持七种方块(I/J/L/O/S/T/Z)的移动、旋转(带水平踢墙)、消行计分与等级加速,右侧信息栏实时显示得分、消行数、等级和下一块预览。音效基于小鸿的 CI1302 声音积木以非阻塞短音呈现,按键接管音量键与 M 键实现全键盘操作。整个游戏只有一个源文件,适合作为小鸿 AI 上屏应用的入门参考案例。
项目信息
| 项 | 值 |
|---|---|
| 工程路径 | vendor/atomgit/xiaohong/samples/tetris/ |
| 核心源码 | screen/bayes_user_screen.c |
| 屏幕/外设 | ST7789 240×240 SPI 彩屏(无额外外设) |
| 音频 | CI1302 语音芯片 + 板载扬声器,bayes_user_audio 积木(16kHz/16bit 非阻塞短音) |
| 输入 | 三个实体按键:音量+(GPIO13)/ 音量-(GPIO12)/ M 键(GPIO11),低电平有效 |
| 固件产物 | firmware/ws63-liteos-app_all.fwpkg |
功能与规则
- 经典 10×20 棋盘俄罗斯方块,七种方块各有固定配色,运行时预生成 4 个旋转朝向的 4×4 位图;
- 旋转支持水平踢墙(±1、±2 格偏移尝试),贴墙、贴堆也能顺利转正;
- 消行计分:1/2/3/4 行分别得 100/300/500/800 分并乘以当前等级;
- 每消 10 行升 1 级,下落周期从 520ms 起每级减 45ms,下限 120ms;
- 右侧信息栏显示 SCORE / LINES / LV / NEXT 预览;
- 菜单、暂停、结束三种全屏覆盖层,状态机切换;
- 全程非阻塞:逻辑跑在两个
lv_timer上,音效走声音积木短音。
| 按键 | 功能 | 效果 |
|---|---|---|
| 音量+(GPIO13) | 左移 | 点按移动一格,按住每约 160ms 连发 |
| 音量-(GPIO12) | 右移 | 点按移动一格,按住每约 160ms 连发 |
| M 键(GPIO11) | 开始 / 旋转 / 暂停 | 菜单与结束界面按下开始;游戏中点按顺时针旋转一格;长按约 0.8s 暂停/继续 |
2. 图片展示


3. 核心代码分析
3.1 源码位置
- 样例目录:
vendor/atomgit/xiaohong/samples/tetris/ - 入口文件:
screen/bayes_user_screen.c
3.2 代码结构
| 段落/模块 | 职责 |
|---|---|
基础工具(rng_next / rot_cw / init_shapes / fits) |
xorshift32 随机数、4×4 位图顺时针旋转、7 种方块 4 朝向预生成、碰撞检测 |
LVGL 小工具(make_rect / make_label / overlay_show / update_hud / update_preview / render_board) |
矩形/文本控件创建、覆盖层显隐、HUD 与预览刷新、棋盘差异渲染 |
游戏逻辑(try_move / try_rotate / lock_piece / spawn_piece / step_down / start_game / game_over) |
移动旋转、锁定入格、消行计分升级、出新块与状态切换 |
定时器回调(gravity_cb / input_cb) |
按等级周期的重力下落;40ms 一帧的按键扫描(边沿检测 + 连发 + 长按暂停) |
界面搭建(build_ui) |
根容器、标题、棋盘底框、右侧信息栏、菜单/暂停/结束三个覆盖层 |
入口(bayes_user_screen_init) |
初始化形状表与 UI、按键接管、创建重力与输入两个 lv_timer |
3.3 关键代码
棋盘渲染采用「差异重绘」:把堆叠格与当前方块叠加成目标视图,只对与上次渲染状态不同的格子做创建、删除或改色,避免每帧重建控件树:
/* 只重画变化的格子:堆叠 + 当前方块叠加后的视图与已渲染状态做差 */
static void render_board(void)
{
uint8_t view[ROWS][COLS];
memcpy(view, s_grid, sizeof(view));
uint16_t m = s_shapes[s_cur][s_rot];
uint8_t col = (uint8_t)(s_cur + 1);
for (int r = 0; r < 4; r++)
for (int c = 0; c < 4; c++)
if (m & (1 << (15 - (r * 4 + c)))) {
int bx = s_px + c;
int by = s_py + r;
if (bx >= 0 && bx < COLS && by >= 0 && by < ROWS)
view[by][bx] = col;
}
for (int r = 0; r < ROWS; r++)
for (int c = 0; c < COLS; c++) {
uint8_t v = view[r][c];
if (v == s_shown[r][c])
continue;
if (v == 0) {
lv_obj_del(s_cells[r][c]);
s_cells[r][c] = NULL;
} else if (s_cells[r][c] == NULL) {
s_cells[r][c] = make_rect(s_root, BOARD_X + c * CELL, BOARD_Y + r * CELL,
CELL - 1, CELL - 1, s_shape_rgb[v - 1]);
} else {
lv_obj_set_style_bg_color(s_cells[r][c], lv_color_hex(s_shape_rgb[v - 1]), 0);
}
s_shown[r][c] = v;
}
}
输入帧用按键积木做边沿检测:点按走一格,按住累计 4 帧(约 160ms)连发一次,手感与手感党都照顾到:
/* 40ms 输入帧:按键扫描 + 边沿/连发 + 长按暂停 */
static void input_cb(lv_timer_t *timer)
{
(void)timer;
switch (s_state) {
case STATE_PLAY: {
bool r_down = bayes_key_down(BAYES_KEY_VOLDW);
bool l_down = bayes_key_down(BAYES_KEY_VOLUP);
bool moved = false;
/* 音量-: 右移(点按一格,按住每 4 帧≈160ms 连发) */
if (bayes_key_pressed(BAYES_KEY_VOLDW)) {
s_rep_r = 0;
moved = try_move(1, 0);
} else if (r_down) {
if (++s_rep_r >= 4) {
s_rep_r = 0;
moved = try_move(1, 0);
}
} else {
s_rep_r = 0;
}
/* 音量+: 左移 */
if (bayes_key_pressed(BAYES_KEY_VOLUP)) {
s_rep_l = 0;
moved = try_move(-1, 0) ? true : moved;
} else if (l_down) {
if (++s_rep_l >= 4) {
s_rep_l = 0;
moved = try_move(-1, 0) ? true : moved;
}
} else {
s_rep_l = 0;
}
if (moved)
bayes_play_tone(600, 120);
/* ... M 键: 点按旋转,长按约 0.8s 暂停 ... */
}
/* ... */
}
}
3.4 运行原理
启动后,LVGL 与屏幕驱动初始化完毕时框架会调用本文件的唯一入口 bayes_user_screen_init()。它先做四件事:用 rot_cw 把 7 个基础方块位图各预旋转出 4 个朝向;用开机 tick 作种子初始化随机数(lv_tick_get() | 1,保证非零);搭建整屏 UI(深色根容器、TETRIS 标题、带边框的棋盘底框、右侧 SCORE/LINES/LV/NEXT 信息栏,以及菜单、暂停、结束三个隐藏的覆盖层);调用 bayes_keys_takeover(true) 接管三个物理键,屏蔽原厂音量/语音行为。最后创建两个 lv_timer:500ms 的重力定时器(随等级动态调速)和 40ms 的输入定时器。
数据流非常直接:s_grid[20][10] 是唯一的世界状态(0 为空、1~7 为固定方块的形状色),当前方块由形状号 + 旋转号 + 坐标表示。输入定时器每 40ms 查询一次按键,移动/旋转前先经 fits() 碰撞检测;重力定时器到点尝试下移,落不下去就 lock_piece() 把方块写进 s_grid,逐行扫描消行(整行上移并计分升级),再 spawn_piece() 取下一块——若出生点即碰撞则进入 GAME OVER。每次状态变化后调用 render_board(),它对比目标视图与已渲染状态,只增删改有变化的格子对象,240×240 的小屏上也毫无压力。所有音效都是 bayes_play_tone() 非阻塞短音(移动 600Hz、旋转 1000Hz、锁定 320Hz、消行 2200Hz、开始 1400Hz、结束 200Hz 等),不阻塞 UI 线程。
3.5 编译与烧录
编译:
rm -fr out/xiaohong/xiaohong/ .ccache/* # 清理
hb set # 选择 mini + xiaohong
hb build -f # 全量编译
需要修改的文件:
| 文件 | 修改内容 |
|---|---|
| 无 | 本案例复用小鸿 AI 产品工程预留的屏幕钩子,将 screen/bayes_user_screen.c 整体替换为样例中的同名文件即可,无需改 samples/BUILD.gn、config.py 或 ohos.cmake |
编译成功后固件产物输出在 firmware/ws63-liteos-app_all.fwpkg。
烧录:
通过 ws63flash(或 Bayes 平台的烧录入口)将 firmware/ws63-liteos-app_all.fwpkg 烧录到设备,串口为 CH340 USB 转串口(如 /dev/cu.wchusbserial*),波特率 115200;工具提示后短按一次 RST 键开始写盘。详细步骤参见《小鸿AI 固件烧录与启动指导手册》。
运行验证:
烧录完成后设备自动重启,屏幕点亮并显示 “TETRIS” 菜单(含 “PRESS M TO START”)即为启动成功。按 M 开始游戏,验证:音量+ 左移、音量- 右移、点按 M 旋转、长按 M 约 0.8s 暂停/继续;消行时应有 2200Hz 提示音,右侧 NEXT 预览随出块刷新。串口以 115200 波特率连接可观察系统启动日志;编译阶段的成功标志如下(运行期日志因 SDK 版本略有差异,以屏幕菜单为准):
[OHOS INFO] xiaohong build success
FWPKG=/work/out/ws63-liteos-app_all.fwpkg
4. 作者信息
- 作者:不羁的木木
- 邮箱:qiaomu8559968@126.com
- 分类:显示
- 许可证:Apache-2.0
- 代码仓库:xiaohong_samples/25_tetris · huqi/xiaohong_samples - AtomGit
站内项目:小鸿 AI 经典俄罗斯方块(Tetris)掌机游戏 · 海星社区
这条主题由主站发布项目时自动创建;主站与论坛的评论会互相同步。