一.基本信息
实现目标:使用C语言在Windows环境的控制台中实现贪吃蛇游戏
游戏运行:
- 地图绘制
- 基本玩法
- 提示信息
- 游戏的开始与结束
基本玩法:
- 通过上下左右键控制蛇的移动
- 蛇可以加速减速
- 吃掉食物可以得分并增加蛇的长度
- 可以自动暂停
游戏结束:
- 撞墙
- 撞到自己
- 主动选择退出
涉及到的知识点:
C语言函数,枚举,结构体,单链表,动态内存管理,预处理指令,Win32API等
Windows控制面板的设置(VS2022):




效果:

(ps:system函数可以用来执行系统命令,头文件<stdlib.h> )
回想一下贪吃蛇的游戏面板,其中蛇应该出现在界面中央,而食物的位置是随机的,这就涉及以下几个问题
1.屏幕光标的坐标设置。
2.如何去操控光标呢?又如何去设置光标的坐标呢?
3.游戏进行过程中并不会看到光标,也就是说,要隐藏光标
4.如果通过按键的输入来控制蛇的移动
至于如何解决这些问题,就要用到Win32API的知识了,下面我就来简单介绍一下。
二.Win32API:
WIN32 API简介:
Windows 这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外, 它同时也是⼀个很⼤的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程序达到开启视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application), 所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows32位平台的应⽤程序编程接⼝。
控制台上的屏幕坐标COORD
COORD 是Windows API中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0) 的原点位于缓冲区的顶部左侧单元格。
COORD原型说明:
typedef struct _COORD
{
SHORT X;
SHORT Y;
}COORD;
COORD pos ={10,15};
2.WIN32API函数
2.1.GetStdHandle函数
作用:
GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。(句柄类似于玩游戏时操控的手柄)
函数原型声明:
HANDLE GetStdHandle(DWORD nStdHandle);
参数:

对于我们今天要实现的贪吃蛇代码来说,只需用到第二个参数就可以了
返回值:
如果该函数成功,则返回值为指定设备的句柄,(或为由先前对 SetStdHandle 的调用设置的重定向句柄。)
如果函数失败,则返回值为 INVALID_HANDLE_VALUE。
实例:
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输入的句柄
2.2. CONSOLE_CURSOR_INFO
这是windows自带的一个结构体(可以直接拿来用),包含了控制台光标的信息
原型声明:
typedef struct _CONSOLE_CURSOR_INFO
{DWORD dwSize;BOOL bVisible;
} CONSOLE_CURSOR_INFO;
• dwSize,由光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
• bVisible,游标的可见性。 如果光标可见,则此成员为 TRUE。
实例:
CONSOLE_CURSOR_INFO CursorInfo;CursorInfo.bVisible = false;
2.3 GetConsoleCursorInfo 函数
作用:
检索有关指定控制台屏幕缓冲区的游标大小和可见性的信息。
语法:
BOOL WINAPI GetConsoleCursorInfo(HANDLE hConsoleOutput,PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
参数含义:
hConsoleOutput
控制台屏幕缓冲区的句柄。lpConsoleCursorInfo
指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关控制台游标的信息
返回值:
如果该函数成功,则返回值为非零值。
如果函数失败,则返回值为零。
实例:
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输入的句柄CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(houtput, &CursorInfo);//检索有关制定控制台屏幕缓冲区的光标大小和可见性信息
2.4 SetConsoleCursorInfo 函数
作用:
设置制定控制台光标大小和可见性
BOOL WINAPI SetConsoleCursorInfo(HANDLE hConsoleOutput,const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
参数
hConsoleOutput
控制台屏幕缓冲区的句柄。lpConsoleCursorInfo [in]
指向 结构的指针CONSOLE_CURSOR_INFO,该结构为控制台屏幕缓冲区的光标提供新的规范。
返回值
如果该函数成功,则返回值为非零值。
如果函数失败,则返回值为零。
实例:
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输入的句柄CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(houtput, &CursorInfo);//检索有关制定控制台屏幕缓冲区的光标大小和可见性信息CursorInfo.bVisible = false;SetConsoleCursorInfo(houtput,&CursorInfo);//设置控制台光标的状态
2.5 SetConsoleCursorPosition 函数
作用:
设置指定控制台屏幕缓冲区中的光标位置。
语法:
BOOL WINAPI SetConsoleCursorPosition(HANDLE hConsoleOutput,COORD dwCursorPosition
);
参数含义:
hConsoleOutput
控制台屏幕缓冲区的句柄。dwCursorPosition
指定新光标位置(以字符为单位)的 COORD 结构。 坐标是屏幕缓冲区字符单元的列和行。 坐标必须位于控制台屏幕缓冲区的边界以内。
实例:
COORD pos = { 10,15 };HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输入的句柄SetConsoleCursorPosition(houtput, pos);//设置光标位置
2.6 GetAsyncKeyState 函数
作用:
确定调用函数时键是向上还是向下,以及上次调用 GetAsyncKeyState 后是否按下了该键。
语法:
SHORT GetAsyncKeyState(int vKey
);
参数:
vkey:虚拟键码
以下是需要用到的虚拟键码
VK_ESCAPE0x1B ESC 键 VK_SPACE0x20 空格键 VK_LEFT0x25 LEFT ARROW 键 VK_UP0x26 UP ARROW 键 VK_RIGHT0x27 RIGHT ARROW 键 VK_DOWN0x28 DOWN ARROW 键
VK_F30x72 F3 键 VK_F40x73 F4 键
返回值:
GetAsyncKeyState 的返回值是short类型,在上一次调用函数GetAsyncKeyState 后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位位置为1则说明,该按键被按过,否则为0。如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1.
总结:
GetStdHandle 函数——获得设备的控制手柄
GetConsoleCursorInfo 函数——获得光标信息
SetConsoleCursorInfo 函数——隐藏光标
SetConsoleCursorPosition 函数——设置光标位置
GetAsyncKeyState 函数 ——检测按键情况
这些函数本质上和库函数无甚区别,知道这些函数的返回值类型和参数后,就可以直接使用了。
三.贪吃蛇游戏的设计与分析
1.布置地图
1.1 控制台窗口的坐标:
横向为x轴,从左到右依次增长,纵向为y轴,从上到下,依次增长
坐标原点(0,0)在最左上角
x轴和y轴的单位长度不同,y轴的单位长度要大于x轴
1.2 地图元素:
墙体:□
蛇身:●
食物:★
以上字符,我采用宽字符的方式打印
普通字符占一个字节,宽字符占两个字节,汉字就是宽字符
1.3.宽字符的打印
背景介绍: C语言在不断适应国际化的过程中,加入了宽字符的类型wchar_t 和宽字符的输入和输出函数,加入了<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
1.3.1setlocale函数
C语言的标准输出是英文模式,即所有字符都只占一个字节,这显然无法满足所有国家(例如中国)的输出要求,因为光汉字数量就多打10万个,更别提字符了,所以C语言提供了本地模式。
setlocale函数用于修改当前地区,可以针对一个内容修改,也可针对不同内容修改。
原型声明:
char* setlocale (int category, const char* locale);
setlocale 的第一个参数如果是LC_ALL,就会影响所有的内容。C标准给第二个参数仅定义了2种可能取值:"C"(正常模式)和" "(本地模式)。在任意程序执行开始,都会隐藏式执行调用:
setlocale(LC_ALL, "C");//标准模式
参数:
category
Portion of the locale affected. It is one of the following constant values defined as macros in <clocale>:
value Portion of the locale affected LC_ALL The entire locale. LC_COLLATE Affects the behavior of strcoll and strxfrm. LC_CTYPE Affects character handling functions (all functions of <cctype>, except isdigit and isxdigit), and the multibyte and wide character functions. LC_MONETARY Affects monetary formatting information returned by localeconv. LC_NUMERIC Affects the decimal-point character in formatted input/output operations and string formatting functions, as well as non-monetary information returned by localeconv. LC_TIME Affects the behavior of strftime. locale
C string containing the name of a C locale. These are system specific, but at least the two following locales must exist:
locale name description "C"Minimal "C" locale ""Environment's default locale If the value of this parameter is
NULL, the function does not make any changes to the current locale, but the name of the current locale is still returned by the function.
而我们只需用到 LC_ALL和""
实例:
setlocale(LC_ALL, "");//切换到本地环境
1.3.2.打印函数
宽字符的字面量必须加上前缀“L”,否则 C 语言会把字面量当作窄字符类型处理。前缀“L”在单引号前面,表示宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前面,表示宽字符串,对应wprintf() 的占位符为 %ls
演示:
#include<stdio.h>
#include<locale.h>
int main()
{setlocale(LC_ALL, "");//切换到本地环境wchar_t ch1 = L'□';wchar_t ch2 = L'吃';wprintf(L"%lc\n", ch1);printf("%c\n", 'a');wprintf(L"%lc\n", ch2);return 0;
}
效果:

普通字符和宽字符的打印宽度:

1.4.地图坐标
实现一个27行(y轴),58列(x轴)的棋盘(读者可以自行修改)
设置光标位置
//设置光标位置
void SetPos(short x, short y)
{COORD pos = { x,y };HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);SetConsoleCursorPosition(houtput, pos);
}
墙体坐标:
y = 0 , x: 0~56
y = 26, x: 0~56
x = 0 , y: 1~26
x = 56, y: 1~26
代码逻辑:
打印墙体的两种情况:
横行:先设置光标位置,再通过循环依次打印剩下的墙体
纵列:在循环体里设置光标位置,再打印墙体
函数代码:
#define WALL L'□'
//创建地图
void CreatMap()
{setlocale(LC_ALL, "");//设置为本地模式int i = 0;//一个墙体占两个字节,//y = 0 , x: 0~56SetPos(0, 0);for (i = 0; i < 58; i += 2)//一个墙体占两个字节{wprintf(L"%lc", WALL);}//y = 26, x: 0~56SetPos(0,26);for (i = 0; i < 58; i += 2)//x轴的一个单位值是一个字节{wprintf(L"%lc", WALL);}//x = 0 , y: 1~26for (i = 1; i < 26; i++)//y轴的一个单位值是两个字节{SetPos(0, i);wprintf(L"%lc", WALL);}//x = 56, y: 1~26for (i = 1; i < 26; i++){SetPos(56, i);wprintf(L"%lc", WALL);}
}
但是我们测试代码时会出现这种情况:

最下面一行的墙体被遮挡了,那时因为循环结束后,光标在(0,26)这个位置
在代码末尾手动将光标坐标置为(27,0)就可以了

2.数据结构体设计
2.1蛇身和食物链表
结构体中应包含蛇的位置坐标和指向下一个位置的指针
代码:
//贪吃蛇蛇身的节点
typedef struct SnakeNode
{int x;//x轴坐标int y;//y轴坐标struct SnakeNode* next;//指向下一个节点的指针
}SnakeNode;
2.2贪吃蛇游戏的结构体
回想一下贪吃蛇游戏,有哪些元素呢?
蛇,食物,食物的分数,蛇走的方向,当下的得分,蛇的速度,游戏状态(死亡or正常),食物的分值
那结构体的元素应包含:
1.指向蛇和食物的指针
2.蛇头的方向:上下左右,可以用枚举体
3.当下的得分
4.游戏状态,也可以用枚举
5.蛇的速度
6.食物的分值
这里就出现了一个问题了,蛇的速度如何设置呢?这里介绍一个方法,通过调节程序休眠的时间(即蛇每走一步,程序就暂停一段时间)间隔,来控制速度
代码:
//蛇头移动的方向
enum DIRECTION
{UP,//上DOWN,//下LEFT,//左RIGHT//右
};
//游戏状态
enum GAME_STATUS
{NORMAL,//正常KILL_BY_WALL,//撞墙KILL_BY_SELF,//撞到自己了END_NORMAL//主动退出
};
//贪吃蛇
typedef struct Snake
{SnakeNode* psnake;//指向蛇的指针SnakeNode* pfood;//指向食物的指针enum DIRECTION dir;//蛇头走的方向enum GAME_STATUS status;//当前游戏的状态int food_score;//食物的分数int score;//当下的得分int sleep_time;//程序休眠时间
}Snake;
3.游戏流程设计
3.1.游戏开始
3.1.1设置窗口大小和名字
3.1.2隐藏光标
3.1.2创建欢迎界面
3.1.3创建地图,蛇,食物
3.2 游戏运行
3.2.1打印提示信息
3.2.2蛇的移动和速度
3.2.3食物的分值(蛇速度越快,食物分值越高)
3.2.4计算当下的得分
3.2.5判断当下的游戏状态(蛇是否死亡)
3.2.6蛇是否吃到食物了(吃到食物,蛇身加长)
3.2.7通过按键情况移动蛇身(蛇头移动方向的下一个位置是蛇身的新节点,尾结点删除)
3.3游戏结束
3.3.1给出结束原因
3.3.2销毁蛇身
3.3.3给出提示信息,要不要继续玩
四.游戏主逻辑的实现
1.游戏开始GameStart:完成游戏的初始化
2.游戏进行GameRun:完成游戏逻辑的实现
3.游戏结束GameEnd:销毁蛇身,释放内存
1.GameStart
1.1初始化游戏数据
1.1.1创建蛇身链表
创建蛇身节点,(蛇的坐标默认是不变的,大概位于控制台控制台屏幕中央)
1.1.2设置贪吃蛇游戏的数据(即给Snake结构体的各个变量赋值)
蛇头方向,游戏状态,当下的得分,食物的分值,以及默认程序休眠时间
1.1.3食物链表
由于食物的创建涉及到创建节点和设置随机坐标两方面,所以将其单独封装成一个函数
设置坐标,申请节点,打印食物
食物的坐标可以完全随机吗?显然不是,它是有一定限制的
食物坐标的位置是有范围限定的
1.不可与蛇身重叠
2.不能在墙体之外
3.食物的x轴坐标必须为2的倍数
为什么呢?
蛇身占两个字节,它往前挪动一步也是两个字节。
第二个问题就来了,为什么不能一个字节一个字节的移动呢?
因为蛇身的下一个位置,就是新的链表节点所在,所以食物的坐标x必须为2的倍数,这样蛇才能吃到食物
代码
#define BODY L'●'
#define FOOD L'★'#define POS_X 24
#define POS_Y 5//初始化贪吃蛇数据
void InitSnake(Snake* ps)
{setlocale(LC_ALL, "");//本地化//初始化蛇身int i = 0;SnakeNode* pcur = NULL;for(i = 0;i<5;i++){pcur = (SnakeNode*)malloc(sizeof(SnakeNode));//申请内存空间if (pcur == NULL){perror("InitSnake():malloc");//打印错误信息exit(1);//非正常退出}//设置蛇的位置坐标pcur->x = POS_X+i*2;//一个蛇身占两个字节pcur->y = POS_Y;pcur->next = NULL; if (ps->psnake == NULL)//蛇身为空{ps->psnake = pcur;}else//头插法{pcur->next = ps->psnake;ps->psnake = pcur;}}pcur = ps->psnake;//pcur指向蛇头//打印蛇身while (pcur){//设置光标位置SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}//初始化其他数据ps->dir = RIGHT;//初始状态向右走ps->food_score = 10;//食物的分值为10ps->score = 0;ps->sleep_time = 200;//单位是毫秒ps->status = NORMAL;
}
//创建食物
void CreatFood(Snake* ps)
{setlocale(LC_ALL, "");//1.创建食物坐标//食物坐标范围x:2~54,y:1~25int x = 0;int y = 0;//蛇身的大小是两个字节,蛇每次整体向前移动一步也是两个字节//食物的坐标必须是2的倍数,这样蛇才能吃到食物
again:do{x = rand() % 52 + 2;y = rand() % 25 + 1;} while (x % 2 != 0);SnakeNode* pcur = ps->psnake;while (pcur)//食物不能和蛇身重合{if (x == ps->psnake->x && y == ps->psnake->y){goto again;//重新生成坐标}pcur = pcur->next;}//2.创建食物节点SnakeNode* pf = (SnakeNode*)malloc(sizeof(SnakeNode));if (pf == NULL){perror("CreatFood():malloc");//打印错误信息exit(1);}//3.给食物节点的变量赋值pf->x = x;pf->y = y;pf->next = NULL;ps->pfood = pf;//pfood指针指向新创建的节点//4.打印食物SetPos(x, y);wprintf(L"%lc", FOOD);
}
效果:

1.2.创建地图
该函数在前面已经实现了,即CreatMap函数
1.3打印欢迎界面
首先,语句的出现位置一定是在界面中央,即要先设置光标位置
其次,提示游戏规则
代码
//欢迎界面
void WelComeToGame()
{//1.打印第一个界面SetPos(40, 15);//设置光标位置printf("%s", "欢迎来到贪吃蛇小游戏!");SetPos(40, 25);//让“按任意键继续”的出现好看点system("pause");//程序暂停system("cls");//清理屏幕信息//2.打印提示游戏规则的界面SetPos(25, 12);//设置光标位置printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");SetPos(40, 25);//让“按任意键继续”的出现好看点system("pause");system("cls");
}
效果:

1.4.在游戏开始界面右边的空白处打印提示信息
打印游戏规则提示语句,但是我们发现,食物的分值和得分是会随着游戏的进行而变化的,所以不妨把这两个信息放在GameRun函数中去实现。
代码:
//打印提示信息
void PrintInfo(Snake* ps)
{SetPos(64, 11);printf("↑ 上 .↓ 下 .← 左 .→ 右\n");SetPos(64, 12);printf(" F3为加速,F4为减速\n");SetPos(64, 14);printf("不能穿墙,不能咬到自己");//SetPos(64, 15);//printf("食物分值:%d,得分:%d", ps->food_score, ps->score);SetPos(64,17);printf("按ECS退出,按空格暂停");
}
GameStart函数代码:
//游戏开始
void GameStart()
{//欢迎界面WelComeToGame();srand((unsigned int)time(NULL));//初始化贪吃蛇数据Snake snake = { 0 };InitSnake(&snake);//创建食物CreatFood(&snake);//创建地图CreatMap();//打印提示信息PrintInfo(&snake);
}
2.GameRun
2.1接收按键信息
在前面有介绍,如果 GetAsyncKeyState 函数返回值的16位short数据中,最低位为1则说明,该按键被按过,否则为0。如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低位是否为1.
那么,如何检测最低位呢?将返回值按位与1,即可得到最低位。
游戏过程中,游戏的有效按键有8个,(上下左右,加速减速,退出暂停),那就需要考虑八种情况。如果每一种情况都写一遍判断语句,太过麻烦,不妨将其分装成一个宏。
#define KEY_PRESS(VK) (GetAsyncKeyState(VK)&1)?1:0
//最低位为1表达式为真,宏为1,反之,宏为0
2.1.1.改变蛇头的方向
在贪吃蛇的结构体中,我们写了一个枚举变量dir,用来表示蛇头的方向。蛇每向前走一步,我们就判断一次玩家的按键情况,然后修改枚举变量的值。
这时候问题来了,假设记录的上一个蛇头方向是向前,那么蛇能往后走吗?显然是不能的(不然不就咬到自己了嘛),所以在代码中还要进行if语句判断。
2.1.2.改变蛇的运动速度
按F3,F4键后,蛇会加速,减速,相应地,食物的分值也会增加和上升,假设每减速一次,休眠时间增加30ms,食物的分值减少2分,那么程序休眠时间最多增加到320s,因为食物的分值必须大于0。为了保持一致性,我将休眠时间的下限设置为80ms(当然读者也可以不设置该下限,但至少休眠时间必须保证大于0)
2.1.3.游戏暂停或退出
玩家按了ESC键后,整个游戏退出,按空格键,游戏暂停,这里就要写一个函数——暂停函数,当玩家再次按了空格键后,暂停状态结束。
2.2蛇移动函数
2.2.1创建新的节点
2.2.2根据蛇头的位置和方向确定下一个节点的坐标
蛇头方向向上或向下:x轴坐标不变,y轴坐标-1,+1
向左或向右:y轴坐标不变,x轴坐标-2,+2,
然后链接新节点
2.2.3.根据蛇有无吃到食物,来决定尾结点是否保留
没吃到食物,尾结点释放;吃到食物,尾结点保留,相当于蛇身的长度增加一节,将其写成不同的两个函数
在两个函数中,都要对新创建的蛇身进行打印,不同的是未吃到食物的函数中,需要在原来蛇尾的地方打印两个空格,用来清理蛇身。且在遍历蛇身的过程中,只遍历到倒数第二个节点,并释放尾结点;吃到食物的函数中,要对得分进行修改,并销毁原食物节点,创建新的食物
2.3.4判断游戏的状态
撞墙否,咬到自己否,并据此设置贪吃蛇结构体中的游戏状态变量
3.GameEnd
打印游戏结束的原因:正常退出or撞墙or咬到自己
在测试文件中,还可以加上提示语句:是否再来一局
到这里,代码的整体逻辑基本就写完了,但还有一些善后的工作,例如光标还未隐藏,控制台的窗口名字还未设置,不妨将其加到Gamestart函数中去。
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);//设置控制台光标的状态
附录:
snake.h文件
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
#include<stdbool.h>
#include<locale.h>
#include<time.h>#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'#define POS_X 24
#define POS_Y 5#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&1)?1:0)
//最低位为1表达式为真,宏为1,反之,宏为0//蛇头移动的方向
enum DIRECTION
{UP,//上DOWN,//下LEFT,//左RIGHT//右
};
//游戏状态
enum GAME_STATUS
{NORMAL,//正常KILL_BY_WALL,//撞墙KILL_BY_SELF,//撞到自己了END_NORMAL//主动退出
};
//贪吃蛇蛇身的节点
typedef struct SnakeNode
{int x;//x轴坐标int y;//y轴坐标struct SnakeNode* next;//指向下一个节点的指针
}SnakeNode;//贪吃蛇
typedef struct Snake
{SnakeNode* psnake;//指向蛇的指针SnakeNode* pfood;//指向食物的指针enum DIRECTION dir;//蛇头走的方向enum GAME_STATUS status;//当前游戏的状态int food_score;//食物的分数int score;//当下的得分int sleep_time;//程序休眠时间
}Snake;//设置光标位置
void SetPos(short x,short y);
//创建地图
void CreatMap();//游戏开始函数
void GameStart(Snake* ps);//初始化贪吃蛇数据
void InitSnake(Snake* ps);//创建食物
void CreatFood(Snake* ps);//欢迎界面
void WelComeToGame();//打印提示信息
void PrintInfo(Snake* ps);//游戏进行函数
void GameRun(Snake* ps);//暂停
void Pause();//蛇移动
void SnakeMove(Snake* ps);//判断下一个节点是否是食物
int NextIsFood(Snake* ps);//吃掉食物
void EatFood(Snake* ps);
//未吃掉食物
void NoFood(Snake* ps);//是否撞墙
int KillBYWall(Snake* ps);
//是否咬到自己
int KillBYSelf(Snake* ps);//游戏结束
void GameEnd(Snake* ps);
snake.c文件
#include"snake.h"
//设置光标位置
void SetPos(short x, short y)
{COORD pos = { x,y };HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);SetConsoleCursorPosition(houtput, pos);
}
//创建地图
void CreatMap()
{setlocale(LC_ALL, "");//设置为本地模式int i = 0;//一个墙体占两个字节,//y = 0 , x: 0~56SetPos(0, 0);for (i = 0; i < 58; i += 2)//一个墙体占两个字节{wprintf(L"%lc", WALL);}//y = 26, x: 0~56SetPos(0,26);for (i = 0; i < 58; i += 2)//x轴的一个单位值是一个字节{wprintf(L"%lc", WALL);}//x = 0 , y: 1~26for (i = 1; i < 26; i++)//y轴的一个单位值是两个字节{SetPos(0, i);wprintf(L"%lc", WALL);}//x = 56, y: 1~26for (i = 1; i < 26; i++){SetPos(56, i);wprintf(L"%lc", WALL);}SetPos(0, 27);
}//初始化贪吃蛇数据
void InitSnake(Snake* ps)
{setlocale(LC_ALL, "");//本地化//初始化蛇身int i = 0;SnakeNode* pcur = NULL;for(i = 0;i<5;i++){pcur = (SnakeNode*)malloc(sizeof(SnakeNode));//申请内存空间if (pcur == NULL){perror("InitSnake():malloc");//打印错误信息exit(1);//非正常退出}//设置蛇的位置坐标pcur->x = POS_X+i*2;//一个蛇身占两个字节pcur->y = POS_Y;pcur->next = NULL;if (ps->psnake == NULL)//蛇身为空{ps->psnake = pcur;}else//头插法{pcur->next = ps->psnake;ps->psnake = pcur;}}pcur = ps->psnake;//pcur指向蛇头//打印蛇身while (pcur){//设置光标位置SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}//初始化其他数据ps->dir = RIGHT;//初始状态向右走ps->food_score = 10;//食物的分值为10ps->score = 0;ps->sleep_time = 200;//单位是毫秒ps->status = NORMAL;
}
//创建食物
void CreatFood(Snake* ps)
{setlocale(LC_ALL, "");//1.创建食物坐标//食物坐标范围x:2~54,y:1~25int x = 0;int y = 0;//蛇身的大小是两个字节,蛇每次整体向前移动一步也是两个字节//食物的坐标必须是2的倍数,这样蛇才能吃到食物
again:do{x = rand() % 52 + 2;y = rand() % 25 + 1;} while (x % 2 != 0);SnakeNode* pcur = ps->psnake;while (pcur)//食物不能和蛇身重合{if (x == ps->psnake->x && y == ps->psnake->y){goto again;//重新生成坐标}pcur = pcur->next;}//2.创建食物节点SnakeNode* pf = (SnakeNode*)malloc(sizeof(SnakeNode));if (pf == NULL){perror("CreatFood():malloc");//打印错误信息exit(1);}//3.给食物节点的变量赋值pf->x = x;pf->y = y;pf->next = NULL;ps->pfood = pf;//pfood指针指向新创建的节点//4.打印食物SetPos(x, y);wprintf(L"%lc", FOOD);
}//欢迎界面
void WelComeToGame()
{//1.打印第一个界面SetPos(40, 15);//设置光标位置printf("%s", "欢迎来到贪吃蛇小游戏!");SetPos(40, 25);//让“按任意键继续”的出现好看点system("pause");//程序暂停system("cls");//清理屏幕信息//2.打印提示游戏规则的界面SetPos(25, 12);//设置光标位置printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");SetPos(40, 25);//让“按任意键继续”的出现好看点system("pause");system("cls");
}
//打印提示信息
void PrintInfo(Snake* ps)
{SetPos(64, 11);printf("↑ 上 .↓ 下 .← 左 .→ 右\n");SetPos(64, 12);printf(" F3为加速,F4为减速\n");SetPos(64, 14);printf("不能穿墙,不能咬到自己");//SetPos(64, 15);//printf("食物分值:%d,得分:%d", ps->food_score, ps->score);SetPos(64,17);printf("按ESC退出,按空格暂停");SetPos(0, 27);
}
//游戏开始
void GameStart(Snake* 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();srand((unsigned int)time(NULL));//初始化贪吃蛇数据InitSnake(ps);//创建食物CreatFood(ps);//创建地图CreatMap();//打印提示信息PrintInfo(ps);
}
//暂停
void Pause()
{while (1){Sleep(300);//休眠if (KEY_PRESS(VK_SPACE))//再按了一次暂停键,暂停结束{break;}}
}//判断下一个节点是否是食物
int NextIsFood(Snake* ps)
{//蛇头的下一个位置坐标与食物坐标重合return (ps->psnake->x == ps->pfood->x && ps->psnake->y == ps->pfood->y);//表达式为真,返回1,为假,返回0
}
//吃掉食物
void EatFood(Snake* ps)
{setlocale(LC_ALL, "");SnakeNode* pcur = ps->psnake;//打印蛇身while (pcur){SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}ps->score += ps->food_score;free(ps->pfood);CreatFood(ps);
}
//未吃掉食物
void NoFood(Snake* ps)
{setlocale(LC_ALL, "");SnakeNode* pcur = ps->psnake;//打印蛇身while (pcur->next->next)//找到倒数第二个节点{SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}//清理屏幕上原来的蛇尾SetPos(pcur->next->x, pcur->next->y);printf(" ");//在原来蛇尾的位置打印两个空格free(pcur->next);pcur->next = NULL;
}//蛇移动
void SnakeMove(Snake* ps)
{//1.创建新的节点SnakeNode* pcur = (SnakeNode*)malloc(sizeof(SnakeNode));if (pcur == NULL){perror("SnakeMove():malloc");exit(1);}//根据蛇头的位置和方向确定下一个节点的坐标switch(ps->dir){case UP://上pcur->x = ps->psnake->x;//x轴坐标不变pcur->y = ps->psnake->y - 1;//y轴坐标-1break;case DOWN://下pcur->x = ps->psnake->x;pcur->y = ps->psnake->y + 1;break;case LEFT://左pcur->x = ps->psnake->x - 2;//x轴坐标要-2pcur->y = ps->psnake->y;break;case RIGHT://右pcur->x = ps->psnake->x + 2;pcur->y = ps->psnake->y;break;}//3.将新节点与蛇身链接起来pcur->next = ps->psnake ;ps->psnake = pcur;if (NextIsFood(ps)){EatFood(ps);}else{NoFood(ps);}
}//是否撞墙
int KillBYWall(Snake* ps)
{return (ps->psnake->x == 0 || ps->psnake->x == 56 || ps->psnake->y == 0 || ps->psnake->y == 26);//表达式为真,返回1,为假,返回0
}
//是否咬到自己
int KillBYSelf(Snake* ps)
{//判断蛇头是否和蛇身重叠SnakeNode* pcur = ps->psnake->next;while (pcur){if (ps->psnake->x == pcur->x && ps->psnake->y == pcur->y){return 1;}pcur = pcur->next;}return 0;//没有重合,返回0
}
//游戏进行函数
void GameRun(Snake* ps)
{do{//打印提示信息SetPos(64, 15);printf("食物分值:%d,得分:%d", ps->food_score, ps->score);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))//ESC键{ps->status = END_NORMAL;break;}else if (KEY_PRESS(VK_F3))//加速键{if(ps->sleep_time>=80)//速度上限{ps->sleep_time -= 30;ps->food_score += 2;}}else if (KEY_PRESS(VK_F4))// 减速键{if (ps->sleep_time <= 320)//速度下限{ps->sleep_time += 30;ps->food_score -= 2;//食物的分数不能小于等于0}}Sleep(ps->sleep_time);SnakeMove(ps);//蛇移动if (KillBYWall(ps)){ps->status = KILL_BY_WALL;}else if (KillBYSelf(ps)){ps->status = KILL_BY_SELF;}} while (ps->status == NORMAL);
}//游戏结束
void GameEnd(Snake* ps)
{//1.打印游戏退出信息if (ps->status == END_NORMAL){SetPos(24,12);printf("您已退出,游戏结束!");}else if (ps->status == KILL_BY_SELF){SetPos(24, 12);printf("咬到自己了,游戏结束!");}else if (ps->status == KILL_BY_WALL){SetPos(24, 12);printf("撞墙了,游戏结束!");}//释放蛇身的节点SnakeNode* pcur = ps->psnake;while (pcur){SnakeNode* del = pcur;pcur = pcur->next;free(del);}ps->psnake = NULL;//释放食物节点free(ps->pfood);ps->pfood = NULL;
}
test.c文件
#include"snake.h"int main()
{char ch;do{Snake snake = { 0 };GameStart(&snake);GameRun(&snake);GameEnd(&snake);SetPos(24, 15);printf("是否再来一局?(Y/N):");scanf("%c", &ch);} while (ch == 'Y' || ch == 'y');SetPos(0, 27);return 0;
}