目录
游戏背景
游戏展示效果
需要实现的功能
贪吃蛇地图绘制:
蛇吃食物的功能:
蛇的移动控制:
蛇撞墙死亡:
蛇撞自身死亡:
计算得分:
蛇身加速、减速:
暂停游戏:
技术要点
win32 API
控制台程序
控制台坐标 COORD
成员:
编辑
什么是句柄?
示例用法:
GetConsoleCursorInfo
CONSOLE_CURSOR_INFO 结构体:
SetConsoleCursorInfo
示例用法:
SetConsoleCursorPosition
示例用法:
getAsyncKeyState
示例用法:
步骤一:获取控制台窗口句柄
步骤二:获取当前窗口样式
步骤三:移除允许调整大小的样式
步骤四:应用新的窗口样式
贪吃蛇
宽字符
setlocale 函数
宽字符的打印
地图
流程图
main.c(解析全在注释中)
Game_start
Game_run
Game_end
代码
main.c
text.c
text.h
游戏背景
贪吃蛇是久负盛名的游戏,它也和俄罗斯⽅块,扫雷等游戏位列经典游戏的⾏列。 在编程语⾔的教学中,我们以贪吃蛇为例,从设计到代码实现来提升学⽣的编程能⼒和逻辑能⼒。
注意:本篇只适合有C语言基础的朋友
游戏展示效果
需要实现的功能
贪吃蛇地图绘制:
需求:创建一个二维的游戏界面,显示由方格组成的地图,其中蛇和食物的位置清晰可见。实现:可以使用图形编程库(如Python的pygame、JavaScript的canvas等)来绘制固定大小的网格地图,用不同颜色或图案表示空地、蛇身、食物等元素。蛇吃食物的功能:
需求:当蛇头移动到与食物相同位置时,蛇吃掉食物,长度增加一节,并重新在地图上随机生成新的食物。实现:在每帧更新时检查蛇头坐标是否与食物坐标重合,若重合则触发进食逻辑,延长蛇身、更新蛇的总长度,并使用随机数生成算法在地图有效区域内产生新的食物位置。蛇的移动控制:
需求:通过上、下、左、右方向键(或相应触控/手势)改变蛇的移动方向,使其在下一帧按照新方向前进一格。实现:监听用户输入事件,识别方向键按下状态,更新蛇的内部“目标方向”变量。在游戏循环中,根据该变量计算蛇头下一位置,并相应更新整个蛇身的位置。蛇撞墙死亡:
需求:当蛇头移动到地图边界之外时,游戏结束,显示死亡画面或提示。实现:在蛇移动逻辑中加入边界检查,如果蛇头的新位置超出地图范围,则触发游戏结束逻辑,停止游戏循环,展示死亡画面及分数。蛇撞自身死亡:
需求:当蛇头移动到其身体其他部分所在的位置时,游戏结束,显示死亡画面或提示。实现:在蛇移动后更新所有身体位置之前,检查蛇头的新位置是否与已有的身体部分重叠,若重叠则触发游戏结束逻辑,同上。计算得分:
需求:根据蛇吃到的食物数量累计得分,分数实时显示在游戏中。实现:每当蛇吃到食物时,将分数变量加一,并在游戏界面上显示当前分数。可以设计简单的计分规则,如每吃一个食物得一分,或者根据食物类型或连续进食次数设定不同的得分规则。蛇身加速、减速:
需求:提供机制使蛇在一定条件下(如吃到特殊食物、达到特定分数等)加速或减速,改变蛇移动的速度(即每帧前进的格子数)。实现:设置蛇的移动速度变量,初始值为基础速度。当满足加速条件时,提高该速度值;满足减速条件时,降低该速度值。游戏循环中蛇的移动距离应基于当前速度值进行计算。暂停游戏:
需求:提供暂停按钮或快捷键,使得玩家能在游戏进行中暂时停止游戏进程,恢复时能从暂停点继续游戏。实现:添加暂停/继续功能的用户输入响应,如点击按钮或按下指定键。在游戏循环中检测暂停状态,暂停时停止更新游戏状态和渲染,继续时恢复游戏循环。技术要点
C语⾔函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32API等
如果不知道什么是链表的就看:
数据结构:单链表-CSDN博客文章浏览阅读1.6k次,点赞36次,收藏14次。链表是一种基本的数据结构,它用于存储一系列元素(节点),每个节点不仅包含数据元素,还包含一个指向下一个节点的指针。在链表中,数据并非连续地存储在内存中,而是通过每个节点的指针链接起来形成一个逻辑上的线性序列通过前面我们学习的顺序表我们现在延伸一个链表我们会发现顺序表的一些缺点。https://blog.csdn.net/2302_78381559/article/details/137829309?spm=1001.2014.3001.5502
其他的知识都是c语言基础的,我们重点讲解一下会用到的几个win32api的函数
win32 API
Win32 API(Application Programming Interface)是Microsoft Windows操作系统为软件开发人员提供的一个标准编程接口集。它专为32位(及后来的64位兼容)版本的Windows设计,允许程序员编写能在这些操作系统上运行的应用程序,并能够充分利用Windows的各种系统功能。Win32 API本质上是一组预先定义好的函数、结构、常量、消息和宏,它们封装了与操作系统交互的复杂细节。
这是文档链接:
Win32 API 编程参考 - Win32 应用 |Microsoft学习https://learn.microsoft.com/en-us/windows/win32/api/
控制台程序
改变我们的控制台的大小
mode con cols=200 lines:200
mode: 这是一个在命令提示符中内置的命令,用于修改各种系统设置和配置。在此处,它用于更改控制台的模式。
con: 这代表“console”(控制台)设备。它表明您希望修改的是与当前控制台窗口相关的设置。
cols=200: cols 短语表示“columns”(列)。cols=200 设置控制台窗口的宽度,即将其水平方向上的字符列数设定为200个。这意味着您可以在一行内显示最多200个字符。
lines=200: lines 表示“lines”(行)。lines=200 设置控制台窗口的高度,即将其垂直方向上的字符行数设定为200行。这意味着您可以看到总共200行的文本输出。
参考链接mode | Microsoft Learnhttps://learn.microsoft.com/zh-cn/windows-server/administration/windows-commands/mode
也可以通过命令设置控制台窗⼝的名字:
title 贪吃蛇
title中文意思就是标题
参考链接title | Microsoft Learnhttps://learn.microsoft.com/zh-cn/windows-server/administration/windows-commands/title
可以通过这个代码去试一下效果
#include<stdlib.h>int main() {//system(系统)system("mode con cols=50 lines=20");//cols行 lines列 con->console(控制台)system("title 贪吃蛇");//title(标题)system("pause");//pause(停顿)return 0;}
控制台坐标 COORD
COORD 结构 - Windows Console | Microsoft Learnhttps://learn.microsoft.com/zh-cn/windows/console/coord-str结构体是用来表示控制台屏幕缓冲区上一个字符坐标的。它是windows.h
头文件中定义的一个数据结构,主要用于与控制台相关的编程操作,如读写屏幕缓冲区、移动光标位置等。以下是对 COORD
结构体的详细介绍:
typedef struct _COORD { SHORT X; SHORT Y;} COORD;
成员:
SHORT X
: 表示字符在缓冲区中的水平位置(列数)。X轴的正方向是从左到右,因此X
值越大,字符在屏幕上的位置越靠右。
SHORT Y
: 表示字符在缓冲区中的垂直位置(行数)。Y轴的正方向是从上到下,因此Y
值越大,字符在屏幕上的位置越靠下。
坐标系:
如您所述,COORD
结构体使用的坐标系以缓冲区的顶部左侧单元格为原点 (0, 0)
。这意味着:
X = 0
且 Y = 0
时,对应的是缓冲区左上角的第一个单元格。随着 X
值增大,字符位置沿水平方向向右移动。随着 Y
值增大,字符位置沿垂直方向向下移动。 GetStdHandle
GetStdHandle 函数 - Windows Console | Microsoft Learnhttps://learn.microsoft.com/zh-cn/windows/console/getstdhandle
GetStdHandle
是 Windows API 中的一个函数,用于获取与标准输入、标准输出或标准错误设备关联的句柄。这些标准设备是操作系统为进程预定义的输入(通常是键盘)、输出(通常是显示器)和错误输出(也是显示器,但通常与标准输出区分显示)通道。通过获取这些设备的句柄,程序员可以利用其他 Windows API 函数对这些设备进行读写操作或其他相关控制。
函数原型:
HANDLE WINAPI GetStdHandle( _In_ DWORD nStdHandle);
参数:
nStdHandle
: 一个 DWORD
类型的参数,指定要获取的标准设备句柄。可取以下常量值之一:
STD_INPUT_HANDLE
(常量值 -10
):获取标准输入设备(通常为键盘)的句柄。STD_OUTPUT_HANDLE
(常量值 -11
):获取标准输出设备(通常为显示器)的句柄。STD_ERROR_HANDLE
(常量值 -12
):获取标准错误设备(通常为显示器,与标准输出区分显示)的句柄。 返回值:
如果成功,函数返回与指定标准设备关联的有效句柄。如果失败,函数返回INVALID_HANDLE_VALUE
。可通过调用 GetLastError()
函数获取具体的错误代码。 什么是句柄?
句柄(Handle)是计算机科学中的一个概念,它在Windows操作系统环境中被广泛使用。简单来说,句柄是一种抽象的、唯一的标识符,用于指向并操作系统内特定的对象或资源,如文件、窗口、进程、设备等。为了帮助您更好地理解句柄的概念,我们可以用一个生动的比喻来解释:
假设您养了一群鸭子,每只鸭子都有一个独特的脚环,上面刻有唯一编号。这些编号就像鸭子的“句柄”,用来标识和区分每一只鸭子:
唯一标识:每个鸭子脚环上的编号都是独一无二的,就像句柄标识系统内的每一个对象一样。通过这个编号(句柄),您可以快速准确地识别出特定的鸭子(对象)。
操作鸭子:有了鸭子的编号(句柄),您可以对它进行各种操作,比如喂食、清理、记录生长情况等。同样,程序员通过句柄可以对系统对象进行读取、写入、删除、移动、控制属性等各种操作。例如,您可以使用句柄来读取文件内容、移动窗口位置、发送消息给进程等。
隐藏复杂性:鸭子脚环上的编号简化了您对鸭群的管理,您不需要深入了解每只鸭子的具体生理特征或生活习性,只需记住编号就能进行操作。同样,句柄隐藏了操作系统内部对象的复杂实现细节,程序员只需要通过句柄与系统交互,无需关心对象在内存中的具体位置、数据结构等底层信息。
生命周期管理:如果某只鸭子不幸去世或被卖出,它的编号(句柄)就不再有效,无法用来操作任何鸭子。在计算机系统中,当某个对象被删除、关闭或释放后,其对应的句柄也会失效,试图通过无效句柄操作对象会导致错误。因此,程序员需要注意句柄的有效性,适时释放不再使用的句柄,避免资源泄漏。
示例用法:
//STD_INPUT_HANDLE--输入设备//STD_OUTPUT_HANDLE--输出设备//STD_ERROR_HANDLE--错误设备int main(){int main() {//获得句柄HANDLE output = NULL;output = GetStdHandle(STD_OUTPUT_HANDLE);return 0;}
GetConsoleCursorInfo
GetConsoleCursorInfo 函数 - Windows Console | Microsoft Learnhttps://learn.microsoft.com/zh-cn/windows/console/getconsolecursorinfo GetConsoleCursorInfo
是 Windows API 中的一个函数,用于获取与指定控制台屏幕缓冲区关联的光标的可见性状态和大小信息。这个函数对于控制台应用程序而言非常重要,因为它允许程序员查询和可能之后修改控制台光标的行为和外观。以下是 GetConsoleCursorInfo
函数的详细说明:
函数原型:
BOOL WINAPI GetConsoleCursorInfo( _In_ HANDLE hConsoleOutput, _Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo);
参数:
hConsoleOutput
: 一个 HANDLE
类型的参数,指定要查询的控制台屏幕缓冲区的句柄。通常通过 GetStdHandle(STD_OUTPUT_HANDLE)
获取标准输出设备的句柄,或者直接使用创建的控制台屏幕缓冲区句柄。
lpConsoleCursorInfo
: 一个指向 CONSOLE_CURSOR_INFO
结构体的指针。该结构体用于接收光标的属性信息。
CONSOLE_CURSOR_INFO 结构体:
typedef struct _CONSOLE_CURSOR_INFO { DWORD dwSize; // 光标的宽度,以百分比形式表示(1%-100%) BOOL bVisible; // 光标的可见性状态(TRUE/FALSE)} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
返回值:
如果函数成功,返回非零值(TRUE
)。如果函数失败,返回零值(FALSE
)。可以通过调用 GetLastError()
函数获取具体的错误代码。 用途:
使用 GetConsoleCursorInfo
函数,您可以:
SetConsoleCursorInfo
SetConsoleCursorInfo 函数 - Windows Console | Microsoft Learnhttps://learn.microsoft.com/zh-cn/windows/console/setconsolecursorinfo SetConsoleCursorInfo
是 Windows API 中的一个函数,用于设置与指定控制台屏幕缓冲区关联的光标的可见性状态和大小。这个函数与 GetConsoleCursorInfo
相对应,提供了修改控制台光标行为和外观的能力。以下是 SetConsoleCursorInfo
函数的详细说明:
函数原型:
BOOL WINAPI SetConsoleCursorInfo( _In_ HANDLE hConsoleOutput, _In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo);
参数:
hConsoleOutput
: 一个 HANDLE
类型的参数,指定要设置的控制台屏幕缓冲区的句柄。通常通过 GetStdHandle(STD_OUTPUT_HANDLE)
获取标准输出设备的句柄,或者直接使用创建的控制台屏幕缓冲区句柄。
lpConsoleCursorInfo
: 一个指向 CONSOLE_CURSOR_INFO
结构体的指针,该结构体包含要设置的光标属性信息。
示例用法:
int main() {//获得句柄HANDLE output = NULL;output = GetStdHandle(STD_OUTPUT_HANDLE);//光标信息CONSOLE_CURSOR_INFO cursor_info = {0};//获取output句柄相关的控制台信息,存放在cursor_info里面GetConsoleCursorInfo(output, &cursor_info);cursor_info.dwSize = 100;//修改光标的占比50%(总100%)cursor_info.bVisible = FALSE;//修改光标的可见性(TRUE/FALSE)SetConsoleCursorInfo(output, &cursor_info);//设置修改的信息return 0;}
SetConsoleCursorPosition
SetConsoleCursorPosition 函数 - Windows Console | Microsoft Learnhttps://learn.microsoft.com/zh-cn/windows/console/setconsolecursorposition SetConsoleCursorPosition
是 Windows API 中的一个函数,用于设置与指定控制台屏幕缓冲区关联的光标位置。通过调用此函数,程序员可以精确地控制光标在控制台屏幕上的位置,这对于在控制台上绘制图形、定位文本输出等操作至关重要。以下是 SetConsoleCursorPosition
函数的详细说明:
函数原型:
BOOL WINAPI SetConsoleCursorPosition( _In_ HANDLE hConsoleOutput, _In_ COORD dwCursorPosition);
参数:
hConsoleOutput
: 一个 HANDLE
类型的参数,指定要设置光标位置的控制台屏幕缓冲区的句柄。通常通过 GetStdHandle(STD_OUTPUT_HANDLE)
获取标准输出设备的句柄,或者直接使用创建的控制台屏幕缓冲区句柄。
dwCursorPosition
: 一个 COORD
结构体,表示新的光标位置。COORD
结构体包含两个成员:X
表示水平位置(列数),Y
表示垂直位置(行数)。坐标系原点位于缓冲区的左上角,即 (0, 0)
。
返回值:
如果函数成功,返回非零值(TRUE
)。如果函数失败,返回零值(FALSE
)。可以通过调用 GetLastError()
函数获取具体的错误代码。 示例用法:
#include<stdio.h>void set_pos(int x,int y) {//给一个坐标COORD pos = { x,y };//获取句柄HANDLE output = NULL;output = GetStdHandle(STD_OUTPUT_HANDLE);//设置坐标位置SetConsoleCursorPosition(output, pos);//设置控制台光标位置}//改变光标位置int main() {set_pos(5,6);printf("hahah");set_pos(10, 20);printf("hahah");return 0;}
getAsyncKeyState
getAsyncKeyState 函数 (winuser.h) - Win32 apps | Microsoft Learnhttps://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-getasynckeystate GetAsyncKeyState
是 Windows API 提供的一个函数,用于异步检测键盘按键的状态。它允许程序查询某个键在当前或最近一次消息循环中是否被按下,而无需阻塞等待用户的按键事件。这种非阻塞的键盘状态检查对于实时响应用户输入、实现热键功能或其他需要即时了解键盘按键情况的应用场景非常有用。下面是 GetAsyncKeyState
函数的详细说明:
函数原型:
SHORT WINAPI GetAsyncKeyState( _In_ int vKey);
参数:
vKey
: 一个整数值,表示要查询的虚拟键码。虚拟键码是系统用来标识键盘上每个键的标准代码,例如 VK_A
表示字母 'A' 键,VK_SPACE
表示空格键,等等。完整的虚拟键码列表可以在 Windows 头文件 winuser.h
中找到,或者查阅 Microsoft 文档。 返回值:
GetAsyncKeyState
函数返回一个短整型值(SHORT
),其中包含按键状态信息。返回值可以按位分解,具有以下含义:
低16位: 如果该键在当前消息队列中有一个按下消息,或者自从上次调用 GetAsyncKeyState
以来该键被按住,那么最低位(bit 0)被设置为1,表示按键处于按下状态。否则,该位为0,表示按键未被按下。
高16位: 如果该键在当前消息队列中有至少一个按下消息,且该消息尚未被读取,那么高16位被设置为一个非零值。这通常用于区分新按键事件与持续按键状态。对于大多数应用,只需关注低16位即可。
示例用法:
//检测数字按键#include <stdio.h>#include <windows.h>#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?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");}else if (KEY_PRESS(0x36)){printf("6\n");}else if (KEY_PRESS(0x37)){ printf("7\n"); }else if (KEY_PRESS(0x38)){ printf("8\n"); }else if (KEY_PRESS(0x39)){ printf("9\n");}}return 0;}
SetConsoleTextAttribute
SetConsoleTextAttribute 函数 - Windows Console | Microsoft Learnhttps://learn.microsoft.com/zh-cn/windows/console/setconsoletextattribute
SetConsoleTextAttribute
是 Windows API 提供的一个函数,用于设置控制台窗口中输出文本的颜色和背景色。通过调用此函数,程序员可以改变控制台窗口中后续输出文本的显示样式,以增强控制台应用程序的可读性或美观度。以下是 SetConsoleTextAttribute
函数的详细说明:
函数原型:
BOOL WINAPI SetConsoleTextAttribute( _In_ HANDLE hConsoleOutput, _In_ WORD wAttributes);
参数:
hConsoleOutput
: 一个 HANDLE
类型的参数,指定要设置文本属性的控制台屏幕缓冲区的句柄。通常通过 GetStdHandle(STD_OUTPUT_HANDLE)
获取标准输出设备的句柄,或者直接使用创建的控制台屏幕缓冲区句柄。
wAttributes
: 一个 WORD
类型的值,表示要设置的文本颜色和背景色组合。这个值由两个部分组成:低四位表示前景色(文本颜色),高四位表示背景色。每个部分可以是以下预定义的常量之一,这些常量定义在 wincon.h
头文件中:
前景色(文本颜色)常量:
FOREGROUND_BLACK
FOREGROUND_BLUE
FOREGROUND_GREEN
FOREGROUND_RED
FOREGROUND_INTENSITY
(加亮效果,可以与以上颜色常量组合使用)FOREGROUND_YELLOW
(FOREGROUND_RED | FOREGROUND_GREEN
)FOREGROUND_MAGENTA
(FOREGROUND_RED | FOREGROUND_BLUE
)FOREGROUND_CYAN
(FOREGROUND_BLUE | FOREGROUND_GREEN
)FOREGROUND_WHITE
(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE
) 背景色常量:
BACKGROUND_BLACK
BACKGROUND_BLUE
BACKGROUND_GREEN
BACKGROUND_RED
BACKGROUND_INTENSITY
(加亮效果,可以与以上颜色常量组合使用)BACKGROUND_YELLOW
(BACKGROUND_RED | BACKGROUND_GREEN
)BACKGROUND_MAGENTA
(BACKGROUND_RED | BACKGROUND_BLUE
)BACKGROUND_CYAN
(BACKGROUND_BLUE | BACKGROUND_GREEN
)BACKGROUND_WHITE
(BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE
) 返回值:
如果函数成功,返回非零值(TRUE
)。如果函数失败,返回零值(FALSE
)。可以通过调用 GetLastError()
函数获取具体的错误代码。 示例用法:
#include <windows.h>int main() { HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); // 设置前景色为红色,背景色为绿色,无加亮效果 SetConsoleTextAttribute(hConsole, FOREGROUND_RED | BACKGROUND_GREEN); printf("Hello, colored console!\n"); // 恢复默认颜色 SetConsoleTextAttribute(hConsole, FOREGROUND_WHITE | BACKGROUND_BLACK); return 0;}
这边在扩展一个固定控制台的几个api函数
GetConsoleWindow 函数 - Windows Console | Microsoft Learnhttps://learn.microsoft.com/zh-cn/windows/console/getconsolewindow
LONG_PTR windowStyle = GetWindowLongPtr(consoleWindow, GWL_STYLE);// 禁止用户调整窗口大小,移除WS_SIZEBOX、WS_MAXIMIZEBOX、WS_MINIMIZEBOX样式SetWindowLongPtr(consoleWindow, GWL_STYLE, windowStyle & ~(WS_SIZEBOX | WS_MAXIMIZEBOX | WS_MINIMIZEBOX));//移除按钮和边框这样就不能改变大小// 应用新的窗口样式SetWindowPos(consoleWindow, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
步骤一:获取控制台窗口句柄
首先,我们需要获取当前活动的控制台窗口句柄。在Windows API中,每个窗口都由一个唯一的句柄(HWND
)标识。通过调用 GetConsoleWindow()
函数,可以轻松获取与当前控制台关联的窗口句柄。
HWND consoleWindow = GetConsoleWindow();
步骤二:获取当前窗口样式
接下来,我们需要了解当前控制台窗口的样式。Windows窗口样式是一组标志,用于定义窗口的外观和行为。这些样式通过调用 GetWindowLongPtr()
函数并传递 GWL_STYLE
参数来获取。
LONG_PTR windowStyle = GetWindowLongPtr(consoleWindow, GWL_STYLE);
步骤三:移除允许调整大小的样式
要禁止用户调整窗口大小,需要移除与窗口大小调整相关的样式标志。这些标志包括:
WS_SIZEBOX
:表示窗口具有可调整大小的边框。WS_MAXIMIZEBOX
:表示窗口具有最大化按钮。WS_MINIMIZEBOX
:表示窗口具有最小化按钮。 通过按位与(&~
)操作符,我们可以清除这些样式标志,得到一个新的窗口样式值。
LONG_PTR fixedWindowStyle = windowStyle & ~(WS_SIZEBOX | WS_MAXIMIZEBOX | WS_MINIMIZEBOX);
步骤四:应用新的窗口样式
最后,使用 SetWindowLongPtr()
函数将更新后的窗口样式应用到控制台窗口。然后,调用 SetWindowPos()
函数,指定SWP_FRAMECHANGED
标志以强制系统重新绘制窗口框架,从而反映样式更改。
SetWindowLongPtr(consoleWindow, GWL_STYLE, fixedWindowStyle);SetWindowPos(consoleWindow, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
这两行代码分别执行了设置控制台窗口新样式和更新其窗口位置及外观的操作。下面对这两行代码进行详细解析:
SetWindowLongPtr(consoleWindow, GWL_STYLE, fixedWindowStyle);
解析:
consoleWindow
: 这是之前通过 GetConsoleWindow()
函数获取的控制台窗口句柄,表示我们要操作的目标窗口。
GWL_STYLE
: 这是一个常量,用于指定我们要修改的窗口属性类型。在这里,它表示我们要修改的是窗口的样式(style),而非扩展样式(extended style)或其他类型的属性。
fixedWindowStyle
: 这是在前一步计算得出的新窗口样式值,其中已移除了允许用户调整窗口大小的样式标志(WS_SIZEBOX
、WS_MAXIMIZEBOX
和 WS_MINIMIZEBOX
)。将其作为参数传入,意在将控制台窗口的样式更新为不允许用户调整大小的状态。
这行代码的整体作用是,将经过修改的窗口样式值 fixedWindowStyle
应用到 consoleWindow
所指向的控制台窗口上,从而实现禁止窗口大小调整的功能。
SetWindowPos(consoleWindow, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
解析:
consoleWindow
: 同样是控制台窗口的句柄,指示本次操作的目标窗口。
NULL
: 这个参数代表窗口将保持其当前的父窗口或子窗口关系不变。如果需要将窗口置于某个特定窗口之下,这里应传入该父窗口的句柄。
(0, 0, 0, 0)
: 这四个整数分别代表新窗口的位置(X坐标、Y坐标)和尺寸(宽度、高度)。由于我们不希望移动或改变窗口大小,所以这些值都被设为0。实际操作时,Windows会忽略这些值,因为接下来我们将指定特定的标志来控制窗口是否移动或调整大小。
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED
: 这些是组合使用的窗口位置和尺寸标志(flags),用来指定此次 SetWindowPos()
调用的具体行为:
SWP_NOMOVE
: 表示不要移动窗口。即使指定了位置坐标(X, Y),这个标志也会让Windows忽略它们,保持窗口当前位置不变。
SWP_NOSIZE
: 表示不要改变窗口大小。即使指定了尺寸(宽度、高度),这个标志也会让Windows忽略它们,保持窗口当前大小不变。
SWP_NOZORDER
: 表示不要改变窗口在Z轴(堆叠顺序)上的位置,即不要改变窗口相对于其他同级窗口的前后顺序。
SWP_FRAMECHANGED
: 这是关键的标志,它告诉Windows窗口的非客户区(如标题栏、菜单、边框等)已经发生了变化,需要重新绘制。在这里,由于我们刚刚修改了窗口样式以去除调整大小的相关元素,因此需要触发这一重绘操作,使窗口立刻呈现出新的样式效果。
好了我们现在所有的需要用的API函数全部都介绍完了,接下来进入正题
贪吃蛇
宽字符
为什么会出现宽字符嘞?
宽字符的来历:从ASCII到Unicode,C语言中的宽字符处理-CSDN博客宽字符作为Unicode在C语言中的具体实现形式,极大地扩展了编程语言对全球多语种字符的支持能力。虽然在实际应用中还需考虑编码转换、平台差异等问题,但宽字符无疑为构建跨语言、跨文化的软件系统奠定了坚实基础。理解并熟练运用宽字符,是现代C程序员必备的技能之一。https://blog.csdn.net/2302_78381559/article/details/138158952?spm=1001.2014.3001.5502请看这篇博客,看完这篇博客我们需要做的是本地化
#include<locale.h>#include<stdio.h>int main() {char* set;set = setlocale(LC_ALL,"C");//C语言本来的模式printf("%s\n", set);set = setlocale(LC_ALL, "");//本地模式:Chinese (Simplified)_China.936printf("%s", set);return 0;}
setlocale
函数
char *setlocale(int category, const char *locale);
功能:设置或查询程序的本地化类别。category
参数指定要设置或查询的类别(如LC_ALL、LC_CTYPE、LC_NUMERIC等),locale
参数指定新的本地化设置(如"en_US.UTF-8"、"zh_CN.GBK"等)或传入空指针以查询当前设置。
返回值:若成功设置新的本地化,返回指向新设置的字符串指针;若查询当前设置,返回指向当前设置的字符串指针;若失败,返回空指针。
本地化类别常量
LC_ALL:影响所有本地化类别。LC_COLLATE:影响字符排序规则。LC_CTYPE:影响字符分类(如字母、数字、空白等)和转换(如大小写转换)。LC_MONETARY:影响货币格式化。LC_NUMERIC:影响数字、小数点和千位分隔符的格式。LC_TIME:影响日期和时间的格式化。宽字符的打印
int main() {setlocale(LC_ALL, "");//窄字符char a = 'a';char b = 'b';printf("%c %c\n", a, b);//宽字符wchar_t c = L'●';wchar_t d = L'▲';wprintf(L"%lc\n", c);wprintf(L"%lc\n", d);}
地图
数据结构设计
链表结构
//创建蛇身节点类型typedef struct snakeNode {//坐标int x;int y;//指向下一个节点的指针struct snakeNode* next;}snakeNode,*psnakeNode_Hand;
要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇:
/贪吃蛇 typedef struct Snake{psnakeNode_Hand Head;//指向蛇头的指针psnakeNode_Hand food;//指向食物的指针enum Direction dir;//蛇的方向enum Game_State state;//游戏状态int food_scores;//一个食物的分数int sum;//总分int sleep_time;//蛇的速度:休息时间(时间越短,速度越快; 时间越长,速度越慢)}Snake, * psnake;
蛇的方向
//蛇的方向enum Direction{UP = 1,//向上DOWN,//向下LEFT,//向左RIGHT//向右};
游戏状态
//游戏状态enum Game_State{OK,//正常WALL,//撞墙TOUCH_YOURSELF,//触碰到自己END//结束};
流程图
接下来就是我们的手撕代码环节
main.c(解析全在注释中)
#define _CRT_SECURE_NO_WARNINGS 1#include"text.h"void text() {int ch = 0;do{system("cls");//创建贪吃蛇Snake snake = { 0 };//初始化Game_start(&snake);//运行Game_run(&snake);//结束Game_end(&snake);set_pos(20,15);printf("再来一局吗?(Y/N):");ch = getchar();while (getchar() != '\n');//getchar();} while (ch == 'Y' || ch =='y');set_pos(0, 27);}int main() {//适配本地环境setlocale(LC_ALL, "");text();return 0;}
Game_start
//隐藏光标+控制台大小static void set_window() {system("mode con cols=100 lines=30");system("title 贪吃蛇");//获取句柄HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);//定义光标结构体来存储当前句柄的光标信息CONSOLE_CURSOR_INFO cursor_info = { 0 };//获取句柄光标信息GetConsoleCursorInfo(output, &cursor_info);//隐藏光标cursor_info.bVisible = FALSE;SetConsoleCursorInfo(output, &cursor_info);//设置窗口固定大小// 获取当前控制台窗口的句柄HWND consoleWindow = GetConsoleWindow();// 获取当前窗口的样式LONG_PTR windowStyle = GetWindowLongPtr(consoleWindow, GWL_STYLE);// 禁止用户调整窗口大小,移除WS_SIZEBOX、WS_MAXIMIZEBOX、WS_MINIMIZEBOX样式SetWindowLongPtr(consoleWindow, GWL_STYLE, windowStyle & ~(WS_SIZEBOX | WS_MAXIMIZEBOX | WS_MINIMIZEBOX));//移除按钮和边框这样就不能改变大小// 应用新的窗口样式SetWindowPos(consoleWindow, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);}//定位光标void set_pos(int x,int y) {//给一个坐标COORD pos = { x,y };//获取句柄HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);//设置坐标位置SetConsoleCursorPosition(output, pos);//设置控制台光标位置}//欢迎开始界面static void wecometogame() {set_pos(36, 10);wprintf(L"欢迎来到贪吃蛇游戏\n");set_pos(36, 25);system("pause");system("cls");set_pos(40, 10);wprintf(L"游戏介绍\n");set_pos(32, 12);wprintf(L"用↑ ↓ ← →键位控制方向,F3(加速) F4(减速)\n");set_pos(32, 14);wprintf(L"加速可得到更高的分数\n");set_pos(36, 25);system("pause");system("cls");}//修改控制台字符颜色static void Textcolor(WORD wAttributes) {//获取句柄HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);SetConsoleTextAttribute(output, wAttributes);}void Drawmap(){ // 设置文本颜色为红色 Textcolor(FOREGROUND_RED); int i = 0; // 打印顶部墙壁(一行) for (i = 0; i < 29; i++) { // 输出代表墙壁的字符 'WALL_l',假设它是某种墙的图形符号 wprintf(L"%lc", WALL_l); } // 设置光标位置到屏幕左下角(第0列,第26行) set_pos(0, 26); // 打印底部墙壁(一行) for (i = 0; i < 29; i++) { // 同样输出墙壁字符 'WALL_l' wprintf(L"%lc", WALL_l); } // 遍历从第0行至第25行(包含两端),打印左侧墙壁 for (i = 0; i <= 25; i++) { // 设置光标位置到指定行(i)的起始列(0) set_pos(0, i); // 输出墙壁字符 'WALL_l' wprintf(L"%lc", WALL_l); } // 遍历同样的行数(0至25),打印右侧墙壁 for (i = 0; i <= 25; i++) { // 设置光标位置到指定行(i)的末尾列(56) set_pos(56, i); // 输出墙壁字符 'WALL_l' wprintf(L"%lc", WALL_l); }}//创建蛇void Create_snake(psnake ps){ // 初始化当前处理的节点指针为空 psnakeNode_Hand cur = NULL; // 循环3次,为蛇创建3个初始节点(即长度为3的身体) for (int i = 0; i < 3; i++) { // 动态申请内存空间创建一个新的蛇节点 cur = (psnakeNode_Hand)malloc(sizeof(snakeNode)); if (cur == NULL) { // 如果内存分配失败,输出错误信息并退出函数 perror("Create_snake::malloc"); return; } // 设置新节点坐标,根据宽度调整间隔,确保身体不重叠 cur->next = NULL; cur->x = pos_x + i * 2; // 每增加一个节点,横坐标向右移动两格 cur->y = pos_y; // 头插法将新节点添加到蛇链表头部 if (ps->Head == NULL) { // 当链表为空时,将新节点作为链表头 ps->Head = cur; } else { // 当链表非空时,将新节点指向原链表头,然后更新链表头为新节点 cur->next = ps->Head; ps->Head = cur; } } // 设置贪吃蛇的身体部分为蓝色,并打印在控制台上 Textcolor(FOREGROUND_BLUE); cur = ps->Head; while (cur != NULL) { // 根据节点坐标设置光标位置并打印蛇身字符BODY set_pos(cur->x, cur->y); wprintf(L"%c", BODY); // 移动到下一个节点 cur = cur->next; } // 初始化贪吃蛇其他属性 ps->dir = RIGHT; // 初始方向为向右 ps->food_scores = 10; // 每吃到一颗食物得10分 ps->sleep_time = 200; // 初始移动间隔时间 ps->state = OK; // 蛇的状态设为正常 ps->sum = 0; // 初始化得分或者其他累计值为0}//创建食物void Create_food(psnake ps){ int x, y; // 声明随机生成的食物坐标变量retry_generation: // 生成随机坐标,限定在有效范围内,并确保x坐标为偶数 do { x = rand() % 53 + 2; // x坐标范围:2至54,取模保证不会超出边界 y = rand() % 25 + 1; // y坐标范围:1至25 } while (x % 2 != 0); // 确保x坐标是偶数,符合屏幕布局要求 // 遍历蛇的所有节点,检查新生成的坐标是否与蛇体坐标冲突 psnakeNode_Hand cur = ps->Head; while (cur) { if (cur->x == x && cur->y == y) // 发现坐标重合 { goto retry_generation; // 重新生成新的坐标,避免与蛇体重叠 } cur = cur->next; // 移动到下一个节点 } // 分配内存空间创建食物节点 psnakeNode_Hand pfood = (psnakeNode_Hand)malloc(sizeof(snakeNode)); if (pfood == NULL) { // 如果内存分配失败,输出错误信息并结束函数 perror("Create_food::malloc"); return; } else { // 设置食物的颜色为绿色 Textcolor(FOREGROUND_GREEN); // 将随机生成的坐标赋值给食物节点 pfood->x = x; pfood->y = y; // 设置光标位置并在相应坐标上打印食物符号 set_pos(pfood->x, pfood->y); wprintf(L"%lc", FOOD); // 将新生成的食物节点赋值给全局的游戏食物指针 ps->food = pfood; }}//初始化void Game_start(psnake ps) {//时间戳srand((unsigned int)time(NULL));//--隐藏光标+控制台大小--set_window();//--打印开始界面+功能介绍--wecometogame();//--绘制地图--Drawmap();//--创建蛇--//--设置游戏相关信息--Create_snake(ps);//--创建食物--Create_food(ps);}
Game_run
//游戏提示void Game_prompt() {set_pos(75, 7);wprintf(L"%ls", L"《游戏提示》\n");set_pos(64, 10);wprintf(L"%ls", L"<不能穿墙,不能咬到自己>\n");set_pos(64, 12);wprintf(L"%ls", L"<用↑.↓.←.→分别控制蛇的移动>\n");set_pos(64, 14);wprintf(L"%ls", L"<F3 为加速,F4 为减速>\n");set_pos(64, 16);wprintf(L"%ls", L"<ESC :退出游戏.space:暂停游戏>\n");set_pos(80, 18);wprintf(L"%ls",L"--普通小青年\n");}//判断是否遇到食物int Nextfood(psnakeNode_Hand pn, psnake ps) {return (pn->x == ps->food->x && pn->y == ps->food->y);//都满足就返回1,否者返回0}//吃掉食物void Eatfood(psnakeNode_Hand pn, psnake ps) {//头插pn->next = ps->Head;ps->Head = pn;psnakeNode_Hand cur = ps->Head;while (cur){Textcolor(FOREGROUND_BLUE);set_pos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}ps->sum += ps->food_scores;//吃掉一个食物加分free(ps->food);//销毁食物Create_food(ps);//再次生成一个食物}//没有食物void Nofood(psnakeNode_Hand pn, psnake ps) {//psnakeNode_Hand pn -- 下一个节点地址//psnake ps 维护蛇的指针//头插pn->next = ps->Head;ps->Head = pn;psnakeNode_Hand cur = ps->Head;//找到倒数第二个节点地址while (cur->next->next){set_pos(cur->x,cur->y);Textcolor(FOREGROUND_BLUE);wprintf(L"%lc", BODY);cur = cur->next;}Textcolor(FOREGROUND_GREEN);set_pos(cur->next->x,cur->next->y);printf(" ");free(cur->next);cur->next = NULL;}//判断撞墙void KillByWall(psnake ps) {//判断头部是否和我们的墙的坐标一样if ((ps->Head->x == 0) ||(ps->Head->x == 56)||(ps->Head->y == 0)||(ps->Head->y == 26)){//游戏状态ps->state = WALL;return 1;}return 0;}//判断撞到自己void KillBySelf(psnake ps) {//遍历坐标如果有自己子节点的坐标psnakeNode_Hand cur = ps->Head->next;//遍历除了头节点的其他节点所以我们要ps->Head->next,也就是第二个节点开始遍历while (cur){if ((ps->Head->x == cur->x)&&(ps->Head->y == cur->y)) {ps->state = TOUCH_YOURSELF;return 1;}cur = cur->next;}return 0;}void SnakeMove(psnake ps){ // 动态分配内存创建一个新的蛇节点 psnakeNode_Hand pNextNode = (psnakeNode_Hand)malloc(sizeof(snakeNode)); if (pNextNode == NULL) { // 内存分配失败时,输出错误信息并返回 perror("SnakeMove::malloc"); return; } // 根据蛇的移动方向确定下一个节点的坐标 switch (ps->dir) { case UP: pNextNode->x = ps->Head->x; pNextNode->y = ps->Head->y - 1; // 向上移动 break; case DOWN: pNextNode->x = ps->Head->x; pNextNode->y = ps->Head->y + 1; // 向下移动 break; case LEFT: pNextNode->x = ps->Head->x - 2; // 左移两个单位,以适应宽字符 pNextNode->y = ps->Head->y; break; case RIGHT: pNextNode->x = ps->Head->x + 2; // 右移两个单位,以适应宽字符 pNextNode->y = ps->Head->y; break; } // 判断蛇的下一个节点位置是否为食物 if (Nextfood(pNextNode, ps)) // 自定义函数,判断下一个节点是否为食物 { Eatfood(pNextNode, ps); // 自定义函数,处理蛇吃到食物的情况 } else { Nofood(pNextNode, ps); // 自定义函数,处理蛇未吃到食物的情况 } // 判断蛇是否撞到墙壁 KillByWall(ps); // 判断蛇是否撞到自己的身体 KillBySelf(ps);}//游戏暂停void Game_stop() {while (1){if (KEY_PRESS(VK_SPACE)) {break;}Sleep(200);}}void Game_run(psnake ps){ // 显示游戏提示信息 Game_prompt(); // 主循环 do { // 打印当前得分和食物的分值 set_pos(64, 9); printf("总分:%2d ", ps->sum); printf("食物分值:%2d\n", ps->food_scores); // 处理玩家按键操作 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)) { // 暂停游戏 Game_stop(); } else if (KEY_PRESS(VK_ESCAPE)) { // 结束游戏 ps->state = END; } else if (KEY_PRESS(VK_F3)) { // 加速游戏 if (ps->sleep_time >= 80) { ps->sleep_time -= 30; // 减少延迟时间,加快游戏速度 ps->food_scores += 2; // 提高食物分值 } } else if (KEY_PRESS(VK_F4)) { // 减速游戏 if (ps->food_scores > 2) { ps->sleep_time += 30; // 增加延迟时间,减慢游戏速度 ps->food_scores -= 2; // 降低食物分值 } } // 暂停一段时间(根据游戏速度) Sleep(ps->sleep_time); // 蛇的实际移动处理 SnakeMove(ps); } while (ps->state == OK); // 若游戏状态为正常,则继续循环 // 当游戏状态不是OK时,意味着游戏已结束或者暂停}
Game_end
// 定义函数Game_end,参数为指向蛇结构体的指针psvoid Game_end(psnake ps) { // 获取蛇头节点的指针 psnakeNode_Hand cur = ps->Head; // 设置光标位置到屏幕(24行,12列) set_pos(24, 12); // 根据蛇的状态输出相应的结束信息 if (ps->state == END) { printf("你主动退出游戏\n"); } else if (ps->state == WALL) { printf("撞到墙了\n"); } else if (ps->state == TOUCH_YOURSELF) { printf("咬到自己了\n"); } // 遍历并释放蛇身所有节点占用的内存资源 while (cur != NULL) { // 创建临时指针指向当前节点 psnakeNode_Hand del = cur; // 移动当前节点指针至下一个节点 cur = cur->next; // 使用free函数释放del指向的节点内存 free(del); }}
好了给个全部代码
代码
main.c
#define _CRT_SECURE_NO_WARNINGS 1#include"text.h"void text() {int ch = 0;do{system("cls");//创建贪吃蛇Snake snake = { 0 };//初始化Game_start(&snake);//运行Game_run(&snake);//结束Game_end(&snake);set_pos(20,15);printf("再来一局吗?(Y/N):");ch = getchar();while (getchar() != '\n');//getchar();} while (ch == 'Y' || ch =='y');set_pos(0, 27);}int main() {//适配本地环境setlocale(LC_ALL, "");text();return 0;}
text.c
#define _CRT_SECURE_NO_WARNINGS 1#include"text.h"//隐藏光标+控制台大小static void set_window() {system("mode con cols=100 lines=30");system("title 贪吃蛇");//获取句柄HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);//定义光标结构体来存储当前句柄的光标信息CONSOLE_CURSOR_INFO cursor_info = { 0 };//获取句柄光标信息GetConsoleCursorInfo(output, &cursor_info);//隐藏光标cursor_info.bVisible = FALSE;SetConsoleCursorInfo(output, &cursor_info);//设置窗口固定大小// 获取当前控制台窗口的句柄HWND consoleWindow = GetConsoleWindow();// 获取当前窗口的样式LONG_PTR windowStyle = GetWindowLongPtr(consoleWindow, GWL_STYLE);// 禁止用户调整窗口大小,移除WS_SIZEBOX、WS_MAXIMIZEBOX、WS_MINIMIZEBOX样式SetWindowLongPtr(consoleWindow, GWL_STYLE, windowStyle & ~(WS_SIZEBOX | WS_MAXIMIZEBOX | WS_MINIMIZEBOX));//移除按钮和边框这样就不能改变大小// 应用新的窗口样式SetWindowPos(consoleWindow, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);}//定位光标void set_pos(int x,int y) {//给一个坐标COORD pos = { x,y };//获取句柄HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);//设置坐标位置SetConsoleCursorPosition(output, pos);//设置控制台光标位置}//欢迎开始界面static void wecometogame() {set_pos(36, 10);wprintf(L"欢迎来到贪吃蛇游戏\n");set_pos(36, 25);system("pause");system("cls");set_pos(40, 10);wprintf(L"游戏介绍\n");set_pos(32, 12);wprintf(L"用↑ ↓ ← →键位控制方向,F3(加速) F4(减速)\n");set_pos(32, 14);wprintf(L"加速可得到更高的分数\n");set_pos(36, 25);system("pause");system("cls");}//修改控制台字符颜色static void Textcolor(WORD wAttributes) {//获取句柄HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);SetConsoleTextAttribute(output, wAttributes);}//地图打印void Drawmap() {Textcolor(FOREGROUND_RED);int i = 0;for (i = 0 ; i < 29; i++){wprintf(L"%lc", WALL_l);}set_pos(0, 26);for (i =0; i < 29; i++){wprintf(L"%lc", WALL_l);}for ( i = 0; i <= 25; i++){set_pos(0, i);wprintf(L"%lc", WALL_l);}for (i = 0; i <= 25; i++){set_pos(56, i);wprintf(L"%lc", WALL_l);}}//创建蛇void Create_snake(psnake ps) {psnakeNode_Hand cur = NULL;for (int i = 0; i < 3; i++){//申请节点cur = (psnakeNode_Hand)malloc(sizeof(snakeNode));if (cur == NULL){perror("Create_snake::malloc");return;}//设置坐标cur->next = NULL;cur->x = pos_x + i * 2;//*2是移动两格,避免重叠(宽字符)cur->y = pos_y;//利用头插将每个节点串在一起if (ps->Head == NULL){ps->Head = cur;//将节点直接给到head的位置}else{cur->next = ps->Head;ps->Head = cur;}}//打印蛇的身体Textcolor(FOREGROUND_BLUE);cur = ps->Head;while (cur){set_pos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}//初始化贪吃蛇的数据ps->dir = RIGHT;ps->food_scores = 10;ps->sleep_time = 200;ps->state = OK;ps->sum = 0;}//创建食物void Create_food(psnake ps) {int x, y;again://生成随机坐标do{x = rand() % 53 + 2;y = rand() % 25 + 1;} while (x % 2 != 0);//x坐标必须是2的倍数(偶数)//判断x,y的坐标和蛇是否冲突psnakeNode_Hand cur = ps->Head;while (cur){if ((cur->x == x) && (cur->y == y)) {//将他们的坐标比较goto again;//相等直接重新生成坐标}cur = cur->next;//每一个节点}psnakeNode_Hand pfood = (psnakeNode_Hand)malloc(sizeof(snakeNode));if (pfood == NULL){perror("Create_food::malloc");return;}else{//设置食物颜色Textcolor(FOREGROUND_GREEN);//赋值随机生成的坐标pfood->x = x;pfood->y = y;set_pos(pfood->x, pfood->y);wprintf(L"%lc", FOOD);//打印ps->food = pfood;}}//初始化void Game_start(psnake ps) {//时间戳srand((unsigned int)time(NULL));//--隐藏光标+控制台大小--set_window();//--打印开始界面+功能介绍--wecometogame();//--绘制地图--Drawmap();//--创建蛇--//--设置游戏相关信息--Create_snake(ps);//--创建食物--Create_food(ps);}//====================================================//游戏提示void Game_prompt() {set_pos(75, 7);wprintf(L"%ls", L"《游戏提示》\n");set_pos(64, 10);wprintf(L"%ls", L"<不能穿墙,不能咬到自己>\n");set_pos(64, 12);wprintf(L"%ls", L"<用↑.↓.←.→分别控制蛇的移动>\n");set_pos(64, 14);wprintf(L"%ls", L"<F3 为加速,F4 为减速>\n");set_pos(64, 16);wprintf(L"%ls", L"<ESC :退出游戏.space:暂停游戏>\n");set_pos(80, 18);wprintf(L"%ls",L"--普通小青年\n");}//判断是否遇到食物int Nextfood(psnakeNode_Hand pn, psnake ps) {return (pn->x == ps->food->x && pn->y == ps->food->y);//都满足就返回1,否者返回0}//吃掉食物void Eatfood(psnakeNode_Hand pn, psnake ps) {//头插pn->next = ps->Head;ps->Head = pn;psnakeNode_Hand cur = ps->Head;while (cur){Textcolor(FOREGROUND_BLUE);set_pos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}ps->sum += ps->food_scores;//吃掉一个食物加分free(ps->food);//销毁食物Create_food(ps);//再次生成一个食物}//没有食物void Nofood(psnakeNode_Hand pn, psnake ps) {//psnakeNode_Hand pn -- 下一个节点地址//psnake ps 维护蛇的指针//头插pn->next = ps->Head;ps->Head = pn;psnakeNode_Hand cur = ps->Head;//找到倒数第二个节点地址while (cur->next->next){set_pos(cur->x,cur->y);Textcolor(FOREGROUND_BLUE);wprintf(L"%lc", BODY);cur = cur->next;}Textcolor(FOREGROUND_GREEN);set_pos(cur->next->x,cur->next->y);printf(" ");free(cur->next);cur->next = NULL;}//判断撞墙void KillByWall(psnake ps) {//判断头部是否和我们的墙的坐标一样if ((ps->Head->x == 0) ||(ps->Head->x == 56)||(ps->Head->y == 0)||(ps->Head->y == 26)){//游戏状态ps->state = WALL;return 1;}return 0;}//判断撞到自己void KillBySelf(psnake ps) {//遍历坐标如果有自己子节点的坐标psnakeNode_Hand cur = ps->Head->next;//遍历除了头节点的其他节点所以我们要ps->Head->next,也就是第二个节点开始遍历while (cur){if ((ps->Head->x == cur->x)&&(ps->Head->y == cur->y)) {ps->state = TOUCH_YOURSELF;return 1;}cur = cur->next;}return 0;}//蛇的移动void SnakeMove(psnake ps) {//创建下一个位置的节点psnakeNode_Hand pNextNode = (psnakeNode_Hand)malloc(sizeof(snakeNode));if (pNextNode ==NULL){perror("SnakeMove::malloc");return;}//确定下一个节点的坐标switch (ps->dir) {case UP:pNextNode->x = ps->Head->x;pNextNode->y = ps->Head->y - 1;break;case DOWN:pNextNode->x = ps->Head->x;pNextNode->y = ps->Head->y + 1;break;case LEFT:pNextNode->x = ps->Head->x - 2;pNextNode->y = ps->Head->y;break;case RIGHT:pNextNode->x = ps->Head->x + 2;pNextNode->y = ps->Head->y;break;}//判断我们要走的下一个节点是不是食物if (Nextfood(pNextNode,ps))//写一个判断下一个节点是不是食物{Eatfood(pNextNode,ps);}else{Nofood(pNextNode, ps);}//判断撞墙KillByWall(ps); //判断撞到自己KillBySelf(ps);}//游戏暂停void Game_stop() {while (1){if (KEY_PRESS(VK_SPACE)) {break;}Sleep(200);}}void Game_run(psnake ps) {//游戏提示Game_prompt();do{//打印分数和食物分值set_pos(64, 9);printf("总分:%2d ", ps->sum);printf("食物分值:%2d\n", ps->food_scores);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) ){//游戏暂停Game_stop();}else if (KEY_PRESS(VK_ESCAPE) ){//退出游戏ps->state = END;}else if (KEY_PRESS(VK_F3)){//加速if (ps->sleep_time>=80) {ps->sleep_time -= 30;//时间越短数度越快ps->food_scores += 2;}}else if (KEY_PRESS(VK_F4)){//减速if (ps->food_scores>2) {ps->sleep_time += 30;//时间越大数度越慢ps->food_scores -= 2;}}Sleep(ps->sleep_time);//蛇的移动SnakeMove(ps);} while (ps->state == OK);//游戏状态}//结束游戏void Game_end(psnake ps) {psnakeNode_Hand cur = ps->Head;set_pos(24,12);if (ps->state == END) {printf("你主动退出游戏\n");}else if( ps->state == WALL){printf("撞到墙了\n");}else if(ps->state == TOUCH_YOURSELF){printf("咬到自己了\n");}//释放蛇身的节点while (cur){psnakeNode_Hand del = cur;cur = cur->next;free(del);}}
text.h
#pragma once#include<locale.h>#include<stdlib.h>#include<stdbool.h>#include<Windows.h>#include<winuser.h>#include<stdio.h>#include<time.h>#define WALL_l L'墙'//#define BODY L'头'#define BODY L'●'//#define FOOD L'头'#define FOOD L'▲'#define pos_x 24#define pos_y 5#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)//蛇的方向enum Direction{UP = 1,//向上DOWN,//向下LEFT,//向左RIGHT//向右};//游戏状态enum Game_State{OK,//正常WALL,//撞墙TOUCH_YOURSELF,//触碰到自己END//结束};//创建蛇身节点类型typedef struct snakeNode {//坐标int x;int y;//指向下一个节点的指针struct snakeNode* next;}snakeNode,*psnakeNode_Hand;//贪吃蛇 typedef struct Snake{psnakeNode_Hand Head;//指向蛇头的指针psnakeNode_Hand food;//指向食物的指针enum Direction dir;//蛇的方向enum Game_State state;//游戏状态int food_scores;//一个食物的分数int sum;//总分int sleep_time;//蛇的速度:休息时间(时间越短,速度越快; 时间越长,速度越慢)}Snake, * psnake;//初始化void Game_start(psnake ps);//定位(x,y)void set_pos(int x, int y);//隐藏光标+控制台大小static void set_window();//欢迎开始界面static void wecometogame();//地图打印void Drawmap();//创建蛇void Create_snake(psnake ps);//创建食物void Create_food(psnake ps);//==============================================//游戏运行void Game_run(psnake ps);//蛇的移动void SnakeMove(psnake ps);//判断是否遇到食物int Nextfood(psnakeNode_Hand pn, psnake ps);//吃掉食物void Eatfood(psnakeNode_Hand pn, psnake ps);//没有食物void Nofood(psnakeNode_Hand pn,psnake ps);//判断撞墙void KillByWall(psnake ps);//判断撞到自己void KillBySelf(psnake ps);//==============================================//结束游戏void Game_end(psnake ps);