• 一篇文章教会你写一个贪吃蛇小游戏(纯C语言)


    1、游戏展示

    在这里插入图片描述

    2、游戏功能

    实现基本的功能

    • 贪吃蛇地图绘制
    • 蛇吃⻝物的功能(上、下、左、右⽅向键控制蛇的动作)
    • 蛇撞墙死亡
    • 蛇撞⾃⾝死亡
    • 计算得分
    • 蛇⾝加速、减速
    • 暂停游戏

    3、Win32 API

    Win32 API是一套由Microsoft提供的应用程序编程接口,用于开发Windows平台上的应用程序。它包括了丰富的函数、数据结构和消息机制,允许开发者与操作系统进行交互。这些接口覆盖了各个方面,如图形用户界面(GUI)、文件和输入输出、多媒体、网络通信等。通过调用这些API,开发者可以实现窗口创建、消息处理、事件响应、内存管理等功能,从而构建功能完善的Windows应用程序。Win32 API是基于C语言的,但也可以通过其他编程语言进行调用。

    实现贪吃蛇会使⽤到的⼀些Win32API知识 ,下面我们来看看

    3.1 控制台程序

    平常我们运行起来的cmd命令框程序其实就是控制台程序

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

    mode con cols=100 lines=30

    也可以通过命令设置控制台窗口的名字

    title 爱学习的鱼佬
    在这里插入图片描述

    这些能在控制台窗口执行的命令,也可以调用C语言函数system来执行。例如:

    #include
    int main(){
    	system("mode con cols=100 lines=30");
    	system("title 爱学习的鱼佬");
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3.2 控制台屏幕上的坐标COORD

    COORD是Windows API中定义的⼀个结构体,表示⼀个字符在控制台屏幕上的坐标

    typedef struct _COORD {
        SHORT X;
        SHORT Y;
    } COORD, *PCOORD;
    
    • 1
    • 2
    • 3
    • 4

    给坐标赋值:

    COORD pos={10,15};
    
    • 1

    3.3 GetStdHandle函数

    GetStdHandle 函数是用于获取标准输入、标准输出和标准错误输出的句柄(handle)的 Windows API 函数。它通常用于控制台应用程序,允许你访问这些标准流以进行输入和输出操作。

    以下是 GetStdHandle 函数的基本用法:

    #include 
    
    int main() {
        HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
        HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
        HANDLE hStderr = GetStdHandle(STD_ERROR_HANDLE);
    
        // 使用 hStdout, hStdin, 和 hStderr 进行输入、输出和报错操作
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这个示例演示如何获取标准输出、标准输入和标准错误输出的句柄,并将它们存储在 HANDLE 变量中。你可以使用这些句柄来执行与控制台输入和输出相关的操作,例如写入到控制台或从控制台读取数据。

    请注意,GetStdHandle 函数需要包含 头文件,并且通常与其他 Windows 控制台函数一起使用,如 WriteFileReadFile 用于实际的输入和输出操作。

    3.4 GetConsoleCursorInfo函数

    GetConsoleCursorInfo 函数是一个用于获取控制台光标信息的 Windows API 函数。它允许你检索控制台光标的可见性和闪烁属性以及光标的大小。

    以下是 GetConsoleCursorInfo 函数的基本用法:

    #include 
    
    int main() {
        HANDLE hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
        CONSOLE_CURSOR_INFO cursorInfo;
    
        if (GetConsoleCursorInfo(hConsoleOutput, &cursorInfo)) {
            // cursorInfo.dwSize 表示光标的大小
            // cursorInfo.bVisible 表示光标是否可见
            // cursorInfo.dwSize 和 cursorInfo.bVisible 可以用于读取光标的属性
        } else {
            // 处理获取光标信息失败的情况
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在上述示例中,我们首先获取标准输出句柄,并然后使用 GetConsoleCursorInfo 函数来检索控制台光标的信息,包括光标的大小 (dwSize) 和可见性 (bVisible)。获取成功时,你可以读取这些属性,以了解当前光标的状态。

    这个函数通常在控制台应用程序中用于获取和修改光标的属性,例如改变光标的可见性或大小。你还可以使用其他控制台函数来设置新的光标属性,如 SetConsoleCursorInfo 来修改光标的属性。

    3.4.1 CONSOLE_CURSOR_INFO结构体

    CONSOLE_CURSOR_INFO 是一个结构体,用于控制和设置控制台光标的属性。这个结构体通常用于控制光标的大小和可见性。

    在 Windows API 中,CONSOLE_CURSOR_INFO 结构体的定义如下:

    typedef struct _CONSOLE_CURSOR_INFO {
        DWORD dwSize;    // 光标的百分比高度 (1 到 100)
        BOOL bVisible;   // 光标是否可见
    } CONSOLE_CURSOR_INFO;
    
    • 1
    • 2
    • 3
    • 4
    • dwSize 表示光标的大小,以百分比高度的形式表示,1 到 100 之间的值。光标的高度由控制台的字符单元高度和 dwSize 决定。
    • bVisible 表示光标的可见性,TRUE 表示光标可见,FALSE 表示光标不可见。

    这个结构体通常与 GetConsoleCursorInfoSetConsoleCursorInfo 函数一起使用,用于获取和设置控制台光标的属性。通过设置 CONSOLE_CURSOR_INFO 结构体的属性,你可以控制控制台中光标的外观和行为。

    例如,可以使用这个结构体来设置光标的大小和可见性,然后将其传递给 SetConsoleCursorInfo 函数,从而更改控制台光标的属性

    3.5 SetConsoleCursorInfo函数

    SetConsoleCursorInfo 函数是一个用于设置控制台光标信息的 Windows API 函数。它允许你更改控制台光标的可见性、闪烁属性以及光标的大小。

    以下是 SetConsoleCursorInfo 函数的基本用法:

    #include 
    
    int main() {
        HANDLE hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
        CONSOLE_CURSOR_INFO cursorInfo;
    
        cursorInfo.dwSize = 25; // 设置光标的大小,单位是百分之一
        cursorInfo.bVisible = TRUE; // 设置光标可见
    
        if (SetConsoleCursorInfo(hConsoleOutput, &cursorInfo)) {
            // 光标属性设置成功
        } else {
            // 处理设置光标属性失败的情况
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在上述示例中,我们首先获取标准输出句柄,并然后使用 SetConsoleCursorInfo 函数来设置控制台光标的信息,包括光标的大小 (dwSize) 和可见性 (bVisible)。你可以根据需要将这些属性设置为所需的值。

    这个函数通常在控制台应用程序中用于自定义控制台光标的属性,例如更改光标的大小或可见性。通过调用 SetConsoleCursorInfo 函数,你可以实时更改控制台光标的外观。如果需要获取光标信息,可以使用 GetConsoleCursorInfo 函数。

    3.6 SetConsoleCursorPosition函数

    SetConsoleCursorPosition 函数是一个 Windows API 函数,用于设置控制台窗口的光标位置。通过调用这个函数,你可以将光标移动到指定的控制台窗口坐标位置。

    以下是 SetConsoleCursorPosition 函数的基本用法:

    #include 
    
    int main() {
        HANDLE hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
        COORD cursorPosition;
    
        cursorPosition.X = 10; // 设置光标的水平位置
        cursorPosition.Y = 5;  // 设置光标的垂直位置
    
        if (SetConsoleCursorPosition(hConsoleOutput, cursorPosition)) {
            // 光标位置设置成功
        } else {
            // 处理设置光标位置失败的情况
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在上述示例中,我们首先获取标准输出句柄,然后创建一个 COORD 结构体,其中包括水平位置(X)和垂直位置(Y)。接着,我们使用 SetConsoleCursorPosition 函数来将光标移动到指定的控制台窗口坐标位置。

    这个函数通常在控制台应用程序中用于控制光标的移动,以便在控制台上绘制文本或执行其他操作。通过设置光标位置,你可以控制光标在控制台窗口中的位置。

    3.7 GetAsyncKeyState函数

    GetAsyncKeyState 是一个 Windows API 函数,用于检查指定虚拟键码对应的键是否处于按下状态。它可以用来检测键盘上的按键是否被按下,而且不会阻塞程序执行,因此适用于实现基本的键盘输入检测。

    以下是 GetAsyncKeyState 函数的基本用法:

    #include 
    
    int main() {
        // 检查某个键是否被按下,比如检查A键是否被按下
        SHORT keyState = GetAsyncKeyState('A');
    
        // 检查键的状态
        if (keyState & 0x8000) {
            // A键被按下
        } else {
            // A键没有被按下
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在上述示例中,我们使用 GetAsyncKeyState 函数检查键盘上的’A’键是否被按下。函数返回一个 SHORT 类型的值,其中高位表示键的状态,如果键被按下,高位的最低位(最低有效位)将被设置为1,即0x8000。

    这个函数通常用于游戏开发、输入检测以及其他需要实时键盘输入的应用程序,因为它不会阻塞程序,可以在循环中持续检查键的状态。需要注意的是,GetAsyncKeyState 可以检测虚拟键码而不仅限于字符键,因此你可以使用虚拟键码来检查其他键盘上的按键。

    每个按键都有对应的虚拟键码(Virtual-Key Codes),以下是一些常用键的虚拟键码:

    • 左键鼠标按钮: VK_LBUTTON (0x01)
    • 右键鼠标按钮: VK_RBUTTON (0x02)
    • 中键鼠标按钮: VK_MBUTTON (0x04)
    • Backspace键: VK_BACK (0x08)
    • Tab键: VK_TAB (0x09)
    • Enter键: VK_RETURN (0x0D)
    • Shift键: VK_SHIFT (0x10)
    • Ctrl键: VK_CONTROL (0x11)
    • Alt键: VK_MENU (0x12)
    • Pause键: VK_PAUSE (0x13)
    • Caps Lock键: VK_CAPITAL (0x14)
    • Esc键: VK_ESCAPE (0x1B)
    • 空格键: VK_SPACE (0x20)
    • Page Up键: VK_PRIOR (0x21)
    • Page Down键: VK_NEXT (0x22)
    • End键: VK_END (0x23)
    • Home键: VK_HOME (0x24)
    • 左方向键: VK_LEFT (0x25)
    • 上方向键: VK_UP (0x26)
    • 右方向键: VK_RIGHT (0x27)
    • 下方向键: VK_DOWN (0x28)
    • 0键 (主键盘): 0x30
    • A键: 0x41
    • F1键: VK_F1 (0x70)
    • F2键: VK_F2 (0x71)
    • F3键: VK_F3 (0x72)
    • …以此类推,F4至F12键的虚拟键码为0x73至0x7C

    GetAsyncKeyState 的返回值是short类型,在上⼀次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0

    所以我们要判断⼀个键是否被按过,可以检测 GetAsyncKeyState 返回值的最低值是否为1

    #define KEY_PRESS(VK)  ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
    
    • 1

    4、设计贪吃蛇地图

    我们先看设计好的界面,控制台窗口的坐标如下所示,横向的是X轴,从左向右依次增长,纵向是Y轴,从上到下依次增长
    在这里插入图片描述

    在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★
    普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节。
    C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。

    C语言字符默认是采用ASCII编码的,ASCI字符集采用的是单字节编码,且只使用了单字节中的低7位,最高位是没有使用的,可表示为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的e的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了,在希伯来语编码中却代表了字母Gimel(区),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0–127表示的符号是一样的,不一样的只是128--255的这一段

    至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示 256 x 256=65536 个字符

    后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入和宽字符的类型wchar_t 和宽字符的输入和输出函数,加入头文件,其中提供了允许程序员针对特定地区 (通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。

    4.1

    是C语言标准库中的头文件,提供了有关本地化(localization)和本地化相关函数的支持。本地化涉及根据特定地区或文化习惯的需求来格式化文本、日期、时间和数字等。这有助于使程序适应不同的地域文化。

    在C语言中, 提供了设置地区和语言环境的函数,例如 setlocale,允许程序员根据所需的语言环境进行设置。它还提供了函数来处理在不同地区设置中使用的格式化、排序、货币和日期时间信息。

    一些常见的函数和功能包括:

    • setlocale: 设置程序的当前地区和语言环境,允许程序根据特定的语言环境来处理数据。
    • localeconv: 返回一个描述当前地区设置中的数值格式的结构体。
    • strftime: 根据指定的格式化字符串将日期和时间格式化为本地化格式。
    • printfscanf 系列函数中的格式化标志: 例如 %n%Ld 等,可以受到地区设置的影响而改变输出或输入的格式。

    这些函数可以根据地区设置调整货币符号、日期格式、时间格式、数字分隔符等,从而使程序在不同的文化环境下更适用和易懂。

    头文件中,定义了一系列常量用于设置不同的本地化范畴。这些常量用于确定在程序中所需的不同本地化设置。以下是一些常用的常量:

    • LC_ALL: 代表所有的本地化设置。
    • LC_COLLATE: 字符串排序规则的设置。
    • LC_CTYPE: 字符分类和转换规则的设置。
    • LC_MONETARY: 通货格式的设置。
    • LC_NUMERIC: 数值格式的设置。
    • LC_TIME: 时间和日期格式的设置。

    这些常量可以作为 setlocale() 函数的参数,用于设置程序中的不同本地化范畴。例如,setlocale(LC_TIME, "en_US") 用于设置日期和时间格式为美国英语,setlocale(LC_MONETARY, "fr_FR") 用于设置货币格式为法国法语等。

    4.2 setlocale函数

    char *setlocale(int category, const char *locale);
    
    • 1
    • category 是要设置的本地化范畴,可以是诸如 LC_ALLLC_COLLATELC_CTYPELC_MONETARYLC_NUMERICLC_TIME 等预定义常量之一。
    • locale 是一个字符串,表示要设置的本地化环境,比如 “zh_CN” 表示中文的本地化设置。

    示例用法如下:

    #include 
    #include 
    
    int main() {
        setlocale(LC_ALL, "en_US.UTF-8");
    
        // 其他程序逻辑...
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    "zh_CN.UTF-8" 中的 "zh_CN" 是表示中文(中国)的语言代码,"UTF-8" 表示使用 UTF-8 编码

    需要注意的是,setlocale 函数在不同的操作系统或环境下可能有不同的支持程度,有些环境可能并不支持特定的本地化设置。

    C标准给第二个参数仅定义了2种可能取值:"C"和” "

    在任意程序执行开始,都会隐藏式执行调用:

    setlocale(LC_ALL, "C");
    
    • 1

    当地区设置为"C"时,库函数按正常方式执行,小数点是一个点。当程序运行起来后想改变地区,就只能显式调用setlocale函数。用" "作为第2个参数,调用setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。

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

    4.3 宽字符的打印

    在 C 语言中,你可以使用 wchar_t 类型和一些相关的宽字符函数来处理和打印宽字符。

    首先,确保你的编译环境支持宽字符,并且使用宽字符格式的输出函数。

    #include 
    #include 
    #include 
    
    int main() {
        setlocale(LC_ALL, ""); // 设置本地化环境,以支持宽字符
        
        char ch1='a';
        char ch2='b'; //普通字符
    
        wchar_t wideChar1 = L'你'; 
        wchar_t wideChar2 = L'●'; // 使用宽字符
    	
        printf("%c%c\n",ch1,ch2); //输出普通字符
        wprintf(L"%lc\n%lc\n", wideChar1,wideChar2); // 使用wprintf输出宽字符
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • wchar_t 是用于表示宽字符的 C 语言数据类型。
    • wprintf 是用于打印宽字符的函数,L"..." 表示宽字符常量。

    setlocale(LC_ALL, ""); 这行代码设置程序的本地化环境以支持宽字符的处理和显示。使用 wprintfwchar_t 类型可以正确地处理和打印宽字符。

    输出结果
    在这里插入图片描述

    从输出的结果来看,我们发现一个普通字符占一个字符的位置但是打印一个汉字字符或特殊符号,占用2个字符的位置,那么我们如果要在贪吃蛇中使用宽字符,就得处理好地图上坐标的计算。

    4.4 地图坐标及蛇身和食物

    按照我们上面实现的效果,这里的游戏地图区域是58列,27行达到的效果,当然你也可以根据自己的情况进行修改

    在这里插入图片描述

    初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如上面的地图中(20,6)处开始出现蛇,连续5个节点。

    注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半儿出现在墙体中,另外一般在墙外的现象,坐标不好对齐。关于食物,就是在墙体内随机生成一个坐标**(x坐标必须是2的倍数)**,坐标不能和蛇的身体重合,然后打印大。

    5. 数据结构设计

    5.1 蛇节点

    在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行.所以蛇节点结构如下:

    typedef struct SnakeNode
    {
        int x;
        int y;
        struct SnakeNode* next;
    }SnakeNode, * pSnakeNode;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    pSnakeNode: 这个别名是一个指向 SnakeNode 结构体的指针类型,使得你可以更简洁地声明指向 SnakeNode 的指针变量

    5.2 蛇状态结构

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

    5.3 蛇的方向

    enum DIRECTION
    {
        UP = 1,
        DOWN,
        LEFT,
        RIGHT
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    5.4 游戏状态

    enum GAME_STATUS
    {
        OK,//正常运行
        KILL_BY_WALL,//撞墙
        KILL_BY_SELF,//咬到自己
        END_NOMAL//正常结束
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    7. 游戏整体流程

    在这里插入图片描述

    8. 核心逻辑实现

    8.1 游戏主逻辑

    #include "Snake.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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    8.2 游戏开始

    void GameStart(pSnake ps)
    {
        system("mode con cols=100 lines=30");
        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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    1. 设置控制台窗口大小和标题
      • system("mode con cols=100 lines=30"); 用于设置控制台窗口的大小,将其设定为 100 列和 30 行。
      • system("title 贪吃蛇"); 设置了控制台窗口的标题为 “贪吃蛇”。
    2. 隐藏控制台光标
      • 使用 GetStdHandle 获取标准输出的句柄 hOutput,然后通过 GetConsoleCursorInfo 获取控制台光标的信息。
      • 将光标状态设置为不可见:CursorInfo.bVisible = false;
      • 最后通过 SetConsoleCursorInfo 设置控制台光标的状态。
    3. 显示欢迎界面
      • 调用 WelcomeToGame() 函数,这个函数可能打印一些欢迎信息和游戏的介绍。
    4. 打印地图、初始化蛇和创建第一个食物
      • CreateMap() 函数负责绘制游戏地图。
      • InitSnake() 初始化蛇的数据结构和显示蛇的初始状态。
      • CreateFood() 创建游戏中的第一个食物。

    8.2.0 SetPos函数的封装

    void SetPos(short x, short y)
    {
        COORD pos = { x, y };
        HANDLE hOutput = NULL;
    
        hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    
        SetConsoleCursorPosition(hOutput, pos);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1. 定义函数 SetPos:
      • void SetPos(short x, short y):指定了该函数接受两个参数,分别是 x 和 y 坐标。
    2. 设置光标位置
      • COORD pos = { x, y };:创建了一个 COORD 类型的结构体变量 pos,用给定的 x 和 y 坐标值初始化。
      • HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);:获取标准输出的句柄,用来标识控制台窗口。
      • SetConsoleCursorPosition(hOutput, pos);:使用 SetConsoleCursorPosition 函数将控制台光标的位置设置为 pos 所指定的坐标。

    8.2.1 欢迎界面和提示

    void WelcomeToGame()
    {
        SetPos(40, 15);
        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");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    1. 设置光标位置并打印欢迎信息
      • SetPos(40, 15); printf("欢迎来到贪吃蛇小游戏"); 将光标移动到 (40, 15) 的位置,打印欢迎信息 “欢迎来到贪吃蛇小游戏”。
    2. 等待用户按下任意键继续
      • SetPos(40, 25); 设置光标位置,这是为了让下一次的 system("pause"); 显示在一个美观的位置。
      • system("pause"); 暂停程序执行,等待用户按下任意键继续。
    3. 清屏
      • system("cls"); 清空控制台屏幕。
    4. 打印游戏操作说明
      • SetPos(25, 12); printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n"); 在指定位置打印游戏操作说明,告诉用户如何控制蛇的移动以及使用 F3 和 F4 来加速或减速。
      • SetPos(25, 13); printf("加速将能得到更高的分数。\n"); 给出一些游戏策略建议。
    5. 再次等待用户按下任意键继续
      • SetPos(40, 25); 再次设置光标位置,保证下一次的 system("pause"); 显示在一个合适的位置。
      • system("pause"); 再次暂停程序执行,等待用户按下任意键继续。

    这个函数的目的是为了向玩家展示游戏的欢迎信息和操作说明,在游戏开始前给玩家提供一些基本的操作提示和规则。

    欢迎界面

    在这里插入图片描述

    提示界面

    在这里插入图片描述

    8.2.2 创建地图

    创建地图就是将墙打印出来,因为是宽字符打印,所有使用wprintf函数,打印格式串前使用L打印地图的关键是要算好坐标,才能在想要的位置打印墙体。

    墙体打印的宽字符:

    #define WALL L'□'
    
    • 1

    创建地图

    void CreateMap()
    {
        int i = 0;
    
        SetPos(0, 0);
        for (i = 0; i < 58; i += 2)
        {
            wprintf(L"%c", WALL);
        }
    
        SetPos(0, 26);
        for (i = 0; i < 58; i += 2)
        {
            wprintf(L"%c", WALL);
        }
    
    
        for (i = 1; i < 26; i++)
        {
            SetPos(0, i);
            wprintf(L"%c", WALL);
        }
    
        for (i = 1; i < 26; i++)
        {
            SetPos(56, i);
            wprintf(L"%c", WALL);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    绘制游戏地图

    • SetPos(0, 0); 将光标移动到 (0, 0) 的位置开始绘制游戏地图。
    • 通过 for 循环和 wprintf 函数,从 (0,0) 到 (56,0) 绘制顶部墙体,由 WALL 符号构成。
    • 从 (0,26) 到 (56,26) 绘制底部墙体。
    • 从 (0,1) 到 (0,25) 绘制左侧墙体。
    • 从 (56,1) 到 (56,25) 绘制右侧墙体。

    8.2.3 初始化蛇身

    蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标
    创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,将蛇的每一节打印在屏幕上

    蛇身打印的宽字符

    #define BODY L'●'
    
    • 1

    初始化蛇身函数

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    创建贪吃蛇身体节点

    • 使用 for 循环创建贪吃蛇的五个身体段。
    • 每个段都是使用 malloc 动态分配的。
    • 为每个段设置坐标,创建了贪吃蛇身体的水平线。

    打印贪吃蛇身体

    • 创建段之后,代码通过遍历贪吃蛇的链表,将光标移动到每个段的坐标并使用 wprintf 打印贪吃蛇的身体,使用宏 BODY 符号。

    初始化游戏变量

    • 最后,函数初始化各种游戏相关变量,如休眠时间、得分、方向和额外得分规则。

    8.2.4 创建食物

    注意

    x坐标必须是2的倍数
    食物的坐标不能和蛇身每个节点的坐标重复

    食物打印的宽字符

    #define FOOD L'★'
    
    • 1

    创建食物函数

    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
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    生成食物位置

    • 使用 do-while 循环生成食物的 x 和 y 坐标。
    • x 坐标被限制为偶数,以便与贪吃蛇的头部坐标对齐。

    避免食物与蛇身冲突

    • 代码通过检查新生成的食物位置,确保它不会与贪吃蛇的身体重叠。
    • 如果新的食物坐标与蛇身重叠,代码会跳回 again 标签处重新生成食物位置。

    创建食物节点

    • 如果食物的位置是有效的,就会动态分配一个新的节点来表示食物。
    • 食物节点的坐标被设置为新的 x 和 y 坐标,然后在控制台上的该位置打印食物的符号,并将食物节点指针保存在游戏状态中。

    8.3 游戏运行

    游戏运行期间,右侧打印帮助信息,提示玩家

    根据游戏状态检查游戏是否继续,如果是状态是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);
    
        } while (ps->_Status == OK);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    这段代码实现了游戏的运行逻辑,在一个循环中持续地更新游戏状态并响应玩家的输入。

    1. 打印帮助信息
      • PrintHelpInfo() 被调用来显示游戏帮助信息。
    2. 游戏循环
      • 游戏的主要逻辑被包含在一个 do-while 循环中。
      • 检查玩家的按键输入,根据按键不同做出相应的动作。
      • 根据按键状态来改变蛇头的移动方向。
      • 如果按下空格键,游戏会暂停。
      • 如果按下 ESC 键,游戏状态被设为正常结束(END_NOMAL),并跳出循环。
    3. 加速和减速
      • 如果按下 F3,游戏中蛇的移动速度会加快,得分增加。
      • 如果按下 F4,游戏中蛇的移动速度会减慢,得分减少。
    4. 蛇的移动
      • 游戏根据当前设置的移动速度来控制蛇的移动。
      • 使用 Sleep() 函数来控制每次蛇移动之间的时间间隔。
      • 调用 SnakeMove() 函数来处理蛇的移动逻辑。
    5. 循环终止
      • 游戏循环会持续进行,直到游戏状态变为非正常状态(不是 OK)为止。

    8.3.1 帮助信息

    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("爱学习的鱼佬");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述

    8.3.2 蛇身移动

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    1. 创建下一个节点:利用 malloc() 分配内存来创建一个新的蛇身节点 pNextNode
    2. 根据方向确定下一个节点的坐标:根据蛇头当前的方向,计算出蛇头下一个移动的位置。
    3. 检查下一个位置是否是食物
      • 如果下一个位置是食物,调用 EatFood(pNextNode, ps) 函数来处理吃食物的逻辑。
      • 如果不是食物,调用 NoFood(pNextNode, ps) 函数来处理不吃食物的逻辑。
    4. 检查游戏结束的条件
      • 调用 KillByWall(ps)KillBySelf(ps) 函数来检查是否撞墙或者咬到自己,如果游戏结束,则会设定相应的游戏状态。

    8.3.3 吃食物的三种情况

    int NextIsFood(pSnakeNode psn, pSnake ps)
    {
        return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
    }
    
    
    
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    1. NextIsFood:该函数检查下一个位置是否有食物。如果坐标与食物位置匹配,返回真;否则返回假。
    2. EatFood:当下一个位置有食物时调用此函数。它采用链表方法,在蛇的开头插入表示下一个位置的新节点,从而延长蛇的身体。它更新蛇的分数,移除被吃掉的食物(通过释放内存),然后创建新的食物以继续游戏。
    3. NoFood:当下一个位置没有食物时调用此函数。它也采用链表方法,在蛇的开头插入表示下一个位置的新节点。然而,它通过移除最后一个节点来管理蛇尾,模拟蛇的移动。然后释放最后一个节点的内存,并设置适当的指针。

    8.3.4 G掉的2种情况

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    1. KillByWall 检查蛇头是否碰到了游戏地图的边界。如果蛇头在地图的边缘,函数将设置蛇的状态为 KILL_BY_WALL 并返回 1(表示蛇碰到了墙壁),否则返回 0。
    2. KillBySelf 检查蛇头是否碰到了自己的身体。它遍历了蛇身链表(除了头节点),检查蛇头的坐标是否与任何其他节点的坐标相匹配。如果发生了碰撞,函数将设置蛇的状态为 KILL_BY_SELF 并返回 1(表示蛇碰到了自己的身体),否则返回 0。

    8.3.5 暂停函数

    void pause()
    {
        while (1)
        {
            Sleep(300);
            if (KEY_PRESS(VK_SPACE))
            {
                break;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    它会在游戏执行时进行循环,每 300 毫秒检查一次是否按下空格键。当检测到空格键按下时,循环结束,游戏继续执行

    8.4 游戏结束

    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    根据游戏状态在屏幕上显示相应的消息,例如玩家主动退出游戏、蛇撞到自己或撞到墙壁导致游戏结束。此后,它会释放蛇身的节点,并清理分配的内存

  • 相关阅读:
    LeetCode ——160. 相交链表,142. 环形链表 II
    Vue常见指令补充(附加案例)
    【Zotero6】插件Zotcard自定义笔记模板流程分享
    JavaScript大作业:基于HTML实现紫色化妆品包装设计公司企业网站
    在线等!!!新版考纲在国内通过率有多少?
    超级强大的菜鸟工具库
    Spring Security(五)密码加密与更新密码
    第十九章绘图
    基于Java+SpringBoot+Thymeleaf+Mysql校园运动场地预约系统设计与实现
    2022最新版Redis入门到精通(云课堂视频学习笔记)
  • 原文地址:https://blog.csdn.net/kingxzq/article/details/134340672