c语言贪食蛇游戏

演示视频

目录

一.概述

二.游戏开始前

修改控制台程序标题和大小

[Win32 API](#Win32 API)

GetStdHandle函数

GetConsoleCursorInfo函数和SetConsoleCursorInfo函数

SetConsoleCursorPosition函数

游戏开篇界面处理

创建地图

蛇身节点以及食物节点初始化

蛇身的初始化

整体蛇节点初始化的代码

食物节点初始化

食物节点初始化完整代码

三.游戏运行阶段

游戏按键的设置

虚拟按键代码

snakemove移动函数

撞墙机制

咬到自己机制

游戏运行阶段的全部代码如下,从下往上看

五.贪食蛇完整代码

测试.c文件代码

贪食蛇的声明.h文件

贪食蛇的实现.h代码


一.概述

贪食蛇游戏设计,分为游戏开始前和游戏运行以及游戏结束三个阶段,我这个是利用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 位虚拟键值的低位字。有关详细信息,请参阅 KEYBDINPUTSendInputWM_KEYDOWNWM_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;
}//游戏结束阶段
相关推荐
m0_748251721 天前
Android webview 打开本地H5项目(Cocos游戏以及Unity游戏)
android·游戏·unity
gantengsheng1 天前
基于51单片机和OLED12864的小游戏《贪吃蛇》
单片机·嵌入式硬件·游戏·51单片机
264玫瑰资源库1 天前
从零开始C++棋牌游戏开发之第三篇:游戏的界面布局设计
开发语言·c++·python·游戏·pygame·源代码管理
264玫瑰资源库1 天前
从零开始C++游戏开发之第七篇:游戏状态机与回合管理
开发语言·c++·游戏
windwind20002 天前
游戏关卡设计方法的杂感
游戏·关卡设计
白乐天_n2 天前
腾讯游戏安全移动赛题Tencent2016A
安全·游戏
这是我582 天前
C++打小怪游戏
c++·其他·游戏·visual studio·小怪·大型·怪物
tealcwu2 天前
【游戏设计原理】21 - 解谜游戏的设计
游戏·游戏策划
清梦20202 天前
经典问题---跳跃游戏II(贪心算法)
算法·游戏·贪心算法
tealcwu2 天前
【游戏设计原理】22 - 石头剪刀布
游戏·游戏策划