实验介绍
**贪吃蛇**是一个起源于1976年的街机游戏 Blockade。此类游戏在1990年代由于一些具有小型屏幕的移动电话的引入而再度流行起来,在现在的手机上基本都可安装此小游戏。版本亦有所不同。 在游戏中,玩家操控一条细长的蛇,它会不停前进,玩家只能操控蛇的头部朝向(上下左右),一路拾起触碰到食物,并要避免触碰到自身或者其他障碍物。每次贪吃蛇吃掉一件食物,它的身体便增长一些。
涉及知识点
OLED绘图 按键事件
开发环境准备
硬件
开发用电脑一台 HAAS EDU K1 开发板一块 USB2TypeC 数据线一根
软件
AliOS Things开发环境搭建
开发环境的搭建请参考 HaaS EDU K1快速开始 (搭建开发环境章节),其中详细的介绍了AliOS Things 3.3的IDE集成开发环境的搭建流程。
HaaS EDU K1 DEMO 代码下载
HaaS EDU K1 DEMO 的代码下载请参考 HaaS EDU K1快速开始 (创建工程章节),其中, 选择解决方案: 基于教育开发板的示例 选择开发板: haaseduk1 board configure
代码编译、烧录
参考 HaaS EDU K1快速开始 (3.1 编译工程章节),点击 ✅ 即可完成编译固件。 参考 HaaS EDU K1快速开始 (3.2 烧录镜像章节),点击 "⚡️" 即可完成烧录固件。
设计思路
游戏空间映射到逻辑空间
当玩家在体验游戏时,他们能操作的都是游戏空间,包括按键的上下左右,对象物体的运动等等。对于开发者而言,我们需要将这些设想的游戏空间映射到逻辑空间中,做好对用户输入的判断,对象运动的处理,对象间交互的判定,游戏整体进程的把控,以及最终将逻辑空间再次映射回游戏空间,返回给玩家。
对象定义
这一步是将游戏空间中涉及到的对象抽象化。在C语言的实现中,我们将对象抽象为结构体,对象属性抽象为结构体的成员。
蛇
typedef struct
{
uint8_t length;
int16_t *XPos;
int16_t *YPos;
uint8_t cur_dir;
uint8_t alive;
} Snake;
食物
typedef struct
{
int16_t x;
int16_t y;
uint8_t eaten;
} Food;
地图
typedef struct
{
int16_t border_top;
int16_t border_right;
int16_t border_botton;
int16_t border_left;
int16_t block_size;
} Map;
游戏
typedef struct
{
int16_t score;
int16_t pos_x_max;
int16_t pos_y_max;
} snake_game_t;
通过Map和snake_game_t的定义,我们将屏幕的 (border_left, border_top, border_bottom, border_right) 部分设定为游戏区域,并且将其切分为 pos_x_max* pos_y_max 个大小为 block_size 的块。继而,我们可以在每个块中绘制蛇、食物等对象。
对象初始化
在游戏每一次开始时,我们需要给对象一些初始的属性,例如蛇的长度、位置、存活状态,食物的位置、状态, 地图的边界、块大小等等。
Food food = {-1, -1, 1};
Snake snake = {4, NULL, NULL, 0, 1};
Map map = {2, 128, 62, 12, 4};
snake_game_t snake_game = {0, 0, 0};
int greedySnake_init(void)
{
snake_game.pos_x_max = (map.border_right - map.border_left) / map.block_size;
snake_game.pos_y_max = (map.border_botton - map.border_top) / map.block_size;
snake.XPos = (int16_t *)malloc(snake_game.pos_x_max * snake_game.pos_y_max * sizeof(int16_t));
snake.YPos = (int16_t *)malloc(snake_game.pos_x_max * snake_game.pos_y_max * sizeof(int16_t));
snake.length = 4;
snake.cur_dir = SNAKE_RIGHT;
for (uint8_t i = 0; i < snake.length; i++)
{
snake.XPos[i] = snake_game.pos_x_max / 2 + i;
snake.YPos[i] = snake_game.pos_y_max / 2;
}
snake.alive = 1;
food.eaten = 1;
gen_food();
snake_game.score = 0;
return 0;
}
void gen_food()
{
int i = 0;
if (food.eaten == 1)
{
while (1)
{
food.x = rand() % snake_game.pos_x_max;
food.y = rand() % snake_game.pos_y_max;
for (i = 0; i < snake.length; i++)
{
if ((food.x == snake.XPos[i]) && (food.y == snake.YPos[i]))
break;
}
if (i == snake.length)
{
food.eaten = 0;
break;
}
}
}
}
对象绘画
这一步其实是将逻辑空间重新映射到游戏空间,理应是整个游戏逻辑的最后一步,但是在我们开发过程中,也需要来自游戏空间的反馈,来验证我们的实现是否符合预期。因此我们在这里提前实现它。
蛇
static uint8_t icon_data_snake1_4_4[] = {0x0f, 0x0f, 0x0f, 0x0f};
static icon_t icon_snake1_4_4 = {icon_data_snake1_4_4, 4, 4, NULL};
static uint8_t icon_data_snake0_4_4[] = {0x09, 0x09, 0x03, 0x03};
static icon_t icon_snake0_4_4 = {icon_data_snake0_4_4, 4, 4, NULL};
void draw_snake()
{
uint16_t i = 0;
OLED_Icon_Draw(
map.border_left + snake.XPos[i] * map.block_size,
map.border_top + snake.YPos[i] * map.block_size,
&icon_snake0_4_4,
0
);
for (; i < snake.length - 2; i++)
{
OLED_Icon_Draw(
map.border_left + snake.XPos[i] * map.block_size,
map.border_top + snake.YPos[i] * map.block_size,
((i % 2) ? &icon_snake1_4_4 : &icon_snake0_4_4),
0);
}
OLED_Icon_Draw(
map.border_left + snake.XPos[i] * map.block_size,
map.border_top + snake.YPos[i] * map.block_size,
&icon_snake1_4_4,
0
);
}
食物
static uint8_t icon_data_food_4_4[] = {0x06, 0x09, 0x09, 0x06};
static icon_t icon_food_4_4 = {icon_data_food_4_4, 4, 4, NULL};
void draw_food()
{
if (food.eaten == 0)
{
OLED_Icon_Draw(
map.border_left + food.x * map.block_size,
map.border_top + food.y * map.block_size,
&icon_food_4_4,
0);
}
}
对象行为
蛇的运动
在贪吃蛇中,对象蛇发生运动,有两种情况,一是在用户无操作的情况下,蛇按照目前的方向继续运动,而是用户按键触发蛇的运动。总而言之,都是蛇的运动,只是运动的方向不同,所以我们可以将蛇的行为抽象为 void Snake_Run(uint8_t dir)。 这里以向上走为例。
void Snake_Run(uint8_t dir)
{
switch (dir)
{
case SNAKE_UP:
if (snake.cur_dir != SNAKE_DOWN)
{
for (uint16_t i = 0; i < snake.length - 1; i++)
{
snake.XPos[i] = snake.XPos[i + 1];
snake.YPos[i] = snake.YPos[i + 1];
}
snake.XPos[snake.length - 1] = snake.XPos[snake.length - 2];
snake.YPos[snake.length - 1] = snake.YPos[snake.length - 2] - 1;
snake.cur_dir = dir;
}
break;
case SNAKE_LEFT:
...
case SNAKE_DOWN:
...
case SNAKE_RIGHT:
...
break;
}
check_snake_alive();
check_food_eaten();
draw_snake();
draw_food();
}
死亡判定
在蛇每次运动的过程中,都涉及到对整个游戏新的更新,包括上述过程中出现的 check_snake_alive check_food_eaten 等。 对于 check_snake_alive, 分为两种情况:蛇碰到地图边界/蛇吃到自己。
void check_snake_alive()
{
if (snake.XPos[snake.length - 1] < 0 ||
snake.XPos[snake.length - 1] >= snake_game.pos_x_max ||
snake.YPos[snake.length - 1] < 0 ||
snake.YPos[snake.length - 1] >= snake_game.pos_y_max)
{
snake.alive = 0;
}
for (int i = 0; i < snake.length - 1; i++)
{
if (snake.XPos[snake.length - 1] == snake.XPos[i] && snake.YPos[snake.length - 1] == snake.YPos[i])
{
snake.alive = 0;
break;
}
}
}
吃食判定
在贪吃蛇中,食物除了被吃的份,还有就是随机生成。生成食物在上一节已经实现,因此这一节我们就来实现检测食物是否被吃。
void check_food_eaten()
{
if (snake.XPos[snake.length - 1] == food.x && snake.YPos[snake.length - 1] == food.y)
{
food.eaten = 1;
snake.length++;
snake.XPos[snake.length - 1] = food.x;
snake.YPos[snake.length - 1] = food.y;
snake_game.score++;
gen_food();
}
}
绑定用户操作
在贪吃蛇中,唯一的用户操作就是用户按键触发蛇的运动。好在我们已经对这个功能实现了良好的封装,即void Snake_Run(uint8_t dir) 我们只需要在按键回调函数中,接收来自底层上报的key_code即可。
#define SNAKE_UP EDK_KEY_2
#define SNAKE_LEFT EDK_KEY_1
#define SNAKE_RIGHT EDK_KEY_3
#define SNAKE_DOWN EDK_KEY_4
void greedySnake_key_handel(key_code_t key_code)
{
Snake_Run(key_code);
}
游戏全局控制
在这个主循环里,我们需要对游戏整体进行刷新、绘图,对玩家的输赢、得分进行判定,并提示玩家游戏结果。
void greedySnake_task(void)
{
while (1)
{
if (snake.alive)
{
OLED_Clear();
OLED_DrawRect(11, 1, 118, 62, 1);
OLED_Icon_Draw(3, 41, &icon_scores_5_21, 0);
draw_score(snake_game.score);
Snake_Run(snake.cur_dir);
OLED_Refresh_GRAM();
}
else
{
OLED_Clear();
OLED_Show_String(30, 24, "GAME OVER", 16, 1);
OLED_Refresh_GRAM();
}
}
}
void aos_msleep(uint32_t ms)
实现效果
接下来请欣赏笔者的操作。