[C语言]链表实现贪吃蛇及部分模块优化

在继上篇[C语言]贪吃蛇_结构数组实现大半年后,链表实现的版本也终于出炉了。两篇隔了这么久除了是懒癌晚期的原因外,对整个游戏流程的改进,模块的精简也花了一些时间(都是借口)。 优化模块的前沿链接: ·游戏流程结构的改进 ·对输入的甄别与判断 ·单链表元素移动 一、游戏流程 贪吃蛇游戏的原理很简单,即在一张地图内,有一条蛇和随机出现的食物,玩家操控蛇的移动,当蛇吃到了食物后,蛇长度增加。游戏过程中,蛇不能撞墙,也不能咬到自身。 反映到程序中,就是这样一张简略的流程图(结构数组实现): 在这个流程中,有许多的不足。当蛇以及存在并且接受了一个合法的输入时,根据下一步是否吃到食物来判断是否需要清除尾巴是合理的,但在控制台里,贪吃蛇每次循环移动其实都只需对两个位置进行操作:一个是接受操作后的蛇头,无论下一步在哪儿,这都是必须要打印的一个;另一个是蛇尾,这则需要根据蛇头是否吃到食物来决定去留。所以每次循环都重新打印所有节点是很多余的,因此需要改进。 我们可以这样改:在接受输入后,先把一定会移动的蛇头打印出来,再判断蛇尾的去留。最后在蛇(链表)各个节点中,依次赋得前一个节点的值。流程图移动模块如下: 按照这个流程图,蛇每次移动就只需要操作控制台上的两个节点了。另外可以将在控制台某坐标打印一个特殊符号抽象成一个函数: #define SPACE 0 #define NODE 1 #define FOOD 2 #define WALL 3 void PrintIn(int size,int x,int y); void PrintIn(int size,int x,int y) { //size //清除节点:0 打印蛇身:1 //打印食物:2 打印墙壁:3 char *arr[4] = {" ","⊙","●","■"}; Pos(x,y); printf("%s",arr[size]); } 二、初始化 1.初始化地图 在[C语言]贪吃蛇_结构数组实现中我提到过,因为控制台一个字符的宽高所占像素点不同,所以再看控制台上想输出一个规整的正方形,就得让宽高之比为2:1。并且为了输出的正方形更完整,就需要使用一些占两个普通字符的特殊字符。 #define WIDTH 60 #define HEIGHT 30 void CreateMap(void); void CreateMap(void) { int i; for(i=0;ihead); pnew = (Node *)malloc(sizeof(Node)); if(pnew == NULL) { printf("pnew == NULL"); system("pause"); return false; } pnew->place.x = 28-2*i; pnew->place.y = 14; pnew->next = NULL; psnake->size++; PrintIn(NODE,pnew->place.x,pnew->place.y); if(scan == NULL) psnake->head = pnew; else { while(scan->next != NULL) scan = scan->next; scan->next = pnew; } } return true; } 3.初始化食物 食物可用一个全局变量来表示,该变量存储一个坐标值。因此可用上之前定义的Place结构。 typedef Place Food; Food food = {0,0}; 而坐标值的范围只要保证两点就好:在地图内;不与蛇身重合。 void CreateFood(void) { int flag = 0; srand((unsigned int)time(0)); while(1) { do{ food.x = rand()%(WIDTH-5)+2; }while(food.x%2!=0); food.y = rand()%(HEIGHT-2)+1; Node *scan = snake.head; while(scan !=NULL) { if(scan->place.x == food.x && scan->place.y == food.y) { flag = -1; break; } scan = scan->next; } if(flag>=0) { PrintIn(FOOD,food.x,food.y); break; } } // AfterEatFood(); } 二、蛇的移动——输入的甄别 蛇的移动本质很简单,就是不断更新蛇的位置,并打印。所以我们需要一个循环: while(true) { //。。。 } 其次我们需要接收输入,用来控制游戏进行 这里介绍一个函数 1. int kbhit(void); 2. // 检查当前是否有键盘输入,若有则返回一个非0值,否则返回0 这是一个非阻塞函数,有键按下时返回非0,但此时按键码仍然在键盘缓冲队列中。所以在确定键盘有响应之后,再用一个char变量将输入从缓冲区中调出来。 1. if(kbhit()) 2. ch = getch(); 现在我们规定游戏中'w' 's' 'a' 'd'控制方向,空格暂停,所以对于用户的输入,我们需要判断是否合法。我用了一个数组+循环来代替一连串的if: char ch,direction = ' '; char charr[5] = {'w','s','a','d',' '}; int flag = 0; if(kbhit()) ch = getch(); for(int i = 0;i<5;i++) //判断输入是否为规定的五个字符 { if(ch == charr[i]) { flag = 1; break; } } 当我们得到的输入合法时,我们仍需判断现在的输入方向是否与之前的方向相反,毕竟在我设计的这个游戏里,蛇身可不能折叠往自己身上碾过去。 在我用数组实现的那个版本里,我用了一大串if-else来避免相反的输入,这虽然简单,却很无脑。所以我用一个更简单的方法代替了它。在我们规定为正确输入的五个字符中,ASCII码分别为a:97,d:100,w:119,s:115,space:32,其中ad是冲突的一对,ws是冲突的一对。ad的差值为±3,ws的差值为±4,空格直接暂停,因此不予考虑。所以我们只需要判断,如果输入ch的值与方向direction的差值为±3或者±4,那么就可以断定输入不合法,丢弃。 if(flag == 1) //确认输入正常 { if(!(direction-ch==4||direction-ch==-4||direction-ch==3||direction-ch==-3)) { //排除与方向相反的输入 direction = ch; } else if(ch == ' ') continue; } 之前版本10行的事情,现在有意义的代码只有5行。 三、蛇的移动 为了方便对移动的坐标进行操作,我们声明一个数组,用来存储不同方向下坐标的变化: int dir_value[2][4] = { {0,0,-2,2}, {-1,1,0,0} }; 不同下标分别对于w s a d,因为长度60的WIDTH其实只有30个单位,所以x值一次加2。 1、画面上的移动 由于蛇身每个节点都一个样,所以没有必要每次循环都把所有的节点重新输出一遍,只需要更新头节点和尾节点就好。在游戏中,无论是撞墙、还是其他情况,蛇只要移动了,那么他头节点的坐标一定会改变,因此我们可以在移动后先把新的蛇头打印出来。至于蛇尾,如果蛇移动后并没有吃到食物,蛇尾则删除,吃到了的话蛇尾则保留。所以在打印了头部之后再判断头部是否吃到食物,再对蛇尾进行处理。 switch(direction) { case 'w': PrintIn(NODE,snake.head->place.x+dir_value[0][0],snake.head->place.y+dir_value[1][0]); //打印头部 if(snake.head->place.x+dir_value[0][0] == food.x && snake.head->place.y+dir_value[1][0] == food.y) { //AddNode(&snake); //尾插法 //CreateFood(); } else //没有吃到 { Node *tail = GetTail(&snake); PrintIn(SPACE,tail->place.x,tail->place.y); //画面上消除尾部节点 } //... } 2、画面外的移动 在内存中,我们则需要更新各个节点的坐标。如果吃到了食物,则加入一个节点(我用的尾插法),并将前一节点的值赋给后一节点。先前的头节点坐标值赋给第二节点,头节点则根据输入,更新新的坐标值。没有吃到的话,也直接赋值,尾节点坐标值因为下一步就要更新,所以可丢弃不管,只需得到前一节点坐标就好。 case 'w': PrintIn(NODE,snake.head->place.x+dir_value[0][0],snake.head->place.y+dir_value[1][0]); if(snake.head->place.x+dir_value[0][0] == food.x && snake.head->place.y+dir_value[1][0] == food.y) { AddNode(&snake); //尾插法 CreateFood(); } else { Node *tail = GetTail(&snake); //得到尾节点 PrintIn(SPACE,tail->place.x,tail->place.y); } RenewSnake(&snake); //链表各节点值的跟新 snake.head->place.x += dir_value[0][0]; //蛇头更新 snake.head->place.y += dir_value[1][0]; break; 其中RenewSnake()函数用来更新一个链表(蛇),使前一个节点的值赋给后一个节点,对这个只需要两个临时变量就可以。 从这简单的流程图可看出一点端倪,现在我们把步骤完善一下。 因此我们得到了一些普适性的方法,代码如下: void RenewSnake(Snake *psnake) { int x_index[2] = {0,0},y_index[2] = {0,0}; Node *scan = psnake->head; int i = 1; x_index[i%2] = scan->place.x; y_index[i%2] = scan->place.y; for(i = 1;isize;i++) { x_index[(i+1)%2] = scan->next->place.x; y_index[(i+1)%2] = scan->next->place.y; scan->next->place.x = x_index[i%2]; scan->next->place.y = y_index[i%2]; scan = scan->next; } } 同理,其余三个方向也是如此。 四、移动后的操作 在这个游戏中,我们需要这么几个变量: int length = -1; int score = -10; int speed = 250; 其中,length其实可以不需要。我们需要在吃到食物后进行一系列的操作,如加分,重新生成食物等等。所以在移动时的判断里加入一些函数。 if(snake.head->place.x+dir_value[0][0] == food.x && snake.head->place.y+dir_value[1][0] == food.y) { AddNode(&snake); //尾插法 CreateFood(); } 生成食物还需要加分等操作,所以我们可以把加分等操作的函数(AfterEatFood();)放到该函数末尾。不过这样的话,游戏开始生成的第一个食物就需要注意了,因此我们的两个全局变量都是负值。 void AfterEatFood() { Pos(WIDTH+20,HEIGHT-20); printf("%d = %d",++length,snake.size); Pos(WIDTH+16,HEIGHT-18); if(speed>150) score += 10; else score += 20; printf("%d",score); if(speed>100) speed-=5; Pos(WIDTH+16,HEIGHT-16); printf("%d",speed); } 在蛇移动后,我们还需判断蛇是否撞墙或者咬到自身。撞墙是蛇头与边界坐标的比较,咬到自身则可以用一个循环。 if(ThroughWall(&snake) == true) { Pos(0,30); system("pause"); exit(0); } if(BiteItself(&snake)==true) { Pos(0,30); system("pause"); exit(0); } bool ThroughWall(Snake *psnake) { if(psnake->head->place.x == 0 || psnake->head->place.x == WIDTH-2 || psnake->head->place.y == 0 || psnake->head->place.y == HEIGHT-1) { Pos(25,15); printf("撞墙,游戏结束!"); return true; } else { Pos(0,HEIGHT); printf(" "); //将闪烁不停的光变放到地图外面---迷之操作=。= return false; } } bool BiteItself(Snake *psnake) { Node *scan = psnake->head; while(scan->next != NULL) { scan = scan->next; if(scan->place.x == psnake->head->place.x && scan->place.y == psnake->head->place.y) { Pos(25,15); printf("咬到自身,游戏结束!"); return true; } } return false; } 最后在循环末尾加入Sleep,控制游戏的节奏。 Sleep(speed); 五、附注 1、源代码地址:贪吃蛇链表实现源码 2、主函数截图: 3、运行截图: https://www.cnblogs.com/magicxyx/p/9456533.html
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信