贪吃蛇大作战【纯c语言】

如果有看到不懂的地方或者对c语言某些知识忘了的话,可以找我之前的文章哦!!!

个人主页:小八哥向前冲~-CSDN博客

所属专栏:c语言_小八哥向前冲~的博客-CSDN博客

贪吃蛇游戏演示:

贪吃蛇游戏动画演示

目录

游戏前期准备:

设置控制台相关信息

GetStdHanle

GetConsoleCursorInfo

SetConsoleCursorInfo

SetConsoleCursorPosition

GetAsynckeyState

贪吃蛇游戏设计与分析

本地化

地图,食物和蛇身设计

游戏的初始化

打印欢迎界面

绘制贪吃蛇地图

初始化蛇

初始化食物

游戏的运行

打印帮助信息

贪吃蛇的运行

游戏的结束

贪吃蛇的总代码


游戏前期准备:

需要注意的是,纯使用c语言实现贪吃蛇会使用到一些Win32 API知识,接下来我们一一介绍学习一下。

那么什么是Win32 API呢?

介绍:

Windows这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外,它同时也是⼀个很⼤ 的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程序达到开启 视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application),所以便 称之为ApplicationProgrammingInterface,简称API函数。WIN32API也就是MicrosoftWindows 32位平台的应⽤程序编程接口。

设置控制台相关信息

我们知道平常我们运行程序弹出来的那个框框就是控制台终端。(如图)

我们可以使用cmd命令来设置控制台窗口的大小。如:

//         列       行
mode con cols=100 lines=30

值得注意的是:

1.使用这个命令之前,需要把这个控制台改为让Windows决定或Windows 控制台主机。

2.使用system函数所需要的头文件既可以是stdlib.h,也可以是Windows.h(不区分大小,也可以使用windows.h或WINDOWS.H等形式引用头文件。

演示一下:

控制台的改变

同样我们也能通过命令来设置控制台名字

title 贪吃蛇

效果:

控制台上的坐标COORD

COORD是什么呢?其实它是Windows API中定义的结构体,表示一个字符在控制台屏幕缓冲区上的坐标,而坐标系(0,0)的原点位于缓冲区的顶部左端单元格。

COORD的结构体声明:

typedef struct _COORD
{
     SHORT X;
     SHORT Y;
}COORD,*PCOORD;

坐标赋值:

COORD pos={20,30};

GetStdHanle

GetStdHanle是一个Windows API函数。它用于从一个特定的标准设备(标准输入,标准输出等)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备

注意:标准输入是指键盘,标准输出指的是屏幕。

我们要知道的是只要得到的这个句柄,咋们就能操控设备。所以我们能用GetStdHanle函数来获得句柄,从而进行一系列操作。

我们来看看这个函数:

那么我们来尝试获取句柄:

HANDLE houtput=NULL;
//从标准输出获取句柄
houtput=GetStdHanle(STD_OUTPUT_HANDLE);

我们在程序运行跳出控制台的时候,是不是有一个光标在闪动?那么我们试想一下,倘若我们不将那个光标隐藏的话,蛇在移动的时候就会有一个光标一直在闪动,不美观。那么我们如何隐藏光标呢?接下来就要用到GetConsoleCursorInfo这个函数。

GetConsoleCursorInfo

同样的我们来看看这个函数的语法:

从中我们知道,这个函数就是用来检索控制台屏幕缓冲区的光标大小和光标可见性的信息

BOOL WINAPI GetConsoleCursorInfo(
  _In_  HANDLE               hConsoleOutput,
  _Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
//注意:PCONSOLE_CURSOR_INFO是指向CONSOLE_CURSOR_INFO 结构的指针,该结构接受有关主机游标

CONSOLE_CURSOR_INFO 这个结构体这个结构体它是包含了光标信息。

我们来看看它的相关信息:

我们来使用一下:

 //获取标准输出的句柄(用来标识不同设备的数值)
 HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

 //影藏光标操作
 CONSOLE_CURSOR_INFO CursorInfo;
 GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息

SetConsoleCursorInfo

那么我们得到了光标信息,下一步就是设置我们想要的光标信息,设置光标相关信息的函数其实就是SetConsoleCursorInfo函数,它是用来设置控制台指定控制台屏幕缓冲区的光标大小和可见性

相关信息:

我们来使用看看:


    //获取标准输出的句柄(用来标识不同设备的数值)
    HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

    //影藏光标操作
    CONSOLE_CURSOR_INFO CursorInfo;
    GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
    CursorInfo.bVisible = false; //隐藏控制台光标
    //设置光标状态
    SetConsoleCursorInfor(hOutput,&CursorInfo);

SetConsoleCursorPosition

设置好了光标的状态,那么能不能设置光标位置,让我们能在任意位置打印我们想要的信息呢?那么我们就要用到SetConsoleCursorPosition函数。我们将坐标位置放到COORD类型中,然后调用SetConsoleCursorPosition函数就能将光标设置指定位置。

看看详情:

使用一下:

    COORD pos = { 10, 20 };
    HANDLE hOutput = NULL;
    //获取标准输出的句柄(用来标识不同设备的数值)
    hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    //设置标准输出上光标的位置为pos
    SetConsoleCursorPosition(hOutput, pos);

需要注意的是:这个pos位置可能会设置不成功。

GetAsynckeyState

玩过贪吃蛇游戏的都知道,键盘上的上,下,左,右按键来控制蛇的方向。那么我们如何获取玩家是否按了哪个按键呢?GetAsynckeyState函数就能解决这个问题。

老样子,我们来看看它的详情:

详细解释:将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果 返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬 起;如果最低位被置为1则说明,该按键被按过,否则为0。 如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1

虚拟按键代码详细见:虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn

我们来使用看看:


#define KEY_PRESS(VK)  ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
 int main()
 { 
    while (1)
    {
        if (KEY_PRESS(0x30))
        {
            printf("0\n");
        }
        else if (KEY_PRESS(0x31))
        {
            printf("1\n");
        }
        else if (KEY_PRESS(0x32))
        {
            printf("2\n");
        }
        else if (KEY_PRESS(0x33))
        {
            printf("3\n");
        }
        else if (KEY_PRESS(0x34))
        {
            printf("4\n");
        }
        else if (KEY_PRESS(0x35))
        {
            printf("5\n");
        }
     }
}

好了,我们现在正式开始写游戏逻辑吧!!!

贪吃蛇游戏设计与分析

要实现的功能:

  • 贪吃蛇地图绘制
  • 蛇的动作(上 ,下,左,右方向按键控制蛇的动作)
  • 蛇撞墙死亡
  • 帮助信息的打印
  • 计算得分
  • 蛇加速,减速
  • 暂停游戏

我们可以打印墙体用宽字符:□,蛇身体用:●,食物用:★。

我们来科普一下宽字符:普通字符是占一个字节,而宽字符占2个字节。

这⾥再简单的讲⼀下C语⾔的国际化特性相关的知识,过去C语⾔并不适合⾮英语国家(地区)使⽤。 C语⾔最初假定字符都是单字节的。但是这些假定并不是在世界的任何地⽅都适⽤。 C语⾔字符默认是采⽤ASCII编码的,ASCII字符集采⽤的是单字节编码,且只使⽤了单字节中的低7 位,最⾼位是没有使⽤的,可表示为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语 国家中,128个字符是基本够⽤的,但是,在其他国家语⾔中,⽐如,在法语中,字⺟上⽅有注⾳符 号,它就⽆法⽤ASCII码表⽰。于是,⼀些欧洲国家就决定,利⽤字节中闲置的最⾼位编入新的符 号。⽐如,法语中的é的编码为130(⼆进制10000010)。这样⼀来,这些欧洲国家使⽤的编码体 系,可以表⽰最多256个符号。但是,这⾥⼜出现了新的问题。不同的国家有不同的字⺟,因此,哪 怕它们都使⽤256个符号的编码⽅式,代表的字⺟却不⼀样。⽐如,130在法语编码中代表了é,在希 伯来语编码中却代表了字⺟Gimel( ),在俄语编码中⼜会代表另⼀个符号。但是不管怎样,所有这 些编码⽅式中,0--127表⽰的符号是⼀样的,不⼀样的只是128--255的这⼀段。 ⾄于亚洲国家的⽂字,使⽤的符号就更多了,汉字就多达10万左右。⼀个字节只能表⽰256种符号, 肯定是不够的,就必须使⽤多个字节表达⼀个符号。⽐如,简体中⽂常⻅的编码⽅式是GB2312,使 ⽤两个字节表⽰⼀个汉字,所以理论上最多可以表⽰256x256=65536个符号。 后来为了使C语⾔适应国际化,C语⾔的标准中不断加⼊了国际化的⽀持。⽐如:加⼊了宽字符的类型 wchar_t 和宽字符的输⼊和输出函数,加⼊了头文件,其中提供了允许程序员针对特定 地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。

而打印宽字符需要本地化。

本地化

通过修改地区,程序可以改变它的⾏为来适应世界的不同区域。但地区的改变可能会影响库的许多部 分,其中⼀部分可能是我们不希望修改的。所以C语⾔⽀持针对不同的类项进⾏修改,下⾯的⼀个宏, 指定⼀个类项:

• LC_COLLATE:影响字符串⽐较函数 strcoll() 和 strxfrm()

• LC_CTYPE:影响字符处理函数的⾏为。

• LC_MONETARY:影响货币格式。

• LC_NUMERIC:影响 printf() 的数字格式。

• LC_TIME:影响strftime() 和 wcsftime() 。

• LC_ALL-针对所有类项修改,将以上所有类别设置为给定的语⾔环境。

setlocale函数:

 char* setlocale (int category, const char* locale);

setlocale 函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。 setlocale 的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参 数是LC_ALL,就会影响所有的类项。 C标准给第⼆个参数仅定义了2种可能取值:"C"(正常模式)和" "(本地模式)。 在任意程序执⾏开始,都会隐藏式执⾏调用:

setlocale(LC_ALL, "C");

当地区设置为"C"时,库函数按正常⽅式执⾏,小数点是⼀个点。 当程序运行起来后想改变地区,就只能显示调用setlocale函数。用""作为第2个参数,调⽤setlocale 函数就可以切换到本地模式,这种模式下程序会适应本地环境。 比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。

setlocale(LC_ALL, " ");//切换到本地环境
 

那如果想在屏幕上打印宽字符,怎么打印呢?宽字符的字面量必须加上前缀“L”,否则C语言会把字面量当作窄字符类型处理。前缀“L”在单引 号前⾯,表示宽字符,对应 wprintf() 的占位符为 wprintf() 的占位符为 %ls

我们来举例一下:

#include<locale.h>
#include<stdio.h>
int main()
{
    //修改当前地区为本地模式,为了支持中文宽字符的打印
    setlocale(LC_ALL, "");
    wprintf(L"%s\n",L"小八哥向前冲");
    return 0;
}

地图,食物和蛇身设计

我们创建一个地图:27行,58列。再围绕这个地图画出墙。

由上图,我们不难知道一行的宽度是一列宽的两倍,只要注意这个咋们就能轻易画出强来!

初始化状态:我们可以将蛇的身体设为5,每个节点为宽字符●,在固定的一个坐标处开始,我们这里假设在(24,5)处开始打印5个蛇身节点。值得注意的是:蛇每个节点的x坐标必须是2的倍数,否则可能出现蛇的某一个节点有一半出现在墙体,另一半出现在墙外。接下来就是食物,在墙内随机生成一个坐标(同样x坐标是2的倍数),再者坐标不能和蛇身体重合,才能打印★。

在游戏运行的时候,蛇每吃一个食物,蛇身就变长一节,这里我们使用链表存储蛇节点。每个节点记录蛇身节点在地图上的坐标以及指向下一个指针变量。蛇节点结构如下:

//蛇身节点
typedef struct SnakeNode
{
    int x;
    int y;
    struct SnakeNode* next;
}SnakeNode, * pSnakeNode;

我们刚刚分析了,要的变量不止于这些,还需要:指向蛇头的指针,初始时蛇的速度,蛇方向,食物,食物分数,总分,蛇的状态。但是这些变量比较分散也比较麻烦,我们可以同时创建另一个结构体来管理这些变量。

typedef struct Snake
{
    pSnakeNode _pSnake;//维护整条蛇的指针
    pSnakeNode _pFood;//维护食物的指针
    enum DIRECTION _Dir;//蛇头的方向默认是向右
    enum GAME_STATUS _Status;//游戏状态
    int _Socre;//当前获得分数
    int _Add;//默认每个食物10分
    int _SleepTime;//每走一步休眠时间
}Snake, * pSnake;

游戏的状态有:正常运行,正常退出,撞墙死亡,撞到自己死亡。

//游戏状态
enum GAME_STATUS
{
    OK,//正常运行
    KILL_BY_WALL,//撞墙
    KILL_BY_SELF,//咬到自己
    END_NOMAL//正常结束
};

蛇的方向:向上,向下,向左,向右。

//方向
enum DIRECTION
{
    UP = 1,//上
    DOWN,//下
    LEFT,//左
    RIGHT//右
};

现在,我们正式设计游戏逻辑。

为了使游戏逻辑梳理更加清晰,我们封装三个大函数:GameStart()——游戏初始化,GameRun()——游戏运行,GameEnd()——游戏结束。

游戏的初始化

初始化的内容:

  1. 打印欢迎界面

  2. 绘制贪吃蛇身体

  3. 初始化贪吃蛇相关变量和食物

打印欢迎界面

我们首先分装函数来定位坐标:

//设置光标的坐标
void SetPos(short x, short y)
{
    COORD pos = { x, y };
    HANDLE hOutput = NULL;
    //获取标准输出的句柄(用来标识不同设备的数值)
    hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    //设置标准输出上光标的位置为pos
    SetConsoleCursorPosition(hOutput, pos);
}

而system("pause")是一个用于暂停程序运行的函数。它会出现一个提示信息,直到用户按下任意键程序才会继续运行。

如:

而由上一个欢迎界面跳到这个界面,我们需要清理一下屏幕,要用到清理控制台界面函数。

//屏幕清理
system("cls");

打印欢迎界面:

void WelcomeToGame()
{
    SetPos(40, 15);
    printf("欢迎来到贪吃蛇小游戏");
    SetPos(42, 17);
    printf("@小八哥向前冲");
    SetPos(40, 25);//让按任意键继续的出现的位置好看点
    system("pause");
    system("cls");
    SetPos(25, 12);
    printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");
    SetPos(25, 13);
    printf("加速将能得到更高的分数。\n");
    SetPos(40, 25);//让按任意键继续的出现的位置好看点
    system("pause");
    system("cls");
}

绘制贪吃蛇地图

我们先来看看地图:

由于墙体是宽字符,我们先将c语言环境转化到本地环境,再利用wprintf打印宽字符。

先打印上,下两行,再打印两列:上一行坐标(2*i,0),i 的范围:0~28。下一行坐标(2*i,25),i 的范围0~28。左一列坐标(0,i),i 的范围:1~25。 右一列坐标:(56,i ),i 范围:1~25。

注意:打印的时候要准确定位好我们的坐标,由于打印的时候默认是从左向右,而我们要从上到下

#define WALL L'□'

//绘制地图
void CreateMap()
{
    int i = 0;
    //上(0,0)-(56, 0)
    SetPos(0, 0);
    for (i = 0; i < 58; i += 2)
    {
        wprintf(L"%c", WALL);
    }
    //下(0,26)-(56, 26)
    SetPos(0, 26);
    for (i = 0; i < 58; i += 2)
    {
        wprintf(L"%c", WALL);
    }
    //左
    //x是0,y从1开始增长
    for (i = 1; i < 26; i++)
    {
        SetPos(0, i);
        wprintf(L"%c", WALL);
    }
    //x是56,y从1开始增长
    for (i = 1; i < 26; i++)
    {
        SetPos(56, i);
        wprintf(L"%c", WALL);
    }
}

初始化蛇

我们得先创建5个节点,并且初始化好节点坐标,然后将这些节点串起来就行!

 //蛇的初始位置
#define POS_X 24
#define POS_Y 5

//创建蛇身的节点
 cur = (pSnakeNode)malloc(sizeof(SnakeNode));
 if (cur == NULL)
 {
     perror("InitSnake()::malloc()");
     return;
 }
 //设置坐标
 cur->next = NULL;
 cur->x = POS_X + i * 2;
 cur->y = POS_Y;

 //头插法
 if (ps->_pSnake == NULL)
 {
     ps->_pSnake = cur;
 }
 else
 {
     cur->next = ps->_pSnake;
     ps->_pSnake = cur;
 }

然后打印蛇:

#define WALL L'□'
#define BODY L'●'  
#define FOOD L'★'   
//打印蛇的身体
  cur = ps->_pSnake;
  while (cur)
  {
      SetPos(cur->x, cur->y);
      wprintf(L"%c", BODY);
      cur = cur->next;
  }

设置蛇属性:

 //初始化贪吃蛇数据
 ps->_SleepTime = 200;
 ps->_Socre = 0;
 ps->_Status = OK;
 ps->_Dir = RIGHT;
 ps->_Add = 10;

初始化食物

既然创建好了蛇,就差食物了,我们首先创建一个节点给食物,然后将食物打印出来。

这个食物的x坐标必须是2的倍数,也要再在墙体中,且这个食物随机生成。

void CreateFood(pSnake ps)
{
    int x = 0;
    int y = 0;

again:
    //产生的x坐标应该是2的倍数,这样才可能和蛇头坐标对齐。
    do
    {
        x = rand() % 53 + 2;
        y = rand() % 25 + 1;
    } while (x % 2 != 0);

    pSnakeNode cur = ps->_pSnake;//获取指向蛇头的指针
    //食物不能和蛇身冲突
    while (cur)
    {
        if (cur->x == x && cur->y == y)
        {
            goto again;
        }
        cur = cur->next;
    }

    pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); //创建食物
    if (pFood == NULL)
    {
        perror("CreateFood::malloc()");
        return;
    }
    else
    {
        pFood->x = x;
        pFood->y = y;
        SetPos(pFood->x, pFood->y);
        wprintf(L"%c", FOOD);
        ps->_pFood = pFood;
    }
}

游戏的运行

游戏运行时我们需要做的事:

  1. 打印帮助信息
  2. 贪吃蛇运行信息
  3. 判断蛇的状态

游戏运行的界面:

打印帮助信息

void PrintHelpInfo()
{
    //打印提示信息
    SetPos(64, 15);
    printf("不能穿墙,不能咬到自己\n");
    SetPos(64, 16);
    printf("用↑.↓.←.→分别控制蛇的移动.");
    SetPos(64, 17);
    printf("F1 为加速,F2 为减速\n");
    SetPos(64, 18);
    printf("ESC :退出游戏.space:暂停游戏.");
    SetPos(64, 20);
    printf("小八哥向前冲@版权");
}

贪吃蛇的运行

蛇的运行,我们需要自己按键去控制蛇的方向。我们需要判断哪个按键是否摁过判断蛇的方向,只要蛇的状态不是OK,此时就不需要走了。

void GameRun(pSnake ps)
{
    //打印右侧帮助信息
    PrintHelpInfo();
    do
    {
        SetPos(64, 10);
        printf("得分:%d ", ps->_Socre);
        printf("每个食物得分:%d分", ps->_Add);
        if (KEY_PRESS(VK_UP) && ps->_Dir != DOWN)
        {
            ps->_Dir = UP;
        }
        else if (KEY_PRESS(VK_DOWN) && ps->_Dir != UP)
        {
            ps->_Dir = DOWN;
        }
        else if (KEY_PRESS(VK_LEFT) && ps->_Dir != RIGHT)
        {
            ps->_Dir = LEFT;
        }
        else if (KEY_PRESS(VK_RIGHT) && ps->_Dir != LEFT)
        {
            ps->_Dir = RIGHT;
        }
        else if (KEY_PRESS(VK_SPACE))
        {
            pause();
        }
        else if (KEY_PRESS(VK_ESCAPE))
        {
            ps->_Status = END_NOMAL;
            break;
        }
        else if (KEY_PRESS(VK_F3))
        {
            if (ps->_SleepTime >= 50)
            {
                ps->_SleepTime -= 30;
                ps->_Add += 2;
            }
        }
        else if (KEY_PRESS(VK_F4))
        {
            if (ps->_SleepTime < 350)
            {
                ps->_SleepTime += 30;
                ps->_Add -= 2;
                if (ps->_SleepTime == 350)
                {
                    ps->_Add = 1;
                }
            }
        }
        //蛇每次一定之间要休眠的时间,时间短,蛇移动速度就快
        Sleep(ps->_SleepTime);
        SnakeMove(ps);
        KillByWall(ps);
        KillBySelf(ps);
    } while (ps->_Status == OK);
}

而暂停函数的实现只需要一直休眠就行,直到按了空格就跳出休眠。

void pause()//暂停
{
    while (1)
    {
        Sleep(300);
        if (KEY_PRESS(VK_SPACE))
        {
            break;
        }
    }
}

通过蛇头的下一个位置和蛇身以及释放蛇的尾节点,如果下一个位置是食物,就要吃掉食物,且再创建一个食物,如果不是食物,就正常走就行,所以我们走一步就需要判断是否为食物。

void SnakeMove(pSnake ps)
{
    //创建下一个节点
    pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
    if (pNextNode == NULL)
    {
        perror("SnakeMove()::malloc()");
        return;
    }
    //确定下一个节点的坐标,下一个节点的坐标根据,蛇头的坐标和方向确定
    switch (ps->_Dir)
    {
    case UP:
    {
        pNextNode->x = ps->_pSnake->x;
        pNextNode->y = ps->_pSnake->y - 1;
    }
    break;
    case DOWN:
    {
        pNextNode->x = ps->_pSnake->x;
        pNextNode->y = ps->_pSnake->y + 1;
    }
    break;
    case LEFT:
    {
        pNextNode->x = ps->_pSnake->x - 2;
        pNextNode->y = ps->_pSnake->y;
    }
    break;
    case RIGHT:
    {
        pNextNode->x = ps->_pSnake->x + 2;
        pNextNode->y = ps->_pSnake->y;
    }
    break;
    }

    //如果下一个位置就是食物
    if (NextIsFood(pNextNode, ps))
    {
        EatFood(pNextNode, ps);
    }
    else//如果没有食物
    {
        NoFood(pNextNode, ps);
    }
}

下一个位置是食物就吃掉食物,再创建新食物

void EatFood(pSnakeNode psn, pSnake ps)
{
    //头插法
    psn->next = ps->_pSnake;
    ps->_pSnake = psn;
    pSnakeNode cur = ps->_pSnake;
    //打印蛇
    while (cur)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%c", BODY);
        cur = cur->next;
    }
    ps->_Socre += ps->_Add;

    free(ps->_pFood);
    CreateFood(ps);
}

下一个不是食物

void NoFood(pSnakeNode psn, pSnake ps)
{
    //头插法
    psn->next = ps->_pSnake;
    ps->_pSnake = psn;
    pSnakeNode cur = ps->_pSnake;
    //打印蛇
    while (cur->next->next)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%c", BODY);
        cur = cur->next;
    }

    //最后一个位置打印空格,然后释放节点
    SetPos(cur->next->x, cur->next->y);
    printf("  ");
    free(cur->next);
    cur->next = NULL;
}

注意:将尾节点释放后,还需要将尾节点位置打印空格,否则蛇身只会越来越长。走一步,休眠一下,让我们知道蛇走到哪里了。

我们还需检测是否撞墙,是否撞墙检测头节点是否撞墙就行。

int KillByWall(pSnake ps)
{
    if ((ps->_pSnake->x == 0)
        || (ps->_pSnake->x == 56)
        || (ps->_pSnake->y == 0)
        || (ps->_pSnake->y == 26))
    {
        ps->_Status = KILL_BY_WALL;
        return 1;
    }
    return 0;
}

而检测是否撞到自己,只需要检测蛇头和某一个身体节点相撞(是否重合)就行。

int KillBySelf(pSnake ps)
{
    pSnakeNode cur = ps->_pSnake->next;
    while (cur)
    {
        if ((ps->_pSnake->x == cur->x)
            && (ps->_pSnake->y == cur->y))
        {
            ps->_Status = KILL_BY_SELF;
            return 1;
        }
        cur = cur->next;
    }
    return 0;
}

游戏的结束

其实到这里基本的游戏就能运行起来了,只不过我们创建的节点需要释放掉,且玩家结束游戏的提示。

void GameEnd(pSnake ps)
{
    pSnakeNode cur = ps->_pSnake;
    SetPos(24, 12);
    switch (ps->_Status)
    {
    case END_NOMAL:
        printf("您主动退出游戏\n");
        break;
    case KILL_BY_SELF:
        printf("您撞上自己了 ,游戏结束!\n");
        break;
    case KILL_BY_WALL:
        printf("您撞墙了,游戏结束!\n");
        break;
    }

    //释放蛇身的节点
    while (cur)
    {
        pSnakeNode del = cur;
        cur = cur->next;
        free(del);
    }
}

贪吃蛇的总代码

Snack.h文件

#pragma once
#include <windows.h>
#include <time.h>
#include <stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#define KEY_PRESS(VK)  ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)

//方向
enum DIRECTION
{
    UP = 1,
    DOWN,
    LEFT,
    RIGHT
};

//游戏状态
enum GAME_STATUS
{
    OK,//正常运行
    KILL_BY_WALL,//撞墙
    KILL_BY_SELF,//咬到自己
    END_NOMAL//正常结束
};

#define WALL L'□'
#define BODY L'●'  //★○●◇◆□■
#define FOOD L'★'  //★○●◇◆□■

//蛇的初始位置
#define POS_X 24
#define POS_Y 5

//蛇身节点
typedef struct SnakeNode
{
    int x;
    int y;
    struct SnakeNode* next;
}SnakeNode, * pSnakeNode;

typedef struct Snake
{
    pSnakeNode _pSnake;//维护整条蛇的指针
    pSnakeNode _pFood;//维护食物的指针
    enum DIRECTION _Dir;//蛇头的方向默认是向右
    enum GAME_STATUS _Status;//游戏状态
    int _Socre;//当前获得分数
    int _Add;//默认每个食物10分
    int _SleepTime;//每走一步休眠时间
}Snake, * pSnake;



//游戏开始前的初始化
void GameStart(pSnake ps);

//游戏运行过程
void GameRun(pSnake ps);

//游戏结束
void GameEnd(pSnake ps);

//设置光标的坐标
void SetPos(short x, short y);

//欢迎界面
void WelcomeToGame();

//打印帮助信息
void PrintHelpInfo();

//创建地图
void CreateMap();

//初始化蛇
void InitSnake(pSnake ps);

//创建食物
void CreateFood(pSnake ps);

//暂停响应
void pause();

//下一个节点是食物
int NextIsFood(pSnakeNode psn, pSnake ps);

//吃食物
void EatFood(pSnakeNode psn, pSnake ps);

//不吃食物
void NoFood(pSnakeNode psn, pSnake ps);

//撞墙检测
int KillByWall(pSnake ps);

//撞自身检测
int KillBySelf(pSnake ps);

//蛇的移动
void SnakeMove(pSnake ps);

//游戏初始化
void GameStart(pSnake ps);

//游戏运行
void GameRun(pSnake ps);

//游戏结束
void GameEnd(pSnake ps);

Snack.c文件

#include"Snake.h"

//设置光标的坐标
void SetPos(short x, short y)
{
    COORD pos = { x, y };
    HANDLE hOutput = NULL;
    //获取标准输出的句柄(用来标识不同设备的数值)
    hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    //设置标准输出上光标的位置为pos
    SetConsoleCursorPosition(hOutput, pos);
}

void WelcomeToGame()
{
    SetPos(40, 15);
    printf("欢迎来到贪吃蛇小游戏");
    SetPos(42, 17);
    printf("@小八哥向前冲");
    SetPos(40, 25);//让按任意键继续的出现的位置好看点
    system("pause");
    system("cls");
    SetPos(25, 12);
    printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");
    SetPos(25, 13);
    printf("加速将能得到更高的分数。\n");
    SetPos(40, 25);//让按任意键继续的出现的位置好看点
    system("pause");
    system("cls");
}

void CreateMap()
{
    int i = 0;
    //上(0,0)-(56, 0)
    SetPos(0, 0);
    for (i = 0; i < 58; i += 2)
    {
        wprintf(L"%c", WALL);
    }
    //下(0,26)-(56, 26)
    SetPos(0, 26);
    for (i = 0; i < 58; i += 2)
    {
        wprintf(L"%c", WALL);
    }
    //左
    //x是0,y从1开始增长
    for (i = 1; i < 26; i++)
    {
        SetPos(0, i);
        wprintf(L"%c", WALL);
    }
    //x是56,y从1开始增长
    for (i = 1; i < 26; i++)
    {
        SetPos(56, i);
        wprintf(L"%c", WALL);
    }
}


void InitSnake(pSnake ps)
{
    pSnakeNode cur = NULL;
    int i = 0;
    //创建蛇身节点,并初始化坐标
    //头插法
    for (i = 0; i < 5; i++)
    {
        //创建蛇身的节点
        cur = (pSnakeNode)malloc(sizeof(SnakeNode));
        if (cur == NULL)
        {
            perror("InitSnake()::malloc()");
            return;
        }
        //设置坐标
        cur->next = NULL;
        cur->x = POS_X + i * 2;
        cur->y = POS_Y;

        //头插法
        if (ps->_pSnake == NULL)
        {
            ps->_pSnake = cur;
        }
        else
        {
            cur->next = ps->_pSnake;
            ps->_pSnake = cur;
        }
    }

    //打印蛇的身体
    cur = ps->_pSnake;
    while (cur)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%c", BODY);
        cur = cur->next;
    }

    //初始化贪吃蛇数据
    ps->_SleepTime = 200;
    ps->_Socre = 0;
    ps->_Status = OK;
    ps->_Dir = RIGHT;
    ps->_Add = 10;
}



void CreateFood(pSnake ps)
{
    int x = 0;
    int y = 0;

again:
    //产生的x坐标应该是2的倍数,这样才可能和蛇头坐标对齐。
    do
    {
        x = rand() % 53 + 2;
        y = rand() % 25 + 1;
    } while (x % 2 != 0);

    pSnakeNode cur = ps->_pSnake;//获取指向蛇头的指针
    //食物不能和蛇身冲突
    while (cur)
    {
        if (cur->x == x && cur->y == y)
        {
            goto again;
        }
        cur = cur->next;
    }

    pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); //创建食物
    if (pFood == NULL)
    {
        perror("CreateFood::malloc()");
        return;
    }
    else
    {
        pFood->x = x;
        pFood->y = y;
        SetPos(pFood->x, pFood->y);
        wprintf(L"%c", FOOD);
        ps->_pFood = pFood;
    }
}

void PrintHelpInfo()
{
    //打印提示信息
    SetPos(64, 15);
    printf("不能穿墙,不能咬到自己\n");
    SetPos(64, 16);
    printf("用↑.↓.←.→分别控制蛇的移动.");
    SetPos(64, 17);
    printf("F1 为加速,F2 为减速\n");
    SetPos(64, 18);
    printf("ESC :退出游戏.space:暂停游戏.");
    SetPos(64, 20);
    printf("小八哥向前冲@版权");
}

void pause()//暂停
{
    while (1)
    {
        Sleep(300);
        if (KEY_PRESS(VK_SPACE))
        {
            break;
        }
    }
}

//pSnakeNode psn 是下一个节点的地址
//pSnake ps 维护蛇的指针
int NextIsFood(pSnakeNode psn, pSnake ps)
{
    return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}

//pSnakeNode psn 是下一个节点的地址
//pSnake ps 维护蛇的指针
void EatFood(pSnakeNode psn, pSnake ps)
{
    //头插法
    psn->next = ps->_pSnake;
    ps->_pSnake = psn;
    pSnakeNode cur = ps->_pSnake;
    //打印蛇
    while (cur)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%c", BODY);
        cur = cur->next;
    }
    ps->_Socre += ps->_Add;

    free(ps->_pFood);
    CreateFood(ps);
}

//pSnakeNode psn 是下一个节点的地址
//pSnake ps 维护蛇的指针
void NoFood(pSnakeNode psn, pSnake ps)
{
    //头插法
    psn->next = ps->_pSnake;
    ps->_pSnake = psn;
    pSnakeNode cur = ps->_pSnake;
    //打印蛇
    while (cur->next->next)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%c", BODY);
        cur = cur->next;
    }

    //最后一个位置打印空格,然后释放节点
    SetPos(cur->next->x, cur->next->y);
    printf("  ");
    free(cur->next);
    cur->next = NULL;
}

//pSnake ps 维护蛇的指针
int KillByWall(pSnake ps)
{
    if ((ps->_pSnake->x == 0)
        || (ps->_pSnake->x == 56)
        || (ps->_pSnake->y == 0)
        || (ps->_pSnake->y == 26))
    {
        ps->_Status = KILL_BY_WALL;
        return 1;
    }
    return 0;
}

//pSnake ps 维护蛇的指针
int KillBySelf(pSnake ps)
{
    pSnakeNode cur = ps->_pSnake->next;
    while (cur)
    {
        if ((ps->_pSnake->x == cur->x)
            && (ps->_pSnake->y == cur->y))
        {
            ps->_Status = KILL_BY_SELF;
            return 1;
        }
        cur = cur->next;
    }
    return 0;
}


void SnakeMove(pSnake ps)
{
    //创建下一个节点
    pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
    if (pNextNode == NULL)
    {
        perror("SnakeMove()::malloc()");
        return;
    }
    //确定下一个节点的坐标,下一个节点的坐标根据,蛇头的坐标和方向确定
    switch (ps->_Dir)
    {
    case UP:
    {
        pNextNode->x = ps->_pSnake->x;
        pNextNode->y = ps->_pSnake->y - 1;
    }
    break;
    case DOWN:
    {
        pNextNode->x = ps->_pSnake->x;
        pNextNode->y = ps->_pSnake->y + 1;
    }
    break;
    case LEFT:
    {
        pNextNode->x = ps->_pSnake->x - 2;
        pNextNode->y = ps->_pSnake->y;
    }
    break;
    case RIGHT:
    {
        pNextNode->x = ps->_pSnake->x + 2;
        pNextNode->y = ps->_pSnake->y;
    }
    break;
    }

    //如果下一个位置就是食物
    if (NextIsFood(pNextNode, ps))
    {
        EatFood(pNextNode, ps);
    }
    else//如果没有食物
    {
        NoFood(pNextNode, ps);
    }

    KillByWall(ps);
    KillBySelf(ps);
}



void GameStart(pSnake ps)
{
    //设置控制台窗口的大小,30行,100列
    //mode 为DOS命令
    system("mode con cols=100 lines=30");
    //设置cmd窗口名称
    system("title 贪吃蛇");

    //获取标准输出的句柄(用来标识不同设备的数值)
    HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

    //影藏光标操作
    CONSOLE_CURSOR_INFO CursorInfo;
    GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
    CursorInfo.bVisible = false; //隐藏控制台光标
    SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态

    //打印欢迎界面
    WelcomeToGame();
    //打印地图
    CreateMap();
    //初始化蛇
    InitSnake(ps);
    //创造第一个食物
    CreateFood(ps);
}


void GameRun(pSnake ps)
{
    //打印右侧帮助信息
    PrintHelpInfo();
    do
    {
        SetPos(64, 10);
        printf("得分:%d ", ps->_Socre);
        printf("每个食物得分:%d分", ps->_Add);
        if (KEY_PRESS(VK_UP) && ps->_Dir != DOWN)
        {
            ps->_Dir = UP;
        }
        else if (KEY_PRESS(VK_DOWN) && ps->_Dir != UP)
        {
            ps->_Dir = DOWN;
        }
        else if (KEY_PRESS(VK_LEFT) && ps->_Dir != RIGHT)
        {
            ps->_Dir = LEFT;
        }
        else if (KEY_PRESS(VK_RIGHT) && ps->_Dir != LEFT)
        {
            ps->_Dir = RIGHT;
        }
        else if (KEY_PRESS(VK_SPACE))
        {
            pause();
        }
        else if (KEY_PRESS(VK_ESCAPE))
        {
            ps->_Status = END_NOMAL;
            break;
        }
        else if (KEY_PRESS(VK_F3))
        {
            if (ps->_SleepTime >= 50)
            {
                ps->_SleepTime -= 30;
                ps->_Add += 2;
            }
        }
        else if (KEY_PRESS(VK_F4))
        {
            if (ps->_SleepTime < 350)
            {
                ps->_SleepTime += 30;
                ps->_Add -= 2;
                if (ps->_SleepTime == 350)
                {
                    ps->_Add = 1;
                }
            }
        }
        //蛇每次一定之间要休眠的时间,时间短,蛇移动速度就快
        Sleep(ps->_SleepTime);
        SnakeMove(ps);

    } while (ps->_Status == OK);
}

void GameEnd(pSnake ps)
{
    pSnakeNode cur = ps->_pSnake;
    SetPos(24, 12);
    switch (ps->_Status)
    {
    case END_NOMAL:
        printf("您主动退出游戏\n");
        break;
    case KILL_BY_SELF:
        printf("您撞上自己了 ,游戏结束!\n");
        break;
    case KILL_BY_WALL:
        printf("您撞墙了,游戏结束!\n");
        break;
    }

    //释放蛇身的节点
    while (cur)
    {
        pSnakeNode del = cur;
        cur = cur->next;
        free(del);
    }
}

test.c文件

#include"Snake.h"
#include<locale.h>
void test()
{
    int ch = 0;
    srand((unsigned int)time(NULL));

    do
    {
        Snake snake = { 0 };
        GameStart(&snake);
        GameRun(&snake);
        GameEnd(&snake);
        SetPos(20, 15);
        printf("再来一局吗?(Y/N):");
        ch = getchar();
        getchar();//清理\n

    } while (ch == 'Y');
    SetPos(0, 27);
}

int main()
{
    //修改当前地区为本地模式,为了支持中文宽字符的打印
    setlocale(LC_ALL, "");
    //测试逻辑
    test();
    return 0;
}

好了!今天的贪吃蛇代码想必你看到这里已经恍然大悟了!下一期我们不见不散!

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2024-04-27 13:30:04       19 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-27 13:30:04       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-27 13:30:04       19 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-27 13:30:04       20 阅读

热门阅读

  1. 32个centos常见的命令使用

    2024-04-27 13:30:04       15 阅读
  2. 若依ruoyi-vue中图标使用介绍

    2024-04-27 13:30:04       15 阅读
  3. [Android]Jetpack Compose页面跳转和传值

    2024-04-27 13:30:04       25 阅读
  4. wow-slist文件说明

    2024-04-27 13:30:04       16 阅读
  5. android 上传视频

    2024-04-27 13:30:04       13 阅读
  6. 信息的定义及其分类分级

    2024-04-27 13:30:04       16 阅读
  7. 20个 Golang 常见面试问题

    2024-04-27 13:30:04       10 阅读
  8. 关于深度学习图像数据集的主要问题和考虑事项

    2024-04-27 13:30:04       13 阅读
  9. 并行计算+Linux process

    2024-04-27 13:30:04       13 阅读