通过写贪吃蛇这个小游戏,加深对c语言的理解以及Linux操作系统的运用。在文章中逐步解析贪吃蛇的整个构建逻辑。并将它记录下来。(运行效果放在文章的最后,记得 点赞+收藏)
1.运用ncurses库编写贪吃蛇小游戏
在开始编译贪吃蛇小游戏时就发现一个难题,运用#include <stdio.h>的头文件实现贪吃蛇小游戏并不现实,在该头文件中每次的输出都要按一次回车键。也就是说,每按一个方向键就需要按下一次回车键程序才能输出。这样写下的的贪吃蛇小游戏无疑是愚蠢的。所以,这里要用到ncurses库。
Ncurses:Ncurses是一套编程库,它提供了一系列的函数以便使用者调用它们去生成基于文本的用户界面。其中的函数“getch()”就不需要按下回车键进行输出。在后续编写中一些函数也适用于贪吃蛇游戏的编写。
#include <curses.h>
//ncurses上下左右键的获取
void initcurses()
{
//三行代码初始化游戏。
initscr();//初始化
keypad(stdscr,1);//在std中接受键盘的功能键
noecho(); //关闭回显模式
}
2.贪吃蛇地图程序编写
我们编写的是一个20×20的小地图:
结果如图所示这是一个20×20的小地图在第0行和第20行中我们用“--”作为边界,在第0列和第20列中我们用“|”作为边界。用函数封装游戏地图,然后对每一行要实现的结果进行编写。头和尾我们需要打印“--”封住,中间的空间用“|”封住,中间的区域打印空格。
void gamePic()
{
int row;
int column;
move(0,0);//修改光标位置
for(row = 0;row <= 20;row++)
{
if(row == 0)
{
for(column = 0;column < 20;column++)
{
printw("--");
}
printw("\n");
}
if(row >= 0 && row <= 20)
{
for(column = 0;column <=2 0;column++)
{
if(column == 0 || column == 20)
{
printw("|");
}
else if(hasSnakenode(row,column))//蛇身子模块在地图显现
{
printw("[]");
}
else if(hasFoodnode(row,column))//食物模块在地图显现
{
printw("##");
}
else
{
printw(" ");
}
}
printw("\n");
}
if(row == 20)
{
for(column = 0;column < 20;column++)
{
printw("--");
}
}
}
printw("\n");
}
3.建立蛇身模块并显现在地图上
在完成地图的制作后我们就需要制作蛇的身子,这里我们用到的是结构体,运用链表的思想把它们连起来。首先声明一个结构体以及头和尾的指针。
struct snake
{
int row;//行坐标
int column;//列坐标
struct snake *next;//下一个节点的位置(地址/指针)
};
struct snake *head = NULL;
struct snake *tail = NULL;
//使用malloc函数动态开辟空间。用malloc函数需要头文件<stdilb.h>
#include <stdlib.h>
void initSnakeNode()
{
struct snake *p;
dir = RIGHT;
while(head != NULL)//将原先head中的数据依次清除在后面会使用到
{
p = head;
head = head->next;
free(p);
}
head = (struct snake *)malloc(sizeof(struct snake));
head->row = 2;
head->column = 2;
head->next = NULL;
tail = head;
addsnakenode();
addsnakenode();
addsnakenode();
}
之后我们要增加节点。这里是对上下左右四方向增加节点,所以这里会用到dir。在刚做这个小游戏的时候我们可以只对右方向增加节点进行试验,其原理不变,单独从switch拿出来即可。
void addSnakeNode()
{
struct snake *new;
new = (struct snake *)malloc(sizeof(struct snake));
switch(dir)
{
case UP:
new->row = tail->row-1;
new->column = tail->column;
break;
case DOWN:
new->row = tail->row+1;
new->column = tail->column;
break;
case LEFT:
new->row = tail->row;
new->column = tail->column-1;
break;
case RIGHT:
new->row = tail->row;
new->column = tail->column+1;
break;
}
new->next = NULL;
tail->next = new;
tail = new;
}
以上代码就是对蛇身子的编写。最后我们希望可以在地图中将蛇身显现。结合上面在地图编写时编注的代码。当符合要求时我们置1,不合要求就置0。这样实现了蛇身在地图中的显现。
int hasSnakeNode(int row,int column)
{
struct snake *p;
p = head;
while(p != NULL)
{
if(p->row == row && p->column == column)
{
return 1;
}
p = p->next;
}
return 0;
}
4.蛇的移动以及死亡方式
将地图和蛇的身子都做完后。我们就要开始做蛇的移动了,其实蛇移动的原理就是:增加一个节点的同时不要第一个节点。就是是说,当我们有3个节点时,向右增加一个节点就变成了4个节点,但不要第一个节点,我们就又回到了3个节点,不过相较于之前的3节点我们向右偏移了一格。
注意:在我们不要原先头结点时要把原先的头结点free()掉,给内存腾空间。
void moveSnakeNode()
{
addSnakeNode();
deleteSnakeNode();
}
void deleteSnakeNode()
{
struct snake *p;
p = head;
head = head->next;
free(p);
}
但是仅仅这样并不能在地图上实现移动,我们还要对地图进行更新才可以在地图上实现移动。所以修改完之后,把地图拿进来进行更新用while函数实现一直移动一直更新。但按一个按键再走一步又不符合贪吃蛇的游戏,并且为此我们还要专门写一个向右按键代码来做试验。所以我们直接运用refresh()函数,并延迟100ms。这样无需按键,贪吃蛇也可以一直向右移动。
void* refreshjiemian()
{
while(1)
{
moveSnakeNode();
gamePic();
refresh();
usleep(100000);
}
}
最后我们要给蛇设个边界免得它出了边界外回不来。所以这里的原理就是当蛇撞边界了,那么我们就让其重新回到建立蛇身子的那个函数,也就是初始化蛇。最后我们要清理掉原先的蛇的数据。
void moveSnakeNode()
{
addSnakeNode();
deleteSnakeNode();
if(Snakedie())
{
initSnakeNode();
}
}
int Snakedie()
{
struct snake *p;
p = head;
//如果蛇尾有撞墙,就重新开始
if(tail->row < 0 || tail->>column == 0 || tail->row>20 || tail->>column == 20)
{
return 1;
}
//只要p的下一个节点不为空,就一直检测蛇的头是否与蛇身有坐标重叠,有则返回1,重新开始
while(p->next != NULL)
{
if( p->row == tail->row && p->>column == tail->>column )
{
return 1;
}
p = p->next;
}
return 0;
}
struct snake *p;
while(head != NULL)//将原先head中的数据依次清除
{
p = head;
head = head->next;
free(p);
}
5.控制蛇的方向以及禁止不合理走位
现在我们给贪吃蛇加入转向功能使其不仅仅向右移动还可以上下左右移动。首先定义变量dir、key,宏定义上下左右。
#define UP 1
#define DOWN -1
#define LEFT 2
#define RIGHT -2 //程序员臭毛病必须对齐
int dir;
int key;
然后封装一个转向的函数。为了防止贪吃蛇出现从左向右,从上到下这样不合理的走位。所以我们写下一个绝对值的函数去禁止这样不合理的走位。
void* changedir()
{
while(1)
{
key = getch();
switch(key)
{
case KEY_UP:
turn(UP);
break;
case KEY_DOWN:
turn(DOWN);
break;
case KEY_LEFT:
turn(LEFT);
break;
case KEY_RIGHT:
turn(RIGHT);
break;
}
}
}
void turn(int direction)
{
//abs() 整数的绝对值
//if dir的绝对值 不等于 direction的绝对值,就把direction的值赋值给dir。
if(abs(dir) != abs(direction))
{
dir = direction;
}
}
6.Linux线程概念
地图的不断刷新和蛇的走位都是需要一直刷新,while()执行了一个就不可能再去执行另一个。为了解决这个问题。我们要引用到Linux系统中的线程概念。引入后就可以多个函数同时跑的问题。引用线程库<pthread.h>。分别将两个不同的函数放进pthread_create中。同时在主函数中再加一个while(1);使这两个函数一直跑,不至于开始运行一次就自动退出。
#include <pthread.h>
int main()
{
pthread_t t1;
pthread_t t2;
initcurses();//初始化游戏。
initSnakeNode();
gamePic();
pthread_create(&t1,NULL,refreshjiemian,NULL);
pthread_create(&t2,NULL,changedir,NULL);
while(1);
getch();
endwin();
return 0;
}
7.食物的编写和随机位置
最后我们编写食物的程序。和贪吃蛇一样。初始化食物,让食物在地图中显现两个函数实现食物的编写。注意:因为我们的列第0行是打印“|”,为避免食物刷到墙上,所以排除y==0的情况。
void initFoodNode()
{
int x = rand()%20;
int y = rand()%20;
if(y == 0)
{
y = rand()%20;
}
food.row = x;
food.column = y;
}
int hasFoodNode(int row,int column)
{
if(food.row == row && food.column == column)
{
return 1;
}
return 0;
}
最后我们还要加个当蛇吃食物,身子伸长的功能。其原理就是吃食物只增长节点,不会缩短节点。所以我们在moveSnake函数中加一个条件语句。当吃食物,食物初始化。蛇身只增长不缩短。
void moveSnakeNode()
{
addSnakeNode();
if(hasFoodNode(tail->row,tail->column))
{
initFoodNode();
}
else
{
deleteSnakeNode();
}
以上就是贪吃蛇游戏的整个构建逻辑,最后附上所有代码。
#include <curses.h>
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
#define UP 1
#define DOWN -1
#define LEFT 2
#define RIGHT -2
struct snake *head = NULL;
struct snake *tail = NULL;
struct snake *new;
struct snake food;
int key;
int dir;
//ncurses上下左右键的获取
void initcurses()
{
initscr();//初始化
keypad(stdscr,1);//在std中接受键盘的功能键
noecho();
}
struct snake
{
int row;
int column;
struct snake *next;
};
int hasSnakeNode(int row,int column)
{
struct snake *p;
p = head;
while(p != NULL)
{
if(p->row == row && p->column == column)
{
return 1;
}
p = p->next;
}
return 0;
}
int hasFoodNode(int row,int column)
{
if(food.row == row && food.column == column)
{
return 1;
}
return 0;
}
void gamePic()
{
int row;
int column;
move(0,0);//修改光标位置
for(row = 0;row <= 20;row++)
{
if(row == 0)
{
for(column = 0;column < 20;column++)
{
printw("--");
}
printw("\n");
}
if(row >= 0 && row <= 20)
{
for(column = 0;column <=2 0;column++)
{
if(column == 0 || column == 20)
{
printw("|");
}
else if(hasSnakeNode(row,column))//蛇身子模块在地图显现
{
printw("[]");
}
else if(hasFoodNode(row,column))//食物模块在地图显现
{
printw("##");
}
else
{
printw(" ");
}
}
printw("\n");
}
if(row == 20)
{
for(column = 0;column < 20;column++)
{
printw("--");
}
}
}
printw("\n");
}
void addSnakeNode()
{
new = (struct snake *)malloc(sizeof(struct snake));
switch(dir)
{
case UP:
new->row = tail->row-1;
new->column = tail->column;
break;
case DOWN:
new->row = tail->row+1;
new->column = tail->column;
break;
case LEFT:
new->row = tail->row;
new->column = tail->column-1;
break;
case RIGHT:
new->row = tail->row;
new->column = tail->column+1;
break;
}
new->next = NULL;
tail->next = new;
tail = new;
}
void deleteSnakeNode()
{
struct snake *p;
p = head;
head = head->next;
free(p);
}
void initSnakeNode()
{
struct snake *p;
dir = RIGHT;
while(head != NULL)//将原先head中的数据依次清除在后面会使用到
{
p = head;
head = head->next;
free(p);
}
head = (struct snake *)malloc(sizeof(struct snake));
head->row = 2;
head->column = 2;
head->next = NULL;
tail = head;
addsnakenode();
addsnakenode();
addsnakenode();
}
void initFoodNode()
{
int x = rand()%20;
int y = rand()%20;
if(y == 0)
{
y = rand()%20;
}
food.row = x;
food.column = y;
}
int Snakedie()
{
struct snake *p;
p = head;
//如果蛇尾有撞墙,就重新开始
if(tail->row < 0 || tail->>column == 0 || tail->row>20 || tail->>column == 20)
{
return 1;
}
//只要p的下一个节点不为空,就一直检测蛇的头是否与蛇身有坐标重叠,有则返回1,重新开始
while(p->next != NULL)
{
if( p->row == tail->row && p->>column == tail->>column )
{
return 1;
}
p = p->next;
}
return 0;
}
void moveSnakeNode()
{
addSnakeNode();
if(hasFoodNode(tail->row,tail->column))
{
initFoodNode();
}
else
{
deleteSnakeNode();
}
if(Snakedie())
{
initSnakeNode();
}
}
void* refreshjiemian()
{
while(1)
{
moveSnakeNode();
gamePic();
refresh();
usleep(100000);
}
}
void turn(int direction)
{
//abs() 整数的绝对值
//if dir的绝对值 不等于 direction的绝对值,就把direction的值赋值给dir。
if(abs(dir) != abs(direction))
{
dir = direction;
}
}
void* changedir()
{
while(1)
{
key = getch();
switch(key)
{
case KEY_UP:
turn(UP);
break;
case KEY_DOWN:
turn(DOWN);
break;
case KEY_LEFT:
turn(LEFT);
break;
case KEY_RIGHT:
turn(RIGHT);
break;
}
}
}
int main()
{
pthread_t th1;
pthread_t th2;
initcurses();//初始化游戏。
initSnakeNode();
gamePic();
pthread_create(&th1,NULL,refreshjiemian,NULL);
pthread_create(&th2,NULL,changedir,NULL);
while(1);
getch();
endwin();
return 0;
}
效果图