
简介这份rar压缩包提供基于LVGL 9.2编写的Flappy Bird小游戏示例面向嵌入式GUI开发者和LVGL入门学习者演示如何在资源受限的嵌入式设备上构建完整游戏界面与交互逻辑。包内共10个文件以8个C源码文件为主涵盖背景渲染、小鸟动作、管道生成、图像资源等模块辅以1个头文件声明接口与1个txt说明文档整体仅27KB代码量精简适合逐行研读。已有337人学习浏览该资源适合用于学习LVGL控件布局、图像贴图、状态机设计以及触摸事件处理等关键技巧。通过分析bird.c、pipe_img.c等模块可理解游戏角色控制、碰撞检测、动态刷新与动画调度等实现思路同时熟悉LVGL 9.2的项目组织方式和编译配置可直接作为图形库游戏开发的基础模板。 lvgl_v9_2_flappybird.rar 这个压缩包我在本地解压跑通之后的第一反应是这玩意儿比想象中适合拿来学 LVGL。如果你正卡在“LVGL 基础控件都摸过一遍但不知道怎么写一个完整应用”的阶段这个项目就是很好的参考样板。它把游戏逻辑、UI 绘制、输入响应、动画刷新全部揉在一个工程里而且用的是 9.2 版本——这个版本的 API 和 8.x 差别不小直接看新版本代码能少走很多弯路。这个项目适合三类人一是刚把 LVGL 跑起来、想看看多页面和自定义绘图怎么组织的人二是想在 ESP32 或 STM32 上做个带交互界面的小玩具、需要参考事件驱动写法的嵌入式开发者三是单纯想在 PC 模拟器上快速验证 LVGL 效果、不想反复烧固件的人。接下来我按自己实际操作和踩坑的顺序把整个项目的干法拆开讲。1. 项目整体设计与思路拆解1.1 为什么偏偏是 Flappy Bird嵌入式图形库的 demo 项目大多喜欢用仪表盘、时钟、菜单列表来展示因为这些场景能覆盖控件库的大部分能力。但 Flappy Bird 这个选题其实更聪明它的核心循环极其简单——只有“上升”和“下降”两个动作碰撞检测也只有两个判断条件但要把这个小游戏做得流畅、好看、操作跟手恰恰要把 LVGL 的定时器、动画、事件回调、图层管理全部用上。这个小游戏天然能把 LVGL 的能力分成三层第一层是静态 UI包括分数、开始按钮、结束弹窗第二层是动态绘制包括背景滚动、管道移动、小鸟旋转第三层是交互反馈包括点击跳转、碰撞震动、分数刷新。这三层恰恰对应了 LVGL 开发的三个进阶阶段。所以说这个 rar 里的代码不只是一个游戏更是一个浓缩版的 LVGL 工程范例。1.2 9.2 版本带来的新变化如果你之前接触的是 8.x 的老代码打开这个项目的第一感觉一定是“这 API 怎么变了这么多”。LVGL 9.2 里比较影响这个项目的改动主要有三处。第一处是初始化流程。lv_init() 之后8.x 里常用 lv_disp_draw_buf_init() 配合 lv_disp_drv_register() 注册显示驱动9.2 改成了 lv_display_create() 直接创建 display 对象再用 lv_display_set_buffers() 设置缓冲区接口更集中了。第二处是输入设备的注册方式旧版的 lv_indev_drv_register() 换成了 lv_indev_create() 加 lv_indev_set_type() 的组合。第三处是样式 API 的调整比如 lv_obj_set_style_bg_color() 这类函数虽然还在但 9.x 对 style 的局部更新做了更多优化部分场景下可以直接用 lv_obj_set_local_style_prop() 来减少样式拷贝。这些改动直接影响你读源码时的理解成本。如果你现在还在用 8.x 的教程去套 9.2 的代码大概率会在编译阶段就卡住。1.3 这个方案的优势与避坑点这个项目选 LVGL 做游戏渲染而不是直接在 TFT 驱动上画像素最大的优势是你不需要关心底层屏幕驱动和内存写入方式LVGL 帮你把帧缓冲管理好了你要做的就是逻辑和界面表现。但也正因为 LVGL 是 GUI 库而不是游戏引擎它有几个天然的限制需要绕开一是它没有物理引擎重力加速度得自己算二是它的默认刷新机制是“脏矩形”模式频繁更新大区域会掉帧三是动画回调的时机和系统 tick 有关不能依赖 lv_anim 做实时物理演算。这些限制恰好促使你写出更高效的代码——比如只刷新变化区域、把固定背景拆成独立 layer、用静态变量缓存临时计算结果。2. 核心细节解析与实操要点2.1 代码结构一窥解压之后工程目录大致是这样组织的main.c程序入口负责初始化 LVGL、显示驱动、输入设备和定时器ui /存放 UI 相关的代码比如主页面的控件布局、样式设置game /游戏逻辑模块包含小鸟、管道、碰撞检测等lv_conf.hLVGL 的配置文件决定使用哪些功能模块、缓冲区大小、内存池大小这种分离方式很推荐照抄。你把 UI 绘制和游戏逻辑分成两个目录后续迭代会轻松很多。比如要加音效直接在 game 模块加音频初始化不会干扰 ui 目录里的控件代码要换皮肤只改 ui 目录里的素材引用即可。LVGL 的配置文件 lv_conf.h 是重中之重。这个项目跑在模拟器上时缓冲区设置得比较宽裕但如果你要移植到 ESP32 或 STM32就要重新设置 LV_MEM_SIZE、LV_COLOR_DEPTH、LV_DPI_DEF 这几个关键参数。我的建议是先把模拟器调通再做硬件移植这样调试效率高得多。2.2 游戏循环不是 while(1)很多从单片机裸机编程转过来的朋友一看“游戏循环”四个字下意识就写个 while(1) 然后不断刷新画面。但在 LVGL 的框架下这么做是大忌因为 LVGL 有自己的任务调度机制它跑在 lv_timer_handler() 里面。这个项目正确做法是用 lv_timer_create() 创建一个周期定时器在回调函数里更新游戏状态小鸟速度、管道位置、碰撞判断然后只更新需要变化的控件属性例如坐标、角度、文字。LVGL 会在自己的心跳里完成重绘。这种方式的好处是你不会阻塞 LVGL 的事件处理也不会让界面卡死。它的核心思路是“视图随状态自动更新”你把状态算好LVGL 负责把它画出来。一个反面的例子是我在移植这个项目时一开始贪图方便在定时器回调里直接调用 lv_obj_align() 去调整一堆控件的位置结果每帧的布局计算开销很大帧率只剩 20 左右后来改成用 lv_obj_set_pos() 直接设置坐标情况立刻好转。2.3 碰撞检测和动画刷新Flappy Bird 的碰撞检测逻辑相当直白玩家控制的小鸟是一个矩形管道上下两根也是一个矩形只要两个矩形相交就算碰撞。但这里有个细节LVGL 的对象坐标系是相对于父对象的所以在做碰撞检测前必须先统一坐标系。常见的做法是用 lv_obj_get_x() 和 lv_obj_get_y() 获取控件相对父对象的坐标然后再加上父对象的坐标偏移换算成屏幕全局坐标。如果直接拿相对坐标去做判断小鸟和管道只要不在同一个父容器里就永远检测不到碰撞。动画方面LVGL 9.2 的 lv_anim 更适合做“平滑过渡”类效果比如小鸟扇翅膀的帧动画、管道进出场的位移动画。但对于每帧都需要实时计算的状态比如重力导致的小鸟位置变化、速度变化就不适合用 lv_anim直接在定时器回调里按时间步长计算更加可控。3. 实操过程与核心环节实现3.1 环境搭建模拟器优先推荐的调试环境是 PC 模拟器。LVGL 官方提供了基于 SDL 的模拟器工程配合 VS Code 使用直接 CMake 编译就能跑起来。这个 rar 包里的代码应该也是基于模拟器框架写的所以编译运行起来比较省事。具体步骤是安装 VS Code并装好 C/C 扩展、CMake 工具扩展。安装 SDL2 开发库。Windows 上直接下载 SDL2-devel-2.0.x-mingw.tar.gz解压到指定目录即可Linux 上直接 apt install libsdl2-dev。用 CMake 配置并编译整个工程。编译成功后直接运行可执行文件就能弹出一个窗口显示小游戏界面。如果编译时报找不到 SDL2多半是 CMakeLists.txt 里的 SDL2_DIR 路径不对改成自己的实际路径即可。这一步解决了后面就顺了。3.2 移植到 ESP32 的关键改动模拟器跑通之后你就想把游戏搬到实体硬件上。这里我以 ESP32 为例说说要改哪些地方。LVGL 的显示驱动要换成你的屏幕驱动比如 ILI9341、ST7789 这类常见的 TFT 驱动芯片。你要做的就是实现一个“把像素刷新到屏幕”的函数LVGL 负责生成像素数据你负责搬运。缓冲区改成两段式LV_MEM_SIZE 设置到 32KB 以上显示缓冲区用 lv_display_set_buffers() 注册两个缓冲区一个用于前台绘制一个用于后台刷新。双缓冲能明显减少撕裂感。输入设备从鼠标变成触摸屏或者按键。如果用的是物理按键来控制小鸟上升那么注册输入设备时要选择 LV_INDEV_TYPE_KEYPAD 或者 ENCODER然后在按键事件里触发“跳一下”的逻辑。如果打算用触摸屏就直接注册 LV_INDEV_TYPE_POINTER。还要注意 CPU 频率。Flappy Bird 这个游戏逻辑本身不复杂定时器周期建议设在 10-20 毫秒之间也就是 50-100Hz。ESP32 跑 240MHz 完全够用但如果用 STM32F103主频只有 72MHz建议提高优化等级同时把定时器周期放宽到 20ms否则碰撞检测的响应会显得迟钝。3.3 关键代码段解析整个项目里最核心的逻辑就是“帧更新函数”。简化版长这样static void game_timer_cb(lv_timer_t *timer) { // 更新小鸟位置 bird_y_velocity GRAVITY; bird_obj_y bird_y_velocity; // 更新管道位置 for (int i 0; i PIPE_NUM; i) { pipe_x[i] - SCROLL_SPEED; if (pipe_x[i] -PIPE_WIDTH) { pipe_x[i] SCREEN_WIDTH; pipe_y[i] rand() % PIPE_RANGE PIPE_OFFSET; } } // 碰撞检测 if (is_collision(bird_obj_x, bird_obj_y, pipe_x, pipe_y)) { game_over(); return; } // 更新控件位置 lv_obj_set_pos(bird_img, bird_obj_x, bird_obj_y); lv_obj_set_x(pipe_upper[i], pipe_x[i]); lv_obj_set_y(pipe_upper[i], pipe_y[i] - PIPE_HEIGHT / 2); }看到没有逻辑非常简单但每行都有讲究。核心是把“游戏世界的坐标”和“LVGL 控件的坐标”分开维护。你计算时用的是 bird_obj_x、pipe_x 这样的临时变量计算完再统一同步给 LVGL 控件。这样碰撞检测和 UI 绘制互不干扰也方便你后续加暂停、重开功能。我见过不少新手把 bird 的 y 坐标直接存在 lv_obj 的属性里每次计算都通过 lv_obj_get_y() 读出来再调用 lv_obj_set_y() 写回去。这样频繁调用 getter 和 setter 不但效率低还容易把逻辑搞乱。所以强烈建议单独用一组变量维护游戏状态UI 只是状态的可视化输出。3.4 界面切换与页面管理这个项目的另一个亮点是页面管理开始页、游戏页、结束页三个界面之间的切换。LVGL 9.x 里有几种做法可以让页面切换更干净而这个项目采用的方式是“对象创建 删除”。具体来说游戏初始化时先创建开始页上面放标题、开始按钮和操作提示。点击开始按钮后销毁开始页的所有控件然后创建游戏页面的所有控件。游戏结束时销毁游戏页面创建结束页面显示分数和重新开始按钮。这种“按需创建、用完即删”的方式在 MCU 项目里很实用。内存是宝贵的页面全部常驻会浪费 RAM。每次切换都动态创建和销毁控件内存占用能控制在比较稳定的水平。缺点是需要认真处理控件指针的生命周期避免操作已删除的控件导致崩溃。这里有一个容易踩的坑在页面切换时若旧的定时器回调还没被移除依然在访问旧页面的控件就会造成野指针访问。正确的做法是在销毁页面之前先用 lv_timer_del() 把游戏定时器删掉再清理页面控件。3.5 中文显示的处理游戏里如果需要显示中文例如“得分”、“重新开始”这些文字LVGL 默认英文字体是不带中文字形的。这个项目如果改成中文界面最简单的方式是使用 LVGL 的字体转换工具把需要的几个汉字转成 C 语言数组。操作流程大致是打开 LVGL 的在线字体转换工具LVGL Font Converter。选择字体文件这里建议选一个常见的开源中文字体注意看授权许可。设置字体大小例如 16 或者 20 像素。在 “Range” 里勾选需要的字符范围。但如果只需要几个汉字最好直接手动输入那几个字符避免生成几 MB 的字库文件。生成后得到一个 .c 文件放到项目里然后通过 LV_FONT_DECLARE 声明使用。这样游戏界面就能正常显示中文了。需要注意的是显示的每个汉字都会占用存储空间所以界面设计时应当使用固定行文避免动态拼接大量中文字符串导致的存储空间浪费。4. 常见问题与排查技巧实录4.1 “画面不刷新只有第一次绘制”这个问题的常见原因是没有在 while 循环里调用 lv_timer_handler()。LVGL 的刷新机制是基于定时器驱动的如果你在主循环里只做了延时没有调用 lv_timer_handler()LVGL 永远不会去处理待重绘的任务。排查思路确认主循环里面 lv_timer_handler() 的调用频率。在模拟器项目里通常用 SDL_Delay 控制帧率同时每帧调用一次 handler。在嵌入式项目里通常在 while(1) 里调用 lv_timer_handler() 后再 delay(5) 左右。4.2 “帧率不稳定有时卡顿”帧率问题大多出在两个地方一是缓冲区太小导致 LVGL 频繁刷屏二是在定时器回调里做了过多耗时的运算。对比一下缓冲区和帧率的表现关系用 240x320 分辨率、16 位色深举例一帧画面大约需要 150KB 显存。假如只有 10KB 缓冲区那一次刷新就需要十几块区域合并效率非常低。缓冲区大小刷新区域个数表现10KB12明显卡顿画面有撕裂感30KB4基本流畅但复杂界面略卡60KB2流畅接近满帧150KB1极致流畅但内存占用高所以如果你在 MCU 上发现帧率上不去优先看能不能加大缓冲区。如果内存确实有限就把游戏背景做成静态图片用 lv_canvas 提前画好更新时只刷新动态区域效果也很不错。4.3 “点击没反应或者点击位置总是偏移”这个问题在触摸屏项目中特别常见。LVGL 的触摸坐标校准是基于输入设备的回调接口如果你设置不到位就会出现点击偏差。排查时先在初始化里确保触控阈值设置合理然后在 LVGL 里注册输入设备时正确设置 read_cb 回调在回调里把触摸芯片读到的坐标转换为 LVGL 的坐标范围。比如你的屏幕是 240x320触摸芯片返回坐标范围是 0-4095那就需要做一个线性映射把坐标映射到 0-239 和 0-319 区间。void touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data) { uint16_t x_raw, y_raw; touch_read(x_raw, y_raw); style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />