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