演示视频
目录
[Win32 API](#Win32 API)
GetConsoleCursorInfo函数和SetConsoleCursorInfo函数
一.概述
贪食蛇游戏设计,分为游戏开始前和游戏运行以及游戏结束三个阶段,我这个是利用win32 API直接在命令框设计的游戏,游戏运行阶段主要是解决游戏界面,提示信息等方面。游戏运行阶段会去解决初始化蛇身和食物,以及根据按键情况去移动蛇的方面。游戏结束阶段会告知游戏结束的原因和释放链表节点(蛇身以及食物都是通过链表来表示,其实也可以通过动态顺序表来做),游戏结束一般来说会是撞墙结束,咬到自己结束,以及正常退出三种情况。
二.游戏开始前
正常的控制台程序结束标题位置一般都是默认给出了,如果要修改标题要怎么修改呢,控制台程序命令框的大小能不能修改呢。同时光标一闪一闪很影响观感,也应该隐藏起来。
修改控制台程序标题和大小
对于windows命令框可以直接通过 title 新名称来修改命令框标题
而命令框的大小可以通过mode con cols=要修改的大小 lines=有修改的大小,来进行修改大小,cols是行大小,lines是列大小
而对于编译器来说可不可以使用同样的语句来修改控制台程序界面的大小呢
还没使用就已经报错了,如果你想使用和windows系统一样的语句进行修改,那么需要加上windows.h头文件,并且使用system才能使用系统语句
代码和运行结果如下,使用getchar()是使程序一直停留在运行阶段,方便测试,如果不这样的话运行结束会直接还原
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<windows.h>
void text()
{
system("title 贪食蛇");
system("mode con cols=100 lines=30");
getchar();
}
int main()
{
text();
}
2.光标的隐藏以及改变输入位
Win32 API
控制程序光标的各种操作是通过win32 API来进行操作的,Windows这个多作业系统除了协调应⽤程序的执行、分配内存、管理资源之外,它同时也是⼀个很大的服务中心,调⽤这个服务中心的各种服务(每⼀种服务就是⼀个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应⽤程序(Application),所以便称之为Application Programming Interface,简称API函数。WIN32 API也就是Microsoft Windows 32位平台的应⽤程序编程接⼝。
不过话说回来,如果你要画图又没怎么接触这些图形库,还是先用Easyx吧,这个更容易初学者上手一点,Win32要处理很多细节
Win32 API是windows系统提供的,所以直接使用windows.h头文件就可以使用了
GetStdHandle函数
GetStdHandle是用来获取句柄的函数,属于windows API函数,GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。句柄说直白点其实就是一个"授权的凭证",你要通过这个"授权许可凭证"才能进行一系列操作,可以操纵鼠标的光标,还可以用来控制窗口的位置、大小和状态。
语法如下
HANDLE GetStdHandle(
DWORD nStdHandle
);
函数参数如下
在Windows编程中,标准输入、标准输出和标准错误的句柄值分别为-10、-11和-12。这些特殊的负数值是为了与普通句柄值区分开来。通常情况下,普通句柄值是正整数,而这些特殊句柄值是为了表示标准输入输出而特意赋予的负数值。其实你不填参数值,它也默认是-10,-11,-12
比如获取标准输出的句柄可以表示为HANDLE WINAPI GetStdHandle( STD_OUTPUT_HANDLE)
HANDLE是一种数据类型,是专门接收句柄用的。
GetConsoleCursorInfo函数和SetConsoleCursorInfo函数
GetConsoleCursorInfo函数是用来查看检索光标大小和可见性信息的函数,SetConsoleCursorInfo是将修改后的结果设置回去的函数。
具体用法是首先获取句柄,然后通过GetConsoleCursorInfo函数来获取光标信息,再然后进行修改,然后通过SetConsoleCursorInfo把修改后的结果设置回去
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取句柄
CONSOLE_CURSOR_INFO Info;
GetConsoleCursorInfo(houtput, &Info);//获取光标控制台信息
Info.bVisible = false;//隐藏控制台光标
SetConsoleCursorInfo(houtput, &Info);//把修改后的结果设置回去
CONSOLE_CURSOR_INFO这是个结构体,是专门用来存放光标信息的结构体,这个结构体成员是是光标可见性和光标大小
bVisible 是光标可见性,false是隐藏,true是正常显示。有些c编译器不支持布尔值 ,用0表示false,1表示true也可以实现操作
dwSize是光标大小,现在的光标大小一般默认是百分之25,介于1到100之间
SetConsoleCursorPosition函数
这个函数是用来设置光标位置的,一般光标是默认放在左上角进行输出的,而想到屏幕中间输出文字可以通过这个函数来实现。
COORD是存放光标位置的结构体,成员是横坐标x,纵坐标y
COORD定义
用法如下
void setpos(int x, int y)
{
COORD pos = { x,y };
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(houtput, pos);
}
因为要用到很多次,所以单独设置一个函数
游戏开篇界面处理
然后就可以开始准备游戏界面的处理了
首先打印开篇界面
此时开篇界面的代码
void setpos(int x, int y)
{
COORD pos = { x,y };
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(houtput, pos);
}
那么如何控制请按任意键继续呢,这些字不是直接打印出来的,windows提供了系统函数pause进行暂停,请按任意键继续是这个函数调用了自动产生的
void welcome()
{
setpos(40, 10);
printf("欢迎来到贪食蛇小游戏\n");
setpos(43, 15);
system("pause");
getchar();
}
然后开始处理第二个界面
但是在打印之前要先清理上一个界面的数据,通过windows自带的cls就可以清屏了
void welcomemap()
{
setpos(40, 10);
printf("欢迎来到贪食蛇游戏\n");
setpos(41, 15);
system("pause");
system("cls");//清屏
setpos(38, 10);
printf("用 ↑ ↓ → ←键操作蛇\n");
setpos(38, 13);
printf("f3表示加速,f4表示减速,加速能得到更多的分数");
setpos(38, 18);
system("pause");
system("cls");
}//欢迎界面
处理完就这个样子了
创建地图
地图的打印我采用的是宽字符"□",单字符(一个字节的空间)通常是指一个字母、数字或标点符号,但是复杂的特殊符号方框五角星之类的用的是宽字符(两个字节的空间),c语言是美国人发明的,默认ASCII编码形式,所以要打印宽字符需要setlocale函数来进行本地化,具体用法如下
setlocale(LC_ALL, "");
头文件是<locale.h>,LC_ALL是针对所有项进行修改,具体有
• 数字量的格式 LC_NUMERIC
• 货币量的格式 LC_MONETARY • 字符集 LC_CTYPE
• ⽇期和时间的表⽰形式 LC_TIME •所有格式LC_ALL
""双引号表示的是默认本地化,vs编译器双引号里面不加空格,否则会本地化失败,也不会报错,反正就一直打印不出来宽字符。也可以写的具体点setlocale(LC_ALL, "zh_CN");或者指定编码格式setlocale(LC_ALL, "zh_CN.UTF-8");
setlocale(LC_ALL, NULL);是不进行任何操作,仅仅用来查看当前locale设置成了什么
设置完之后就可以着手打印地图了,需要注意的是宽字符只是横坐标x占两个字节,但是纵坐标依旧是一个字节,大致如下
所以一个方格横坐标相当于纵坐标的两倍,如果我想把墙体设置为正方形的话,横坐标是0到54,坐标最后到了56除二为28个格子,而纵坐标是1到27共27个格子,因为横坐标已经打印了0,所以纵坐标从1开始,加上0坐标的各种也28个格子
#define wall L'□'
宽字符打印格式是wprintf,我已经提前 把要作为墙体的宽字符方框□要宏定义了,方便修改#define wall L'□'
因为宽字符是占两个字节,所以它每次打印的横坐标x都必须是2的倍数(0也是2的倍数),所以每次i都要+2而不是往常的i++,而第一个横着的墙体纵坐标y是不变的,一直是0,只需要改变x就行
接下来打印最左边竖着的墙,x轴是不变的,y轴每次都要变,所以只需要改变y值就行了,y轴还是一个字节每次加1就行,第0行横坐标的时候已经打印了,所以从1开始打印
然后是最下面横着的墙打印,y轴是一直保持27不变的,竖着的墙坐标到27就停止了,所以跟它连接的横着的墙y轴从27开始。x轴因为要和竖着的行衔接,所以第0行其实已经被打印过了,所以从2开始,每次增加2(宽字符x轴占两个字节)
再然后最右边竖着的墙 ,x轴不变,打印最上面墙的时候条件是i<56,这里没有等号,所以到不了56,而每次i都加2,所以最右边竖着的墙从54开始往下打印就行。也就是x一直保持54,而y轴变化
,由于第一行和最后一行都已经打印过了,所以条件为int i = 1; i < 27; i++
再然后打印右侧的提示信息,这个自己设置好光标直接打印就行,最右的墙最后坐标是56,所以x轴坐标设置要大于56
蛇身节点以及食物节点初始化
我采用的是单链表作为蛇身节点和食物节点用的,所以我采用了结构体snakenode首先对蛇身节点和食物节点都进行初始化,其次贪食蛇整体不只有蛇身节点这一个属性,还有方向 游戏状态,食物分数,总分等多个属性,所以又用另外一个结构体snakegame来表示游戏的各种属性
snakenode节点只有两个成员横坐标x,纵坐标y
snakegame是维护整条贪食蛇的
因为游戏状态和方向比较多,所以用枚举方式一一表示了
蛇身的初始化
蛇身的初始化采用的是不带头结点的单链表的头插法,在调用初始化函数snakeInit之前就已经把头结点置为空了,然后循环建立节点cur,如果蛇链表的头结点为空,那么就让第一个cur作为头结点
if (ps->psnake == NULL)
{
cur->next = ps->psnake;//最后一个节点置空
ps->psnake = cur;
}
因为头插法第一个插入的节点是链表的最后一个,是需要置空的,而ps->psnake之前初始化的时已经置为空了,所以cur->next=ps->psnake;
如果头节点不为空,那么就将新建节点的next指向头结点,然后将ps->snake头节点指向新建立的节点,这样新建立的节点成了新的头节点
else
{
cur->next = ps->psnake;
ps->psnake = cur;
}
蛇身节点我是准备设置五个节点,因为每个宽字符占两个字节,所以i+=2,把cur的x坐标和y坐标设置,y的坐标设置为5不动,只改变x的坐标。cur的坐标设置完了之后用setpos函数在同样的坐标上打印身体宽字符图案
for循环的条件如下
for (int i = 0; i <10; i+=2)
cur坐标和打印身体如下
cur->x = 24 + i;
cur->y = 5;//设置cur的x和y坐标
setpos(cur->x, cur->y);//在cur坐标上打印蛇的身体
wprintf(L"%lc", BODY);
再然后顺便初始化一下贪食蛇的其他数据
ps->Dir = right;//方向
ps->foodweight = 10;//一个食物的分数
ps->score = 0;//总分
ps->sleeptime = 200;//休眠时间
ps->statues = ok;//游戏状态
整体蛇节点初始化的代码
void snakeInit(Snake* ps)
{
for (int i = 0; i <10; i+=2)
{
snakenode* cur = (snakenode*)malloc(sizeof(snakenode));
if (ps->psnake == NULL)
{
cur->next = ps->psnake;
ps->psnake = cur;
}
else
{
cur->next = ps->psnake;
ps->psnake = cur;
}
cur->x = 24 + i;
cur->y = 5;//设置cur的x和y坐标
setpos(cur->x, cur->y);//在cur坐标上打印蛇的身体
wprintf(L"%lc", BODY);
}
ps->Dir = right;//方向
ps->foodweight = 10;//一个食物的分数
ps->score = 0;//总分
ps->sleeptime = 200;//休眠时间
ps->statues = ok;//游戏状态
}//蛇身节点初始化
食物节点初始化
食物的位置是随机出现的,食物节点的初始化要用到随机函数生成随机数,请注意这个随机是针对节点的成员x和y来说的。创建随机x和y轴时要注意不能生成的节点在蛇身体的五个节点上,然后要在墙里面不能在墙上或者墙外面。其次y没什么要求,在墙内就行,而x必须是2的倍数,因为节点都采用了宽字符打印,如果不是2的倍数,有可能生成的食物半边在墙内,另外半边在墙外
int x; int y;
again:
do {
x = rand()%51 + 2;
y = rand()%26 + 1;//控制节点坐标生成在墙内
} while (x % 2 != 0);//宽字符x必须是2的倍数
snakenode* cur = ps->psnake;
while (cur)
{
if (x == cur->x &&y == cur->y)//判断生成的坐标是否是蛇身节点
goto again;//如果是蛇身节点那么就跳转回上面重新生成x,y
cur = cur->next;
}
对于rand函数,rand()%51是生成0到50之间的随机数,加2 就变成了生成2到52之间的随机数(包括52),因为如果坐标是0,那么就生成在竖着的墙上了,坐标是54就生成在最右边竖着的墙上了。所以范围是2到52。rand()%26 + 1也是同理
食物节点初始化完整代码
void foodInit(Snake* ps)
{
int x; int y;
again:
do {
x = rand()%51 + 2;
y = rand()%26 + 1;//控制节点坐标生成在墙内
} while (x % 2 != 0);//宽字符x必须是2的倍数
snakenode* cur = ps->psnake;
while (cur)
{
if (x == cur->x &&y == cur->y)//判断生成的坐标是否是蛇身节点
goto again;//如果是蛇身节点那么就跳转回上面重新生成x,y
cur = cur->next;
}
ps->pfood = (snakenode*)malloc(sizeof(snakenode));//pfood是维护食物节点的指针
ps->pfood->x = x; ps->pfood->y = y;
setpos(ps->pfood->x, ps->pfood->y);
wprintf(L"%lc", FOOD);
}
三.游戏运行阶段
这个阶段回去处理游戏按键与节点怎么对应起来以及游戏是怎么运行的
游戏按键的设置
如何将按键与游戏操作结合起来呢,将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。win32 API给了一个函数GetAsyncKeyState ,GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
贪食蛇游戏检查最低位是不是1就可以了,可以用GetAsyncKeyState的返回值按位&1就可以知道最低是1还是0了
GetAsyncKeyState函数有一个参数,即虚拟键码(Virtual Key Code),用于指定要检查状态按键。
贪食蛇游戏我们只需要用到上下左右,F3 F4 空格,esc退出就可以了
在代码开头写成宏方便操作#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
这个VK就是我们要检查的具体虚拟按键代码,比如要检查↑,那么就是 KEY_PRESS(VK_UP),具体虚拟按键表格如下
虚拟按键代码
虚拟按键 | 值 | 描述 |
---|---|---|
VK_LBUTTON |
0x01 | 鼠标左键 |
VK_RBUTTON |
0x02 | 鼠标右键 |
VK_CANCEL |
0x03 | 控制中断处理 |
VK_MBUTTON |
0x04 | 鼠标中键 |
VK_XBUTTON1 |
0x05 | X1 鼠标按钮 |
VK_XBUTTON2 |
0x06 | X2 鼠标按钮 |
- |
0x07 | 保留 |
VK_BACK |
0x08 | BACKSPACE 键 |
VK_TAB |
0x09 | TAB 键 |
- |
0x0A-0B | 保留 |
VK_CLEAR |
0x0C | CLEAR 键 |
VK_RETURN |
0x0D | ENTER键 |
- |
0x0E-0楼 | 未分配 |
VK_SHIFT |
0x10 | 换档键 |
VK_CONTROL |
0x11 | CTRL 键 |
VK_MENU |
0x12 | Alt 键 |
VK_PAUSE |
0x13 | 暂停键 |
VK_CAPITAL |
0x14 | CAPS LOCK 键 |
VK_KANA |
0x15 | IME 假名模式 |
VK_HANGUL |
0x15 | IME 韩文模式 |
VK_IME_ON |
0x16 | IME 开启 |
VK_JUNJA |
0x17 | IME Junja 模式 |
VK_FINAL |
0x18 | IME 最终模式 |
VK_HANJA |
0x19 | IME 汉字模式 |
VK_KANJI |
0x19 | IME 汉字模式 |
VK_IME_OFF |
0x1A | IME 关闭 |
VK_ESCAPE |
0x1B | 电调键 |
VK_CONVERT |
0x1C | IME 转换 |
VK_NONCONVERT |
0x1D | IME 非转换 |
VK_ACCEPT |
0x1E | IME 接受 |
VK_MODECHANGE |
0x1F | IME 模式更改请求 |
VK_SPACE |
0x20 | 空格键 |
VK_PRIOR |
0x21 | PAGE UP 键 |
VK_NEXT |
0x22 | PAGE DOWN 键 |
VK_END |
0x23 | END 键 |
VK_HOME |
0x24 | HOME键 |
VK_LEFT |
0x25 | 向左箭头键 |
VK_UP |
0x26 | 向上箭头键 |
VK_RIGHT |
0x27 | 向右箭头键 |
VK_DOWN |
0x28 | 向下箭头键 |
VK_SELECT |
0x29 | SELECT 键 |
VK_PRINT |
0x2A | PRINT 密钥 |
VK_EXECUTE |
0x2B | EXECUTE 键 |
VK_SNAPSHOT |
0x2C | PRINT SCREEN 键 |
VK_INSERT |
0x2D | INS 密钥 |
VK_DELETE |
0x2E | DEL键 |
VK_HELP |
0x2F | HELP 键 |
0x30 | 0 键 | |
0x31 | 1 键 | |
0x32 | 2 键 | |
0x33 | 3键 | |
0x34 | 4键 | |
0x35 | 5键 | |
0x36 | 6键 | |
0x37 | 7键 | |
0x38 | 8键 | |
0x39 | 9键 | |
- |
0x3A-40 | 定义 |
0x41 | 一把钥匙 | |
0x42 | B键 | |
0x43 | C键 | |
0x44 | D键 | |
0x45 | E键 | |
0x46 | F键 | |
0x47 | G键 | |
0x48 | H键 | |
0x49 | I 键 | |
0x4A | J 键 | |
0x4B | K键 | |
0x4C | L键 | |
0x4D | M键 | |
0x4E | N键 | |
0x4F | O键 | |
0x50 | P键 | |
0x51 | Q键 | |
0x52 | R键 | |
0x53 | S 键 | |
0x54 | T键 | |
0x55 | U键 | |
0x56 | V键 | |
0x57 | W键 | |
0x58 | X键 | |
0x59 | Y 键 | |
0x5A | Z键 | |
VK_LWIN |
0x5B | 左 Windows 键 |
VK_RWIN |
0x5C | 右 Windows 键 |
VK_APPS |
0x5D | 应用程序密钥 |
- |
0x5E | 保留 |
VK_SLEEP |
0x5F | 计算机睡眠键 |
VK_NUMPAD0 |
0x60 | 数字键盘 0 键 |
VK_NUMPAD1 |
0x61 | 数字键盘 1 键 |
VK_NUMPAD2 |
0x62 | 数字键盘 2 键 |
VK_NUMPAD3 |
0x63 | 数字键盘 3 键 |
VK_NUMPAD4 |
0x64 | 数字键盘 4 键 |
VK_NUMPAD5 |
0x65 | 数字键盘 5 键 |
VK_NUMPAD6 |
0x66 | 数字键盘 6 键 |
VK_NUMPAD7 |
0x67 | 数字键盘 7 键 |
VK_NUMPAD8 |
0x68 | 数字键盘 8 键 |
VK_NUMPAD9 |
0x69 | 数字键盘 9 键 |
VK_MULTIPLY |
0x6A | 乘法键 |
VK_ADD |
0x6B | 添加密钥 |
VK_SEPARATOR |
0x6C | 分隔键 |
VK_SUBTRACT |
0x6D | 减去键 |
VK_DECIMAL |
0x6E | 十进制键 |
VK_DIVIDE |
0x6F | 分割键 |
VK_F1 |
0x70 | F1 键 |
VK_F2 |
0x71 | F2 键 |
VK_F3 |
0x72 | F3 键 |
VK_F4 |
0x73 | F4 键 |
VK_F5 |
0x74 | F5 键 |
VK_F6 |
0x75 | F6 键 |
VK_F7 |
0x76 | F7 键 |
VK_F8 |
0x77 | F8 键 |
VK_F9 |
0x78 | F9 键 |
VK_F10 |
0x79 | F10 键 |
VK_F11 |
0x7A | F11 键 |
VK_F12 |
0x7B | F12 键 |
VK_F13 |
0x7C | F13 键 |
VK_F14 |
2岳 | F14 键 |
VK_F15 |
0x7E | F15 键 |
VK_F16 |
0x7F | F16 键 |
VK_F17 |
0x80 | F17 键 |
VK_F18 |
0x81 | F18 键 |
VK_F19 |
0x82 | F19 键 |
VK_F20 |
0x83 | F20 键 |
VK_F21 |
0x84 | F21 键 |
VK_F22 |
0x85 | F22 键 |
VK_F23 |
0x86 | F23 键 |
VK_F24 |
0x87 | F24 键 |
- |
0x88-8楼 | 保留 |
VK_NUMLOCK |
0x90 | NUM LOCK 键 |
VK_SCROLL |
0x91 | SCROLL LOCK键 |
- |
0x92-96 | OEM 特定 |
- |
0x97-9楼 | 未分配 |
VK_LSHIFT |
0xA0 | 左 SHIFT 键 |
VK_RSHIFT |
0xA1 | 右 SHIFT 键 |
VK_LCONTROL |
0xA2 | 左 CONTROL 键 |
VK_RCONTROL |
0xA3 | 右 CONTROL 键 |
VK_LMENU |
0xA4 | 左 Alt 键 |
VK_RMENU |
0xA5 | 右 Alt 键 |
VK_BROWSER_BACK |
0xA6 | 浏览器后退键 |
VK_BROWSER_FORWARD |
0xA7 | 浏览器转发键 |
VK_BROWSER_REFRESH |
0xA8 | 浏览器刷新键 |
VK_BROWSER_STOP |
0xA9 | 浏览器停止键 |
VK_BROWSER_SEARCH |
0xAA | 浏览器搜索键 |
VK_BROWSER_FAVORITES |
0xAB | 浏览器收藏夹键 |
VK_BROWSER_HOME |
0xAC | 浏览器"开始"和"主页"键 |
VK_VOLUME_MUTE |
0xAD | 音量静音键 |
VK_VOLUME_DOWN |
0xAE | 降低音量键 |
VK_VOLUME_UP |
0xAF | 音量调高键 |
VK_MEDIA_NEXT_TRACK |
0xB0 | 下一曲目键 |
VK_MEDIA_PREV_TRACK |
0xB1 | 上一页 Track 键 |
VK_MEDIA_STOP |
0xB2 | 停止媒体键 |
VK_MEDIA_PLAY_PAUSE |
0xB3 | 播放/暂停媒体键 |
VK_LAUNCH_MAIL |
0xB4 | 启动邮件密钥 |
VK_LAUNCH_MEDIA_SELECT |
0xB5 | 选择媒体密钥 |
VK_LAUNCH_APP1 |
0xB6 | 启动应用程序 1 键 |
VK_LAUNCH_APP2 |
0xB7 | 启动应用程序 2 键 |
- |
0xB8-B9型 | 保留 |
VK_OEM_1 |
0xBA | 用于杂项字符;它可能因键盘而异。对于美标键盘,按键;: |
VK_OEM_PLUS |
0xBB | 对于任何国家/地区,关键+ |
VK_OEM_COMMA |
0xBC | 对于任何国家/地区,关键, |
VK_OEM_MINUS |
0xBD | 对于任何国家/地区,关键- |
VK_OEM_PERIOD |
0xBE | 对于任何国家/地区,关键. |
VK_OEM_2 |
0xBF | 用于杂项字符;它可能因键盘而异。对于美标键盘,按键/? |
VK_OEM_3 |
0xC0 | 用于杂项字符;它可能因键盘而异。对于美标键盘,按键```~`` |
- |
0xC1-DA | 保留 |
VK_OEM_4 |
0xDB | 用于杂项字符;它可能因键盘而异。对于美标键盘,按键[{ |
VK_OEM_5 |
0xDC | 用于杂项字符;它可能因键盘而异。对于美标键盘,按键\| |
VK_OEM_6 |
0xDD | 用于杂项字符;它可能因键盘而异。对于美标键盘,按键]} |
VK_OEM_7 |
0xDE | 用于杂项字符;它可能因键盘而异。对于美标键盘,按键'" |
VK_OEM_8 |
0xDF | 用于杂项字符;它可能因键盘而异。 |
- |
0xE0 | 保留 |
- |
0xE1 | OEM 特定 |
VK_OEM_102 |
0xE2 | 美国标准键盘上的键,或非美国 102 键键盘上的键<>``\| |
- |
0xE3-E4型 | OEM 特定 |
VK_PROCESSKEY |
0xE5 | IME PROCESS 密钥 |
- |
0xE6 | OEM 特定 |
VK_PACKET |
0xE7 | 用于传递 Unicode 字符,就好像它们是击键一样。键是用于非键盘输入法的 32 位虚拟键值的低位字。有关详细信息,请参阅 KEYBDINPUT、SendInput、WM_KEYDOWN 和 WM_KEYUP 中的备注VK_PACKET |
- |
0xE8 | 未分配 |
- |
0xE9-F5型 | OEM 特定 |
VK_ATTN |
0xF6 | 收件人键 |
VK_CRSEL |
0xF7 | CrSel 密钥 |
VK_EXSEL |
0xF8 | ExSel 密钥 |
VK_EREOF |
0xF9 | 擦除EOF密钥 |
VK_PLAY |
0xFA | 播放键 |
VK_ZOOM |
0xFB | 缩放键 |
VK_NONAME |
0xFC | 保留 |
VK_PA1 |
0xFD | PA1 密钥 |
VK_OEM_CLEAR |
0xFE | 清除键 |
对于左键,如果按了左键还要判断现在方向是不是朝右,然后才能把方向改为左边,因为朝右是绝对改变不了方向为左边的
if (KEY_PRESS(VK_LEFT) && ps->Dir != right)
ps->Dir = left;
对于右键,如果按了右键还要判断现在方向是不是朝左,然后才能把方向改为右边,因为朝左是绝对改变不了方向为右边的
if (KEY_PRESS(VK_RIGHT) && ps->Dir != left)
ps->Dir = right;
方向为上和下也是同理,不能产生冲突
if (KEY_PRESS(VK_UP) && ps->Dir != down)
ps->Dir = up;
if (KEY_PRESS(VK_DOWN) && ps->Dir != up)
ps->Dir = down;
具体方向键的移动另外做了一个函数snakemove(ps);
方向键设置完了,现在设置功能键
我是打算把空格键设置为暂停,Sleep是修眠函数,可以设置一个死循环,如果按了空格键就进入循环休眠,再按一次空格键就break跳出循环停止休眠。这样就达到暂停的效果了
if (KEY_PRESS(VK_SPACE))
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
break;
}
}
然后是退出ESC键,只需要在按这个键的时候,把状态从ok改为end就行了
if (KEY_PRESS(VK_ESCAPE))
ps->statues = end;
然后是F3加速键,因为预先设定了sleeptime休眠时间等于200,所以每按一次休眠时间都要减少,以达到加速的效果。同时越加速食物的分数也应该增加。还要设定一个范围,这个自己定,我设置的是不超过50
if (KEY_PRESS(VK_F3))
{
if (ps->sleeptime >=50)
{
ps->sleeptime -= 20;
ps->foodweight += 2;
}
}
然后是F4减速键,一个要考虑减速,然后要考虑食物的分数是越来越少的,但是不能低于0
if (KEY_PRESS(VK_F4))
{
if (ps->sleeptime < 350)
{
ps->sleeptime += 20;
ps->foodweight -= 2;
if (ps->sleeptime >= 350)
{
ps->foodweight = 1;
}
}
}
snakemove移动函数
上面解决了按键问题,现在来解决一下按键对应的移动。先新建一个nextnode节点,这是下一步产生的节点
对于按了左键来说(一般此时都是向上或者向下的状态按左键),nextnode的x坐标要蛇头节点x-2,因为每次都是蛇头移动,而纵坐标y是保持不变的。
if (ps->Dir == left)
{
nextnode->x = ps->psnake->x - 2;
nextnode->y = ps->psnake->y;
}
对于按了右键来说(一般此时也都是向上或者向下的状态按右键),而nextnode的x坐标是加2,y轴依旧是不变的
if (ps->Dir == right)
{
nextnode->x = ps->psnake->x + 2;
nextnode->y = ps->psnake->y;
}
对于按了上键来说(一般此时蛇方向是向左或者向右状态),此时nextnode的x轴是不变的,y轴-1就可以了
if (ps->Dir == up)
{
nextnode->y = ps->psnake->y - 1;
nextnode->x = ps->psnake->x;
}
对于按了下键来说(一般此时蛇方向是向左或者向右状态),此时nextnode的x轴是不变的,y轴+1就可以了
if (ps->Dir == down)
{
nextnode->y = ps->psnake->y + 1;
nextnode->x = ps->psnake->x;
}
解决了下一个节点的坐标问题,然后来考虑下一个节点正好是食物节点以及不是食物节点的情况。判断是否是食物节点很好判断,直接if (nextnode->x == ps->pfood->x && nextnode->y == ps->pfood->y)
对于下一个节点是食物,我另外做了一个函数eatfood();如果下一个节点是食物,那直接头插法插进蛇身链表就可以了,同时蛇头变为这个食物节点,然后以新的食物节点作为头结点打印蛇身。 完了之后,因为吃了食物,所以总分数score要加上食物的分数foodweight。最后删除原有的食物节点,重新初始化一个食物节点
void eatfood(Snake* ps, snakenode* nextnode)
{
nextnode->next= ps->psnake;
ps->psnake =nextnode;
snakenode* cur = ps->psnake;//头插食物节点
while (cur)
{
setpos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;//打印蛇身
}
ps->score += ps->foodweight;//分数更新
free(ps->pfood);//释放旧的食物节点
foodInit(ps);//新建食物节点
}//吃食物
对于下一个位置不是食物节点,首先下一个位置也要成为新头节点,然后打印蛇身,但是最后一个节点要打印空格,这样看上去就像删除了最后一个节点一样。
void notfood(Snake*ps, snakenode*nextnode)
{
nextnode->next = ps->psnake;
ps->psnake = nextnode;
snakenode* cur = ps->psnake;
while (cur->next->next)//找到最后一个节点
{
setpos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
setpos(cur->next->x, cur->next->y);
printf(" ");//这个空格是两个空格
free(cur->next);
cur->next = NULL;
}
撞墙机制
因为我之前已经枚举了游戏状态,有一个状态是killbywall,所以只有判断蛇头是不是和四面墙的坐标重叠,你也可以是刚碰到墙不是重叠就判断撞墙
void _killbywall(Snake* ps)
{
if (ps->psnake->x== 0 || ps->psnake->x == 54 || ps->psnake->y == 0 || ps->psnake->y == 27)
ps->statues = killbywall;
}//撞墙
咬到自己机制
同样也是用蛇头去判断,我选取的是蛇头的next节点以及之后的节点,作为咬到的判断条件。然后把状态改成killbyself
void _killbyself(Snake* ps)
{
snakenode* cur = ps->psnake->next;
while (cur)
{
if (ps->psnake->x == cur->x && ps->psnake->y == cur->y)
{
ps->statues = killbyself;
break;
}
cur = cur->next;
}
}//咬到自己
整个游戏的运行阶段就结束了,因为F3和F4按键是运行阶段设置的,所以我把打印分数和食物分数放到了 gamerun(Snake* ps)函数里,同时要注意的是每次按键都要设置休眠时间,不然的话会卡顿。
游戏运行阶段的全部代码如下,从下往上看
//游戏运行阶段
void _killbywall(Snake* ps)
{
if (ps->psnake->x== 0 || ps->psnake->x == 54 || ps->psnake->y == 0 || ps->psnake->y == 27)
ps->statues = killbywall;
}//撞墙
void _killbyself(Snake* ps)
{
snakenode* cur = ps->psnake->next;
while (cur)
{
if (ps->psnake->x == cur->x && ps->psnake->y == cur->y)
{
ps->statues = killbyself;
break;
}
cur = cur->next;
}
}//咬到自己
void eatfood(Snake* ps, snakenode* nextnode)
{
nextnode->next= ps->psnake;
ps->psnake =nextnode;
snakenode* cur = ps->psnake;//头插食物节点
while (cur)
{
setpos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;//打印蛇身
}
ps->score += ps->foodweight;//分数更新
free(ps->pfood);//释放旧的食物节点
foodInit(ps);//新建食物节点
}//吃食物
void notfood(Snake*ps, snakenode*nextnode)
{
nextnode->next = ps->psnake;
ps->psnake = nextnode;
snakenode* cur = ps->psnake;
while (cur->next->next)//找到最后一个节点
{
setpos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
setpos(cur->next->x, cur->next->y);
printf(" ");//这个空格是两个空格
free(cur->next);
cur->next = NULL;
}
void snakemove(Snake* ps)
{
snakenode* nextnode = (snakenode*)malloc(sizeof(snakenode));
if (ps->Dir == left)
{
nextnode->x = ps->psnake->x - 2;
nextnode->y = ps->psnake->y;
}
if (ps->Dir == right)
{
nextnode->x = ps->psnake->x + 2;
nextnode->y = ps->psnake->y;
}
if (ps->Dir == up)
{
nextnode->y = ps->psnake->y - 1;
nextnode->x = ps->psnake->x;
}
if (ps->Dir == down)
{
nextnode->y = ps->psnake->y + 1;
nextnode->x = ps->psnake->x;
}
if (nextnode->x == ps->pfood->x && nextnode->y == ps->pfood->y)
eatfood(ps, nextnode);
else
{
notfood(ps, nextnode);
}
_killbywall(ps);
_killbyself(ps);
}
void gamerun(Snake* ps)
{
do
{
setpos(61, 8);
printf("总分:%3d", ps->score);
setpos(70, 8);
printf("食物的分数:%02d", ps->foodweight);
if (KEY_PRESS(VK_LEFT) && ps->Dir != right)
ps->Dir = left;
if (KEY_PRESS(VK_RIGHT) && ps->Dir != left)
ps->Dir = right;
if (KEY_PRESS(VK_UP) && ps->Dir != down)
ps->Dir = up;
if (KEY_PRESS(VK_DOWN) && ps->Dir != up)
ps->Dir = down;
if (KEY_PRESS(VK_SPACE))
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
break;
}
}
if (KEY_PRESS(VK_ESCAPE))
ps->statues = end;
if (KEY_PRESS(VK_F3))
{
if (ps->sleeptime >=50)
{
ps->sleeptime -= 20;
ps->foodweight += 2;
}
}
if (KEY_PRESS(VK_F4))
{
if (ps->sleeptime < 350)
{
ps->sleeptime += 20;
ps->foodweight -= 2;
if (ps->sleeptime >= 350)
{
ps->foodweight = 1;
}
}
}
Sleep(ps->sleeptime);
snakemove(ps);
} while (ps->statues == ok);
}
四.游戏结束阶段
游戏结束阶段就是收尾阶段,把各种死亡原因打印一下,告知为什么死的,然后释放蛇身节点
void endgame(Snake* ps)
{
if (ps->statues==end)
{
setpos(20, 13);
printf("您主动已经退出了游戏\n");
}
else if (ps->statues == killbyself)
{
setpos(20, 13);
printf("您咬到了自己\n");
}
else if (ps->statues == killbywall)
{
setpos(20, 13);
printf("您撞墙了\n");
}
while (ps->psnake)
{
snakenode* cur = ps->psnake->next;
free(ps->psnake);
ps->psnake = cur;
}
ps = NULL;
}//游戏结束阶段
然后我还在main那里加了一个循环以便于重新开始游戏
#include"贪食蛇的声明.h"
void text()
{
char ch = 0;
do
{
setlocale(LC_ALL, "");
gamestart();
Snake ps = {0};//里面的成员先全赋值为0
snakeInit(&ps);
foodInit(&ps);
gamerun(&ps);
endgame(&ps);
setpos(20, 15);
printf("再来一局吗?(Y/N):");//Y是重新开始
scanf("%c", &ch);
getchar();// 清理\n
} while (ch == 'Y' || ch == 'y');
}
int main()
{
srand((unsigned int)time(NULL));
text();
}
五.贪食蛇完整代码
测试.c文件代码
#include"贪食蛇的声明.h"
void text()
{
char ch = 0;
do
{
setlocale(LC_ALL, "");
gamestart();
Snake ps = {0};//里面的成员先全赋值为0
snakeInit(&ps);
foodInit(&ps);
gamerun(&ps);
endgame(&ps);
setpos(20, 15);
printf("再来一局吗?(Y/N):");//Y是重新开始
scanf("%c", &ch);
getchar();// 清理\n
} while (ch == 'Y' || ch == 'y');
}
int main()
{
srand((unsigned int)time(NULL));
text();
}
贪食蛇的声明.h文件
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
#include<stdio.h>
#include<windows.h>
#include<stdbool.h>
#include<locale.h>
#include<time.h>
#define wall L'□'
#include<stdlib.h>
#define BODY L'■'
#define FOOD L'★'
void setpos(int x, int y);
void gamestart();
enum statue
{
ok = 1,
end,
killbywall,//撞墙
killbyself//咬到自己
};
enum direction
{
up=1,
down,
left,
right
};
typedef struct snakenode
{
int x;
int y;
struct snakenode* next;
}snakenode;//蛇身节点和食物节点
typedef struct snakegame
{
snakenode* psnake;//蛇身体节点
snakenode *pfood;//食物节点
int score;//总分
int foodweight;//食物分数
enum statue statues;//游戏状态
enum direction Dir;//方向
int sleeptime;//睡眠时间
}Snake;//维护蛇的结构体
void snakeInit(Snake* ps);
void foodInit(Snake* ps);
void gamerun(Snake* ps);
void snakemove(Snake* ps);
void endgame(Snake* ps);
贪食蛇的实现.h代码
#include"贪食蛇的声明.h"
void setpos(int x, int y)
{
COORD pos = { x,y };
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(houtput, pos);
}
void welcome()
{
setpos(40, 10);
printf("欢迎来到贪食蛇小游戏\n");
setpos(43, 15);
system("pause");
system("cls");//清屏
setpos(38, 10);
printf("用 ↑ ↓ → ←键操作蛇\n");
setpos(38, 13);
printf("f3表示加速,f4表示减速,加速能得到更多的分数");
setpos(38, 18);
system("pause");
system("cls");
}
void createmap()
{
for (int i = 0; i < 56; i += 2)
{
setpos(i, 0);
wprintf(L"%lc", wall);
}//打印第一个横着的墙
for (int i = 1; i < 28; i++)
{
setpos(0, i);
wprintf(L"%lc", wall);
}//打印最左边竖着的墙
for (int i = 2; i < 56; i += 2)
{
setpos(i, 27);
wprintf(L"%lc", wall);
}//最下面横着的墙
for (int i = 1; i < 27; i++)
{
setpos(54, i);
wprintf(L"%lc", wall);
}
setpos(61, 10);
printf("用 ↑ ↓ → ←键操作蛇\n");
setpos(61, 12);
printf("f3表示加速,f4表示减速\n");
setpos(61, 15);
printf("加速能得到更多的分数\n");//打印墙体旁边的提示信息
setpos(61, 17);
printf("esc键退出游戏\n");
setpos(61, 19);
printf("空格键暂停\n");
}
void gamestart()
{
system("title 贪食蛇");
system("mode con cols=100 lines=30");
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO Info;
GetConsoleCursorInfo(houtput, &Info);//获取光标控制台信息
Info.bVisible = 0;//隐藏光标
SetConsoleCursorInfo(houtput, &Info);//把修改后的结果设置回去
welcome();
createmap();
}
void snakeInit(Snake* ps)
{
for (int i = 0; i <10; i+=2)
{
snakenode* cur = (snakenode*)malloc(sizeof(snakenode));
if (ps->psnake == NULL)
{
cur->next = ps->psnake;
ps->psnake = cur;
}
else
{
cur->next = ps->psnake;
ps->psnake = cur;
}
cur->x = 24 + i;
cur->y = 5;//设置cur的x和y坐标
setpos(cur->x, cur->y);//在cur坐标上打印蛇的身体
wprintf(L"%lc", BODY);
}
ps->Dir = right;//方向
ps->foodweight = 10;//一个食物的分数
ps->score = 0;//总分
ps->sleeptime = 200;//休眠时间
ps->statues = ok;//游戏状态
}//蛇身节点初始化
void foodInit(Snake* ps)
{
int x; int y;
again:
do {
x = rand()%51 + 2;
y = rand()%26 + 1;//控制节点坐标生成在墙内
} while (x % 2 != 0);//宽字符x必须是2的倍数
snakenode* cur = ps->psnake;
while (cur)
{
if (x == cur->x &&y == cur->y)//判断生成的坐标是否是蛇身节点
goto again;//如果是蛇身节点那么就跳转回上面重新生成x,y
cur = cur->next;
}
ps->pfood = (snakenode*)malloc(sizeof(snakenode));//pfood是维护食物节点的指针
ps->pfood->x = x; ps->pfood->y = y;
setpos(ps->pfood->x, ps->pfood->y);
wprintf(L"%lc", FOOD);
}//食物节点初始化
//游戏运行阶段
void _killbywall(Snake* ps)
{
if (ps->psnake->x== 0 || ps->psnake->x == 54 || ps->psnake->y == 0 || ps->psnake->y == 27)
ps->statues = killbywall;
}//撞墙
void _killbyself(Snake* ps)
{
snakenode* cur = ps->psnake->next;
while (cur)
{
if (ps->psnake->x == cur->x && ps->psnake->y == cur->y)
{
ps->statues = killbyself;
break;
}
cur = cur->next;
}
}//咬到自己
void eatfood(Snake* ps, snakenode* nextnode)
{
nextnode->next= ps->psnake;
ps->psnake =nextnode;
snakenode* cur = ps->psnake;//头插食物节点
while (cur)
{
setpos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;//打印蛇身
}
ps->score += ps->foodweight;//分数更新
free(ps->pfood);//释放旧的食物节点
foodInit(ps);//新建食物节点
}//吃食物
void notfood(Snake*ps, snakenode*nextnode)
{
nextnode->next = ps->psnake;
ps->psnake = nextnode;
snakenode* cur = ps->psnake;
while (cur->next->next)//找到最后一个节点
{
setpos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
setpos(cur->next->x, cur->next->y);
printf(" ");//这个空格是两个空格
free(cur->next);
cur->next = NULL;
}
void snakemove(Snake* ps)
{
snakenode* nextnode = (snakenode*)malloc(sizeof(snakenode));
if (ps->Dir == left)
{
nextnode->x = ps->psnake->x - 2;
nextnode->y = ps->psnake->y;
}
if (ps->Dir == right)
{
nextnode->x = ps->psnake->x + 2;
nextnode->y = ps->psnake->y;
}
if (ps->Dir == up)
{
nextnode->y = ps->psnake->y - 1;
nextnode->x = ps->psnake->x;
}
if (ps->Dir == down)
{
nextnode->y = ps->psnake->y + 1;
nextnode->x = ps->psnake->x;
}
if (nextnode->x == ps->pfood->x && nextnode->y == ps->pfood->y)
eatfood(ps, nextnode);
else
{
notfood(ps, nextnode);
}
_killbywall(ps);
_killbyself(ps);
}
void gamerun(Snake* ps)
{
do
{
setpos(61, 8);
printf("总分:%3d", ps->score);
setpos(70, 8);
printf("食物的分数:%02d", ps->foodweight);
if (KEY_PRESS(VK_LEFT) && ps->Dir != right)
ps->Dir = left;
if (KEY_PRESS(VK_RIGHT) && ps->Dir != left)
ps->Dir = right;
if (KEY_PRESS(VK_UP) && ps->Dir != down)
ps->Dir = up;
if (KEY_PRESS(VK_DOWN) && ps->Dir != up)
ps->Dir = down;
if (KEY_PRESS(VK_SPACE))
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
break;
}
}
if (KEY_PRESS(VK_ESCAPE))
ps->statues = end;
if (KEY_PRESS(VK_F3))
{
if (ps->sleeptime >=50)
{
ps->sleeptime -= 20;
ps->foodweight += 2;
}
}
if (KEY_PRESS(VK_F4))
{
if (ps->sleeptime < 350)
{
ps->sleeptime += 20;
ps->foodweight -= 2;
if (ps->sleeptime >= 350)
{
ps->foodweight = 1;
}
}
}
Sleep(ps->sleeptime);
snakemove(ps);
} while (ps->statues == ok);
}
void endgame(Snake* ps)
{
if (ps->statues==end)
{
setpos(20, 13);
printf("您主动已经退出了游戏\n");
}
else if (ps->statues == killbyself)
{
setpos(20, 13);
printf("您咬到了自己\n");
}
else if (ps->statues == killbywall)
{
setpos(20, 13);
printf("您撞墙了\n");
}
while (ps->psnake)
{
snakenode* cur = ps->psnake->next;
free(ps->psnake);
ps->psnake = cur;
}
ps = NULL;
}//游戏结束阶段