【STM32】是否使用U8G2图形库?从小游戏项目看开发差异
- 一、前言
- 二、系统介绍
-
- [1. 项目实现的功能](#1. 项目实现的功能)
- [2. 项目硬件组成](#2. 项目硬件组成)
- 三、项目软件开发
-
- [1. 小恐龙游戏的显示控制方案对比](#1. 小恐龙游戏的显示控制方案对比)
- [2. 贪吃蛇的显示控制方案对比](#2. 贪吃蛇的显示控制方案对比)
- 四、总结
一、前言
上文通过菜单设计演示U8G2图形库的基础用法(【STM32】U8G2图形库应用--菜单设计与开发 ),本文将借助两个小游戏案例,对比多种显示控制方案,从而理解U8G2图形库的++优势领域++ 和++使用场景++。

二、系统介绍
1. 项目实现的功能
(1)小恐龙游戏:使用IIC通信和U8G2图形库显示方式;只使用IIC通信分别测试"全屏刷新"与"区域刷新"两种显存更新方式。
(2)贪吃蛇游戏:使用IIC通信和U8G2图形库显示方式;只使用IIC通信"区域刷新"显存更新方式。
2. 项目硬件组成
(1)主控芯片:该项目使用的是STM32F103RCT6。
(2)OLED显示屏:本模块搭载的是SSD1306驱动芯片,采用IIC接口,分辨率是128*64。
(3)按键:机械式轻触开关。
三、项目软件开发
1. 小恐龙游戏的显示控制方案对比
(1)下文基于U8G2图形库的全缓冲模式,简要说明小恐龙游戏的代码思路。

小恐龙游戏显示思路:
● ++清全屏缓冲++ 。
● ++绘制分数变量到缓冲++ 。由于U8G2图形库没有直接显示变量参数,需要将变量转成字符串,再通过绘制字符串实现分数显示。
● ++绘制背景元素(地面、云、太阳)到缓冲++ 。通过偏移量循环绘制地面与云层以实现左移滚动效果;同样随机绘制太阳。
● ++根据按键操作绘制恐龙跳跃障碍物或平地走动的图形到缓冲++ 。跳跃时按轨迹更新垂直坐标实现上下移动动画;行走时交替绘制两帧位图实现循环走动效果。
● ++绘制仙人掌图形到缓冲++ 。更新仙人掌水平坐标实现左移,当完全移出屏幕后,重置其位置并随机选择新的仙人掌形态重新生成。
● ++最后发送缓冲区数据,完成画面刷新++。
c
//======================================//
/* 绘制恐龙游戏页面 */
//======================================//
void Display_Dinosaur_Game(void)
{
char string_buff[11]; /* 用于存储变量转换的字符串 */
Dino_Init();
while(1)
{
// 游戏结束
if(dino_failed_flag==1)
{
Dino_Draw_Restart();
if(key_value == KEY_CONFIRM_PRESSED5) /* 重启游戏 */
{
Dino_Init();
key_value=0;
}
}
// 开始游戏
else
{
u8g2_ClearBuffer(&u8g2);
// --- 显示当前分数和最高分数
dino_score++;
snprintf(string_buff,sizeof(string_buff),"%d",dino_score); /* %d : 10进制 */
u8g2_DrawStr(&u8g2,0,12,string_buff);
snprintf(string_buff,sizeof(string_buff),"%d",dino_highest_score);
u8g2_DrawStr(&u8g2,64,12,string_buff);
// --- 复位标志位
if(dino_height==0)
dino_reset_flag = 0;
// --- 显示地面
Dino_Draw_Ground();
// --- 显示云
Dino_Draw_Cloud();
// --- 显示太阳
if(rand()%500==250||sun_over_flag==1) /* 生成0到499之间整数是否为250 */
{
if(Dino_Draw_Sun())
sun_over_flag=1;
else
sun_over_flag=0;
}
// --- 显示恐龙
if(dino_height > 0 || key_value == KEY_UP_PRESSED1) /* 恐龙跳起来 */
{
dino_height = Dino_Draw_Jump(dino_reset_flag);
key_value = 0;
}
else /* 恐龙不跳 */
{
Dino_Draw_Walk();
}
// --- 显示仙人掌
if(cactus_type==0 )
cactus_width=8;
else if(cactus_type==1 || cactus_type==3)
cactus_width=16;
else if(cactus_type==2 || cactus_type==4)
cactus_width=24;
cactus_pos_x=Dino_Draw_Rand_Cactus(cactus_type,0); /* 获取仙人掌坐标并显示仙人掌 */
if(cactus_pos_x + cactus_width < 0) /* 仙人掌左移超出显示范围 */
{
cactus_type = rand()%5; /* 0~4随机生成 */
Dino_Draw_Rand_Cactus(cactus_type,1);
}
// --- 判断是否触发失败条件
if(dino_height<16 && cactus_pos_x<=24) /* 恐龙跳起高度小于阻挡物体高度 && 阻挡物体起始x坐标小于恐龙末x坐标 */
{
if(cactus_type==0 && cactus_pos_x>=0) /* 阻挡物末x坐标大于恐龙起始x坐标 */
dino_failed_flag=1;
else if((cactus_type==1 || cactus_type==3) && cactus_pos_x>=-8)
dino_failed_flag=1;
else if((cactus_type==2 || cactus_type==4) && cactus_pos_x>=-16)
dino_failed_flag=1;
}
}
u8g2_SendBuffer(&u8g2);
}
}
其中Dino_Init函数是将参数恢复至初始值;Dino_Draw_Restart函数是绘制游戏结束界面。
碰撞检测逻辑(同时满足以下条件触发游戏失败):
● ++垂直方向判定++ :恐龙的Y坐标低于障碍物顶部高度。
● ++水平方向判定++:恐龙右侧末端坐标大于障碍物左侧起始坐标,且恐龙左侧起始坐标小于障碍物右侧末端坐标。

(2)仅采用IIC通信实现的"全屏刷新"不需要清全屏显存,只需要修改要更新的显存,其他的逻辑与U8G2库底层机制一致,因此刷新频率相当。
主要区别在于++开发效率++:而仅采用IIC通信的需手动编写底层显示驱动代码,而U8G2通过封装好的API函数简化了流程,无需关注底层细节,从而显著提升开发速度。
(3)仅采用IIC通信的"区域刷新"显存更新方式,核心思路与前述逻辑类似,但通过摒弃全屏清屏与整帧更新操作,仅针对变动区域进行局部数据写入与即时显示,显著降低了数据传输量,从而实现了比前两种方式更快的刷新速率。
不过,这种控制方式要求开发者手动编写底层显示驱动代码,无法直接依赖现成库函数,因此在++提升性能++ 的同时也++增加了开发复杂度++。
2. 贪吃蛇的显示控制方案对比
(1)下文基于U8G2图形库的全缓冲模式,简要说明贪吃蛇游戏的代码思路。

贪吃蛇游戏显示思路 :
● ++清全屏缓冲++ 。
● ++绘制分数变量到缓冲++ 。
● ++绘制边界到缓冲++ 。明确界定蛇移动区域范围。
● ++绘制食物和蛇到缓冲++ 。需要获取最新的坐标参数。
● ++最后发送缓冲区数据,完成画面刷新++。
c
//=======================================//
/* 绘制贪吃蛇游戏页面 */
//=======================================//
#define MOVE_DELAY 5 /* 控制移动速度 */
void Display_Snake_Game(void)
{
uint8_t snake_move_delay=0; /* 移动速度 */
char string_buff[3]; /* 用于存储变量转换的字符串 */
Snake_Init();
while(1)
{
// 游戏结束
if(snake_failed_flag)
{
Snake_Draw_Restart();
if(key_value == KEY_CONFIRM_PRESSED5) /* 重启游戏 */
{
snake_move_delay=0;
Snake_Init();
key_value=0;
}
}
// 开始游戏
else
{
u8g2_ClearBuffer(&u8g2);
// --- 显示当前分数和最高分数
snprintf(string_buff,sizeof(string_buff),"%d",snake_score); /* %d : 10进制 */
u8g2_DrawStr(&u8g2,0,12,string_buff);
snprintf(string_buff,sizeof(string_buff),"%d",snake_highest_score);
u8g2_DrawStr(&u8g2,64,12,string_buff);
// --- 显示边界
u8g2_DrawFrame(&u8g2,0,15,u8g2_GetDisplayWidth(&u8g2),u8g2_GetDisplayHeight(&u8g2)-15);
// --- 显示食物
u8g2_DrawXBMP(&u8g2,snake_food.x*8,snake_food.y*8,8,8,BMP_SNAKE_FOOD);
// --- 显示蛇
Snake_Draw_Body();
// --- 确定移动方向
Snake_Move_Direction();
// --- 移动蛇
snake_move_delay++;
if(snake_move_delay > MOVE_DELAY) /* 每隔MOVE_DELAY移动一次 */
{
snake_move_delay=0;
Snake_Move(); /* 移动一个单位 */
}
}
u8g2_SendBuffer(&u8g2);
}
}
其中各函数功能的描述如下:
Snake_Init:++初始化++ 游戏状态,重置食物坐标,蛇身坐标、长度及方向等参数至初始值。
Snake_Draw_Restart:用于显示++游戏结束++ 界面。
Snake_Move_Direction:响应按键输入以++更新移动方向++ ,并执行反向校验,禁止蛇头直接掉头(即新方向不得与当前方向相反)。
Snake_Move:++计算蛇头下一帧坐标++,执行碰撞检测(判断是否撞墙或自撞)及食物检测;若吃到食物,则触发增长逻辑并生成新食物坐标。

(2) 仅采用IIC通信下的"全屏刷新"与"区域刷新"在底层传输效率上存在差异,但对于低频或静态画面而言,这种性能差距并不显著。通过合理调节软件延时(如代码中的MOVE_DELAY参数),即可平衡视觉帧率,使不同刷新模式在人眼感知上达到基本一致的流畅度。
四、总结
以下为三种方式的优劣势对比。

从表格可见, 根据不同场景选择合适的控制显示方式。

搜索关注「YouziTech」 ,获取完整代码!