【C语言】贪吃蛇实现思路详解

  贪吃蛇小游戏主要运用了链表和线程实现游戏的运行,三要素分别是:地图->蛇身移动、增加、撞墙和咬自己->在地图范围内随机生成食物。接下来分步实现:

1. 地图

1.1 ncurse图形库库

1.2 接收功能键

1.3 通过ncurse绘制地图

2. 蛇身

2.1 静态构造蛇身

2.2 动态构造蛇身

2.3 蛇身的移动

2.4 控制方向

2.5 完善蛇的死亡方式

3. 食物


1. 地图

1.1 ncurse图形库库

在讲地图之前,先简单介绍一下ncurse图形库,在C语言库函数中常用获取按键响应的方式主要有:scanf()、getchar()、gets()等,但是必须按键后回车才能完成接收,为了蛇身自主移动方便通过按键控制其方向,我们就要引入ncurse库实现不需要回车响应就能完成按键的接收。

当然,有人会说ncurse早就out了,更甚于完爆它的GTK、c++图形库QT也逐渐落伍,现在大部分嵌入式设备也都开始安卓系统,所以在这里我们只是简单引用一下通过它实现对链表的操作不做过多了解。

那怎么使用ncurse呢?

Ubuntu下输入指令安装:

sudo apt-get install libncurses5-dev
#include<curses.h> //调用库函数;

int main()
{
    initscr();//初始化ncurse界面;
    printw("we are into ncurse\n");//相当于printf;
    getch();等待用户输入,如果没这句话程序会直接退出,看不到运行结果;
    endwin();程序退出,通过它来恢复shell终端的显示,如果没这句话,shell终端会乱码甚至崩掉;
    return 0;
}

 运行结果 :

 接下来再接收一个按键:

#include<curses.h>

int main()
{
    int n;
    initscr();
    n = getch();
    printw("your input is :%d\n",n);
    getch();
    endwin();
    return 0;
}

运行结果:

 按a,a的ASCII码是97,接收成功;

1.2 接收功能键

那如何获取↑ ↓ ← →功能键呢?

#include<curses.h>

int main()
{
    int key;
    initscr();
    keypad(stdscr,1);//接收功能键,1表示是
    while(1){
        key = getch();
        printw("your input is :%d\n",key);
    }
    
    getch();
    endwin();
    return 0;
}

结果如下依次输入↑ ↓ ← →:

获取到使用功能键的值,方便接下来通过它来控制小蛇,O的K

1.3 通过ncurse绘制地图

很显然我们的地图可以看作一个二维数组,既然是二维数组,我们就可以用for()进行遍历打印,先构建一个20×30的数组打印地图边界(用#表示):

#include<curses.h>

int main()
{
        int i,j;
        initscr();
        for(i=0;i<20;i++){
                for(j=0;j<30;j++){
                        printw("#");
                }
                printw("\n");
        }
        getch();
        endwin();
        return 0;
}

运行结果:

 地图我们只留下边框就好,中间部分需要去除一下:

#include<curses.h>
void initCury()
{
        initscr();
        keypad(stdscr,1);
        /*keypad设置了在stdscr中可以接收键盘的功能键,
        如:↑ ↓ ← → F1等*/
}

void mapGame()
{
        int i,j;
        for(i=0;i<20;i++){
                if(i == 0){
                        for(j=0;j<30;j++){
                                printw("#");
                        }
                        printw("\n");
                }//第零行,地图上侧,全部打印#
                if(i>=0 && i<=18){
                        for(j=0;j<=29;j++){
                                if(j == 0 || j == 29){
                                        printw("#");
                                }else{
                                        printw(" ");
                                }
                        }
                        printw("\n");
                }//第1~18行,列只有为0和29的时候才打印#
                if(i == 19){
                        for(j=0;j<=29;j++){
                                printw("#");
                        }
                }//第19行,同第0行
        }
        printw("\n");
}

void main()
{
        initCury();//将初始化ncurse封装
        mapGame();//将地图打印封装
        getch();
        endwin();
}

运行结果:

地图 O的K。

2. 蛇身

2.1 静态构造蛇身

 我们知道蛇身始终在地图内,这里用@表示,先静态定义几个蛇身以作演示:

#include<curses.h>

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

int score = 10;

struct Snake initSnake1 = {0,14,NULL};//蛇尾,表头
struct Snake initSnake2 = {1,14,NULL};
struct Snake initSnake3 = {2,14,NULL};
struct Snake initSnake4 = {3,14,NULL};//蛇头

void initCury()
{
        initscr();
        keypad(stdscr,1);
}

int hasNode(int x,int y)
{
        struct Snake *p;
        p = &initSnake1;//定义一个局部变量将链表头传过来
        while(p->next != NULL){
                if(p->hang < x && p->lie == y){    //判断该坐标是否有蛇身
                        return 1;
                }
                p = p->next;
        }
        return 0;
}

void mapGame()
{
        int i,j;
        for(i=0;i<20;i++){
                if(i == 0){
                        for(j=0;j<=29;j++){
                                printw("#");
                        }
                        printw("\n");
                }
                if(i>=0 && i<=18){
                        for(j=0;j<=29;j++){
                                if(j == 0 || j == 29){
                                        printw("#");
                                }
                                else if(hasNode(i,j)){
                                        printw("@");
                                        /*如果蛇身坐标等于所遍历的坐标,则打印蛇身@ */
                                }
                                else{
                                        printw(" ");
                                }
                        }
                                }
                        }
                        printw("\n");
                }
                if(i == 19){
                        for(j=0;j<=29;j++){
                                printw("#");
                        }
                }
        }
}

void main()
{
        initSnake1.next = &initSnake2;
        initSnake2.next = &initSnake3;
        initSnake3.next = &initSnake4;
        initCury();
        mapGame();
        getch();
        endwin();
}

2.2 动态构造蛇身

静态蛇身不利于蛇身移动,我们来动态开辟一个蛇身:

struct Snake{
        int hang;
        int lie;
        struct Snake *next;
};//将蛇身定义为一个结构体方便移动和增加

struct Snake *head;
struct Snake *tail;
/*将head和tail定义为全局变量*/
void initSnake()
{
        head = (struct Snake*)malloc(sizeof(struct Snake));//malloc动态开辟一个head空间
        head->hang = 0;
        head->lie  = 15;
        head->next = NULL;//定义一个表头初始坐标
        tail = head;
}

2.3 蛇身的移动

 蛇身打印好那怎么让他还是动呢?这里采取的方法是将将链表头移到表尾:

 既然移动要删除头节点并且向表尾增加一个节点,我们先来构造增加和删除节点的函数:

void addBody()
{
        struct Snake *newBody = (struct Snake*)malloc(sizeof(struct Snake));
        //动态开辟一个新空间存储删掉的节点
        newBody->hang = tail->hang+1;//由于初始方向我们给定向下,所以行坐标+1,纵坐标不变
        newBody->lie  = tail->lie;
        newBody->next = NULL;
        tail->next = newBody;
        tail = newBody;//重新使表尾为新节点
}

void delBody()
{
        struct Snake *p;
        p = head;
        head = head->next;//将链表头指向下一个节点,表头就独立出来了
        free(p);//释放掉旧表头
}

那如何使用构造出来的两个函数呢?

void moveBody()
{
        addBody();
        delBody();//通过不断增加表尾释放表头使蛇向下移动
        if(tail->hang==0||tail->lie==0||tail->hang==20||tail->lie==30){
                /*判断如果蛇撞墙,重新初始化蛇身,意味着游戏重新开始*/
                initSnake();
        }
}

用while(1)使用这个函数蛇身就能不断向下移动

void main()
{
        int conKey;//定义一个局部变量存储按键
        initCury();
        initSnake();
        mapGame();
        while(1){
                conKey=getch();
                if(conKey==KEY_DOWN){//KEY_DOWN是ncurse中的宏参数,即↓键
                        moveBody();
                        mapGame();
                        refresh();//每次移动坐标都要刷新一下才能显示移动的效果
                }
        }
        getch();
        endwin();
}

我们玩贪吃蛇不能靠我们自己按键来移动吧,所以要构造一个函数帮我们刷新:

void refreshPage()
{
        while(1){
                moveBody();
                mapGame();
                refresh();
                usleep(150000);//延时150毫秒
        }
}

2.4 控制方向

调用它我们就能不使用方向键让蛇自己向下移动,接下来就是通过方向键改变蛇的方向了,首先需要宏定义一下方向,其次需要定义一个全局变量dir来表示蛇运动的方向,并将值赋为DOWN也就是初始向下,然后通过按键来控制蛇的方向:

#define UP    1
#define DOWN  2
#define LEFT  3
#define RIGHT 4

int dir;//定义dir(方向)全局变量,dir初始方向为DOWN,在initSnake()中初始化

void addBody()
{
        struct Snake *newBody = (struct Snake*)malloc(sizeof(struct Snake));
        newBody->hang = tail->hang+1;
        newBody->lie  = tail->lie;
        newBody->next = NULL;
        switch(dir){    //控制蛇头方向:比如按↑,蛇头移动到链表尾上方
                case UP:
                        newBody->hang = tail->hang-1;
                        newBody->lie  = tail->lie;
                        break;
                case DOWN:
                        newBody->hang = tail->hang+1;
                        newBody->lie  = tail->lie;
                        break;
                case LEFT:
                        newBody->hang = tail->hang;
                        newBody->lie  = tail->lie-1;
                        break;
                case RIGHT:
                        newBody->hang = tail->hang;
                        newBody->lie  = tail->lie+1;
                        break;
        }
        tail->next = newBody;
        tail = newBody;
}


void contKey()
{
        while(1){
                key = getch();
                switch(key){
                        case KEY_UP:
                                printw("UP\n");
                                dir = UP;
                                break;
                        case KEY_DOWN:
                                printw("DOWN\n");
                                dir = DOWN;
                                break;
                        case KEY_LEFT:
                                printw("LEFT\n");
                                dir = LEFT;
                                break;
                        case KEY_RIGHT:
                                printw("RIGHT\n");
                                dir = RIGHT;
                                break;
                }
        }
}
                                                        150,1-8       89%

写到这里运行的话就会崩掉,因为刷新界面和控制方向存在同步关系,为了使得移动刷新的同时操作蛇改变方向就要用到线程来解决:

线程的创建:

int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);

pthread_t *restrict tidp:传递一个 pthread_t 类型的指针变量,也可以直接传递某个 pthread_t 类型变量的地址。pthread_t 是一种用于表示线程的数据类型,每一个 pthread_t 类型的变量都可以表示一个线程。

const pthread_attr_t *restrict attr:通过该参数可以定制线程的属性,比如可以指定新建线程栈的大小、调度策略等。如果创建线程无特殊的要求,该值也可以是NULL,表示采用默认属性,通常都用NULL。

oid *(*start_rtn)(void *):线程需要执行的函数。创建线程,是为了让线程执行一定的任务。线程创建成功之后, 该线程就会执行start_routine函数,该函数之于线程,就如同main函数之于主线程。

void *restrict arg:传递给start_routine函数的实参,当不需要传递任何数据时,将arg赋值为NULL即可。

创建三个线程同时进行:

#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void* fun1()
{
        while(1){
                printf("this is fun1\n");
                sleep(1);
        }
}

void* fun2()
{
        while(1){
                printf("this is fun2\n");
                sleep(1);
        }
}
void* fun3()
{
        while(1){
                printf("this is fun3\n");
                sleep(1);
        }
}

int main()
{
        pthread_t t1;
        pthread_t t2;
        pthread_t t3;
        pthread_create(&t1,NULL,fun1,NULL);
        pthread_create(&t2,NULL,fun2,NULL);
        pthread_create(&t3,NULL,fun3,NULL);
        while(1);
        return 0;
}

 现在三个while就可以同时运行了,创建两个线程同时运行refreshPage()和contKey():

void main()
{
        pthread_t t1;
        pthread_t t2;

        initCury();
        initSnake();
        mapGame();
        pthread_create(&t1,NULL,refreshPage,NULL);
        pthread_create(&t2,NULL,contKey,NULL);
        while(1);

        getch();
        endwin();
}

现在就可以控制蛇上下左右移动了,但是有个bug,如果蛇头方向为下但是按上键,会发现蛇会直接往上移动,简直开了挂!这里就要用到一个函数:abs()

abs()函数用于求整数的绝对值,比如abs(-1)和abs(1)的返回值都是1;

根据abs()函数我们就可将方向进行define,按键绝对值相同时不发生方向的改变:

#define UP    1
#define DOWN  -1
#define LEFT  2
#define RIGHT -2

void turn(int direction)
{
        if(abs(dir) != abs(direction)){    
        /*将目前方向传入,判断按键方向绝对值是否等于目前方向,如果不等于,改变其方向,(即:dir=1,        方向向上,按↓时dir=-1,但abs(-1)=1=原先的dir,方向不发生改变,只有按←和→时,abs(dir)变为2,方向才会发生改变)*/
                dir = direction;
        }
}

void* contKey()
{
        while(1){
                key = getch();
                switch(key){
                        case KEY_UP:
                                printw("key:UP\n");
                                turn(UP);
                                break;
                        case KEY_DOWN:
                                printw("key:DOWN\n");
                                turn(DOWN);
                                break;
                        case KEY_LEFT:
                                printw("key:LEFT\n");
                                turn(LEFT);
                                break;
                        case KEY_RIGHT:
                                printw("key:RIGHT\n");
                                turn(RIGHT);
                                break;
                }
        }
        noecho();//屏蔽掉控制字符(如组合键操作)   
}

现在蛇身的控制就已经写完了。

2.5 完善蛇的死亡方式

蛇的死亡方式有两种:撞墙和咬自己,现在封装一个函数来完美实现蛇的死亡用于mapGame()的判断。

int snakeKilled()
{
        struct Snake *p;
        p = head;
        if(tail->hang<0||tail->lie==0||tail->hang==20||tail->lie==30){    //撞墙
                return 1;
        }
        while(p->next!=NULL){
                if(p->hang==tail->hang && p->lie==tail->lie){    
                /*判断tail(蛇头)是否与链表节点相等,相等就是咬到自己*/
                        return 1;
                }
                p = p->next;
        }
        return 0;
}

在moveBody()中根据该函数返回值判断蛇是否死亡就好,如果死亡重新initSnake()就实现死亡后重新开始游戏。

3. 食物

在贪吃蛇中食物是随机生成的,这里要用到rand()函数:

rand():C语言中用来产生一个随机数的函数.使用方法是rand ()% (n-m+1)+m,这个式子表示产生 [m,n]范围内的随机数。

struct Snake food;
int score=0;

void* initFood()
{
        int x = rand()%20;//行:随机在0~20行生成食物x坐标
        int y = rand()%30;//列:随即在0~30列生成食物y坐标
        while(x==0 || y==0 || x==20 || y==30){    //避免食物在墙体内部,重新随机生成食物
                x = rand()%20;
                y = rand()%30;
        }
        food.hang = x;
        food.lie = y;
        score+=1;    //每增加一个食物,得分+1
        noecho();
}

int hasFood(int x,int y)    
/*将墙内坐标传入,如果食物坐标与传入坐标相等,在mapGame()中打印$表示食物*/
{
        if(food.hang == x && food.lie == y){
                return 1;
        }
        return 0;
}

void mapGame()
{
        int i,j;
        move(0,0);
        for(i=0;i<=19;i++){
                if(i == 0){
                        for(j=0;j<=30;j++){
                                printw("#");
                        }
                        printw("\n");
                }// 0  
                if(i>=0 || i<=19){
                        for(j=0;j<=30;j++){
                                if(j == 0 || j == 30){
                                        printw("#");
                                }
                                else if(hasNode(i,j)){
                                        printw("@");
                                }
                                else if(hasFood(i,j)){
                                        printw("$");
                                }
                                else{
                                        printw(" ");
                                }
                        }
                        printw("\n");
                }
                if(i == 19){
                        for(j=0;j<=30;j++){
                                printw("#");
                        }
                }
        }
        printw("\nScore: %d$\n",score);
        printw("food.x:%d, food.y:%d\n",food.hang,food.lie);
        noecho();
}

到这里贪吃蛇小游戏就已经成功了,时不时写一个贪吃蛇有助于加强操作链表的记忆,欢迎交流分享,完整代码在专栏中可以找到。

用windows api 做的贪吃蛇 #include #include"resource.h" #include"Node.h" #include #include TCHAR szAppname[] = TEXT("Snack_eat"); #define SIDE (x_Client/80) #define x_Client 800 #define y_Client 800 #define X_MAX 800-20-SIDE //点x的范围 #define Y_MAX 800-60-SIDE //点y的范围 #define TIME_ID 1 #define SECOND 100 #define NUM_POINT 10 //点的总个数 #define ADD_SCORE 10 LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { HWND hwnd; //窗口句柄 MSG msg; //消息 WNDCLASS wndclass; //窗口类 HACCEL hAccel;//加速键句柄 wndclass.style = CS_HREDRAW | CS_VREDRAW; //窗口的水平和垂直尺寸被改变时,窗口被重绘 wndclass.lpfnWndProc = WndProc; //窗口过程为WndProc函数 wndclass.cbClsExtra = 0; //预留额外空间 wndclass.cbWndExtra = 0; //预留额外空间 wndclass.hInstance = hInstance; //应用程序的实例句柄,WinMain的第一个参数 wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); //设置图标 wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); //载入预定义的鼠标指针 wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); //设置画刷 wndclass.lpszMenuName = szAppname; //设置菜单 wndclass.lpszClassName = szAppname; //设置窗口类的名字 if (!RegisterClass(&wndclass))//注册窗口类 { MessageBox(NULL, TEXT("这个程序需要windows NT!"), szAppname, MB_ICONERROR); return 0; } hwnd = CreateWindow(szAppname, TEXT("Snack_eat"),//CreateWindow函数调用时,WndProc将受到WM_CREATE WS_OVERLAPPEDWINDOW&~WS_THICKFRAME& ~WS_MAXIMIZEBOX,//普通的层叠窗口&禁止改变大小&禁止最大化 CW_USEDEFAULT, //初始x坐标(默认) CW_USEDEFAULT, //初始y坐标 x_Client, //初始x方向尺寸 770 y_Client, //初始y方向尺寸 750 NULL, //父窗口句柄 NULL, //窗口菜单句柄 hInstance, //程序实例句柄 WinMain函数中第二个参数 NULL); //创建参数 ShowWindow(hwnd, iCmdShow);//显示窗口,iCmdShow是WinMain的第四个参数,决定窗口在屏幕中的初始化显示形式,例:SW_SHOWNORMAL表示正常显示 UpdateWindow(hwnd);//使窗口客户区重绘,通过向WndProc发送一条WM_PAINT消息而完成的 hAccel = LoadAccelerators(hInstance, szAppname);//加载加速键 while (GetMessage(&msg, NULL, 0, 0)) { if (!TranslateAccelerator(hwnd, hAccel, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } }/* while (GetMessage(&msg, NULL, 0, 0))//GetMessage函数从消息队列中得到消息,填充msg。如果msg.message等于WM_QUIT,返回0,否则返回非0 { TranslateMessage(&msg);//将msg返回给windows已进行某些键盘消息的转换 DispatchMessage(&msg);//将msg再次返回给windows }*/ return msg.wParam;//msg.wParam是PostQuitMessage函数的参数值,通常是0 } ...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值