上一篇帖子中已经完成了按键模块的构建(按键模块),接下来就是具体游戏内容的完成了。
我的设想是创建三个界面:菜单界面、设置界面和游戏界面。菜单界面可以选择不同关卡(地图)、选择进入设置界面或游戏界面;设置界面可以设置蛇的颜色;游戏界面则是根据之前的设置进行游玩。
已完成的菜单界面如下图所示:
目前总共设计了5张地图,分别对应5个关卡。
设置界面如下所示,可以分别设置蛇头、蛇身和蛇尾的颜色:
以上两个界面的实现都很简单,就不过多赘述,关键在接下来的游戏界面,已完成的效果如下图所示:
贪吃蛇所需要用到的数据结构如下
typedef enum { SNAKE_DIR_UP, SNAKE_DIR_RIGHT, SNAKE_DIR_DOWN, SNAKE_DIR_LEFT }_snake_dir_enum; typedef struct { uint8_t x; uint8_t y; uint8_t dir; }snake_body_t; static struct { snake_body_t head; snake_body_t body[313]; snake_body_t tail; uint16_t body_num; uint8_t level; uint8_t dir_next; }snake; uint8_t food_x = 0; uint8_t food_y = 0; char snake_map[15][21] = { 0 };
其中snake_map[15][21]为我设计的游戏地图大小规格为21(宽度)X15(高度),所以蛇身最大长度为21*15-2=313。snake.level表示游戏等级,根据等级大小每次移动的时间间隔越短。
贪吃蛇初始化,根据菜单中所选择的关卡复制地图数据,然后将蛇头和蛇尾放到左上角,最后绘制地图。
void snake_init(void) { game_level = 0; memset(&snake,0,sizeof(snake)); if(map_select==0) memset(snake_map, 0, sizeof(snake_map)); else if (map_select==1) memcpy(snake_map, game_map1, sizeof(snake_map)); else if (map_select==2) memcpy(snake_map, game_map2, sizeof(snake_map)); else if (map_select==3) memcpy(snake_map, game_map3, sizeof(snake_map)); else if (map_select==4) memcpy(snake_map, game_map4, sizeof(snake_map)); snake.tail.x = 1; snake.tail.y = 1; snake.head.x = 2; snake.head.y = 1; snake.head.dir = SNAKE_DIR_RIGHT; snake.dir_next = SNAKE_DIR_RIGHT; snake.tail.dir = SNAKE_DIR_RIGHT; for (int i = 0; i < 15; i++) { for (int j = 0; j < 21; j++) { if (snake_map[i][j] == 1)LCD_ShowSnake(9*j,9*i,WHITE,gImage_snake_body);//LCD_Fill(9*j,9*i,9*j+9,9*i+9,WHITE); } } }
将贪吃蛇映射到地图中,这样是为了后续判断蛇头是否会撞到自身。
void snake_to_map(void) { if(map_select==0) memset(snake_map, 0, sizeof(snake_map)); else if (map_select==1) memcpy(snake_map, game_map1, sizeof(snake_map)); else if (map_select==2) memcpy(snake_map, game_map2, sizeof(snake_map)); else if (map_select==3) memcpy(snake_map, game_map3, sizeof(snake_map)); else if (map_select==4) memcpy(snake_map, game_map4, sizeof(snake_map)); snake_map[snake.head.y][snake.head.x] = 1; if (snake.body_num > 0) { for (int i = 0; i < snake.body_num; i++) { snake_map[snake.body[i].y][snake.body[i].x] = 1; } } snake_map[snake.tail.y][snake.tail.x] = 1; }
贪吃蛇移动函数为snake_run(),首先判断蛇头将要移动到的下个地方是否是食物,如果是食物,则只需要移动蛇头并将蛇身增加一格,如果不是食物,则整个蛇身都需要整体移动一格。按照正常思路来说我们会按蛇头到蛇尾的顺序去移动,但这样写代码是会发现需要额外使用一个中间变量去频繁保存上一个已移动的单位的坐标,这样实现起来会比较麻烦并且容易出错。我这里的实现方式为按从蛇尾到蛇头的逆向方式移动,这样就只需将倒数第二个单位的坐标复制到最后一个单位这样按顺序一直执行就行。最后移动蛇头前需要先将身体映射到地图后判断一下是否撞墙或者撞到自身,如果发生碰撞则直接提示游戏结束并在2秒后返回到菜单界面。
void snake_head_move(snake_body_t* body) { switch (body->dir) { case SNAKE_DIR_UP: if (body->y == 0)body->y = 15; body->y--; break; case SNAKE_DIR_RIGHT: body->x++; if (body->x == 21)body->x = 0; break; case SNAKE_DIR_DOWN: body->y++; if (body->y == 15)body->y = 0; break; case SNAKE_DIR_LEFT: if (body->x == 0)body->x = 21; body->x--; break; } } //将src的数值传递给dst,也即是蛇身dst移动到src void snake_body_move(snake_body_t* dst, snake_body_t* src) { dst->x = src->x; dst->y = src->y; dst->dir = src->dir; } //获取蛇头下次是否能吃到食物,此处需要使用形参传递数值,不能传地址,因为此时蛇头还不能移动 int next_is_food(snake_body_t body) { snake_head_move(&body); if (body.x == food_x && body.y == food_y)return 0; else return -1; } //判断下次蛇头是否会撞到身体或者墙体,此处需要使用形参传递数值,不能传地址,因为此时蛇头还不能移动 int next_is_body(snake_body_t body) { snake_head_move(&body); if (snake_map[body.y][body.x] == 1)return 0; else return -1; } /** * @brief 蛇运行函数 * @retval 1:你赢了 * 0:正常运行 * -1:你输了 */ int snake_run(void) { uint8_t flag_is_eat_food = 0; //需要移动了,将dir_next复制到蛇头的移动方向中 snake.head.dir = snake.dir_next; if (next_is_food(snake.head) == 0) { snake_body_move(&snake.body[snake.body_num],&snake.head); snake.body_num++; flag_is_eat_food = 1; } else { //没吃到食物,需要移动,先消除蛇尾显示 LCD_Fill(snake.tail.x*9,snake.tail.y*9,snake.tail.x*9+9,snake.tail.y*9+9,BLACK); if (snake.body_num > 0) { snake_body_move(&snake.tail, &snake.body[0]); for (int i = 0; i < snake.body_num-1; i++) { snake_body_move(&snake.body[i], &snake.body[i + 1]); } snake_body_move(&snake.body[snake.body_num - 1], &snake.head); } else { snake_body_move(&snake.tail, &snake.head); } } snake_to_map(); if (next_is_body(snake.head) == 0) { return -1; } snake_head_move(&snake.head); if(flag_is_eat_food==1){ show_scores_update(); return generate_food(); } return 0; }
按键响应函数如下,四个方向负责切换方向,按实际情况我们只能控制垂直蛇头90度方向切换,否则可能会原地掉头直接结束游戏。另外按B键也可以直接结束游戏
static void key_b_pressed(void) { game_stat = 0; show_ganeover(1); sleep_ms(2000); change_ui_to_mainmenu(); } static void key_left_pressed(void) { snake.dir_next = (snake.head.dir == SNAKE_DIR_UP || snake.head.dir == SNAKE_DIR_DOWN) ? SNAKE_DIR_LEFT : snake.head.dir; } static void key_right_pressed(void) { snake.dir_next = (snake.head.dir == SNAKE_DIR_UP || snake.head.dir == SNAKE_DIR_DOWN) ? SNAKE_DIR_RIGHT : snake.head.dir; } static void key_up_pressed(void) { snake.dir_next = (snake.head.dir == SNAKE_DIR_LEFT || snake.head.dir == SNAKE_DIR_RIGHT) ? SNAKE_DIR_UP : snake.head.dir; } static void key_down_pressed(void) { snake.dir_next = (snake.head.dir == SNAKE_DIR_LEFT || snake.head.dir == SNAKE_DIR_RIGHT) ? SNAKE_DIR_DOWN : snake.head.dir; }
切换到游戏界面时首先绘制基础地图和初始分数等级,再将蛇和食物映射并绘制到地图中,最后将按键响应函数映射到按键扫描函数中。
void change_ui_to_snake(void) { LCD_Fill(0,0,LCD_W,LCD_H,BLACK); LCD_DrawLine(189,0,189,135,WHITE); LCD_ShowZh24(190,0,zh_string_scores,2,WHITE,BLACK); LCD_ShowZh24(190,69,zh_string_level,2,WHITE,BLACK); key_pressed_cb_clear(); snake_init(); snake_to_map(); generate_food(); game_refresh(); show_scores_update(); key_pressed_cb_register(KEY_B,key_b_pressed); key_pressed_cb_register(KEY_UP,key_up_pressed); key_pressed_cb_register(KEY_DOWN,key_down_pressed); key_pressed_cb_register(KEY_LEFT,key_left_pressed); key_pressed_cb_register(KEY_RIGHT,key_right_pressed); game_stat = 1; }
最后在主循环中一直无条件执行game_run()即可,game_refresh()用于刷新显示蛇的显示
void game_refresh(void) { LCD_ShowSnake(snake.head.x*9,snake.head.y*9,get_snake_head_color(),gImage_snake_head[snake.head.dir]); if (snake.body_num > 0) { for(int i=0;i<snake.body_num;i++){ LCD_ShowSnake(snake.body[i].x*9,snake.body[i].y*9,get_snake_body_color(),gImage_snake_body); } } LCD_ShowSnake(snake.tail.x*9,snake.tail.y*9,get_snake_tail_color(),gImage_snake_tail[snake.tail.dir]); } void game_run(void) { uint8_t flag_game_faileed = 0; int ret = 0; static uint64_t time_to_wait_game = 0; if(game_stat==1){ if(try_to_wait(&time_to_wait_game,speed_table[game_level]*1000)==0){ time_to_wait_game = 0; ret = snake_run(); if(ret==1){ game_stat = 0; show_ganeover(0); sleep_ms(2000); change_ui_to_mainmenu(); return; } else if(ret==-1){ game_stat = 0; show_ganeover(1); sleep_ms(2000); change_ui_to_mainmenu(); return; } snake_to_map(); game_refresh(); } } }
MDK源码:源码
Windows版源码:snake_windows(贪吃蛇windows模拟器).rar