欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C语言实现贪吃蛇(3):结构+链表实现

程序员文章站 2022-03-27 21:33:48
本博客所讲到的知识跟前面我的两篇博客:《C语言实现贪吃蛇(一)—-数组实现》、《C语言实现贪吃蛇(二)—-局部刷新》 有很大的关系,建议大家前往看一下,尤其...

本博客所讲到的知识跟前面我的两篇博客:《C语言实现贪吃蛇(一)—-数组实现》《C语言实现贪吃蛇(二)—-局部刷新》 有很大的关系,建议大家前往看一下,尤其是第一篇,那篇博客是所有关于贪吃蛇游戏的基础。

之前的两篇博客将运用的C语言知识限定在了一般的数组上,但如果已经完整地了解过C语言的话,运用结构和链表会让程序的结构更明了,逻辑更清晰。这篇博客就将介绍如何用结构和链表改善之前的程序。

程序实现:

首先,把蛇看成一条单链表(还挺形象),蛇的每一节就是一个节点,以下是蛇节点的定义:

//蛇结点 
typedef struct node{
    //COORD是Windows API中定义的一种结构,表示一个字符在控制台屏幕上的坐标
    COORD cor;      //蛇节点坐标
    struct node * next;     //该节点的下一节点
} snake_node;

老套路,我们来看看程序修改后的一些变量和函数声明:

#include 
#include 
#include 
#include 
#include 

//72,80,75,77是方向键对应的键值
#define UP 72
#define DOWN 80
#define LEFT 75
#define RIGHT 77
#define SNAKE 1
#define FOOD 2
#define BAR 3

//用链表实现 

//蛇结点 
typedef struct node{
    //COORD是Windows API中定义的一种结构,表示一个字符在控制台屏幕上的坐标
    COORD cor;
    struct node * next;
}snake_node;

//初始化食物坐标
COORD food = {3,5};

//初始化蛇头
snake_node * snake_head;

//蛇长
char len = 1; 

//坐标比较函数(程序中有多次的坐标比较)
int cor_cmp(COORD pt1,COORD pt2); 
//将光标移动到命令行的 (x,y)位置,参数是一个坐标结构体 
void gotoxy(COORD pt);
//初始化游戏
void init_game(void);
//生成食物坐标
COORD generate_food(void); 
//移动蛇 
void move_snake(int dir); 
//释放申请的内存空间 
void free_node(snake_node * n);
//获取方向函数(注意当蛇身长度超过一节时不能回头)
int get_dir(int old_dir);
//判断蛇死活的函数(判断了蛇是否撞到边界或者自食)
int isalive(void);

int main(void) {
    int dir = UP;   //初始方向默认向上,UP是我们定义的宏
    init_game();    //初始化游戏 
    while(1){
        dir = get_dir(dir);     //获取方向(我们摁下的方向)
        move_snake(dir);        //移动蛇身
        if(!isalive()){         //判断蛇的生命状态
            break;
        }
    }
    //清除屏幕 
    system("cls");
    printf("Game Over!\n");
    //释放申请的内存空间 
    free_node(snake_head); 

    return 0;
}

这篇博客是对 《C语言实现贪吃蛇(二)—-局部刷新》 进行的优化,在该优化中我们修改的方面有一下几点(由于坐标存储的方式发生改变,因此所有的函数都得改变):

蛇节点使用结构体存储 坐标改用 COORD 结构体存储 增加坐标比较函数(因为坐标比较次数较多) 增加链表释放函数(蛇挂了之后,释放蛇单链表) 由于坐标存储的方式发生改变,因此所有的函数都得改变

我们先来看坐标比较函数,其实这个函数是很简单的:
也就对每个点的 x、y 坐标分别比较

//坐标比较函数(程序中有多次的坐标比较)
int cor_cmp(COORD pt1,COORD pt2);

int cor_cmp(COORD pt1, COORD pt2)
{
    return (pt1.X == pt2.X && pt1.Y == pt2.Y);
}

//将光标移动到命令行的 (x,y)位置,参数是一个坐标结构体
void gotoxy(COORD pt);

void gotoxy(COORD pt)
{
    //句柄 
    HANDLE hout;

    //GetStdHandle函数获取一个指向特定标准设备的句柄,包括标准输入,标准输出和标准错误。
    //STD_OUTPUT_HANDLE正是代表标准输出(也就是显示屏)的宏 
    hout = GetStdHandle(STD_OUTPUT_HANDLE);

    //SetConsoleCursorPosition函数用于设置控制台光标的位置
    SetConsoleCursorPosition(hout, pt);
}

//初始化游戏
void init_game(void);

void init_game(void){
    //初始化蛇头 (3,9)
    snake_head = (snake_node *)malloc(sizeof(snake_node));    //从无到有,必须手动申请内存
    (snake_head->cor).X = 3;
    (snake_head->cor).Y = 9;
    snake_head->next= NULL;

    int i,j;    //i,j对应x,y轴  
    for(j = 0;j < 17;j ++){
        for (i = 0; i<17; i++) {
            //围墙
            if (i == 0 || i == 16 || j == 0 || j == 16)
            {
                putchar('#');
            }
            //蛇身
            else if (i == (snake_head->cor).X && j == (snake_head->cor).Y)
            {
                putchar('*');
            }
            //食物
            else if (i == food.X && j == food.Y)
            {
                putchar('$');
            }
            //空白地方
            else
            {
                putchar(' ');
            }
        }
        putchar('\n');
    }
}

//生成食物坐标
COORD generate_food(void);

COORD generate_food(void){
    COORD food_;
    snake_node * p = NULL;      //把定义的指针初始为NULL是一个好习惯
    int in_snake = 0;
    //以当前时间为参数提供种子供rand()函数生成更为随机的数
    srand((unsigned int)time(NULL));
    //循环产生在边界内且不在蛇身上的食物
    do {
        food_.X = rand() % 16;
        food_.Y = rand() % 16;
        for (p = snake_head; p != NULL; p = p->next){
            //在蛇身上 
            if (cor_cmp(food_,p->cor)){
                in_snake = 1;       
            }
        }

    } while (food_.X == 0 || food_.X == 16 || food_.Y == 0 || food_.Y == 16 || in_snake);

    return food_;
}

好了,有了这些改进过的 函数,我们就可以写出move_snake函数了。这时我们还需要考虑的一个问题是我们该如何利用链表存储蛇的坐标。链表的一个优势在于它是动态的,它的单元数不像数组一样一开始就是被写死的。我们自然而然地想到当蛇吃到食物时,就向链表尾部追加一个单元。至于如何更新蛇的坐标,有两个办法,第一种是像之前数组的方法一样,从蛇头开始向后传递坐标,其代码如下:

//移动蛇
void move_snake(int dir);

//像之前数组一样移动蛇
//通过将蛇头原来的坐标赋给第二节,原来的第二节赋给第三节,依次下去,完成蛇坐标的更新 
void move_snake(int dir)
{
    snake_node * p = NULL;
    COORD last = snake_head->cor, current;    //last与current用于之后蛇坐标的更新
    int grow = 0;

    switch (dir) {
        case UP:
            (snake_head->cor).Y--;
            break; 
        case DOWN:
            (snake_head->cor).Y++;
            break;
        case LEFT:
            (snake_head->cor).X--;
            break;
        case RIGHT:
            (snake_head->cor).X++;
            break;
    }

    //蛇吃到食物 
    if (cor_cmp(snake_head->cor,food)) {
        grow = 1;
        //生成新的食物 
        food = generate_food();
    }

    for (p = snake_head->next; p != NULL; p = p->next) {
        current = p->cor;
        p->cor = last;
        last = current;
    }

    gotoxy(snake_head->cor);
    putchar('*');     //打印新的蛇头

    if (grow) {
        //找到蛇尾 p
        p = snake_head;
        while(p->next != NULL){
            p = p->next;
        }

        p->next = (snake_node *)malloc(sizeof(snake_node));     //在蛇尾追加节点
        p->next->cor = last;
        p->next->next = NULL;

        len++;

        //打印新的食物 
        gotoxy(food);
        putchar('$');
    }else {
        //这是为了避免当你把蛇绕成一个圈的时候(蛇头紧跟蛇尾,没咬到),清除蛇尾顺便也把蛇头清除掉了 
        if(!cor_cmp(snake_head->cor,last)){
            gotoxy(last);
            putchar(' ');
        }
    }

    //避免光标一直跟着蛇尾(或食物 )
    COORD foot = {0,17};    //将光标置于左下角 
    gotoxy(foot);

    Sleep(500);
}

另一种更新坐标的方法是每次移动时申请一块新的内存空间存储移动后的蛇头坐标并让 snake_head 指针指向它,如果蛇不长节的话就释放蛇尾的内存空间,通俗来说就是添头去尾,这样就避免了蛇中间节点的移动过程(原来的蛇头自然而然成为第二节,原来第二节也成了第三节,而他们的坐标都没发生变化) 。

void move_snake(int dir)
{
    snake_node * p,* t = NULL;
    snake_node * newh = NULL;    //新的蛇头
    newh = (snake_node *)malloc(sizeof(snake_node));    //为新蛇头开辟内存空间
    newh->cor = snake_head->cor;    //把旧头的坐标赋给新头
    newh->next = snake_head;        //把新头的next指针指向旧头
    snake_head = newh;              //让头指针指向新开辟的内存空间

    int grow = 0;

    switch (dir) {
        case UP:
            (snake_head->cor).Y--;
            break; 
        case DOWN:
            (snake_head->cor).Y++;
            break;
        case LEFT:
            (snake_head->cor).X--;
            break;
        case RIGHT:
            (snake_head->cor).X++;
            break;
    }
    //蛇吃到了食物 
    if (cor_cmp(snake_head->cor,food)) {
        grow = 1;
        //生成新的食物 
        food = generate_food();
    }

    gotoxy(snake_head->cor);
    putchar('*');

    //如果长节的话不再对链表进行操作
    if (grow) {
        len ++;
        gotoxy(food);
        putchar('$');
    }
    //不长节的话就得删除蛇尾
    else {
        //找到倒数第二节
        p = snake_head;
        while(p->next->next != NULL){
            p = p->next;
        } 
        //记录下最后一节的地址
        t = p->next;
        //将倒数第二节的next指针赋值为NULL
        p->next = NULL;

        //这是为了避免当你把蛇绕成一个圈的时候(蛇头紧跟蛇尾,没咬到),清除蛇尾顺便也把蛇头清除掉了 
        if(!cor_cmp(snake_head->cor,t->cor)){
            gotoxy(t->cor);
            putchar(' ');
        }

        free(t);        //释放蛇尾的内存空间
    }

    //避免光标一直跟着蛇尾(或食物 )
    COORD foot = {0,17};    //将光标置于左下角 
    gotoxy(foot);

    Sleep(500);
}

在游戏结束之后,我们还得手动释放构造链表而申请的内存空间:

//释放申请的内存空间
void free_node(snake_node * n);

void free_node(snake_node * h){
    snake_node * p = h,* q;
    while(p != NULL){
        q = p;
        p = p->next;
        free(q);    
    }
}

最后判断该蛇挂没挂:

//判断蛇死活的函数(判断了蛇是否撞到边界或者自食)
int isalive(void);

int isalive(void){
    int self_eat = 0;
    snake_node * p = NULL;
    for (p = snake_head->next; p != NULL; p = p->next)
    {
        if (cor_cmp(p->cor, snake_head->cor))
        {
            self_eat = 1; 
        } 
    }

    return ((snake_head->cor).X == 0 || (snake_head->cor).X == 16 || (snake_head->cor).Y == 0 || (snake_head)->cor.Y >= 16 || self_eat) ? 0 : 1;
}

根据以上的改进,我们顺利的利用 结构+链表 实现了贪吃蛇的小游戏,对前面的代码实现了优化。

没错!这篇博客还是参考自前面两篇博客的原作者的 《C语言实现贪吃蛇之结构链表篇 》

这已经是第三篇参考(Copy)该作者的博客了,按理说也差不多了,再这样下去我都感觉自己有点小无耻了。

但毕竟是我感觉而已,本着坚持就是胜利的求知精神,我决定将无耻进行到底。