1@page greedySnake 贪吃蛇 2# 实验介绍 3**贪吃蛇**是一个起源于1976年的街机游戏 Blockade。此类游戏在1990年代由于一些具有小型屏幕的移动电话的引入而再度流行起来,在现在的手机上基本都可安装此小游戏。版本亦有所不同。 4在游戏中,玩家操控一条细长的蛇,它会不停前进,玩家只能操控蛇的头部朝向(上下左右),一路拾起触碰到食物,并要避免触碰到自身或者其他障碍物。每次贪吃蛇吃掉一件食物,它的身体便增长一些。 5 6<div align=center> 7 <img src="https://img.alicdn.com/imgextra/i4/O1CN01I9rGrL1cdq0ojzFbz_!!6000000003624-1-tps-1200-800.gif" style="zoom:50%;" /> 8</div> 9 10# 涉及知识点 11OLED绘图 12按键事件 13 14# 开发环境准备 15## 硬件 16 开发用电脑一台 17 HAAS EDU K1 开发板一块 18 USB2TypeC 数据线一根 19 20## 软件 21### AliOS Things开发环境搭建 22 开发环境的搭建请参考 @ref HaaS_EDU_K1_Quick_Start (搭建开发环境章节),其中详细的介绍了AliOS Things 3.3的IDE集成开发环境的搭建流程。 23 24### HaaS EDU K1 DEMO 代码下载 25 HaaS EDU K1 DEMO 的代码下载请参考 @ref HaaS_EDU_K1_Quick_Start (创建工程章节),其中, 26 选择解决方案: 基于教育开发板的示例 27 选择开发板: haaseduk1 board configure 28 29### 代码编译、烧录 30 参考 @ref HaaS_EDU_K1_Quick_Start (3.1 编译工程章节),点击 ✅ 即可完成编译固件。 31 参考 @ref HaaS_EDU_K1_Quick_Start (3.2 烧录镜像章节),点击 "⚡️" 即可完成烧录固件。 32 33# 设计思路 34## 游戏空间映射到逻辑空间 35当玩家在体验游戏时,他们能操作的都是游戏空间,包括按键的上下左右,对象物体的运动等等。对于开发者而言,我们需要将这些设想的游戏空间映射到逻辑空间中,做好对用户输入的判断,对象运动的处理,对象间交互的判定,游戏整体进程的把控,以及最终将逻辑空间再次映射回游戏空间,返回给玩家。 36## 对象定义 37这一步是将游戏空间中涉及到的对象抽象化。在C语言的实现中,我们将对象抽象为结构体,对象属性抽象为结构体的成员。 38### 蛇 39```c 40typedef struct 41{ 42 uint8_t length; // 当前长度 43 int16_t *XPos; // 逻辑坐标x 数组 44 int16_t *YPos; // 逻辑坐标y 数组 45 uint8_t cur_dir; // 蛇头的运行方向 46 uint8_t alive; // 存活状态 47} Snake; 48``` 49### 食物 50```c 51typedef struct 52{ 53 int16_t x; 54 int16_t y; // 食物逻辑坐标 55 uint8_t eaten; // 食物是否被吃掉 56} Food; 57``` 58### 地图 59```c 60typedef struct 61{ 62 int16_t border_top; 63 int16_t border_right; 64 int16_t border_botton; 65 int16_t border_left; // 边界像素坐标 66 int16_t block_size; // 网格大小 在本实验的实现中 蛇身和食物的大小被统一约束进网格的大小中 67} Map; 68``` 69### 游戏 70```c 71typedef struct 72{ 73 int16_t score; // 游戏记分 74 int16_t pos_x_max; // 逻辑最大x坐标 pos_x_max = (map.border_right - map.border_left) / map.block_size; 75 int16_t pos_y_max; // 逻辑最大y坐标 pos_y_max = (map.border_botton - map.border_top) / map.block_size; 76} snake_game_t; 77``` 78通过Map和snake_game_t的定义,我们将屏幕的 (border_left, border_top, border_bottom, border_right) 部分设定为游戏区域,并且将其切分为 pos_x_max* pos_y_max 个大小为 block_size 的块。继而,我们可以在每个块中绘制蛇、食物等对象。 79## 对象初始化 80在游戏每一次开始时,我们需要给对象一些初始的属性,例如蛇的长度、位置、存活状态,食物的位置、状态, 地图的边界、块大小等等。 81```c 82Food food = {-1, -1, 1}; 83Snake snake = {4, NULL, NULL, 0, 1}; 84Map map = {2, 128, 62, 12, 4}; 85snake_game_t snake_game = {0, 0, 0}; 86 87int greedySnake_init(void) 88{ 89 // 计算出游戏的最大逻辑坐标 用于约束游戏范围 90 snake_game.pos_x_max = (map.border_right - map.border_left) / map.block_size; 91 snake_game.pos_y_max = (map.border_botton - map.border_top) / map.block_size; 92 // 为蛇的坐标数组分配空间 蛇的最大长度是填满整个屏幕 即 pos_x_max* pos_y_max 93 snake.XPos = (int16_t *)malloc(snake_game.pos_x_max * snake_game.pos_y_max * sizeof(int16_t)); 94 snake.YPos = (int16_t *)malloc(snake_game.pos_x_max * snake_game.pos_y_max * sizeof(int16_t)); 95 // 蛇的初始长度设为4 96 snake.length = 4; 97 // 蛇的初始方向设为 右 98 snake.cur_dir = SNAKE_RIGHT; 99 // 生成蛇的身体 蛇头在逻辑区域最中间的坐标上 即 (pos_x_max/2, pos_y_max/2) 100 for (uint8_t i = 0; i < snake.length; i++) 101 { 102 snake.XPos[i] = snake_game.pos_x_max / 2 + i; 103 snake.YPos[i] = snake_game.pos_y_max / 2; 104 } 105 // 复活这条蛇 106 snake.alive = 1; 107 108 // 将食物设置为被吃掉 109 food.eaten = 1; 110 // 生成食物 因为食物需要反复生成 所以封装为函数 111 gen_food(); 112 113 // 游戏开始分数为0 114 snake_game.score = 0; 115 116 return 0; 117} 118``` 119```c 120void gen_food() 121{ 122 int i = 0; 123 // 如果食物被吃了 124 if (food.eaten == 1) 125 { 126 while (1) 127 { 128 // 随机生成一个坐标 129 food.x = rand() % snake_game.pos_x_max; 130 food.y = rand() % snake_game.pos_y_max; 131 132 // 开始遍历蛇身 检查坐标是否重合 133 for (i = 0; i < snake.length; i++) 134 { 135 // 如果生成的食物坐标和蛇身重合 不合法 重新随机生成 136 if ((food.x == snake.XPos[i]) && (food.y == snake.YPos[i])) 137 break; 138 } 139 // 遍历完蛇身 并未发生重合 140 if (i == snake.length) 141 { 142 // 生成有效 终止循环 143 food.eaten = 0; 144 break; 145 } 146 } 147 } 148} 149``` 150## 对象绘画 151这一步其实是将逻辑空间重新映射到游戏空间,理应是整个游戏逻辑的最后一步,但是在我们开发过程中,也需要来自游戏空间的反馈,来验证我们的实现是否符合预期。因此我们在这里提前实现它。 152### 蛇 153 154<div align=center> 155 <img src="https://img.alicdn.com/imgextra/i3/O1CN01PLODHI1CWnv7gzRRc_!!6000000000089-2-tps-682-137.png" style="zoom:50%;" /> 156</div> 157 158```c 159static uint8_t icon_data_snake1_4_4[] = {0x0f, 0x0f, 0x0f, 0x0f}; // 纯色方块 160static icon_t icon_snake1_4_4 = {icon_data_snake1_4_4, 4, 4, NULL}; 161 162static uint8_t icon_data_snake0_4_4[] = {0x09, 0x09, 0x03, 0x03}; // 纹理方块 163static icon_t icon_snake0_4_4 = {icon_data_snake0_4_4, 4, 4, NULL}; 164 165void draw_snake() 166{ 167 uint16_t i = 0; 168 169 OLED_Icon_Draw( 170 map.border_left + snake.XPos[i] * map.block_size, 171 map.border_top + snake.YPos[i] * map.block_size, 172 &icon_snake0_4_4, 173 0 174 ); // 蛇尾一定使用纹理方块 175 176 for (; i < snake.length - 2; i++) 177 { 178 OLED_Icon_Draw( 179 map.border_left + snake.XPos[i] * map.block_size, 180 map.border_top + snake.YPos[i] * map.block_size, 181 ((i % 2) ? &icon_snake1_4_4 : &icon_snake0_4_4), 182 0); 183 } // 蛇身交替使用纯色和纹理方块 来模拟蛇的花纹 184 185 OLED_Icon_Draw( 186 map.border_left + snake.XPos[i] * map.block_size, 187 map.border_top + snake.YPos[i] * map.block_size, 188 &icon_snake1_4_4, 189 0 190 ); // 蛇头一定使用纯色方块 191} 192``` 193### 食物 194 195<div align=center> 196 <img src="https://img.alicdn.com/imgextra/i2/O1CN01nNY7wm1TIvNus5RHz_!!6000000002360-2-tps-137-137.png" style="zoom:50%;" /> 197</div> 198 199```c 200static uint8_t icon_data_food_4_4[] = {0x06, 0x09, 0x09, 0x06}; 201static icon_t icon_food_4_4 = {icon_data_food_4_4, 4, 4, NULL}; 202 203void draw_food() 204{ 205 if (food.eaten == 0) // 如果食物没被吃掉 206 { 207 OLED_Icon_Draw( 208 map.border_left + food.x * map.block_size, 209 map.border_top + food.y * map.block_size, 210 &icon_food_4_4, 211 0); 212 } 213} 214``` 215## 对象行为 216### 蛇的运动 217在贪吃蛇中,对象蛇发生运动,有两种情况,一是在用户无操作的情况下,蛇按照目前的方向继续运动,而是用户按键触发蛇的运动。总而言之,都是蛇的运动,只是运动的方向不同,所以我们可以将蛇的行为抽象为 218void Snake_Run(uint8_t dir)。 219这里以向上走为例。 220 221<div align=center> 222 <img src="https://img.alicdn.com/imgextra/i2/O1CN01l6K3Ls292AlGKKn17_!!6000000008009-1-tps-3288-1188.gif" style="zoom:25%;" /> 223</div> 224 225```c 226void Snake_Run(uint8_t dir) 227{ 228 switch (dir) 229 { 230 // 对于右移 231 case SNAKE_UP: 232 // 如果当前方向是左则不响应 因为不能掉头 233 if (snake.cur_dir != SNAKE_DOWN) 234 { 235 // 将蛇身数组向前移 236 // 值得注意的是,这里采用数组起始(XPos[0],YPos[0])作为蛇尾, 237 // 而使用(XPos[snake.length - 1], YPos[snake.length - 1])作为蛇头 238 // 这样实现会较为方便 239 for (uint16_t i = 0; i < snake.length - 1; i++) 240 { 241 snake.XPos[i] = snake.XPos[i + 1]; 242 snake.YPos[i] = snake.YPos[i + 1]; 243 } 244 // 将蛇头位置转向右侧 即 snake.XPos[snake.length - 2] + 1 245 snake.XPos[snake.length - 1] = snake.XPos[snake.length - 2]; 246 snake.YPos[snake.length - 1] = snake.YPos[snake.length - 2] - 1; 247 snake.cur_dir = dir; 248 } 249 break; 250 case SNAKE_LEFT: 251 ... 252 case SNAKE_DOWN: 253 ... 254 case SNAKE_RIGHT: 255 ... 256 break; 257 } 258 259 // 检查蛇是否存活 260 check_snake_alive(); 261 // 检查食物状态 262 check_food_eaten(); 263 // 更新完所有状态后绘制蛇和食物 264 draw_snake(); 265 draw_food(); 266} 267``` 268### 死亡判定 269在蛇每次运动的过程中,都涉及到对整个游戏新的更新,包括上述过程中出现的 check_snake_alive check_food_eaten 等。 270对于 check_snake_alive, 分为两种情况:蛇碰到地图边界/蛇吃到自己。 271```c 272void check_snake_alive() 273{ 274 // 判断蛇头是否接触边界 275 if (snake.XPos[snake.length - 1] < 0 || 276 snake.XPos[snake.length - 1] >= snake_game.pos_x_max || 277 snake.YPos[snake.length - 1] < 0 || 278 snake.YPos[snake.length - 1] >= snake_game.pos_y_max) 279 { 280 snake.alive = 0; 281 } 282 283 // 判断蛇头是否接触自己 284 for (int i = 0; i < snake.length - 1; i++) 285 { 286 if (snake.XPos[snake.length - 1] == snake.XPos[i] && snake.YPos[snake.length - 1] == snake.YPos[i]) 287 { 288 snake.alive = 0; 289 break; 290 } 291 } 292} 293``` 294### 吃食判定 295在贪吃蛇中,食物除了被吃的份,还有就是随机生成。生成食物在上一节已经实现,因此这一节我们就来实现检测食物是否被吃。 296```c 297void check_food_eaten() 298{ 299 // 如果蛇头与食物重合 300 if (snake.XPos[snake.length - 1] == food.x && snake.YPos[snake.length - 1] == food.y) 301 { 302 // 说明吃到了食物 303 food.eaten = 1; 304 // 增加蛇的长度 305 snake.length++; 306 // 长度增加表现为头的方向延伸 307 snake.XPos[snake.length - 1] = food.x; 308 snake.YPos[snake.length - 1] = food.y; 309 // 游戏得分增加 310 snake_game.score++; 311 // 重新生成食物 312 gen_food(); 313 } 314} 315``` 316## 绑定用户操作 317在贪吃蛇中,唯一的用户操作就是用户按键触发蛇的运动。好在我们已经对这个功能实现了良好的封装,即void Snake_Run(uint8_t dir) 318我们只需要在按键回调函数中,接收来自底层上报的key_code即可。 319```c 320#define SNAKE_UP EDK_KEY_2 321#define SNAKE_LEFT EDK_KEY_1 322#define SNAKE_RIGHT EDK_KEY_3 323#define SNAKE_DOWN EDK_KEY_4 324 325void greedySnake_key_handel(key_code_t key_code) 326{ 327 Snake_Run(key_code); 328} 329``` 330## 游戏全局控制 331在这个主循环里,我们需要对游戏整体进行刷新、绘图,对玩家的输赢、得分进行判定,并提示玩家游戏结果。 332```c 333void greedySnake_task(void) 334{ 335 while (1) 336 { 337 if (snake.alive) 338 { 339 // 清除屏幕memory 340 OLED_Clear(); 341 // 绘制地图边界 342 OLED_DrawRect(11, 1, 118, 62, 1); 343 // 绘制“SCORE” 344 OLED_Icon_Draw(3, 41, &icon_scores_5_21, 0); 345 // 绘制玩家当前分数 346 draw_score(snake_game.score); 347 // 让蛇按当前方向运行 348 Snake_Run(snake.cur_dir); 349 // 将屏幕memory输出 350 OLED_Refresh_GRAM(); 351 // 间隔200ms 352 aos_msleep(200); 353 } 354 else 355 { 356 // 清除屏幕memory 357 OLED_Clear(); 358 // 提示 GAME OVER 359 OLED_Show_String(30, 24, "GAME OVER", 16, 1); 360 // 将屏幕memory输出 361 OLED_Refresh_GRAM(); 362 // 间隔500ms 363 aos_msleep(500); 364 } 365 } 366} 367``` 368## 实现效果 369接下来请欣赏笔者的操作。 370 371<div align=center> 372 <img src="https://img.alicdn.com/imgextra/i1/O1CN01pMnXKQ1eoUSHcZoof_!!6000000003918-1-tps-1200-800.gif" style="zoom:50%;" /> 373</div> 374 375