[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;i<WIDTH;i+=2)// 上下30 宽
- {
- PrintIn(WALL,i,0);
- PrintIn(WALL,i,HEIGHT-1);
- }
- for(i=1;i<HEIGHT-1;i++)//左右 28+2 高
- {
- PrintIn(WALL,0,i);
- PrintIn(WALL,WIDTH-2,i);
- }
- }
2.初始化蛇
在初始化蛇之前,我们得给蛇一个定义:蛇应该是一个链表,其中每个节点都包含了一个坐标。所以有如下定义:
- typedef struct {
- int x;
- int y;
- }Place; //坐标
- typedef struct node{
- Place place;
- struct node *next;
- }Node; //节点
- typedef struct snake{
- Node *head;
- int size; //长度
- }Snake; //指向一条蛇
因此当我们声明
- Snake snake;
时,我们其实就声明了一条蛇。
好了,现在可以给蛇赋予节点了。原理也很简单,在链表尾部加三个节点就好。我们规定蛇头在右,共有三个节点,位置居中,所以蛇头的坐标应该为(28,14),后两个节点依次为(26,14)、(24,14)。
- bool InitializeSnake(Snake *psnake)
- {
- Node *pnew;
- Node *scan;
- for(int i = 0;i<3;i++)
- {
- scan = (psnake->head);
- 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);
- 值,否则返回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;i<psnake->size;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、运行截图:
[C语言]链表实现贪吃蛇及部分模块优化的更多相关文章
- 【C语言项目】贪吃蛇游戏(上)
目录 00. 目录 01. 开发背景 02. 功能介绍 03. 欢迎界面设计 3.1 常用终端控制函数 3.2 设置文本颜色函数 3.3 设置光标位置函数 3.4 绘制字符画(蛇) 3.5 欢迎界面函 ...
- 【C语言项目】贪吃蛇游戏(下)
目录 00. 目录 07. 游戏逻辑 7.5 按下ESC键结束游戏 7.6 判断是否撞到墙 7.7 判断是否咬到自己 08. 游戏失败界面设计 8.1 游戏失败界面边框设计 8.2 撞墙失败界面 8. ...
- C语言之贪吃蛇
利用链表的贪吃蛇,感觉自己写的时候还是有很多东西不熟悉, 1.预编译 2.很多关于系统的头文件也不是很熟悉 3.关于内存 第一个是.h头文件 #ifndef _SNAKE_H_H_H #define ...
- JavaScript版—贪吃蛇小组件
最近在学习JavaScript,利用2周的时间看完了<JavaScript高级编程>,了解了Js是一门面向原型编程的语言,没有像C#语言中的class,也没有私有.公有.保护等访问限制的级 ...
- 贪吃蛇(C语言版)链表实现
贪吃蛇 gitee:贪吃蛇C语言版: Snake 蛇的结构 typedef struct Snake { int x; int y; struct Snake *next; }; 游戏开始欢迎界面 / ...
- 小项目特供 贪吃蛇游戏(基于C语言)
C语言写贪吃蛇本来是打算去年暑假写的,结果因为ACM集训给耽搁了,因此借寒假的两天功夫写了这个贪吃蛇小项目,顺带把C语言重温了一次. 是发表博客的前一天开始写的,一共写了三个版本,第一天写了第一版,第 ...
- c语言贪吃蛇详解3.让蛇动起来
c语言贪吃蛇详解3.让蛇动起来 前几天的实验室培训课后作业我布置了贪吃蛇,今天有时间就来写一下题解.我将分几步来教大家写一个贪吃蛇小游戏.由于大家c语言未学完,这个教程只涉及数组和函数等知识点. 上次 ...
- 贪吃蛇小游戏-----C语言实现
1.分析 众所周知,贪吃蛇游戏是一款经典的益智游戏,有PC和手机等多平台版本,既简单又耐玩.该游戏通过控制蛇头方向吃食物,从而使得蛇变得越来越长,蛇不能撞墙,也不能装到自己,否则游戏结束.玩过贪吃蛇的 ...
- C/C++编程笔记:C语言贪吃蛇源代码控制台(二),分数和食物!
接上文<C/C++编程笔记:C语言贪吃蛇源代码控制台(一),会动的那种哦!>如果你在学习C语言开发贪吃蛇的话,零基础建议从上一篇开始哦!接下来正式开始吧! 三.蛇的运动 上次我已经教大家画 ...
随机推荐
- [翻译]Writing Component Editors 编写组件的编辑器
Writing Component Editors 编写组件的编辑器 All common control editors (opened from a control's context me ...
- vs2008快捷键极其技巧
vs2008快捷键极其技巧 1. 工具: Microsoft Visual Studio 2008 Version 9.0.21022.8 RTM Microsoft .NET Framework V ...
- ArcGIS(批量)删除属性字段
ArcGIS下删除属性字段有两种方式:① 单个删除:② 批量删除. 单个删除 批量删除 尽管如此,ArcGIS桌面软件在属性字段的编辑上并不太方便,所以我们自己做了一些工具辅助平时的内业处理工作.(* ...
- TortoiseSVN本地版本控制管理
TortoiseSVN 是 Subversion 版本控制系统的一个免费开源客户端.下载地址:https://tortoisesvn.net/downloads.html. 安装好TortoiseSV ...
- TSQL--标示列、GUID 、序列
--1. IDENTIY 列不能为空,不能设默认值,创建后不能使用ALTER TABLE TableName ALTER COLUMN修改,每张表只能有一个自增列--2. 查看当前值:SELECT I ...
- 通过hive向写elasticsearch的写如数据
通过hive向写elasticsearch的写如数据 hive 和 elasticsearch 的整合可以参考官方的文档: ES-hadoop的hive整合 : https://www.elastic ...
- docker容器日志清理
1.先查看磁盘空间 df -h 2.找到容器的containerId-json.log文件,并清理(治标不治本,log迟早还会大的) 查看各个容器的log文件大小 find /var/lib/dock ...
- ASP.NET Core学习总结(2)
public class ControllerActionInvoker : ResourceInvoker, IActionInvoker 我们知道,ControllerActionInvoker实 ...
- PostMan工具使用之基础篇
PostMan工具使用之基础篇 一.什么是PostMan Postman一款非常流行的API调试工具.(其他测试工具 Jmeter.soapUI) 二.下载及安装: 1.下载: 下载地址:https: ...
- 虚拟化安全 sandbox 技术分析
原文链接:https://cloud.tencent.com/developer/news/215218 前言: libvirt-4.3搭配qemu-2.12使用,如果使用默认的编译选项,可能会让qe ...