引言
C语言已经学了不短的时间的,这期间已经开始C++和Python的学习,想给我的C语言收个尾,想起了小时候见过别人的老人机上的贪吃蛇游戏,自己父母的手机又没有这个游戏,当时成为了我的一大遗憾,这两天发现C语言实现这个项目似乎并不难,于是查了一些WindowsAPI的控制台函数,实现了这一游戏。如果你觉得你的C语言基础语法学的差不多了,又想实现贪吃蛇这样一个小游戏,那么就跟我一起来实现它吧。下面是最终成品的样子:
本贪吃蛇是用控制台实现,其中¥是贪吃蛇的食物,⚪是贪吃蛇,■是墙体。
Win32 API
在开始我们的代码之前,像讲一下关于Win32 API的相关知识,Windows这个多作业系统除了协调应用程序的执行,分配内存,管理资源之外,它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗,绘制图形,使用周边设备等目的,由于这些函数的服务对象是应用程序(Application),所以便称之为Application Programming Interface,简称API函数。Win32 API也就是Microsoft Windows32位平台的应用程序编程接口。
控制台程序
平常我们运行起来的黑框其实是就是控制台程序
我们可以用cmd命令来控制控制台窗口的长宽:比如设置窗口大小,30行,100列
mode con cols=100 lines=30
同时也可以通过命令修改窗口的名字:
title 贪吃蛇
这里注意一下,在改名字之后加一个getchar()保证程序处在运行状态,这样才能正确观察到要改后的名字。
这些能在控制台窗口执行的命令,像我上方图片中的代码一样,可以用C语言函数system来执行。
代码放在下面:
#include<stdlib.h>
int main()
{
system("mode con cols=100 lines=30");//设置窗口大小
system("title 贪吃蛇");//改窗口标题
getchar();
return 0;
}
这里注意一下system的头文件是
#include <stdlib.h>
控制台上的坐标COORD
COORD是Windows API中定义的一个结构体,表示一个字符在控制台屏幕上的坐标,下面是关于对COORD的定义:
typedef struct _COORD{
SHORT x;
SHORT y;
}COORD, *PCOORD;
其中x轴和y轴如图
同时可以给上方结构体(坐标)赋值:
COORD pos = {10,15};
GetStdHandle
GetStdHandle是一个Windows API函数。它用于一个特定的标准设备(标准输入,标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
HANDLE GetStdHandle(DWORD nStdHandle);//函数的参数为标准设备
句柄是什么?
句柄相当于一个操作工具,你可以通过操作某设备的句柄去获得和修改某标准设备的信息
实例(获得句柄)
HANDLE hOutput = NULL;
//获取标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标大小和可见性信息
BOOL WINAPI GetConsoleInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
实例:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
//通过传CursorInfo的地址并通过函数将当前光标信息传给CursorInfo
CONSOLE_CURSOR_INFO
在上一份代码中CursorInfo,里面存的是光标信息,类型是CONSOLE_CURSOR_INFO,我们可以来看看这个类型是如何定义的
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;//这个变量是表示光标所占一个格的百分比
BOOL bVisible;//这个变量是决定光标是否可见
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
- dwSize,由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完 全填充单元格到单元底部的水平线条。
- bVisible,游标的可见性。如果光标可见,则此成员为TRUE
我们在运行打印贪吃蛇的过程中将光标设置为不可见,就不会影响到整个游戏的美观
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo
上方的GetConsoleCursorInfo是通过函数获取光标信息,这次的函数是通过函数实在改变控制台光标信息,下面是本函数声明
BOOL WINAPI SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
下面看一组改变控制台光标信息的实例:
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//影藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
SetConsoleCursorPosition
设置指定控制台缓冲区的光标位置,我们可以将坐标信息放到COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的控制台位置
BOOL WINAPI SetConsoleCursorPosition(
HANDLE hConsoleOutput,
COORD pos
);
实例:
COORD pos = { 10, 5};
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
//通过以上代码可以将光标设置到10 5 位置上
看到这里,我们是否可以考虑封装一个函数,可以专门通过传入坐标来控制光标位置,于是封装了一个这样的函数Setpos
//设置光标的坐标
void SetPos(short x, short y)
{
COORD pos = { x, y };
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE); //设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
}
GetAsyncKeyState
这个函数用于获取按键情况,原型如下:
SHORT GetAsyncKeyState(
int vKey
);
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState的返回值是short类型,在上一次调用GetAsyncKeyState函数后,如果返回的16位short数据中,最高位是1,说明按键的状态是按下,如果是0,说明最高位的状态是抬起;如果最低为被设置为1则说明,该按键被按过,否则为0。
如果我们要判断一个按键是否被按过,可以检测GetAsyncKeyState的返回值最低为的值是否为1,可据此写出一个宏
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1)?1:0)
这样就可以通过向KEY_PRESS传入键值直接监测按键是否被按过了。
下面是关于不同键值介绍的链接
Virtual-Key Codes (Winuser.h) - Win32 apps | Microsoft Learn
不过目前我们知道:
- VK_UP 向上箭头键
- VK_DOWN 向下箭头键
- VK_LEFT 向左箭头键
- VK_RIGHT 向右箭头键
- VK_ESCAPE ESC按键
- VK_F3 F3按键
- VK_F4 F4按键
这些VK_XXX已经是头文件中用宏定义好的常量,直接用就行,不需要知道具体的值
就足够用了
贪吃蛇地图设计与分析
地图
如果想用控制台窗口打印地图,就需要了解一下控制台窗口坐标的知识
如下图所示,横向是X轴,从左向右增长,纵向是Y轴,从上到下依次增长
在地图上,我们打印墙体用宽字符■,打印蛇用宽字符●,打印食物我这里用的是宽字符¥(因为我个人比较喜欢)如果你在字符表里如果有别的喜欢的字符,也当然可以灵活的根据个人爱好改变
刚刚我介绍的时候介绍的字符是宽字符,意思是占两个字节的字符,普通的字符占一个字节
可以看看占两个字节字符和占一个字节字符的区别:
由观察可以发现,一个占两字节的字符在控制台打印的时候也是占两个一字节字符所占的位置的
这里还需要引入一下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和宽字符的输入,输出函数,加入<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
刚才打印方框的过程提到了本地化,如果不进行本地化,■将无法被程序编译识别,最终只会打印问号,所以接下来我们讲讲如何运用<locale.h>以及其函数对编译环境进行本地化。
<locale.h>本地化
<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样的行为的部分。
标准中,依赖地区的部分有以下几项:
- 数字的格式
- 货币量的格式
- 字符集
- 日期和时间的表示形式
类项
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部 分,其中⼀部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的⼀个宏, 指定⼀个类项:
- LC_COLLATE
- LC_CTYPE
- LC_MONETARY
- LC_NUMERIC
- LC_TIME
- LC_ALL---针对所有类项修改
关于每个类项的详细说明,可参考
setlocale 函数
char* setlocale (int category, const char* locale);
setlocale函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。
setlocale的第一个参数可以是前面说明的类项中的一个,那么只会影响一个类项,如果第一个参数是LC_ALL,那么就直接影响所有类项。
C标准给第二个参数仅定义了2种可能的取值:"C"和""。
在任意程序执行开始,都会隐藏式执行调用:
setlocale(LC_ALL,"C");
当地区设为"C"时,库函数按正常方式执行。
如果想在程序运行时改变地区,就只能显示调用setlocale函数。用""作为第二个参数,调用setlocale函数就可以切换到本地模式,这种模式会适应本地环境。
当切换到我们本地模式后,就可以支持一些宽字符(如汉字)的占位输出了。
setlocale(LC_ALL, " ");//切换到本地环境
宽字符的打印
#include <stdio.h>
#include<locale.h>
int main() {
setlocale(LC_ALL, "");
wchar_t ch1 = L'●';
wchar_t ch2 = L'你';
wchar_t ch3 = L'好';
wchar_t ch4 = L'¥';
printf("%c%c\n", 'a', 'b');
wprintf(L"%c\n", ch1);
wprintf(L"%c\n", ch2);
wprintf(L"%c\n", ch3);
wprintf(L"%c\n", ch4);
return 0;
}
这里比对的更加清晰一些,⼀个普通字符占⼀个字符的位置 但是打印⼀个汉字字符,占⽤2个字符的位置,那么我们如果 要在贪吃蛇中使⽤宽字符,就得处理好地图上坐标的计算。
关于普通字符和宽字符的处理展示大概是这个样子:
我们可以假设实现一个地图,27行,58列,围绕周围画出地图:
蛇和食物
初始化的时候,假设蛇长为5,蛇的每个节点宽字符●。这里要注意的是,蛇的每个节点和食物出现的X轴位置都要保证是二的倍数,不然会出现蛇和食物无法对齐或者蛇一半卡在墙体中的情况。
代码环节
数据的结构设计
上面说了这么多,到现在终于可以讲代码了,在学了这些控制台操作和地图分析之后,相信其实聪明的你已经基本能大概想出来如何去实现贪吃蛇的逻辑了,在开始代码之前,来介绍一下我们对我们对贪吃蛇数据的维护和设计
这里讲一下定义的每个节点的结构体:
typedef struct SnakeNode
{
int x;//节点横坐标
int y;//节点纵坐标
struct SnakeNode* next;//指向下一个节点的指针
}SnakeNode, * pSnakeNode;//重命名结构体类型
如果要管理整条蛇,还需要我们封装一个Snake来维护整条蛇🐍:
typedef struct Snake
{
pSnakeNode pSnake;//指向蛇头节点的指针
pSnakeNode pFood;//指向食物的食物指针
int Score;//当前分数
int FoodWeight;//食物比重
int SleepTime;//休眠时间
enum GAME_STATUES status;//游戏当前状态
enum DIRECTION dir;//蛇当前方向
}Snake, * pSnake;
在维护整条蛇的结构体类型中,定义了两个枚举类型,分别用来表示
游戏当前的状态:
enum GAME_STATUES {
OK = 1,//游戏正常运行
ESC, //点击ESC主动退出
KILL_BY_WALL,//撞到墙游戏结束
KILL_BY_SELF //咬到自己游戏结束
};
蛇当前的前进方向:
enum DIRECTION {
UP = 1,//上
DOWN, //下
LEFT, //左
RIGHT //右
};
贪吃蛇项目流程设计
这里介绍整个游戏过程中的运行逻辑,我们基本也是这个顺序展开代码
游戏主函数:运行逻辑
#include"greedy_snake.h"
int main()
{
srand((unsigned int)time(NULL));//随机初始化种子,相关内容可以参考我之前的扫雷博客
int ch;
do {
Snake snake = { 0 };//创建一个维护整个贪吃蛇的数据类型
snake.pSnake = NULL;
GameStart(&snake);
GameRun(&snake);
GameEnd(&snake);
SetPos(20, 15);
printf("想要再来一局吗?Y/N:");
ch = getchar();
} while (ch == 'Y' || ch == 'y');
SetPos(0, 26);
return 0;
}
GameStart-游戏开始的数据初始化和维护
void GameStart(pSnake ps)
{
//下面五行使光标不可见
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO rem;
GetConsoleCursorInfo(houtput, &rem);
rem.bVisible = 0;
SetConsoleCursorInfo(houtput, &rem);
setlocale(LC_ALL, "");//设置为本地类项
//初始化界面
system("mode con cols=100 lines=30");//设置窗口大小
system("title 贪吃蛇");//改窗口标题
//下面是打印欢迎和介绍信息
SetPos(32, 10);
printf("欢迎来到贪吃蛇小游戏!\n");
SetPos(33, 15);
system("pause");
system("cls");
SetPos(29, 9);
printf("游戏介绍:");
SetPos(33, 11);
printf("通过↑ ← ↓ →控制蛇的移动");
SetPos(33, 13);
printf("可以通过F3加速,F4减速");
SetPos(33, 15);
printf("更高的速度下可以获得更高的分数");
SetPos(33, 17);
printf("可以使用空格暂停");
SetPos(33, 19);
system("pause");//这个命令可以使游戏暂停,按任意键继续
//绘制地图
CreateMap();
//初始化创建蛇,传ps
InitSnake(ps);
//初始化创建食物,传ps
CreateFood(ps);
}
SetPos-设置光标位置
void SetPos(int x, int y)
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x,y };
SetConsoleCursorPosition(handle, pos);
}
CreateMap-绘制地图
void CreateMap()
{
system("cls");
SetPos(0, 0);
//这里的WALL在头文件中用宏定义:#define WALL L'■'
//上
for (int i = 0; i <= 56; i += 2)
wprintf(L"%lc", WALL);
//下
SetPos(0, 26);
for (int i = 0; i <= 56; i += 2)
wprintf(L"%lc", WALL);
//左和右
for (int i = 1; i <= 25; i++) {
SetPos(0, i);
wprintf(L"%lc", WALL);
SetPos(56, i);
wprintf(L"%lc", WALL);
}
//打印右侧边框的提示介绍信息
SetPos(62, 15);
printf("通过↑←↓→控制蛇的移动");
SetPos(62, 16);
printf("可以通过F3加速,F4减速");
SetPos(62, 17);
printf("更高的速度下可以获得更高的分数");
SetPos(62, 18);
printf("可以使用空格暂停");
}
CreateFood-初始化创建食物
void CreateFood(pSnake ps)
{
int xx = 0;
int yy = 0;
//生成的地址不能在地图外,不能在蛇身上
do
{
xx = rand() % 53 + 2;
yy = rand() % 25 + 1;
if (xx % 2 == 0) {
pSnakeNode pcur = ps->pSnake;
while (pcur) {
if (xx == pcur->x && yy == pcur->y)
goto again;
pcur = pcur->next;
}
break;
}
again:;//循环直到生成正确的地址
} while (1);
pSnakeNode PFood = (SnakeNode*)malloc(sizeof(SnakeNode));
if (PFood == NULL) {
perror("malloc food fail:");
exit(1);
}
PFood->x = xx;
PFood->y = yy;
ps->pFood = PFood;
SetPos(xx, yy);
//食物在宏中定义为:#define FOOD L'¥'
wprintf(L"%lc", FOOD);
}
GameRun-游戏运行维护函数
void GameRun(pSnake ps)
{
do {
//打印游戏帮助信息
SetPos(62, 10);
printf("总分:%d\n", ps->Score);
SetPos(62, 11);
printf("食物分值:%2d\n", ps->FoodWeight);
//检测按键
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_ESCAPE)) {
ps->status = ESC;
break;
}
else if (KEY_PRESS(VK_F3)) {//F3设置加速
if (ps->SleepTime >= 80) {
ps->SleepTime -= 30;
ps->FoodWeight += 2;
}
}
else if (KEY_PRESS(VK_F4)) {//F4设置减速
if (ps->FoodWeight > 2) {
ps->SleepTime += 30;
ps->FoodWeight -= 2;
}
}
else if (KEY_PRESS(VK_SPACE))//空格设置暂停
{
while (1) {
Sleep(100);
if (KEY_PRESS(VK_SPACE)) {
break;
}
}
}
//睡一下
Sleep(ps->SleepTime);
//根据按键控制蛇的运动和吃食物,并打印
SnakeMove(ps);
} while (ps->status == OK);
}
SnakeMove-蛇移动
void SnakeMove(pSnake ps)
{
//根据在GameRun中获得的方向设置生成蛇的下一个节点
pSnakeNode pNext = (SnakeNode*)malloc(sizeof(SnakeNode));
if (pNext == NULL) {
perror("malloc pNext fail:");
exit(1);
}
pNext->next = NULL;
switch (ps->dir)
{
case UP:
pNext->x = ps->pSnake->x;
pNext->y = ps->pSnake->y - 1;
break;
case DOWN:
pNext->x = ps->pSnake->x;
pNext->y = ps->pSnake->y + 1;
break;
case LEFT:
pNext->x = ps->pSnake->x - 2;
pNext->y = ps->pSnake->y;
break;
case RIGHT:
pNext->x = ps->pSnake->x + 2;
pNext->y = ps->pSnake->y;
break;
}
//判断下一个节点的位置并操控蛇的状态
if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y) {//如果吃上食物
EatFood(ps, pNext);
}
else {//如果没吃上食物
NotEatFood(ps, pNext);
KillByWall(ps);//判断是否撞墙
KillBySelf(ps);//判断是否咬到自己
}
}
EatFood-吃到食物后蛇增长
void EatFood(pSnake ps,pSnakeNode pNext)
{
//将新节点赋给蛇
pNext->next = ps->pSnake;
ps->pSnake = pNext;
//打印蛇
pSnakeNode pcur = ps->pSnake;
while (pcur) {
SetPos(pcur->x, pcur->y);
wprintf(L"%lc", BODY);
pcur = pcur->next;
}
ps->Score += ps->FoodWeight;
//释放并创建新食物
free(ps->pFood);
CreateFood(ps);
}
NotEatFood-没有吃到食物向后移动
void NotEatFood(pSnake ps, pSnakeNode pNext)
{
pNext->next = ps->pSnake;
ps->pSnake = pNext;
//打印蛇
pSnakeNode 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;
SetPos(pcur->x, pcur->y);
//蛇的身体在头文件中用宏定义为:#define BODY L'●'
wprintf(L"%lc", BODY);
}
KillByWall-撞墙判定
void 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;//如果撞墙改变游戏状态
}
}
KillBySelf-咬到自己判定
void KillBySelf(pSnake ps)
{
pSnakeNode pcur = ps->pSnake->next;
while (pcur) {
if (pcur->x == ps->pSnake->x && pcur->y == ps->pSnake->y)
{
ps->status = KILL_BY_SELF;//如果要到自己改变游戏状态
return;
}
pcur = pcur->next;
}
}
GameEnd-游戏善后,释放蛇
void GameEnd(pSnake ps)
{
//打印结束信息
SetPos(20, 11);
switch (ps->status)
{
case ESC:
printf("正常退出游戏\n");
SetPos(20, 13);
printf("你的得分是%d", ps->Score);
break;
case KILL_BY_WALL:
printf("撞墙了,游戏结束!\n");
SetPos(23, 13);
printf("你的得分是%d", ps->Score);
break;
case KILL_BY_SELF:
printf("咬到自己了,游戏结束!\n");
SetPos(23, 13);
printf("你的得分是%d", ps->Score);
break;
}
//释放蛇
pSnakeNode pcur = ps->pSnake;
pSnakeNode del = ps->pSnake;
while (pcur) {
del = pcur;
pcur = pcur->next;
free(del);
}
ps->pSnake = NULL;
SetPos(0, 26);
free(ps->pFood);
ps = NULL;
}
代码汇总
写了这么多,大概就介绍完了所有函数,现在将它们放到三个文件中,相应创建文件CV一下应该就能在你们的VS跑了
头文件-greedy_snake.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<windows.h>
#include<locale.h>
#include<time.h>
#define POS_X 24
#define POS_Y 5
#define WALL L'■'
#define BODY L'●'
#define FOOD L'¥'
#define KEY_PRESS(vk) (GetAsyncKeyState(vk)&0x1?1:0)
enum GAME_STATUES {
OK = 1,
ESC,
KILL_BY_WALL,
KILL_BY_SELF
};
enum DIRECTION {
UP = 1,
DOWN,
LEFT,
RIGHT
};
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
typedef struct Snake
{
pSnakeNode pSnake;//指向蛇头节点的指针
pSnakeNode pFood;//指向食物的食物指针
int Score;//当前分数
int FoodWeight;//食物比重
int SleepTime;//休眠时间
enum GAME_STATUES status;//游戏当前状态
enum DIRECTION dir;//蛇当前方向
}Snake, * pSnake;
//游戏开始的维护
void GameStart(pSnake ps);
//绘制地图
void CreateMap();
//初始化蛇
void InitSnake(pSnake ps);
//初始化食物
void CreateFood(pSnake ps);
//设置光标位置
void SetPos(int x, int y);
//游戏运行维护函数
void GameRun(pSnake ps);
//游戏结束善后
void GameEnd(pSnake ps);
//蛇移动
void SnakeMove(pSnake ps);
源文件-greedy_snake.c
#include"greedy_snake.h"
//设置光标位置
void SetPos(int x, int y)
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x,y };
SetConsoleCursorPosition(handle, pos);
}
void CreateMap()
{
system("cls");
SetPos(0, 0);
//上
for (int i = 0; i <= 56; i += 2)
wprintf(L"%lc", WALL);
//下
SetPos(0, 26);
for (int i = 0; i <= 56; i += 2)
wprintf(L"%lc", WALL);
//左
for (int i = 1; i <= 25; i++) {
SetPos(0, i);
wprintf(L"%lc", WALL);
SetPos(56, i);
wprintf(L"%lc", WALL);
}
SetPos(62, 15);
printf("通过↑←↓→控制蛇的移动");
SetPos(62, 16);
printf("可以通过F3加速,F4减速");
SetPos(62, 17);
printf("更高的速度下可以获得更高的分数");
SetPos(62, 18);
printf("可以使用空格暂停");
}
//初始化蛇
void InitSnake(pSnake ps)
{
//创建五个蛇身节点
pSnakeNode pcur = NULL;
for (int i = 0; i < 5; i++) {
pcur = (SnakeNode*)malloc(sizeof(SnakeNode));
if (pcur == NULL) {
perror("malloc 节点 fail:");
exit(1);
}
pcur->x = POS_X + 2 * i;
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;
while (pcur) {
SetPos(pcur->x, pcur->y);
wprintf(L"%lc", BODY);
pcur = pcur->next;
}
//贪吃蛇信息初始化
ps->dir = RIGHT;
ps->FoodWeight = 10;
ps->pFood = NULL;
ps->Score = 0;
ps->SleepTime = 200;
ps->status = OK;
}
void CreateFood(pSnake ps)
{
int xx = 0;
int yy = 0;
do
{
xx = rand() % 53 + 2;
yy = rand() % 25 + 1;
if (xx % 2 == 0) {
pSnakeNode pcur = ps->pSnake;
while (pcur) {
if (xx == pcur->x && yy == pcur->y)
goto again;
pcur = pcur->next;
}
break;
}
again:;
} while (1);
pSnakeNode PFood = (SnakeNode*)malloc(sizeof(SnakeNode));
if (PFood == NULL) {
perror("malloc food fail:");
exit(1);
}
PFood->x = xx;
PFood->y = yy;
ps->pFood = PFood;
SetPos(xx, yy);
wprintf(L"%lc", FOOD);
}
void GameStart(pSnake ps)
{
//下面五行使光标不可见
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO rem;
GetConsoleCursorInfo(houtput, &rem);
rem.bVisible = 0;
SetConsoleCursorInfo(houtput, &rem);
setlocale(LC_ALL, "");//设置为本地类项
//初始化界面
system("mode con cols=100 lines=30");//设置窗口大小
system("title 贪吃蛇");//改窗口标题
SetPos(32, 10);
printf("欢迎来到贪吃蛇小游戏!\n");
SetPos(33, 15);
system("pause");
system("cls");
SetPos(29, 9);
printf("游戏介绍:");
SetPos(33, 11);
printf("通过↑ ← ↓ →控制蛇的移动");
SetPos(33, 13);
printf("可以通过F3加速,F4减速");
SetPos(33, 15);
printf("更高的速度下可以获得更高的分数");
SetPos(33, 17);
printf("可以使用空格暂停");
SetPos(33, 19);
system("pause");
//绘制地图
CreateMap();
//初始化创建蛇,传ps
InitSnake(ps);
//初始化创建食物,传ps
CreateFood(ps);
}
void EatFood(pSnake ps,pSnakeNode pNext)
{
pNext->next = ps->pSnake;
ps->pSnake = pNext;
//打印蛇
pSnakeNode pcur = ps->pSnake;
while (pcur) {
SetPos(pcur->x, pcur->y);
wprintf(L"%lc", BODY);
pcur = pcur->next;
}
ps->Score += ps->FoodWeight;
free(ps->pFood);
CreateFood(ps);
}
void NotEatFood(pSnake ps, pSnakeNode pNext)
{
pNext->next = ps->pSnake;
ps->pSnake = pNext;
//打印蛇
pSnakeNode 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;
SetPos(pcur->x, pcur->y);
wprintf(L"%lc", BODY);
}
void 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;
}
}
void KillBySelf(pSnake ps)
{
pSnakeNode pcur = ps->pSnake->next;
while (pcur) {
if (pcur->x == ps->pSnake->x && pcur->y == ps->pSnake->y)
{
ps->status = KILL_BY_SELF;
return;
}
pcur = pcur->next;
}
}
void SnakeMove(pSnake ps)
{
pSnakeNode pNext = (SnakeNode*)malloc(sizeof(SnakeNode));
if (pNext == NULL) {
perror("malloc pNext fail:");
exit(1);
}
pNext->next = NULL;
switch (ps->dir)
{
case UP:
pNext->x = ps->pSnake->x;
pNext->y = ps->pSnake->y - 1;
break;
case DOWN:
pNext->x = ps->pSnake->x;
pNext->y = ps->pSnake->y + 1;
break;
case LEFT:
pNext->x = ps->pSnake->x - 2;
pNext->y = ps->pSnake->y;
break;
case RIGHT:
pNext->x = ps->pSnake->x + 2;
pNext->y = ps->pSnake->y;
break;
}
if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y) {
EatFood(ps, pNext);
}
else {
NotEatFood(ps, pNext);
KillByWall(ps);
KillBySelf(ps);
}
}
void GameRun(pSnake ps)
{
do {
//打印游戏帮助信息
SetPos(62, 10);
printf("总分:%d\n", ps->Score);
SetPos(62, 11);
printf("食物分值:%2d\n", ps->FoodWeight);
//检测按键
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_ESCAPE)) {
ps->status = ESC;
break;
}
else if (KEY_PRESS(VK_F3)) {
if (ps->SleepTime >= 80) {
ps->SleepTime -= 30;
ps->FoodWeight += 2;
}
}
else if (KEY_PRESS(VK_F4)) {
if (ps->FoodWeight > 2) {
ps->SleepTime += 30;
ps->FoodWeight -= 2;
}
}
else if (KEY_PRESS(VK_SPACE))
{
while (1) {
Sleep(100);
if (KEY_PRESS(VK_SPACE)) {
break;
}
}
}
//睡一下
Sleep(ps->SleepTime);
//根据按键控制蛇的运动和吃食物,并打印
SnakeMove(ps);
} while (ps->status == OK);
}
void GameEnd(pSnake ps)
{
SetPos(20, 11);
switch (ps->status)
{
case ESC:
printf("正常退出游戏\n");
SetPos(20, 13);
printf("你的得分是%d", ps->Score);
break;
case KILL_BY_WALL:
printf("撞墙了,游戏结束!\n");
SetPos(23, 13);
printf("你的得分是%d", ps->Score);
break;
case KILL_BY_SELF:
printf("咬到自己了,游戏结束!\n");
SetPos(23, 13);
printf("你的得分是%d", ps->Score);
break;
}
pSnakeNode pcur = ps->pSnake;
pSnakeNode del = ps->pSnake;
while (pcur) {
del = pcur;
pcur = pcur->next;
free(del);
}
ps->pSnake = NULL;
SetPos(0, 26);
free(ps->pFood);
ps = NULL;
}
运行文件-snake_run.c
#include"greedy_snake.h"
int main()
{
srand((unsigned int)time(NULL));
int ch;
do {
Snake snake = { 0 };
snake.pSnake = NULL;
GameStart(&snake);
GameRun(&snake);
GameEnd(&snake);
SetPos(20, 15);
printf("想要再来一局吗?Y/N:");
ch = getchar();
} while (ch == 'Y' || ch == 'y');
SetPos(0, 26);
return 0;
}
运行截图
结尾
到这里,本篇博客的内容基本上就结束了,写博客不易,如果感觉对你有帮助的话,还请留个赞留个关注再走啊。博主的C语言语法学习之路到现在也算是真正结束,统计下来C语言将近学了三四遍了,在后面的时间里,我准备好好开始过数据结构的内容,这些时日是没有特别多的时间去写题攻算法了,给自己报了一堆比赛还需要去准备,还是要先把C++和Python在假期赶快速成一下,数据结构系统仔细的过上一遍,给未来打好基础。后期我还会继续产出有意思的内容,请大家多多关注我吧!
在这里记录一下,今天是2024.1.31,大一的寒假♥