
前言这学期嵌入式课程设计我选了做个小游戏练练手。一开始觉得不就是个恐龙跳一跳嘛能有多难结果真写起来光是碰撞检测就卡了我两天…… 踩了不少坑这里把整个项目的思路和代码都整理出来希望能帮到同样在做课设的小伙伴。一、项目效果先看基于 STM32F103C8T6 最小系统板 0.96 寸 OLED 屏幕实现的谷歌浏览器小恐龙跳跃游戏恐龙自动奔跑动画两帧切换按键跳跃正弦抛物线手感丝滑随机障碍物三种仙人掌样式地面无限滚动 云朵背景实时计分系统AABB 矩形碰撞检测撞上直接 Game Over硬件就一块最小系统板加个 OLED接线超简单I2C 接口四根线搞定。二、整体架构思路整个游戏的核心其实就三件事画面怎么动起来 —— 所有物体坐标每一帧都变恐龙怎么跳起来 —— 用数学函数模拟物理跳跃怎么判断撞上了 —— 矩形包围盒碰撞算法我把代码分成了几个模块物体坐标结构体统一碰撞检测画面绘制模块地面 / 云朵 / 恐龙 / 障碍物游戏逻辑模块Tick 时钟 跳跃 计分碰撞检测模块AABB 算法就这四个主要模块三、核心模块详解物体坐标结构体 —— 碰撞检测的基础一开始我想的是碰撞嘛对比像素点不就行了后来发现单片机根本扛不住像素级检测太费时间了。于是用了包围盒的思路给每个物体套一个矩形只判断矩形有没有重叠。struct Object_Position{ uint8_t min_x,min_y,max_x,max_y; };就这么简单一个结构体恐龙和障碍物各定义一个实例碰撞检测全靠它。小 Tips包围盒不用完全贴死图像稍微留一点余量玩家体验会更好不会 擦边就死。场景滚动 —— 地面和云朵怎么 动地面滚动是我觉得最巧妙的部分原理其实超简单准备一张比屏幕宽的地面图片我做了 256 像素宽用一个偏移量 Ground_Pos 不断自增每次从数组里截取 128 像素屏幕宽度显示偏移量超过 256 就归零实现无缝循环地面不断地往左移动地面像素不断循环uint16_t Ground_Pos; void Show_Ground(void){ if(Ground_Pos128){ for(uint8_t i0;i128;i){ OLED_DisplayBuf[7][i]Ground[iGround_Pos]; } }else{ for(uint8_t i0;i255-Ground_Pos;i){ OLED_DisplayBuf[7][i]Ground[iGround_Pos]; } for(uint8_t i255-Ground_Pos;i128;i){ OLED_DisplayBuf[7][i]Ground[i-(255-Ground_Pos)]; }云朵也是一模一样的逻辑就是速度慢一点营造层次感。恐龙跳跃 —— 正弦函数 yyds跳跃怎么实现一开始我想的是直接写死高度先上升再下降。但那样跳起来很生硬像机器人。后来想到了正弦函数0 到 π 之间不就是一个完美的抛物线吗jump_Pos 28 * sin(pi * jump_t / 1000.0f);jump_t 是跳跃计时器从 0 加到 1000除以 1000 再乘 π正好映射到 0 ~ π 的范围乘 28 是跳跃最大高度像素这样跳起来的轨迹就是慢起→最高点最慢→加速下落和真实物理感特别像。踩坑记录一开始没加跳跃状态锁在空中按按键会重新起跳直接卡出 bug 飞天了。一定要加 dino_jump_flag 判断只有在地面才能再次起跳4.障碍物生成 —— 随机但可控障碍物和地面同步向左移动跑出屏幕后重新生成int8_t Barrier_flag; uint8_t Barrier_Pos; struct Object_Position barrier; void Show_Barrier(){ if(Barrier_Pos143){ Barrier_flagrand()%3; }四、重点来了AABB 碰撞检测原理四种条件必须同时达成才会被判定为碰撞。这是整个游戏的核心也是我卡最久的地方。什么是 AABBAABB Axis-Aligned Bounding Box轴对齐包围盒。说白了就是两个边都和坐标轴平行的矩形判断它们有没有重叠。怎么判断碰撞直接想 两个矩形什么时候重叠 有点绕换个思路什么时候一定不碰撞两个矩形不碰撞只有四种情况矩形 A 在 矩形 B 左边 —— A 的右边 B 的左边矩形 A 在 矩形 B 右边 —— A 的左边 B 的右边矩形 A 在 矩形 B 上边 —— A 的下边 B 的上边矩形 A 在 矩形 B 下边 —— A 的上边 B 的下边反过来四个条件都不满足 → 两个矩形重叠 → 发生碰撞对应代码int isColliding(struct Object_Position *a, struct Object_Position *b) { // 完整括号 成员名正确 if( (a-max_x b-min_x) (a-min_x b-max_x) (a-max_y b-min_y) (a-min_y b-max_y) ) { OLED_Clear(); OLED_ShowString(28, 24, Game Over, OLED_8X16); OLED_Update(); Delay_s(1); OLED_Clear(); OLED_Update(); return 1; } return 0; }就这四行判断搞定碰撞检测。用大白话再解释一遍恐龙的右手 伸过了 障碍物的左手恐龙的左手 还在 障碍物的右手 左边 → 左右方向已经交叉了上下方向同理 → 横竖都交叉 两个方块叠在一起 撞上了为什么用 AABB 不用像素级检测对比项 AABB 矩形碰撞 像素级碰撞 运算量 4 次整数比较 上百次像素逐点对比 单片机压力 几乎为零 严重卡顿 代码复杂度 简单易懂 复杂难维护 游戏体验 足够用 没必要五、游戏心跳Tick 函数整个游戏的节奏全靠 Dino_Tick () 这个函数统一控制它就像游戏的心脏每调用一次时间往前走一格。void Dino_Tick(){ static uint8_t Score_Count,Ground_Count,Cloud_Count; Score_Count; Ground_Count; Cloud_Count; if(Score_Count100){ Score_Count0; Score; } if(Ground_Count20){ Ground_Count0; Ground_Pos; Barrier_Pos; if(Ground_Pos256){ Ground_Pos0; } if(Barrier_Pos144){ Barrier_Pos0; } } if(Cloud_Count50){ Cloud_Count0; Cloud_Pos; if(Cloud_Pos200){ Cloud_Pos0; } } if(dino_jump_flag1){ jump_t; if(jump_t1000){ jump_t0; dino_jump_flag0; } } }不同物体用不同的计数器阈值实现不同的移动速度画面就有了层次感。这个函数一般放在定时器中断里调用保证游戏速度稳定不会因为 CPU 负载变化而忽快忽慢。六、主游戏循环整个游戏的主循环其实非常简洁int DinoGame_Animation() { while (1) { OLED_Clear(); Show_Score(); Show_Ground(); Show_Barrier(); Show_Cloud(); Show_Dino(); OLED_Update(); if (isColliding(dino, barrier)) { return 0; } } } 每一帧的流程就是清屏 → 把所有东西画上去 → 刷新屏幕 → 判断死没死。 经典的游戏循环范式学过 Unity 或者其他游戏引擎的同学应该很熟悉。七、我踩过的那些坑坑 1碰撞检测偶尔失灵现象有时候明明撞上了恐龙却穿过去了。原因障碍物的坐标是在 Show_Barrier () 里更新的如果调用顺序不对就会出现 画面已经更新了但碰撞坐标还没更新 的情况。解决把坐标计算和画面绘制分开先算好所有位置再统一画图最后检测碰撞。坑 2恐龙可以无限连跳现象在空中狂按按键恐龙越飞越高。原因没有判断当前是否在跳跃状态按键直接重置跳跃时间。解决加跳跃状态锁只有 dino_jump_flag 0站在地上时才能起跳。坑 3画面闪烁严重现象屏幕肉眼可见地闪。原因边画边刷新或者清屏和刷新之间间隔太长。解决先在缓冲区里把所有东西都画好最后只调用一次 OLED_Update () 整屏刷新。八、完整核心代码/** ****************************************************************************** * file dino_game.c * brief STM32 OLED 谷歌小恐龙跳跃游戏 * hardware STM32F103C8T6 0.96寸 OLED (I2C) * features 自动奔跑、按键跳跃、随机障碍物、计分、AABB碰撞检测 ****************************************************************************** */ #include stm32f10x.h #include OLED.h #include key.h #include stdlib.h #include math.h #include Delay.h /* 宏定义 */ #define OLED_WIDTH 128 // OLED屏幕宽度 #define OLED_HEIGHT 64 // OLED屏幕高度 #define GROUND_WIDTH 256 // 地面图片总宽度 #define GROUND_ROW 7 // 地面所在行第8行索引7 #define GROUND_SPEED 20 // 地面滚动速度计数阈值 #define DINO_WIDTH 16 // 恐龙宽度 #define DINO_HEIGHT 18 // 恐龙高度 #define DINO_X 0 // 恐龙固定X坐标 #define DINO_GROUND_Y 44 // 恐龙地面Y坐标 #define JUMP_MAX_HEIGHT 28 // 跳跃最大高度 #define JUMP_DURATION 1000 // 跳跃持续时间tick数 #define BARRIER_WIDTH 16 // 障碍物宽度 #define BARRIER_HEIGHT 18 // 障碍物高度 #define BARRIER_Y 44 // 障碍物Y坐标 #define BARRIER_RESET_POS 144 // 障碍物重置位置 #define BARRIER_TYPE_COUNT 3 // 障碍物种类数 #define CLOUD_WIDTH 16 // 云朵宽度 #define CLOUD_HEIGHT 8 // 云朵高度 #define CLOUD_Y 9 // 云朵Y坐标 #define CLOUD_RESET_POS 200 // 云朵重置位置 #define CLOUD_SPEED 50 // 云朵移动速度计数阈值 #define SCORE_SPEED 100 // 加分速度计数阈值 #define SCORE_X 98 // 分数显示X坐标 #define SCORE_Y 0 // 分数显示Y坐标 #define SCORE_DIGITS 5 // 分数显示位数 #define GAME_OVER_X 28 // GameOver文字X坐标 #define GAME_OVER_Y 24 // GameOver文字Y坐标 /* 类型定义 */ /** * brief 物体位置结构体AABB包围盒用于碰撞检测 */ struct Object_Position { uint8_t min_x; // 矩形左边界 uint8_t min_y; // 矩形上边界 uint8_t max_x; // 矩形右边界 uint8_t max_y; // 矩形下边界 }; /* 全局变量 */ int Score; // 游戏分数 uint16_t Ground_Pos; // 地面滚动偏移量 uint8_t Barrier_flag; // 障碍物样式0~2 uint8_t Barrier_Pos; // 障碍物位置偏移 uint8_t Cloud_Pos 0; // 云朵位置偏移 uint8_t dino_jump_flag 0; // 跳跃状态0奔跑1跳跃中 uint16_t jump_t; // 跳跃计时器 uint8_t jump_Pos; // 当前跳跃高度 struct Object_Position barrier; // 障碍物包围盒 struct Object_Position dino; // 恐龙包围盒 extern double pi; // 圆周率外部定义 extern uint8_t KeyNum; // 按键值外部定义 /* 图片资源声明在其他文件中定义 */ extern const uint8_t Ground[]; // 地面图片数组 extern const uint8_t Barrier[][BARRIER_WIDTH * BARRIER_HEIGHT / 8]; // 障碍物图片 extern const uint8_t Dino[][DINO_WIDTH * DINO_HEIGHT / 8]; // 恐龙图片 extern const uint8_t Cloud[]; // 云朵图片 /* 函数声明 */ void Show_Score(void); void Show_Ground(void); void Show_Barrier(void); void Show_Cloud(void); void Show_Dino(void); void Calc_Barrier_Pos(void); void Calc_Dino_Pos(void); int isColliding(struct Object_Position *a, struct Object_Position *b); int DinoGame_Animation(void); void Dino_Tick(void); void DinoGame_Pos_Init(void); /* 画面绘制函数 */ /** * brief 在屏幕右上角显示当前分数 */ void Show_Score(void) { OLED_ShowNum(SCORE_X, SCORE_Y, Score, SCORE_DIGITS, OLED_6X8); } /** * brief 绘制循环滚动的地面 * note 通过偏移量截取不同位置的地面图片实现无缝滚动 */ void Show_Ground(void) { if (Ground_Pos OLED_WIDTH) { /* 偏移量较小直接单段截取 */ for (uint8_t i 0; i OLED_WIDTH; i) { OLED_DisplayBuf[GROUND_ROW][i] Ground[i Ground_Pos]; } } else { /* 偏移量较大分两段拼接后半段 前半段 */ uint16_t offset GROUND_WIDTH - 1 - Ground_Pos; for (uint8_t i 0; i offset; i) { OLED_DisplayBuf[GROUND_ROW][i] Ground[i Ground_Pos]; } for (uint8_t i offset; i OLED_WIDTH; i) { OLED_DisplayBuf[GROUND_ROW][i] Ground[i - offset]; } } } /** * brief 绘制障碍物仙人掌 */ void Show_Barrier(void) { OLED_ShowImage(barrier.min_x, barrier.min_y, BARRIER_WIDTH, BARRIER_HEIGHT, Barrier[Barrier_flag]); } /** * brief 绘制背景云朵 */ void Show_Cloud(void) { OLED_ShowImage(OLED_WIDTH - 1 - Cloud_Pos, CLOUD_Y, CLOUD_WIDTH, CLOUD_HEIGHT, Cloud); } /** * brief 绘制恐龙奔跑动画 / 跳跃姿态 */ void Show_Dino(void) { if (dino_jump_flag 0) { /* 地面奔跑两张图片交替显示形成跑动动画 */ if (Cloud_Pos % 2 0) { OLED_ShowImage(DINO_X, DINO_GROUND_Y, DINO_WIDTH, DINO_HEIGHT, Dino[0]); } else { OLED_ShowImage(DINO_X, DINO_GROUND_Y, DINO_WIDTH, DINO_HEIGHT, Dino[1]); } } else { /* 跳跃中使用跳跃姿态图片Y坐标随高度变化 */ OLED_ShowImage(DINO_X, DINO_GROUND_Y - jump_Pos, DINO_WIDTH, DINO_HEIGHT, Dino[2]); } } /* 坐标计算函数 */ /** * brief 计算障碍物的包围盒坐标 * note 坐标计算与画面绘制分离避免碰撞检测滞后 */ void Calc_Barrier_Pos(void) { /* 障碍物跑出屏幕后重置位置并随机样式 */ if (Barrier_Pos BARRIER_RESET_POS - 1) { Barrier_flag rand() % BARRIER_TYPE_COUNT; Barrier_Pos 0; } barrier.min_x OLED_WIDTH - 1 - Barrier_Pos; barrier.max_x barrier.min_x BARRIER_WIDTH; barrier.min_y BARRIER_Y; barrier.max_y barrier.min_y BARRIER_HEIGHT; } /** * brief 计算恐龙的包围盒坐标并处理按键跳跃 */ void Calc_Dino_Pos(void) { KeyNum Key_GetNum(); /* 只有站在地面上才能再次起跳防止连跳 */ if (KeyNum 1 dino_jump_flag 0) { dino_jump_flag 1; jump_t 0; } /* 根据跳跃状态计算高度正弦抛物线 */ if (dino_jump_flag 1) { jump_Pos (uint8_t)(JUMP_MAX_HEIGHT * sin(pi * jump_t / JUMP_DURATION)); } else { jump_Pos 0; } /* 更新恐龙包围盒 */ dino.min_x DINO_X; dino.max_x DINO_X DINO_WIDTH; dino.min_y DINO_GROUND_Y - jump_Pos; dino.max_y DINO_GROUND_Y DINO_HEIGHT - jump_Pos; } /* 碰撞检测 */ /** * brief AABB轴对齐包围盒碰撞检测 * param a 物体A的包围盒指针 * param b 物体B的包围盒指针 * retval 1发生碰撞0未碰撞 * note 核心原理两个矩形在X轴和Y轴上都有重叠则判定为碰撞 */ int isColliding(struct Object_Position *a, struct Object_Position *b) { /* X轴重叠 且 Y轴重叠 碰撞发生 */ if ((a-max_x b-min_x) (a-min_x b-max_x) (a-max_y b-min_y) (a-min_y b-max_y)) { /* 碰撞处理显示Game Over */ OLED_Clear(); OLED_ShowString(GAME_OVER_X, GAME_OVER_Y, Game Over, OLED_8X16); OLED_Update(); Delay_s(1); OLED_Clear(); OLED_Update(); return 1; } return 0; } /* 游戏主循环 */ /** * brief 游戏主循环 * retval 0游戏结束碰撞 * note 每一帧流程清屏 → 计算坐标 → 绘制画面 → 刷新 → 碰撞检测 */ int DinoGame_Animation(void) { while (1) { OLED_Clear(); /* 先计算所有物体坐标 */ Calc_Barrier_Pos(); Calc_Dino_Pos(); /* 再统一绘制画面 */ Show_Score(); Show_Ground(); Show_Barrier(); Show_Cloud(); Show_Dino(); /* 刷新屏幕 */ OLED_Update(); /* 最后检测碰撞 */ if (isColliding(dino, barrier) 1) { return 0; } } } /* 游戏心跳时钟 */ /** * brief 游戏Tick函数统一控制所有物体的运动节奏 * note 建议放在定时器中断中调用保证游戏速度稳定 */ void Dino_Tick(void) { static uint8_t Score_Count; static uint8_t Ground_Count; static uint8_t Cloud_Count; Score_Count; Ground_Count; Cloud_Count; /* 分数增长 */ if (Score_Count SCORE_SPEED) { Score_Count 0; Score; } /* 地面与障碍物滚动 */ if (Ground_Count GROUND_SPEED) { Ground_Count 0; Ground_Pos; Barrier_Pos; if (Ground_Pos GROUND_WIDTH) { Ground_Pos 0; } if (Barrier_Pos BARRIER_RESET_POS) { Barrier_Pos 0; } } /* 云朵滚动速度最慢营造层次感 */ if (Cloud_Count CLOUD_SPEED) { Cloud_Count 0; Cloud_Pos; if (Cloud_Pos CLOUD_RESET_POS) { Cloud_Pos 0; } } /* 跳跃计时 */ if (dino_jump_flag 1) { jump_t; if (jump_t JUMP_DURATION) { jump_t 0; dino_jump_flag 0; } } } /* 初始化 */ /** * brief 游戏位置初始化开始新一局前调用 */ void DinoGame_Pos_Init(void) { Score 0; Ground_Pos 0; Barrier_Pos 0; Barrier_flag 0; Cloud_Pos 0; jump_Pos 0; jump_t 0; dino_jump_flag 0; }九、总结做这个项目最大的收获不是 写出了一个游戏而是搞懂了几个核心思想包围盒碰撞 —— 原来游戏里的碰撞不是像素对像素而是用几何图形简化游戏循环范式 —— 清屏→更新→绘制→检测所有游戏都是这个套路分层滚动 —— 不同速度的背景叠加就有了纵深感数学的妙用 —— 一个 sin 函数就能搞定跳跃物理比写死高度自然多了如果你也在做嵌入式课设强烈推荐试试做个小游戏比单纯点灯有意思多了而且能把很多知识点串起来。有问题欢迎评论区交流