您当前的位置:首页 > 计算机 > 编程开发 > 数据结构与算法

怎样用双向链表实现贪吃蛇游戏?(含源码)

时间:03-05来源:作者:点击数:

前面章节中,给读者详细介绍了双向链表及其基本操作。在此基础上,本节教大家:如何利用双向链表实现一个简易的 C 语言版贪吃蛇游戏(如图 1 所示)。

双向链表实现贪吃蛇游戏
图 1 贪吃蛇小游戏的实现效果

其中,黄色框代表贪吃蛇,红色 代表食物!

使用双向链表实现此游戏,有以下几点需要做重点分析。

1) 我们知道,双向链表中各个节点的标准构成是一个数据域和 2 个指针域,但对于实现贪吃蛇游戏来说,由于各个节点的位置是随贪吃蛇的移动而变化的,因此链表中的各节点还需要随时进行定位。

在一个二维画面中,定义一个节点的位置,至少需要所在的行号和列号这 2 个数据。由此,我们可以得出构成贪吃蛇的双向链表中各节点的构成:

//创建表示蛇各个节点的结构体
typedef struct SnakeNode {
    int x, y;//记录节点所在的行和列
    struct SnakeNode *pre;//指向前驱节点的指针
    struct SnakeNode *next;//指向后续节点的指针
}Node, *pNode;

2) 贪吃蛇的移动,本质上就是对链表中各个节点的重新定位。换句话说,除非贪吃蛇吃到食物,否则无论怎样移动,都不会对双向链表的整个结构(节点数)产生影响,唯一受影响的就只是各个节点中 (x,y) 这对定位数据。

由此,我们可以试着设计出实现贪吃蛇移动的功能函数,本节所用的实现思想分为 2 步:

  1. 从蛇尾(双向链表尾节点)开始,移动向前遍历,过程中依次将当前节点的 (x,y) 修改为前驱节点的 (x,y),由此可实现整个蛇身(除首元节点外的其它所有节点)的向前移动;
  2. 接收用户输入的移动指令,根据用户指示贪吃蛇向左、向右、向上还是向下移动,首元节点中的 (x,y) 分别做 x-1、x+1、y-1 和 y+1 运算。

如下所示,move() 函数就实现了贪吃蛇的移动:

//贪吃蛇移动的过程,即链表中所有节点从尾结点开始逐个向前移动一个位置
bool Move(pNode pHead, char key) {
    bool game_over = false;
    pNode pt = pTail;
    while (pt != pHead) { // 每个节点依次向前完成蛇的移动
        pt->x = pt->pre->x;
        pt->y = pt->pre->y;
        pt = pt->pre;
    }
    switch (key) {
        case'd': {
            pHead->x += 1;
            if (pHead->x >= ROW)
                game_over = true;
            break;
        }
        case'a': {
            pHead->x -= 1;
            if (pHead->x < 0)
                game_over = true;
            break;
        }
        case's': {
            pHead->y += 1;
            if (pHead->y >= COL)
                game_over = true;
            break;
        }
        case'w': {
            pHead->y -= 1;
            if (pHead->y < 0)
                game_over = true;;
            break;
        }
    }
    if (SnakeDeath(pHead))
        game_over = true;
    return game_over;
}

注意,此段代码中还调用了 SnakeDeath() 函数,此函数用于判断贪吃蛇移动时是否撞墙、撞自身,如果是则游戏结束。

3) 当贪吃蛇吃到食物时,贪吃蛇需要增加一截,其本质也就是双向链表增加一个节点。前面章节中,已经详细介绍了如何在双向链表中增加一个节点,因此实现这个功能唯一的难点在于:如何为该节点初始化 (x,y)?

本节所设计的贪吃蛇游戏,针对此问题,提供了最简单的解决方案,就是不对新节点 x 和 y 做初始化。要知道,贪吃蛇是时刻移动的,而在上面的 move() 函数中,会时刻修正贪吃蛇每个节点的位置,因此当为双向链表添加新节点后,只要贪吃蛇移动一步,新节点的位置就会自行更正。

当然,读者也可发散思维,设计其他的解决方案。

也就是说,贪吃蛇吃到食物的实现,就仅是给双向链表添加一个新节点。如下即为实现此功能的代码:

//创建表示食物的结构体,其中只需要记录其所在的行和列
typedef struct Food {
    int x;
    int y;
}Food, *pFood;
//吃食物,等同于链表中新增一个节点
pNode EatFood(pNode pHead, pFood pFood) {
    pNode p_add = NULL, pt = NULL;
    if (pFood->x == pHead->x&&pFood->y == pHead->y) {
        p_add = (pNode)malloc(sizeof(Node));
        score++;
        pTail->next = p_add;
        p_add->pre = pTail;
        p_add->next = NULL;
        pTail = p_add;
        // 检查食物是否出现在蛇身的位置上
        do {
            *pFood = CreateFood();
        } while (FoodInSnake(pHead, pFood));
    }
    return pHead;
}

其中,Food 结构体用来表示食物,其内部仅包含能够定位食物位置的 (x,y) 即可。另外,此段代码中,还调用了 FoodeInSnake() 函数,由于食物的位置是随机的,因此极有可能会和贪吃蛇重合,所以此函数的功能就是:如果重合,就重新生成食物。

FoodInSnake() 函数的实现很简单,这里不再赘述:

//判断食物的出现位置是否和蛇身重合
bool FoodInSnake(pNode pHead, pFood pFood) {
    pNode pt = NULL;
    for (pt = pHead; pt != NULL; pt = pt->next) {
        if (pFood->x == pt->x&&pFood->y == pt->y)
            return true;
    }
    return false;
}

4) 贪吃蛇游戏界面的显示,最简单的制作方法就是:贪吃蛇每移动一次,都清除屏幕并重新生成一次。这样实现的问题在于,如果贪吃蛇的移动速度过快,则整个界面在渲染的同时,会掺杂着光标,并且屏幕界面会频繁闪动。

因此,在渲染界面时,有必要将光标隐藏起来,这需要用到<windows.h>头文件,实现代码如下:

// 隐藏光标
void gotoxy(int x, int y) {
    HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
    COORD pos;
    pos.X = x;
    pos.Y = y;
    SetConsoleCursorPosition(handle, pos);
}
void HideCursor() {
    CONSOLE_CURSOR_INFO cursor_info = { 1, 0 };
    SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);
}

同时,为了给整个界面渲染上颜色,也需要引入<windows.h>头文件,并使用如下函数:

void color(int m) {
    HANDLE consolehend;
    consolehend = GetStdHandle(STD_OUTPUT_HANDLE);
    SetConsoleTextAttribute(consolehend, m);
}

5) 需要注意的一点是,由此结束后,一定要手动释放双向链表占用的堆空间:

//退出游戏前,手动销毁链表中各个节点
void ExitGame(pNode *pHead)
{
    pNode p_delete = NULL, p_head = NULL;
    while (*pHead != NULL) {
        p_head = (*pHead)->next;
        if (p_head != NULL)
            p_head->pre = NULL;
        p_delete = *pHead;
        free(p_delete);
        p_delete = NULL;
        *pHead = p_head;
    }
}

解决以上问题之后,用双向链表实现贪吃蛇,基本上就没有难点了。读者可根据本节提供的实现思想,尝试独立实现。

本节设计实现的贪吃蛇游戏,源码文件有 3 个,分别为 snake.h、snake.c 和 main.c,读者可直接点击贪吃蛇游戏进行下载。

另一个使用C语言实现贪吃蛇小游戏(双向链表实现)【单文件】

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <string.h>
#include <windows.h>
#include <winnt.h>
#include <wincon.h>
#include <time.h>
#include <math.h>

#define ROW         20  // 域行数(高y)
#define COL         40  // 域列数(宽x)
#define SUCCESS      0
#define FAILURE     -1
#define NAME_LENGTH 32  // 名字最大长度
#define MOVE_SPEED  10  // 移动速度(每秒移动的次数,10:正常,100:变态)

#define DEBUG 0         // 调试模式,0:关闭调试,other:打开调试

#if DEBUG
#define print_error() do { \
    printf("Error: %s, %d\n", __FUNCTION__, __LINE__); \
} while(0)
#else
#define print_error()
#endif

typedef char int8;
typedef unsigned char uint8;
typedef short int int16;
typedef unsigned short int uint16;
typedef long int int32;
typedef unsigned long int uint32;

typedef struct {  // 坐标位置
    int16 x;
    int16 y;
} pos_t;

typedef struct node_t {  // 蛇的节点位置
    pos_t pos;
    struct node_t *next;
    struct node_t *prev;
} snake_node_t;

typedef enum {  // 移动方向
    UP = 0,
    DOWN,
    LEFT,
    RIGHT,
    DIR_BUTT
} dir_t;

typedef struct {  // 蛇信息
    int32 id;
    int8 name[NAME_LENGTH];
    int32 speed;
    int32 length;
    dir_t dir;
    snake_node_t *head;
    snake_node_t *tail;
} snake_t;

typedef struct {  // 域信息
    pos_t food;
    snake_t snake;
} field_t;

int8 initial(field_t *field);                // 域初始化
int8 create_snake(int32 id, int8 *name, int32 speed, pos_t head, int32 length, dir_t dir, snake_t *snake);  // 创建蛇
int8 collision(pos_t *pos, snake_t *snake);  // 碰撞检测
int8 get_food(field_t *field);               // 获取食物
void get_move_dir(snake_t *snake);           // 获取移动方向
void free_field(snake_t *snake);             // 销毁
void set_pos(int32 x, int32 y);              // 设置光标位置
void hide(void);                             // 隐藏光标
void snake_step(snake_t *snake);             // 蛇移动一步
int8 snake_grow_up(snake_t *snake);          // 蛇吃到食物
int8 snake_move(field_t *field);             // 蛇移动
void display_init(field_t *field);           // 显示初始化
void display(field_t *field);                // 显示
void snake_debug(field_t *field);            // 调试打印

/*******************************************************************************
* 函 数 名:main
* 函数功能:主函数
* 输    入:void
* 输    出:int:是否异常
* 说    名:none
*******************************************************************************/
int main(void)  // 主函数
{
    field_t field;

    initial(&field);
#if (!DEBUG)
    display_init(&field);
#endif
    while(1) {
#if DEBUG
        snake_debug(&field);
#endif
        get_move_dir(&(field.snake));
        if (snake_move(&field) != SUCCESS) {
            break;
        }
        get_food(&field);
#if (!DEBUG)
        display(&field);
#endif
        Sleep(1000 / field.snake.speed);
    }
    free_field(&field.snake);

    return 0;
}

/*******************************************************************************
* 函 数 名:initial
* 函数功能:域初始化
* 输    入:field:域信息
* 输    出:int8:是否成功
* 说    名:none
*******************************************************************************/
int8 initial(field_t *field)  // 域初始化
{
    // 长度3不能比起始位置(pos_t){ 2, 0 }的x值2超过1
    // 多条贪吃蛇尽量只修改id,name,起始位置(pos_t){ 2, 0 }的y值
    create_snake(2020, "CX19970", MOVE_SPEED, (pos_t){ 2, 0 }, 3, RIGHT, &(field->snake));

    field->food.x = COL / 2;
    field->food.y = ROW / 2;

    return SUCCESS;
}

/*******************************************************************************
* 函 数 名:create_snake
* 函数功能:创建蛇
* 输    入:id:序号
            name:蛇的名字
            speed:移动速度
            head:蛇头位置
            length:蛇的长度
            dir:蛇的移动方向
            snake:蛇信息
* 输    出:int8:是否成功
* 说    名:create_snake(2020, "moon", MOVE_SPEED, (pos_t){ 2, 0 }, 3, RIGHT, &(field->snake));
            长度3不能比起始位置(pos_t){ 2, 0 }的x值2超过1,
            多条贪吃蛇尽量只修改id,name,起始位置(pos_t){ 2, 0 }的y值。
*******************************************************************************/
int8 create_snake(int32 id, int8 *name, int32 speed, pos_t head, int32 length, dir_t dir, snake_t *snake)  // 创建蛇
{
    int32 i, len;

    len = strlen((char *)name);
    if ((len >= NAME_LENGTH) || (length < 1) ||
        (head.x >= COL) || (head.y >= ROW) || (length > head.x + 1)) {
        print_error();
        return FAILURE;
    }

    snake->id = id;
    memcpy(snake->name, name, len);
    snake->name[len] = '\0';
    snake->speed = speed;
    snake->length = length;
    snake->dir = dir;

    snake_node_t *p = NULL;
    p = (snake_node_t *)malloc(sizeof(snake_node_t));
    if (p == NULL) {
        print_error();
        return FAILURE;
    }
    snake->head = p;
    snake->tail = p;
    p->next = NULL;
    p->prev = NULL;
    snake->head->pos.x = head.x;
    snake->head->pos.y = head.y;
    for (i = 1; i < length; i++) {
        p = (snake_node_t *)malloc(sizeof(snake_node_t));
        if (p == NULL) {
            print_error();
            return FAILURE;
        }
        snake->tail->next = p;
        p->prev = snake->tail;
        p->next = NULL;
        snake->tail = p;
        p->pos.x = head.x - i;
        p->pos.y = head.y;
    }

    return SUCCESS;
}

/*******************************************************************************
* 函 数 名:collision
* 函数功能:碰撞检测
* 输    入:pos:待检测位置,snake:蛇信息
* 输    出:int8:0未发生碰撞,1发生碰撞
* 说    名:none
*******************************************************************************/
int8 collision(pos_t *pos, snake_t *snake)  // 碰撞检测
{
    snake_node_t *p = NULL;

    if ((pos->x < 0) || (pos->y < 0) || (pos->x > COL - 1) || (pos->y > ROW - 1)) {  // 越界
        return 1;
    }

    p = snake->head;  // 蛇体
    while (p != NULL) {
        if ((pos->x == p->pos.x) && (pos->y == p->pos.y)) {
            return 1;
        }
        p = p->next;
    }

    return 0;
}

/*******************************************************************************
* 函 数 名:get_food
* 函数功能:获取食物
* 输    入:field:域信息
* 输    出:int8:是否成功
* 说    名:none
*******************************************************************************/
int8 get_food(field_t *field)  // 获取食物
{
    pos_t pos;

    if ((field->food.y != -1) || (field->food.x != -1)) {  // 已经存在食物
        return SUCCESS;
    }

    srand((unsigned)time(NULL));
    pos.x = rand() % COL;
    pos.y = rand() % ROW;
    while (collision(&pos, &(field->snake))) {
        pos.x = rand() % COL;
        pos.y = rand() % ROW;
    }

    field->food.x = pos.x;
    field->food.y = pos.y;

    return SUCCESS;
}

/*******************************************************************************
* 函 数 名:get_move_dir
* 函数功能:获取移动方向
* 输    入:snake:蛇信息
* 输    出:void
* 说    名:none
*******************************************************************************/
void get_move_dir(snake_t *snake)  // 获取移动方向
{
    int8 ch;

    if (!_kbhit()) {
        return;
    }

    ch = _getch();
    if ((ch == 72) || (ch == 'w') || (ch == 'W')) {
        if (snake->dir != DOWN) {
            snake->dir = UP;
        }
    } else if ((ch == 80) || (ch == 's') || (ch == 'S')) {
        if (snake->dir != UP) {
            snake->dir = DOWN;
        }
    } else if ((ch == 75) || (ch == 'a') || (ch == 'A')) {
        if (snake->dir != RIGHT) {
            snake->dir = LEFT;
        }
    } else if ((ch == 77) || (ch == 'd') || (ch == 'D')) {
        if (snake->dir != LEFT) {
            snake->dir = RIGHT;
        }
    }
}

/*******************************************************************************
* 函 数 名:free_field
* 函数功能:销毁
* 输    入:snake:蛇信息
* 输    出:void
* 说    名:none
*******************************************************************************/
void free_field(snake_t *snake)  // 销毁
{
    snake_node_t *p = snake->tail;
    snake_node_t *tail = NULL;

    while (p != NULL) {
        tail = p->prev;
        free(p);
        p = tail;
    }
}

/*******************************************************************************
* 函 数 名:set_pos
* 函数功能:设置光标位置
* 输    入:x/y:坐标位置
* 输    出:void
* 说    名:none
*******************************************************************************/
void set_pos(int32 x, int32 y)  // 设置光标位置
{
    COORD pos;
    HANDLE output;

    pos.X = x;
    pos.Y = y;
    output = GetStdHandle(STD_OUTPUT_HANDLE);
    SetConsoleCursorPosition(output, pos);
}

/*******************************************************************************
* 函 数 名:hide
* 函数功能:隐藏光标
* 输    入:snake:蛇信息
* 输    出:void
* 说    名:none
*******************************************************************************/
void hide(void)  // 隐藏光标
{
    HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE);
    CONSOLE_CURSOR_INFO cursor_info = {1, 0};

    SetConsoleCursorInfo(hout, &cursor_info);
}

/*******************************************************************************
* 函 数 名:snake_step
* 函数功能:蛇移动一步
* 输    入:void
* 输    出:void
* 说    名:none
*******************************************************************************/
void snake_step(snake_t *snake)  // 蛇移动一步
{
    snake_node_t *p = NULL;

    p = snake->tail;
    snake->tail = snake->tail->prev;
    snake->tail->next = NULL;
    p->next = snake->head;
    p->prev = NULL;
    snake->head->prev = p;
    snake->head = p;
}

/*******************************************************************************
* 函 数 名:snake_grow_up
* 函数功能:蛇吃到食物
* 输    入:snake:蛇信息
* 输    出:int8:是否成功
* 说    名:none
*******************************************************************************/
int8 snake_grow_up(snake_t *snake)  // 蛇吃到食物
{
    snake_node_t *p = NULL;

    p = (snake_node_t *)malloc(sizeof(snake_node_t));
    if (p == NULL) {
        print_error();
        return FAILURE;
    }

    p->next = snake->head;
    snake->head->prev = p;
    p->prev = NULL;
    snake->head = p;

    return SUCCESS;
}

/*******************************************************************************
* 函 数 名:snake_move
* 函数功能:蛇移动
* 输    入:field:域信息
* 输    出:int8:是否成功
* 说    名:none
*******************************************************************************/
int8 snake_move(field_t *field)  // 蛇移动
{
    dir_t dir;
    snake_t *snake;
    pos_t new_pos;

    dir = field->snake.dir;
    snake = &(field->snake);
    if ((dir == UP) && (snake->head->pos.y < ROW - 1)) {
        new_pos.x = snake->head->pos.x;
        new_pos.y = snake->head->pos.y + 1;
    } else if ((dir == DOWN) && (snake->head->pos.y > 0)) {
        new_pos.x = snake->head->pos.x;
        new_pos.y = snake->head->pos.y - 1;
    } else if ((dir == LEFT) && (snake->head->pos.x > 0)) {
        new_pos.x = snake->head->pos.x - 1;
        new_pos.y = snake->head->pos.y;
    } else if ((dir == RIGHT) && (snake->head->pos.x < COL - 1)) {
        new_pos.x = snake->head->pos.x + 1;
        new_pos.y = snake->head->pos.y;
    } else {
        print_error();
        return FAILURE;
    }

    if (collision(&new_pos, snake)) {
        print_error();
        return FAILURE;
    } else {
        if((new_pos.x == field->food.x) && (new_pos.y == field->food.y)) {
            snake_grow_up(snake);
            snake->length++;
            snake->head->pos.x = new_pos.x;
            snake->head->pos.y = new_pos.y;
            field->food.x = -1;
            field->food.y = -1;
        } else {
            snake_step(snake);
            snake->head->pos.x = new_pos.x;
            snake->head->pos.y = new_pos.y;
        }
    }

    return SUCCESS;
}

/*******************************************************************************
* 函 数 名:display_init
* 函数功能:显示初始化
* 输    入:field:域信息
* 输    出:void
* 说    名:none
*******************************************************************************/
void display_init(field_t *field)  // 显示初始化
{
    int8 temp[128];
    int32 i, row, col;
    snake_node_t *p = NULL;

    row = ROW + 5;
    col = COL + 4;
    hide();
    sprintf(temp, "mode con cols=%d lines=%d", (int)col, (int)row);
    system((char *)temp);

    set_pos(0, 0);
    for (i = 0; i < col; i++) {
        printf("=");
    }
    printf("\n");
    set_pos((col - 14) / 2, 1);
    printf("王 氏 贪 吃 蛇\n");
    set_pos(0, 2);
    for (i = 0; i < col; i++) {
        printf("#");
    }
    printf("\n");
    for (i = 0; i < ROW; i++) {
        set_pos(0, ROW - 1 - i + 3);
        printf("# ");
        p = field->snake.head;
        while (p != NULL) {
            if (p->pos.y != i) {
                p = p->next;
                continue;
            }
            set_pos(p->pos.x + 2, ROW - 1 - p->pos.y + 3);
            if ((p->pos.x == field->snake.head->pos.x) && (p->pos.y == field->snake.head->pos.y)) {
                printf("*");
            } else if ((p->pos.x == field->snake.tail->pos.x) && (p->pos.y == field->snake.tail->pos.y)) {
                printf("+");
            } else if ((p->pos.x == field->food.x) && (p->pos.y == field->food.y)) {
                printf("$");
            } else {
                printf("=");
            }
            p = p->next;
        }
        set_pos(COL + 2, i + 3);
        printf(" #\n");
    }
    set_pos(0, ROW + 3);
    for (i = 0; i < col; i++) {
        printf("#");
    }
    set_pos(0, ROW + 4);
    printf("length: ");

    display(field);
}

/*******************************************************************************
* 函 数 名:snake_debug
* 函数功能:调试打印
* 输    入:field:域信息
* 输    出:void
* 说    名:none
*******************************************************************************/
void snake_debug(field_t *field)  // 调试打印
{
    snake_node_t *p = NULL;

    printf("food: (x:%d, y:%d) \n", (int)field->food.x, (int)field->food.y);

    printf("snake: \n");
    printf("  |--id: %d\n", (int)field->snake.id);
    printf("  |--name: %s\n", (char *)field->snake.name);
    printf("  |--speed: %d\n", (int)field->snake.speed);
    printf("  |--length: %d\n", (int)field->snake.length);
    printf("  |--dir: %d (0:U, 1:D, 2:L, 3:R)\n", (int)field->snake.dir);
    printf("  |--head: ");
    p = field->snake.head;
    while (p != NULL) {
        printf("(x:%d, y:%d)->", (int)p->pos.x, (int)p->pos.y);
        p = p->next;
    }
    printf("NULL\n");
    printf("  |--tail: ");
    p = field->snake.tail;
    while (p != NULL) {
        printf("(x:%d, y:%d)->", (int)p->pos.x, (int)p->pos.y);
        p = p->prev;
    }
    printf("NULL\n\n");
}

/*******************************************************************************
* 函 数 名:display
* 函数功能:显示
* 输    入:field:域信息
* 输    出:void
* 说    名:none
*******************************************************************************/
void display(field_t *field)  // 显示
{
    static int16 prev_food_x = -1;
    static int16 prev_food_y = -1;
    static int16 prev_head_x = -1;
    static int16 prev_head_y = -1;
    static int16 prev_tail_x = -1;
    static int16 prev_tail_y = -1;

    if (prev_food_x != -1 && prev_food_y != -1) {
        set_pos(prev_food_x + 2, ROW - 1 - prev_food_y + 3);
        printf(" ");
    }
    set_pos(field->food.x + 2, ROW - 1 - field->food.y + 3);
    printf("$");
    prev_food_x = field->food.x;
    prev_food_y = field->food.y;

    if (prev_head_x != -1 && prev_head_y != -1) {
        set_pos(prev_head_x + 2, ROW - 1 - prev_head_y + 3);
        printf("=");
    }
    set_pos(field->snake.head->pos.x + 2, ROW - 1 - field->snake.head->pos.y + 3);
    printf("*");
    prev_head_x = field->snake.head->pos.x;
    prev_head_y = field->snake.head->pos.y;

    if (prev_tail_x != -1 && prev_tail_y != -1) {
        set_pos(prev_tail_x + 2, ROW - 1 - prev_tail_y + 3);
        printf(" ");
    }
    set_pos(field->snake.tail->pos.x + 2, ROW - 1 - field->snake.tail->pos.y + 3);
    printf("+");
    prev_tail_x = field->snake.tail->pos.x;
    prev_tail_y = field->snake.tail->pos.y;
    set_pos(8, ROW + 4);
    printf("%d", (int)field->snake.length);
}

 

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门