小言_互联网的博客

HaaS EDU场景式应用学习 - 贪吃蛇

202人阅读  评论(0)

HaaS EDU场景式应用整体介绍

 

1、实验介绍

 

贪吃蛇是一个起源于1976年的街机游戏 Blockade。此类游戏在1990年代由于一些具有小型屏幕的移动电话的引入而再度流行起来,在现在的手机上基本都可安装此小游戏。版本亦有所不同。

在游戏中,玩家操控一条细长的蛇,它会不停前进,玩家只能操控蛇的头部朝向(上下左右),一路拾起触碰到食物,并要避免触碰到自身或者其他障碍物。每次贪吃蛇吃掉一件食物,它的身体便增长一些。

 

 

2、涉及知识点

 

  • OLED绘图
  • 按键事件

 

3、软硬件环境准备

3.1、硬件

开发用电脑一台

HAAS EDU K1 开发板一块

USB2TypeC 数据线一根

 

3.2、软件

"贪吃蛇"功能已经包含在edu_demo应用中,并且包含在发布版本中。

3.2.1、固件版本

固件版本:V1.0.0

 

3.2.2、代码路径


  
  1. git clone https: //gitee.com/alios-things/AliOS-Things.git -b dev_3.1.0_haas
  2. cd AliOS-Things /application/example /edu_demo/k1_apps/greedySnake

 

3.2.3、编译

进入代码的顶层目录如AliOS-Things进行编译。直接编译application/example/目录下的edu_demo应用。

两种方法进行编译

命令行方式


  
  1. aos make distclean
  2. aos make edu_demo@haaseduk1 -c config
  3. aos make

AliOS Studio IDE方式

 

3.2.4、编译烧录

见开发环境章节

 

4、设计思路

 

4.1、游戏空间映射到逻辑空间

当玩家在体验游戏时,他们能操作的都是游戏空间,包括按键的上下左右,对象物体的运动等等。对于开发者而言,我们需要将这些设想的游戏空间映射到逻辑空间中,做好对用户输入的判断,对象运动的处理,对象间交互的判定,游戏整体进程的把控,以及最终将逻辑空间再次映射回游戏空间,返回给玩家。

 

4.2、对象定义

这一步是将游戏空间中涉及到的对象抽象化。在C语言的实现中,我们将对象抽象为结构体,对象属性抽象为结构体的成员。


  
  1. typedef struct
  2. {
  3.     uint8_t length;     // 当前长度
  4.     int16_t *XPos;      // 逻辑坐标x 数组
  5.     int16_t *YPos;      // 逻辑坐标y 数组
  6.     uint8_t cur_dir;    // 蛇头的运行方向
  7.     uint8_t alive;      // 存活状态
  8. } Snake;

食物


  
  1. typedef struct
  2. {
  3.     int16_t x;
  4.     int16_t y;          // 食物逻辑坐标
  5.     uint8_t eaten;      // 食物是否被吃掉
  6. } Food;

地图


  
  1. typedef struct
  2. {
  3.     int16_t border_top;
  4.     int16_t border_right;
  5.     int16_t border_botton;
  6.     int16_t border_left;    // 边界像素坐标
  7.     int16_t block_size;     // 网格大小 在本实验的实现中 蛇身和食物的大小被统一约束进网格的大小中
  8. } Map;

游戏


  
  1. typedef struct
  2. {
  3.     int16_t score;          // 游戏记分
  4.     int16_t pos_x_max;      // 逻辑最大x坐标  pos_x_max = (map.border_right - map.border_left) / map.block_size;
  5.     int16_t pos_y_max;      // 逻辑最大y坐标  pos_y_max = (map.border_botton - map.border_top) / map.block_size;
  6. } snake_game_t;

通过Map和snake_game_t的定义,我们将屏幕的 (border_left, border_top, border_bottom, border_right) 部分设定为游戏区域,并且将其切分为 pos_x_max* pos_y_max 个大小为 block_size 的块。继而,我们可以在每个块中绘制蛇、食物等对象。

4.3、对象初始化

在游戏每一次开始时,我们需要给对象一些初始的属性,例如蛇的长度、位置、存活状态,食物的位置、状态, 地图的边界、块大小等等。


  
  1. Food food = { -1, -1, 1};
  2. Snake snake = { 4, NULL, NULL, 0, 1};
  3. Map map = { 2, 128, 62, 12, 4};
  4. snake_game_t snake_game = { 0, 0, 0};
  5. int greedySnake_init(void)
  6. {
  7.     // 计算出游戏的最大逻辑坐标 用于约束游戏范围
  8.     snake_game.pos_x_max = ( map.border_right - map.border_left) / map.block_size;
  9.     snake_game.pos_y_max = ( map.border_botton - map.border_top) / map.block_size;
  10.     // 为蛇的坐标数组分配空间 蛇的最大长度是填满整个屏幕 即 pos_x_max* pos_y_max
  11.     snake.XPos = ( int16_t *) malloc(snake_game.pos_x_max * snake_game.pos_y_max * sizeof( int16_t));
  12.     snake.YPos = ( int16_t *) malloc(snake_game.pos_x_max * snake_game.pos_y_max * sizeof( int16_t));
  13.     // 蛇的初始长度设为4
  14.     snake.length = 4;
  15.     // 蛇的初始方向设为 右
  16.     snake.cur_dir = SNAKE_RIGHT;
  17.     // 生成蛇的身体 蛇头在逻辑区域最中间的坐标上 即 (pos_x_max/2, pos_y_max/2)
  18.     for ( uint8_t i = 0; i < snake.length; i++)
  19.     {
  20.         snake.XPos[i] = snake_game.pos_x_max / 2 + i;
  21.         snake.YPos[i] = snake_game.pos_y_max / 2;
  22.     }
  23.     // 复活这条蛇
  24.     snake.alive = 1;
  25.    
  26.     // 将食物设置为被吃掉
  27.     food.eaten = 1;
  28.     // 生成食物 因为食物需要反复生成 所以封装为函数
  29.     gen_food();
  30.     // 游戏开始分数为0
  31.     snake_game.score = 0;
  32.    
  33.     return 0;
  34. }
  35. void gen_food()
  36. {
  37.     int i = 0;
  38.     // 如果食物被吃了
  39.     if (food.eaten == 1)
  40.     {
  41.         while ( 1)
  42.         {
  43.             // 随机生成一个坐标
  44.             food.x = rand() % snake_game.pos_x_max;
  45.             food.y = rand() % snake_game.pos_y_max;
  46.             // 开始遍历蛇身 检查坐标是否重合
  47.             for (i = 0; i < snake.length; i++)
  48.             {
  49.                 // 如果生成的食物坐标和蛇身重合 不合法 重新随机生成
  50.                 if ((food.x == snake.XPos[i]) && (food.y == snake.YPos[i]))
  51.                     break;
  52.             }
  53.             // 遍历完蛇身 并未发生重合
  54.             if (i == snake.length)
  55.             {
  56.                 // 生成有效 终止循环
  57.                 food.eaten = 0;
  58.                 break;
  59.             }
  60.         }
  61.     }
  62. }

4.4、对象绘画

这一步其实是将逻辑空间重新映射到游戏空间,理应是整个游戏逻辑的最后一步,但是在我们开发过程中,也需要来自游戏空间的反馈,来验证我们的实现是否符合预期。因此我们在这里提前实现它。


  
  1. static uint8_t icon_data_snake1_4_4[] = { 0x0f, 0x0f, 0x0f, 0x0f};   // 纯色方块
  2. static icon_t icon_snake1_4_4 = {icon_data_snake1_4_4, 4, 4, NULL};
  3. static uint8_t icon_data_snake0_4_4[] = { 0x09, 0x09, 0x03, 0x03};   // 纹理方块
  4. static icon_t icon_snake0_4_4 = {icon_data_snake0_4_4, 4, 4, NULL};
  5. void draw_snake()
  6. {
  7.     uint16_t i = 0;
  8.     OLED_Icon_Draw(
  9.         map.border_left + snake.XPos[i] * map.block_size,
  10.         map.border_top + snake.YPos[i] * map.block_size,
  11.         &icon_snake0_4_4,
  12.         0
  13.     );  // 蛇尾一定使用纹理方块
  14.     for (; i < snake.length - 2; i++)
  15.     {
  16.         OLED_Icon_Draw(
  17.             map.border_left + snake.XPos[i] * map.block_size,
  18.             map.border_top + snake.YPos[i] * map.block_size,
  19.             ((i % 2) ? &icon_snake1_4_4 : &icon_snake0_4_4),
  20.             0);
  21.     }   // 蛇身交替使用纯色和纹理方块 来模拟蛇的花纹
  22.     OLED_Icon_Draw(
  23.         map.border_left + snake.XPos[i] * map.block_size,
  24.         map.border_top + snake.YPos[i] * map.block_size,
  25.         &icon_snake1_4_4,
  26.         0
  27.     );  // 蛇头一定使用纯色方块
  28. }

食物


  
  1. static uint8_t icon_data_food_4_4[] = { 0x06, 0x09, 0x09, 0x06};
  2. static icon_t icon_food_4_4 = {icon_data_food_4_4, 4, 4, NULL};
  3. void draw_food()
  4. {
  5.     if (food.eaten == 0)    // 如果食物没被吃掉
  6.     {
  7.         OLED_Icon_Draw(
  8.             map.border_left + food.x * map.block_size,
  9.             map.border_top + food.y * map.block_size,
  10.             &icon_food_4_4,
  11.             0);
  12.     }
  13. }

4.5、对象行为

4.5.1、蛇的运动

在贪吃蛇中,对象蛇发生运动,有两种情况,一是在用户无操作的情况下,蛇按照目前的方向继续运动,而是用户按键触发蛇的运动。总而言之,都是蛇的运动,只是运动的方向不同,所以我们可以将蛇的行为抽象为

void Snake_Run(uint8_t dir)

这里以向上走为例。


  
  1. void Snake_Run(uint8_t dir)
  2. {
  3.     switch (dir)
  4.     {
  5.         // 对于右移
  6.         case SNAKE_UP:
  7.             // 如果当前方向是左则不响应 因为不能掉头
  8.             if (snake.cur_dir != SNAKE_DOWN)
  9.             {
  10.                 // 将蛇身数组向前移
  11.                 // 值得注意的是,这里采用数组起始(XPos[0],YPos[0])作为蛇尾,
  12.                 // 而使用(XPos[snake.length - 1], YPos[snake.length - 1])作为蛇头
  13.                 // 这样实现会较为方便
  14.                 for (uint16_t i = 0; i < snake.length - 1; i++)
  15.                 {
  16.                     snake.XPos[i] = snake.XPos[i + 1];
  17.                     snake.YPos[i] = snake.YPos[i + 1];
  18.                 }
  19.                 // 将蛇头位置转向右侧 即 snake.XPos[snake.length - 2] + 1
  20.                 snake.XPos[snake.length - 1] = snake.XPos[snake.length - 2];
  21.                 snake.YPos[snake.length - 1] = snake.YPos[snake.length - 2] - 1;
  22.                 snake.cur_dir = dir;
  23.             }
  24.             break;
  25.         case SNAKE_LEFT:
  26.             ...
  27.         case SNAKE_DOWN:
  28.             ...
  29.         case SNAKE_RIGHT:
  30.             ...
  31.             break;
  32.     }
  33.    
  34.     // 检查蛇是否存活
  35.     check_snake_alive();
  36.     // 检查食物状态
  37.     check_food_eaten();
  38.     // 更新完所有状态后绘制蛇和食物
  39.     draw_snake();
  40.     draw_food();
  41. }

4.5.2、死亡判定

在蛇每次运动的过程中,都涉及到对整个游戏新的更新,包括上述过程中出现的 check_snake_alive check_food_eaten 等。

对于 check_snake_alive, 分为两种情况:蛇碰到地图边界/蛇吃到自己。


  
  1. void check_snake_alive()
  2. {
  3.     // 判断蛇头是否接触边界
  4.     if (snake.XPos[ snake.length - 1] < 0 ||
  5.         snake.XPos[ snake.length - 1] >= snake_game.pos_x_max ||
  6.         snake.YPos[ snake.length - 1] < 0 ||
  7.         snake.YPos[ snake.length - 1] >= snake_game.pos_y_max)
  8.     {
  9.         snake.alive = 0 ;
  10.     }
  11.    
  12.     // 判断蛇头是否接触自己
  13.     for (int i = 0 ; i < snake.length - 1 ; i++)
  14.     {
  15.         if (snake.XPos[ snake.length - 1] == snake.XPos[ i] && snake.YPos[ snake.length - 1] == snake.YPos[ i] )
  16.         {
  17.             snake.alive = 0 ;
  18.             break;
  19.         }
  20.     }
  21. }

4.5.3、吃食判定

在贪吃蛇中,食物除了被吃的份,还有就是随机生成。生成食物在上一节已经实现,因此这一节我们就来实现检测食物是否被吃。


  
  1. void check_food_eaten()
  2. {
  3.     // 如果蛇头与食物重合
  4.     if (snake.XPos[snake.length - 1] == food.x && snake.YPos[snake.length - 1] == food.y)
  5.     {
  6.         // 说明吃到了食物
  7.         food.eaten = 1;
  8.         // 增加蛇的长度
  9.         snake.length++;
  10.         // 长度增加表现为头的方向延伸
  11.         snake.XPos[snake.length - 1] = food.x;
  12.         snake.YPos[snake.length - 1] = food.y;
  13.         // 游戏得分增加
  14.         snake_game.score++;
  15.         // 重新生成食物
  16.         gen_food();
  17.     }
  18. }

4.6、绑定用户操作

在贪吃蛇中,唯一的用户操作就是用户按键触发蛇的运动。好在我们已经对这个功能实现了良好的封装,即void Snake_Run(uint8_t dir)

我们只需要在按键回调函数中,接收来自底层上报的key_code即可。


  
  1. #define SNAKE_UP    EDK_KEY_2
  2. #define SNAKE_LEFT  EDK_KEY_1
  3. #define SNAKE_RIGHT EDK_KEY_3
  4. #define SNAKE_DOWN  EDK_KEY_4
  5. void greedySnake_key_handel(key_code_t key_code)
  6. {
  7.     Snake_Run(key_code);
  8. }

4.7、游戏全局控制

在这个主循环里,我们需要对游戏整体进行刷新、绘图,对玩家的输赢、得分进行判定,并提示玩家游戏结果。


  
  1. void greedySnake_task(void)
  2. {
  3.     while ( 1)
  4.     {
  5.         if (snake.alive)
  6.         {
  7.             // 清除屏幕memory
  8.             OLED_Clear();
  9.             // 绘制地图边界
  10.             OLED_DrawRect( 11, 1, 118, 62, 1);
  11.             // 绘制“SCORE”
  12.             OLED_Icon_Draw( 3, 41, &icon_scores_5_21, 0);
  13.             // 绘制玩家当前分数
  14.             draw_score(snake_game.score);
  15.             // 让蛇按当前方向运行
  16.             Snake_Run(snake.cur_dir);
  17.             // 将屏幕memory输出
  18.             OLED_Refresh_GRAM();
  19.             // 间隔200ms
  20.             aos_msleep( 200);
  21.         }
  22.         else
  23.         {
  24.              // 清除屏幕memory
  25.             OLED_Clear();
  26.             // 提示 GAME OVER
  27.             OLED_Show_String( 30, 24, "GAME OVER", 16, 1);
  28.             // 将屏幕memory输出
  29.             OLED_Refresh_GRAM();
  30.             // 间隔500ms
  31.             aos_msleep( 500);
  32.         }
  33.     }
  34. }

5、实现效果

接下来请欣赏笔者的操作。

 

6、开发者技术支持

如需更多技术支持,可加入钉钉开发者群,或者关注微信公众号

更多技术与解决方案介绍,请访问阿里云AIoT首页https://iot.aliyun.com/

 


转载:https://blog.csdn.net/HaaSTech/article/details/114117977
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场