基于linux的贪吃蛇项目开发

文章详细介绍了如何使用C语言结合ncurses库开发一个贪吃蛇游戏,包括初始化curses、地图扫描、贪吃蛇身体定义、双线程处理按键及运动、食物随机出现和死亡机制。项目中涉及到了链表操作、多线程编程以及Linux环境下的文件操作等技术。
摘要由CSDN通过智能技术生成

目录

项目意义

项目内容

项目环境

项目模块简介

0.主函数对curses的初始化

1.地图扫描

2.贪吃蛇身体定义

定义贪吃蛇身体节点的结构体以及贪吃蛇的头尾指针

初始化贪吃蛇身体第一个节点的结构体

加入链表成员的函数addNode()代码:

删除链表成员的函数deleNode()代码:

3.双线程对按键进行获取且转弯

按键转弯输入处理函数

linux双线程的使用

贪吃蛇添加节点代码

贪吃蛇删除节点的代码

贪吃蛇运动的代码

双线程中实现运动效果的代码

初始化贪吃蛇时加入定义方向

转弯代码的优化

扫描贪吃蛇节点的代码

4.食物随机出现与让贪吃蛇长长、死亡机制的机制

食物初始化代码如下:

还有就是判断食物坐标是否扫描到封装的函数:

食物坐标定义出来后我们要在地图上打印出来:

贪吃蛇长长和食物机制的代码

尾言


项目意义

学习和开发该项目时,很大程度的优化了我对C语言的学习成果,比如一级指针二级指针的用处用法,和链表的应用方法以及途经等等。

熟悉了linux环境下的文件操作指令,和代码编写,写完该项目后能熟练的应用linux环境下的大部分命令语句,对我接下来进入linux开发有着打基础的重要作用。

项目内容

项目环境

因为在普通的C语言编译的画幕下,键盘的输入进入缓冲区后要回车才能基于计算机读取,而我们项目里贪吃蛇要及时读取键盘的上下左右键进行转向操作,必然不可能一个转向按两个按键。所以我们应用了ncurses的库,他的输入数据可以及时的读取,getch下有按键值进入就读取一次,很好的解决了我们的问题。

接下来要说说ncurses库下的几个函数

打印输出函数:printw();使用方法与printf一样

输入检测函数:getch();从键盘接收一个字节,并且函数返回该字节

curses初始化必要函数

初始化ncurses:initscr();

endwin();结束窗口;

项目模块简介

项目由五个模块实现。0.主函数对curses的初始化,1.地图的扫描,2.贪吃蛇身体的定义,3.双线程对按键进行获取且转弯,4.食物随机出现与让贪吃蛇长长的机制。

0.主函数对curses的初始化

在主函数中调用curses.h的库#include<curses.h>

然后初始化ncurses:initscr();

如果单纯的调用curses的输入函数获取上下左右按键的值会是一个乱码,所以我们还要调用ncurses库中的keypad(stdscr,1);该函数的作用是对接收到的功能键进行一个数值包装,包装了几个宏定义:KEY_UP,KEY_DOWM,KEY_LIFT,KEY_RIGHT。

必须要调用endwin();不然会卡在不知名的地方,而且为了让函数不会一瞬间就跳到运行endwin退出画幕,所以在endwin前我们调用一个getch,当我们与按键输入时再退出画幕。

对了,值得一提的是当我们编译c代码的时候,linux自带的库中是没有ncurses的库的,所以我们要通过链接一个-lcurses,编译代码如下gcc a.c -lcurese

所以目前主函数是这样的:

#include <curses.h>

int main(void)
{
    int key;    

    initcur();
    keypad(stdscr,1);
    
    key = getch();
    printw("%d",key);
    
    getch();
    endwin();
}

1.地图扫描

实现我们说说什么是扫描的方式打印地图(如下面的代码)

//如果我们用符号点扫描出一个8*8的地图
//y为行标,x为列标


for(y=0;y<8;y++)
{
    printw(".");
    for(x=0;x<8;x++)
    {
        printw(".");
    }
}

这就是最简单的扫描代码,通过循环打印第一行的所有列,打印完后循环下一行。

而我们为什么要用扫描的呢,为什么不直接将一列要打印的数据一次性打印出来呢,因为我们后面不仅要在画幕上显示地图,还要在画幕上将蛇身子判断位置并打印出来,所以我们在打印地图时选择扫描的方式。

现在我们说说地图该怎么打印,我们要打印一个20*20的地图。上下边界用‘--’,左右边界用‘|’,为一个单元,要20个单元,中间用打印空格来填充。

因为我们的蛇要在20*20的范围内运动(实际要求是中间的空格打印是20*20)所以我们打印的边界肯定不能是20*20。

第一点:要有20个行是打印空格的行,加两行上下边界,所以行数为22

第二点:要有20个列是打印空格的‘|’的占用的空间为一个‘-’的空间,所以要打印的列数为21列,也就是说上下边界的行输出为:--*20次。中间打印空格的行为‘|’ + ‘空格’*20 + ‘|’。

所以初始的地图扫描代码如下:

void gamePic(void)
{
	int hang,lie;
	for(hang = 0;hang <= 21;hang++)
	{
		if(hang==0)                            //上边界
		{
			for(lie=0;lie<21;lie++)            //打印21*'--'
			{
				printw("--");
			}
			printw("\n");
		}
		if((hang>0)&&(hang<21))                //中间贪吃蛇运行范围(空格空间)20行
		{
			for(lie=0;lie<=21;lie++)
			{
				if((lie==0)||(lie==21))        //左右边界
				{
					printw("|");
				}
				else                            //左右边界中间的空格运行范围‘  ’*20
				{
					printw("  ");
				}
			}
			printw("\n");
		}
		if(hang==21)                            //下边界
		{
			for(lie=0;lie<21;lie++)
			{
				printw("--");
			}
			printw("\n");
		}
	}
	printw("by_nbb");
}

2.贪吃蛇身体定义

我们定义贪吃蛇身体的代码是用链表将他的身体连接起来,然后通过链表尾加一个和链表头减一个实现贪吃蛇的运动。

为什么要用那么反人类的尾加一前进呢,因为当我们如果要链表尾减一我们要遍历链表到尾的上一个进行,将尾的上一个的->next=NULL;然后将那个尾释放空间,这样我们的代码就需要增加一定的计算量,而用头减的话,直接将全局变量的头指向头的下一个结构体,然后释放原来头结构体的空间就好了,不需要遍历链表。

定义贪吃蛇身体节点的结构体以及贪吃蛇的头尾指针

struct Snake
{
	int hang;
	int lie;
	struct Snake *next;
};

struct Snake *head = NULL;
struct Snake *tail = NULL;

初始化贪吃蛇身体第一个节点的结构体

void InitSnake(void)
{
	struct Snake *p = NULL;
	
	head = (struct Snake *)malloc(sizeof(struct Snake));
	head->hang = 2;
	head->lie = 2;
	head->next = NULL;
	
	tail = head;
	addNode();
	addNode();
	addNode();	
}

先开辟了一个head的结构体指针空间

初始化这个指针里的参数,头尾指针都指向这个第一个参数

然后用addNode();增加长度;

加入链表成员的函数addNode()代码:

void addNode(void)
{
	struct Snake *new;
	new = (struct Snake *)malloc(sizeof(struct Snake));        //开辟一个new结构体指针空间
	
	new->hang = tail->hang;                                    //初始化new结构体指针
	new->lie = tail->lie+1;
	
	new->next = NULL;                                          //将尾结构体的下一个指向new
	tail->next = new;                                          //将尾指针重新赋值为new
	tail = new;
}

生成一个新的struct Snake 型的结构体指针,并将尾指针的下一个指向他,将尾指针重新赋值为新进入的链表成员new。

他的行坐标为上一个的行坐标,列坐标为上一个的列坐标加1,那么就是说是向右加一个。

然后处理完新成员结构体的地址等参数且接入链表后,我们将该链表尾重新定义为该新成员。(这是链表的基本思路,如果不理解的话,可以重新看看链表的定义,以及链表的基本操作:添加,删除,查找)

删除链表成员的函数deleNode()代码:

void deleNode(void)
{
	struct Snake *p = head;
	head = head->next;
	free(p);
}

先用一个局部的结构体指针变量,预存运行前的head的值,然后将head指向head的下一个结构体指针,然后取出刚刚预存的地址,将地址空间释放。

3.双线程对按键进行获取且转弯

按键转弯输入处理函数

首先我们讲一下按键的控制程序,非常简单,就是一个缓存变量key存储按键输入的值,用key = getch();然后将其值分为keypad();函数中包装好的KEY_DOWM、KEY_UP等宏定义的名称;在识别到上按键时我们将我们预先定义好的方向控制变量:dir赋值为UP;

#define DOWM	1
#define UP	-1
#define LIFT	2 
#define RIGHT	-2

void turn(int direction)
{
	if(abs(direction) !=abs(dir))
	{
		dir = direction;
	}
}

void *changeDir(void)
{
	int key;
	while(1)
	{
		key = getch();
		switch(key)
		{
			case KEY_DOWN:
				turn(DOWM);
				break;
			case KEY_UP:
				turn(UP);
				break;
			case KEY_LEFT:
				turn(LIFT);
				break;
			case KEY_RIGHT:
				turn(RIGHT);
				break;
		}
	}

}

linux双线程的使用

因为一般在main中只能执行一个while,然后一直重复执行循环,但我们函数中有两个主要的代码需要执行,一个是贪吃蛇的运动,一个是等待按键输入,如果放入一个while里的话,贪吃蛇运动了一下就检测是否有按键输入,如果没有就会一直等待,卡死不动了。

接下来讲一下在linux环境下该怎么实现双线程

首先要定义一个pthread_t型的指针t1:pthread_t t1;

需要几个线程我们就要定义几个这个类型的指针,我们需要实现双线程所以我们需要定义一个t1和一个t2

然后根据我们需要的作为线程单独运行的函数指针,在主函数中,初始化这两个线程:

pthread_create(&t1 , NULL , changDir NULL);

实现第一个参数是之前定义的那个类型的指针,然后第二个可能是是否需要返回值,第三个是我们线程函数的函数指针,第四个是要传入的参数;

要提一下的是,在编译时我们需要链接-lpthread的库才能找到线程相关的函数或者宏定义。

int main(void)
{	
	pthread_t t1;
	pthread_t t2;

	InitCurses();		
	InitSnake();
	Initfood();
	
	pthread_create(&t1,NULL,refreshJieMian,NULL);
	pthread_create(&t2,NULL,changeDir,NULL);
	while(1)
	{
		
	}
	getch();
	endwin();
}

贪吃蛇添加节点代码

做完前面的操作后我们需要将按键输入后的反馈操作也写出来;

实现我们要写的是一个贪吃蛇的移动代码,实际上他的原理很简单,就是用之前分装好的加入链表删除链表的函数,向不同方向运动就在不同方向加入新链表成员,然后再删除一个链表成员

根据上述思路我们实现要在加入新链表成员的函数里加入判断dir方向的代码,然后根据不同方向,向链表尾的不同方向加成员,如我们向dir为UP,那么在我们以链表尾地址的行坐标-1为新成员的地址。

还要提一下我们要给这个新的结构体开辟空间用malloc函数

void addNode(void)
{
	struct Snake *new;
	new = (struct Snake *)malloc(sizeof(struct Snake));
	
	if(dir == RIGHT)
	{
		new->hang = tail->hang;
		new->lie = tail->lie+1;
	}
	else if(dir == LIFT)
	{
		new->hang = tail->hang;
		new->lie = tail->lie-1;
	}
	else if(dir == DOWM)
	{
		new->hang = tail->hang+1;
		new->lie = tail->lie;
	}
	else if(dir == UP)
	{
		new->hang = tail->hang-1;
		new->lie = tail->lie;
	}
	new->next = NULL;
	tail->next = new;
	tail = new;
}

贪吃蛇删除节点的代码

void deleNode(void)
{
	struct Snake *p = head;
	head = head->next;
	free(p);
}

贪吃蛇运动的代码

做完不同方向向不同地方加链表成员后,我们要整合删除成员和添加成员函数,将我们的贪吃蛇运动代码写出来。

void moveSnake(void)
{
	
	addNode();


	deleNode();
}

双线程中实现运动效果的代码

当然这个函数是在下一个画幕下打印,或者说是在打印完第一个地图后又打印了一个新的地图,相当于画了两张画,这不是我们想要的效果,我们想要的效果是,在一张画中不停的运动,那该怎么办呢。

我们要在第二幅画开始画前将打印的光标复位到0,0。然后下一幅画画的时候就会覆盖了,上一副画然后停留1秒,只要覆盖的快我们看不到覆盖的过程,那么我们这个就是一副会动的画。

讲一下用到的函数:move(0,0);就是光标复位到0,0位

usleep(405000);这是一个以微秒为单位的延时函数。

void *refreshJieMian(void)
{	
	while(1)
	{
		move(0,0);
		refresh();
		gamePic();
		moveSnake();
		usleep(405000);	
	}
}

初始化贪吃蛇时加入定义方向

然后在蛇的身子的初始化函数里面,给dir赋个初始值

void InitSnake(void)
{
	dir = RIGHT;	
	
	struct Snake *p = NULL;
	
	head = (struct Snake *)malloc(sizeof(struct Snake));
	head->hang = 2;
	head->lie = 2;
	head->next = NULL;
	
	tail = head;
	addNode();
	addNode();
	addNode();	
}

转弯代码的优化

但做完以上操作之后我们发现,程序存在一点小bug,我们的贪吃蛇向右运动时左键触发,贪吃蛇立马掉头,我们要做的是复刻而不是升级所以,我们不需要这么运动,拿我们该这么优化呢。

很简单,我们只需要在将按键值赋值入dir函数中,修改一下就好了,还记得我们一开始宏定义UP等标志位时,相反的方向的数值是一个相反数吗,那么我们在赋值函数中判断一下这个传入的按键与运动状态dir的绝对值是否相等。

void turn(int direction)
{
	if(direction != (0-dir))
	{
		dir = direction;
	}
}

扫描贪吃蛇节点的代码

写到这里我才发现贪吃蛇身子在地图上扫描打印的代码忘记说了

基本思路是打印地图的时候在hang和lie++的时候我们判断是否与贪吃蛇身体的hang和lie相等,当然这个判断函数需要遍历贪吃蛇身体的链表。

判断扫描到的坐标是否是贪吃蛇身体节点的函数,需要遍历贪吃蛇身体的链表:

int hanSnakeNode(int i,int j)
{
	struct Snake *p = head;
	while(p != NULL)
	{
		if((p->hang == i)&&(p->lie == j))
		{
			return 1;
		}
		p = p->next;
	}
	return 0;
}

在地图中改变的代码我用特殊符号在其上下两行进行了标记

int hanSnakeNode(int i,int j)
{
	struct Snake *p = head;
	while(p != NULL)
	{
		if((p->hang == i)&&(p->lie == j))
		{
			return 1;
		}
		p = p->next;
	}
	return 0;
}

void gamePic(void)
{
	int hang,lie;
	for(hang = 0;hang <= 21;hang++)
	{
		if(hang==0)
		{
			for(lie=0;lie<20;lie++)
			{
				printw("--");
			}
			printw("\n");
		}
		if((hang>0)&&(hang<21))
		{
			for(lie=0;lie<=20;lie++)
			{
				if((lie==0)||(lie==20))
				{
					printw("|");
				}
/********************************************************/
				else if(hanSnakeNode(hang,lie))
				{
					printw("[]");
				}
/*******************************************************/
				else
				{
					printw("  ");
				}
			}
			printw("\n");
		}
		if(hang==21)
		{
			for(lie=0;lie<20;lie++)
			{
				printw("--");
			}
			printw("\n");
		}
	}
	printw("by_nbb");
}


到这来我们的贪吃蛇按键操作转弯和双线程的功能已经成型了。

4.食物随机出现与让贪吃蛇长长、死亡机制的机制

实现说说食物随机出现的函数,我们又称食物的初始化函数:

要说一个生成随机数的C语言函数rand,这个调用这个函数要调用stdilb.h的库

所以我们食物的结构体中的行坐标和列坐标由rand产生,即food->hang = rand%20;

为什么要加一个%20呢,因为rand会生成一个很大的随机数,而我们希望他的随机数在0-20(我们地图范围内),所以要给他取余20(其实我这个函数有点bug但懒得改了,如果你感兴趣可以自己修改一下,就是我的列坐标为0时,并不是打印空格,而是打印边界“|”,所以如果食物生成的坐标为0时会有bug,我的想法是,while判断到列坐标为0就运行重新赋值随机数即可,也没实际运行过,不知道会不会有未知bug)

食物初始化代码如下:

void Initfood(void)
{
	int x,y;
	
	if(food == NULL)
	{
		food = (struct Snake *)malloc(sizeof(struct Snake));
	}
	
	x = rand()%20;
	y = rand()%20;

	while(y == 0)
	{
		y = rand()%20;
	}
	
	food->hang = x;
	food->lie = y;
}

还有就是判断食物坐标是否扫描到封装的函数:

int hasfood(int i,int j)
{
	struct Snake *p = food;
	if((p->hang == i)&&(p->lie == j))
	{
		return 1;
	}
	return 0;
}

食物坐标定义出来后我们要在地图上打印出来:

void gamePic(void)
{
	int hang,lie;
	for(hang = 0;hang <= 21;hang++)
	{
		if(hang==0)
		{
			for(lie=0;lie<20;lie++)
			{
				printw("--");
			}
			printw("\n");
		}
		if((hang>0)&&(hang<21))
		{
			for(lie=0;lie<=20;lie++)
			{
				if((lie==0)||(lie==20))
				{
					printw("|");
				}
				else if(hanSnakeNode(hang,lie))
				{
					printw("[]");
				}
				else if(hasfood(hang,lie))
				{
					printw("##");
				}
				else
				{
					printw("  ");
				}
			}
			printw("\n");
		}
		if(hang==21)
		{
			for(lie=0;lie<20;lie++)
			{
				printw("--");
			}
			printw("\n");
		}
	}
	printw("by_nbb");
}

贪吃蛇长长和食物机制的代码

然后我们将贪吃蛇的节点与食物坐标是否重合判断,如果重合我们在贪吃蛇移动的代码中不删除一个节点。


void moveSnake(void)
{
	
	addNode();

	if(hasfood(tail->hang,tail->lie))
	{
		Initfood();
	}
	else
	{
		deleNode();
	}

	if(ifSnakeDie())
	{	
		InitSnake();
	}

}

上面函数中的ifSnakeDie()是我们定义的贪吃蛇是否撞墙或者要到自己的死亡判定函数。

在该函数中我们判断贪吃蛇的tail节点坐标是否到达边界,和tail节点坐标与链表从head开始遍历链表成员看看是否有坐标点与tail的坐标点重合。

int ifSnakeDie(void)
{
	struct Snake *p = head;
	
	if((tail->lie == 0)||(tail->lie == 20)||(tail->hang == 21)||(tail->hang == 0))
	{
		return 1;
	}
	while(p->next != NULL)
	{
		if((p->hang == tail->hang)&&(p->lie == tail->lie))
		{
			return 1;
		}
		p = p->next;
	}
	return 0;
}

尾言

以上就是我的贪吃蛇代码的全部内容了可能有点散,当时我学习的时候是一个模块一个模块升级优化的,但考虑到博客哪样子写的话会很累赘,所以就这样写了,第一次总结一个系统性的项目,不太懂,如果有什么地方漏了,或者没表达清楚,欢迎大家评论留言。这里给上我的全部源代码

#include <curses.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <time.h>

#define DOWM	1
#define UP	-1
#define LIFT	2 
#define RIGHT	-2

struct Snake
{
	int hang;
	int lie;
	struct Snake *next;
};

struct Snake *head = NULL;
struct Snake *tail = NULL;
int dir;
struct Snake *food = NULL;

int hanSnakeNode(int i,int j)
{
	struct Snake *p = head;
	while(p != NULL)
	{
		if((p->hang == i)&&(p->lie == j))
		{
			return 1;
		}
		p = p->next;
	}
	return 0;
}

int hasfood(int i,int j)
{
	struct Snake *p = food;
	if((p->hang == i)&&(p->lie == j))
	{
		return 1;
	}
	return 0;
}

void addNode(void)
{
	struct Snake *new;
	new = (struct Snake *)malloc(sizeof(struct Snake));
	
	if(dir == RIGHT)
	{
		new->hang = tail->hang;
		new->lie = tail->lie+1;
	}
	else if(dir == LIFT)
	{
		new->hang = tail->hang;
		new->lie = tail->lie-1;
	}
	else if(dir == DOWM)
	{
		new->hang = tail->hang+1;
		new->lie = tail->lie;
	}
	else if(dir == UP)
	{
		new->hang = tail->hang-1;
		new->lie = tail->lie;
	}
	new->next = NULL;
	tail->next = new;
	tail = new;
}

void deleNode(void)
{
	struct Snake *p = head;
	head = head->next;
	free(p);
}

void Initfood(void)
{
	if(food==NULL)
	{
		food = (struct Snake *)malloc(sizeof(struct Snake));
	}
	
	int x = rand()%20;
	int y = rand()%20;
	
	while(y==0)
	{
		y = rand()%20;
	}
	
	food->hang = y;
	food->lie = x;
}

void InitSnake(void)
{
	dir = RIGHT;	
	
	struct Snake *p = NULL;
	while(head != NULL)
	{	
		p = head;
		head = head -> next;
		free(p);
	}
	
	head = (struct Snake *)malloc(sizeof(struct Snake));
	head->hang = 2;
	head->lie = 2;
	head->next = NULL;
	
	tail = head;
	addNode();
	addNode();
	addNode();	
}

void InitCurses(void)
{
	initscr();
	keypad(stdscr,1);		
	noecho();	
}

void moveSnake(void)
{
	
	addNode();

	if(hasfood(tail->hang,tail->lie))
	{
		Initfood();
	}
	else
	{
		deleNode();
	}

	if(ifSnakeDie())
	{	
		InitSnake();
	}

}


int ifSnakeDie(void)
{
	struct Snake *p = head;
	
	if((tail->lie == 0)||(tail->lie == 20)||(tail->hang == 21)||(tail->hang == 0))
	{
		return 1;
	}
	while(p->next != NULL)
	{
		if((p->hang == tail->hang)&&(p->lie == tail->lie))
		{
			return 1;
		}
		p = p->next;
	}
	return 0;
}


void gamePic(void)
{
	int hang,lie;
	for(hang = 0;hang <= 21;hang++)
	{
		if(hang==0)
		{
			for(lie=0;lie<20;lie++)
			{
				printw("--");
			}
			printw("\n");
		}
		if((hang>0)&&(hang<21))
		{
			for(lie=0;lie<=20;lie++)
			{
				if((lie==0)||(lie==20))
				{
					printw("|");
				}
				else if(hanSnakeNode(hang,lie))
				{
					printw("[]");
				}
				else if(hasfood(hang,lie))
				{
					printw("##");
				}
				else
				{
					printw("  ");
				}
			}
			printw("\n");
		}
		if(hang==21)
		{
			for(lie=0;lie<20;lie++)
			{
				printw("--");
			}
			printw("\n");
		}
	}
	printw("by_nbb");
}

void turn(int direction)
{
	if(abs(direction) !=abs(dir))
	{
		dir = direction;
	}
}

void *changeDir(void)
{
	int key;
	while(1)
	{
		key = getch();
		switch(key)
		{
			case KEY_DOWN:
				turn(DOWM);
				break;
			case KEY_UP:
				turn(UP);
				break;
			case KEY_LEFT:
				turn(LIFT);
				break;
			case KEY_RIGHT:
				turn(RIGHT);
				break;
		}
	}

}

void *refreshJieMian(void)
{	int i;
	while(1)
	{
		move(0,0);
		refresh();
		gamePic();
		moveSnake();
		usleep(105000);	
	}
}

int main(void)
{	
	pthread_t t1;
	pthread_t t2;

	InitCurses();		
	InitSnake();
	Initfood();
	
	pthread_create(&t1,NULL,refreshJieMian,NULL);
	pthread_create(&t2,NULL,changeDir,NULL);
	while(1)
	{
		
	}
	getch();
	endwin();
}

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值