C语言——贪吃蛇的实现

目录

一.项目基本介绍

1.win32API介绍

2.控制台窗口控制

3.结构体COORD

4.GetStdHandle函数

5.GetConsoleCursorInfo函数

6.CONSOLE_CURSOR_INFO结构体

7.SetConsoleCursorInfo函数

8.SetConsoleCursorPosition函数

9.GetAsyncKeyState函数

10.总结

1.结构体COORD和SetConsoleCursorPosition函数

2.CONSOLE_CURSOR_INFO结构体、GetConsoleCursorInfo、SetConsoleCursorInfo

3.GetAsyncKeyState

二.项目的具体实现

1.地图绘制

1.欢迎界面大体概况

2.第一个欢迎界面

3.第二个欢迎界面

4.第四个欢迎界面

setlocale函数

如何进行宽字符定义和打印呢?

地图参考图

蛇的设计

注:

生成食物

5.贪吃蛇运行操作的实现

实时检测键位

暂停

贪吃蛇的移动

问题修改1

判断是否撞墙和撞到自己

撞墙

撞到自己

隐藏光标

结束游戏

6.结语

7.完整代码

snake.h文件

snake.c文件

test.c文件


贪吃蛇、俄罗斯方块、扫雷游戏等都是编程学习过程中的经典游戏,今天练习的是贪吃蛇。

通过这个项目,我们想要实现怎样的一个效果呢?

1.贪吃蛇地图绘制
2.蛇吃食物的功能(通过键盘上、下、左、右按键改变蛇的动作)
3.蛇撞墙死亡
4.蛇撞自身死亡
5.得分计算
6.蛇的加速、减速
7.暂停游戏
8.退出游戏

所有相关语法均基于前面学习的链表、自定义类型等,另外加上win32API的部分语法

1.win32API介绍

Windows 这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外, 它同时也是⼀个很⼤ 的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程序达到开启 视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application), 所以便 称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows 32位平台的应⽤程序编程接⼝。

用大白话说就是windows系统自带的现成功能函数,专门给程序员调用用的。

2.控制台窗口控制

我们平时运行程序输出到屏幕上的黑框就是控制台

而C语言里面的函数system的参数几乎等于cmd里面运行的命令

注意:这里只能调控制台的窗口大小,无法调终端的控制台大小,想要实现这个功能,我们一点要把终端打印换成控制台打印

这个叫做控制台

这个是终端,一定要在设置里面把终端改为控制台

现在就可以对控制台执行窗口大小控制

这里用到一个命令:mode con cols=100 lines=30

表示30行,100列。那么这个行和列又是怎么算的呢?从哪开始呢?

其实windows本身一直在悄悄运行这个命令,有一个默认值使得控制台大小一直都是这么大,现在我们来通过代码控制行和列,需要注意的是,控制台的单行高和单列宽并不等大,两列的宽度才近似等于 一个行高 。

我们设置成20列20行就变成了这样,如果设置的值太小,系统会不允许。如

也可以通过命令修改控制台的名字,当程序运行结束时名字会恢复到原名,所以想观察中间控制台名字是否发生了变化,需要用函数来暂停或延迟程序的运行时间

复制代码
title 名字

以贪吃蛇为例子

当然暂停也可以使用

复制代码
system("pause");
_getch();
sleep();

3.结构体COORD

COORD是windows内置的结构体,用于控制控制台的光标的坐标,但是在使用的时候要带前下划线

复制代码
struct _COORD{
SHORT X;
SHORT Y;
};

为什么里面的类型会是大写呢?因为在微软的内置函数和类型里面,几乎把原基本类型用自定的方式改写了

复制代码
SHORT = short
LONG = long
DWORD = unsigned long
BOOL = int
HANDLE = void*

4.GetStdHandle函数

GetStdHandle函数也属于win32API中的一个,这个函数这么看就易懂了

Get是拿到、取到

Std是standard 意为标准

handle是手柄的意思

连起来就是取得一个标准句柄或手柄的意思,它的作用就是从系统取得一个手柄,手柄是用来干嘛的?控制用的对吧,再加上一个参数,就是如何控制系统,更准确的说是取得控制控制台的权限

复制代码
HANDLE GetStdHandle(DWORD nStdHandle);

nStdHandle有这三种类型

复制代码
STD_OUTPUT_HANDLE //标准输出手柄
STD_INPUT_HANDLE //标准输入手柄
STD_ERROR_HANDLE //标准错误输出手柄

那么输入输出是相对于内存或程序来说的,我们用键盘输入信息,有数据要进入程序,就是输入,让数据从程序出来就是输出。程序从键盘获得的输入就叫做标准输入(STD_INPUT_HANDLE),把信息输出到屏幕上就叫做标准输出(STD_OUTPUT_HANDLE),把错误信息输出到屏幕上就叫做标准错误输出(STD_ERROR_HANDLE)。

5.GetConsoleCursorInfo函数

Get:取得

Console:控制台

Cursor:光标

Info:是information的缩写,译为信息

所以GetConsoleCursorInfo就是用来获取控制台光标信息

复制代码
BOOL WINAPI GetConsoleCursorInfo
(
 HANDLE hConsoleOutput,
 PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

这里的WINAPI不属于数据类型,它是一种调用约定,告诉编译器------这是windows系统的函数,在声明函数时必须要用,以上每个系统函数在声明时都带有的,只是全部在windows.h头文件里面已经声明

6.CONSOLE_CURSOR_INFO结构体

上面说GetConsoleCursorInfo是获取控制台光标信息的,那控制台光标信息就可以通过CONSOLE_SURSOR_INFO结构体来自定义改变,这个结构体是属于windows内置结构体

在头文件中声明如下:

复制代码
typedef struct _CONSOLE_CURSOR_INFO {
 DWORD dwSize;
 BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

dwSize:double word size,用双字类型表示大小。

bVisible:bool visible,bool表示false或true,visible译为可见的。

  • DWORD dwsize控制的是光标多粗,多高,值在1~100之间,如果值为1,光标就变成了一条细线;值为50就变成半高的方块;值为50就变成整块方块。
  • BOOL bVisible控制光标的显示与隐藏,bvisible为true表示光标可见;bVisible为false表示光标不可见。

7.SetConsoleCursorInfo函数

SetConsoleCursorInfo是把光标设置为我想要的样子,区别于GetConsoleCursorInfo,GetConsoleCursorInfo是查看我的光标信息,而CONSOLE_SURSOR_INFO结构体是设置我的光标信息,但仅仅是改而没有生效,SetConsoleCursorInfo就是让设置的光标信息生效。

在头文件中的定义如下:

复制代码
BOOL WINAPI SetConsoleCursorInfo(
 HANDLE hConsoleOutput,
 const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);

8.SetConsoleCursorPosition函数

SetConsoleCursorPosition函数是设置光标在输出屏幕的控制台中的位置,前面的结构体COORD是填写光标的坐标信息,但是没有生效,仅仅是填写而已,就好比你写了一个程序代码,想让代码生效就得靠相关的编译环境。

在头文件中的定义如下:

复制代码
BOOL WINAPI SetConsoleCursorPosition(
 HANDLE hConsoleOutput,
 COORD pos
);

9.GetAsyncKeyState函数

  • Get:取得、获得
  • Async:全写asynchronous,译为 异步的
  • Key:按键
  • State:状态
  • 直译:获取按键异步状态

实时检测某一个按键是否被按下,检测的那个按键只有0和1两种状态(即没按和按下),如果是长按,会和瞬间按下是同样的状态,均为1。

GetAsyncKeyState函数并不会像scanf函数那样等待你按下按键才会走,它是一直往下运行,如果你输入了他就检测到,没输入就和不存在一样对程序的运行没有任何阻碍。

在头文件的定义如下

复制代码
SHORT WINAPI GetAsyncKeyState(
  int vKey   // 按键虚拟键码(如 VK_UP、'A'、0x30)
);

返回类型属于短整型,16位,最高位为15位。

如果最高位为1:说明检测的按键正在被按住

如果最高位为0:说明检测按键已经松开或者未按

在判断按键是否被按住时,于相应的进制进行比较即可

复制代码
二进制位编号:
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0

0x8000 = 二进制  1000 0000 0000 0000
                ↑
              第15位

win32-虚拟键码https://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes

在这里我们可以得到键盘上所有键的虚拟值,用来代表对应的键位,如我把0x48传入,对应的就是H键。

10.总结

前面介绍了这么多函数,我们来联合运用,理解他们之间的相互联系

介绍GetStdHandle函数时我们说,它是对控制台进行操作时获取权限的函数,这个要区别于system里面的命令,system调用是不需要windows.h头文件的,所以它不需要GetStdHandle函数来获取操作权限

如上,我们已经注释了windows.h头文件,依然可以运行改控制台名的命令。

1.结构体COORD和SetConsoleCursorPosition函数

我们通过COORD自定义光标位置,SetConsoleCursor来使其生效,打印时就会从光标设置的位置开始打印。

2.CONSOLE_CURSOR_INFO结构体、GetConsoleCursorInfo、SetConsoleCursorInfo

GetConsoleCursorInfo是拿到当前光标的信息,所以想用这个函数要先设置好光标信息并使其生效(即先调用SetConsoleCursorInfo函数),否则就会拿到未修改前系统的光标信息,将你定义的光标信息覆盖。

这就是光标大小改为100的样子,如果把true改为false,就会隐藏光标

3.GetAsyncKeyState

使用GetAsyncKeyState时,每一次都要与判断是否被按下,但它是实时检测的,如果GetAsyncKeyState没有在循环里面反复检测,由于程序运行的速度很快,根本等不到你按下按键,所以使用这个函数一定要在循环里面使用。在比较时要用&(按位与)符号来做个比对二进制位,以判断按键是否被按下。

同时,GetAsyncKeyState和上面几个函数不同,虽然都是windows.h里的函数,但它是直接想系统(硬件)访问的,而不是控制台,全局读取硬盘的状态。准确来说,控制台就是带窗口的两个缓冲区,一个是输入缓冲区,一个是输出缓冲区,前面说获取系统操作权限指的就是操作缓冲区的权限。

本质上也是利用死循环来做到实时监测

二.项目的具体实现

1.贪吃蛇地图绘制
2.蛇吃食物的功能(通过键盘上、下、左、右按键改变蛇的动作)
3.蛇撞墙死亡
4.蛇撞自身死亡
5.得分计算
6.蛇的加速、减速
7.暂停游戏
8.退出游戏

这是我们想要得到的游戏效果。那么我们创建两个源文件,snake.c用来实现各种功能,test.c用来实时测试功能是否正常,snake.h用来声明各种头文件和宏定义。

大致框架如下

1.地图绘制

想让蛇只在指定区域里面活动,那肯定是要给它列好边框的。

首先我们先给玩家一些文字提醒吧,还有如何进行操作

1.欢迎界面大体概况

第一页

紧接着

然后才开始绘制地图。好了现在有了大概思路,我们先来实现第一个画面。

2.第一个欢迎界面

想让光标大概置于居中位置,需要我们设置好控制台坐标,然后自己凭感觉估计位置,若效果不好再缓慢调整,涉及光标位置的调整,我们就包装一个函数,专门控制下一次操作光标的位置。

参数我们选择用short而不是int,因为这里涉及到字符本地化,我们要打印区域边界时,要用到特殊字符 " □ ● ← →↑ ↓ ★ "等,这些叫做宽字符,均占两个字节,具体在打印边框时介绍。

根据上面的框架,我们来完成基本逻辑框架搭建

现在我们要设置控制台的大小和修改控制台的名字

前面我们已经包装好了控制光标位置的函数,现在控制光标的位置大概在中间位置打印"欢迎来到贪吃蛇",再把光标设置到其下面显示

效果如下

我们发现,把这几串串代码放在GameStart函数里面显得有点冗杂,所以我们另外包装一个欢迎界面的函数

3.第二个欢迎界面

现在要跳转到第二个欢迎界面,那我原来的界面就要清理掉,利用system("cls")清屏,再重新设置光标坐标和打印

打印效果

4.第四个欢迎界面

完成前两个欢迎界面打印之后,我们就要开始绘制地图了,这里要用到宽字符。

用●表示蛇身,★表示食物,□表示边界

普通的字符是占⼀个字节的,这类宽字符是占用2个字节。

这⾥再简单的讲⼀下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,使 ⽤两个字节表⽰⼀个汉字,所以理论上最多可以表⽰ 256 x 256 = 65536 个符号。

后来为了使C语⾔适应国际化,C语⾔的标准中不断加⼊了国际化的⽀持。比如:加入和宽字符的类型

wchar_t 和宽字符的输⼊和输出函数,加⼊和<locale.h>头⽂件,其中提供了允许程序员针对特定

地区(通常是国家或者说某种特定语言的地理区域)调整程序⾏为的函数。

想使字符适合我们,就需要将各种类项进行本地化处理

什么是类项?

复制代码
LC_COLLATE 排序规则
LC_CTYPE 字符类型(汉字、宽字符)
LC_MONETARY 货币格式
LC_NUMERIC 数字格式
LC_TIME 时间日期格式
LC_ALL 全部一起改

LC_XXXX这种就是类项,C语言不希望你一次性修改,就分了这么多项,不用去管具体概念

setlocale函数

我们想要C语言本地化(即自动适应我们系统的需要),就需要用到一个非常重要的函数setlocale函数,同事要包含它的头文<locale.h>。

语法如下

复制代码
char* setlocale (int category, const char* locale);

第一个参数指的就是上面说的类项,第二个参数代表地区

复制代码
setlocale(LC_ALL, "Chinese");        // 简体中文
setlocale(LC_ALL, "Chinese-simplified"); // 简体中文
setlocale(LC_ALL, "ENU");            // 英文
setlocale(LC_ALL, "JPN");            // 日文
setlocale(LC_ALL, "KOR");            // 韩文

但是我们最常用的是让它自动适配我们的系统语言

复制代码
setlocale(LC_ALL," ")//自动适配我们的系统语言

想变回全英文用

复制代码
setlocale(LC_ALL,"C")//变回全英文

实际上,如果不进行设置,系统会默认用全英文

如何进行宽字符定义和打印呢?
  • 类型:wchar_t
  • 字符前缀:L'
  • 字符串前缀:L"
  • 打印函数:wprintf

先在我们来创建围墙,为了方便维护,蛇身、墙和食物等宽字符我们均用宏来定义

想象一下,我们的围墙要多宽的呢?一个宽字符不仅占两个字节,在打印的时候也是占两列的宽度

因此,原来的100列的坐标就相当于变成了50列,我们做的时候要时刻清楚,现在的一个宽字符是占两个列的宽度。

地图参考图

要知道,坐标是从(0,0)开始的,所以奇数坐标就是每一列的尾坐标,为了写代码时利用对称性来辅助我们,先打印上下边界,再打印左右边界。

上边界的纵坐标一直是0,横坐标从0到56,每次变化加2

用同样的方法,改变光标的位置后打印下边界,列坐标依然是0~56,行坐标恒为26;

用同样的方法打印左右边界,左边界列坐标为0恒不变,行坐标从1~25;右边界列坐标为56恒不变,行坐标从1~25,且上述代码可以优化。

打印效果

蛇的设计

围墙是画好了,但是初始化的时候肯定也要把蛇初始化在地图里啊,右边区域还要有实时计算得分吧。那我们先来处理蛇

贪吃蛇每吃到一个食物,尾巴都会加变成1个单位,那么我们就选用单链表来实现,如果你愿意,也可以使用双向链表来实现,但是双向链表多出一个指针,我们在遍历的时候都要从头节点开始遍历,所以为了降低操作的繁琐性,就选用单链表来设计蛇的节点。

蛇的节点坐标声明好了,再想想蛇是不是还要有一些属性?

  • 蛇的状态

  • 蛇的方向

  • 蛇每走一步要睡眠的时间

  • 蛇的维护

    1 typedef struct Snake
    2 {
    3 pSnakeNode _pSnake;//维护整条蛇的指针
    4 pSnakeNode _pFood;//维护⻝物的指针
    5 enum DIRECTION _Dir;//蛇头的⽅向默认是向右
    6 enum GAME_STATUS _Status;//游戏状态
    7 int _Score;//当前获得分数
    8 int _foodWeight;//默认每个⻝物10分
    9 int _SleepTime;//每⾛⼀步休眠时间
    10 }Snake, * pSnake;

我们创建这么一个结构体来包装蛇的各种属性,好既然谈到属性,那是不是得把它的所有属性给声明一下

现在开始初始化蛇身

这样五个节点就创建好了,但是我们的蛇的各种属性也需要初始化呀

其中我们传入snake的地址是为了初始化属性,同时存储贪吃蛇的头结点。

注:

随机种子和属性变量是已经声明了的。

初始化结果如下

因为最后一次设置光标位置是在尾节点的位置(头节点在右边),所以如果不换行的话红框里的字会在倒数第二个节点开始打印,会覆盖掉除最后一个节点的所有节点。

生成食物

生成的食物的位置要随机,但是要在墙的范围内,又不能和蛇重叠,需要加入一些条件判断,逐个遍历蛇结点

来看效果

但是这样就可以了吗?

思考一个问题,如果我这样生成食物,那当蛇吃到食物时我要如何在尾巴生成一个节点,又要传参重新包装一个函数来生成尾节点吗?似乎可行,但是这样过于麻烦,我们直接利用蛇的结构体来创建食物,这样当判断蛇是否吃到食物时就可以直接利用食物的指针解引用来判断了,这个食物又是放在属性里面,不需要额外传参。所以生成食物的函数修改如下

复制代码
//生成食物
void CreateFood(pSnake ps)
{
	//声明食物的列和行坐标
	int x = 0;
	int y = 0;
	again:
	do
	{
		//食物的范围要在墙内
		x = rand() % 53 + 2;//2~54
		y = rand() % 25 + 1;//1~24

	} while (x % 2 == 0);

	pSnake cur = ps;
	while (cur->_pSnake)
	{
		//如果与蛇重叠就重新生成
		if (cur->_pSnake->x == x && cur->_pSnake->y == y)
			goto again;
		cur->_pSnake = cur->_pSnake->next;
	}
	
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood)
	{
		pFood->x = x;
		pFood->y = y;
		pFood->next = NULL;
		ps->_pFood = pFood;
		SetCursorPosition(x, y);
		wprintf(L"%c", FOOD);

	}
	else
	{
		perror("CreateFood:malloc()");
		return;
	}
}

效果如图:

右边空白的地方我们是不是得加一些文字叙述或者实时记录的信息呢?比如实时得分计算等功能,那就包装一个函数,把一些操作信息放在那边

效果如下

以上我们已经完成贪吃蛇的基本布局,接下来完成运行和善后操作

5.贪吃蛇运行操作的实现

游戏运行期间,根据贪吃蛇的状态属性判断游戏是否正常,否则结束游戏。因此主体框架为

进来后我们是不是得在右边显示得分情况,找一个位置(不要覆盖帮助信息),打印当前得分情况

效果如下

实时检测键位

现在我们就要利用GetAsyncKeyState函数来实时检测我输入的键位,为了方便调用,我们利用宏来定义一个值来接受它的结果

我们只需要输入的相应的VK值即可,每一个判断操作均和这个相似

分别识别上、下、左、右、空格、esc、F3、F4等按键

复制代码
//运行游戏
void RunGame(pSnake ps)
{
	do
	{
		SetCursorPosition(64, 10);
		printf("得分:%d ", ps->_Score);
		printf("每个食物得分:%d分 ", 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_RIGHT) && ps->_Dir != left)
		{
			ps->_Dir = right;
		}
		else if (KEY_PRESS(VK_LEFT) && ps->_Dir != right)
		{
			ps->_Dir = left;
		}
		else if (KEY_PRESS(VK_SPACE))//暂停
		{
			pasue();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Status = END_NOMAL;
			break;
		}
		else if (KEY_PRESS(VK_F1))//F1为加速,加速就缩短睡眠时间
		{
			if (ps->_SleepTime >= 50)//最快要大于或等于50毫秒
			{
				ps->_SleepTime -= 30;
				ps->_foodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F2))//F2为减速,减速就加长睡眠时间
		{
			if (ps->_SleepTime < 350)//最长不能超过350毫秒
			{
				ps->_SleepTime += 30;
				ps->_foodWeight -= 2;
			}
			//基础分为10分,当速度等于350的时候刚好减到0
			//当然就算速度最慢,单个食物的分也不能为0
			if (ps->_SleepTime == 350)
				ps->_foodWeight = 1;
		}
		//蛇每次移动前休眠一段时间
		Sleep(ps->_SleepTime);
		//然后开始移动
		SnakeMove();
	} while (ps->_Status == OK);
}

现在要实现蛇的移动函数,和按空格时暂停的函数。

暂停
复制代码
//暂停函数
void pause()
{
	while (1)
	{
		Sleep(300);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

暂停的方式有很多种,这里使用死循环的方式实现暂停,但是为了不让CPU飞快计算死循环,所以每循环一次我们让他休眠300毫秒。

贪吃蛇的移动

我们蛇的移动是通过在头部将要移动的方向创建一个新的节点,然后释放最后一个节点来实现的,同时还要把最后一个节点的字符置为空字符。

复制代码
//贪吃蛇的移动
void SnakeMove(pSnake ps)
{
	pSnakeNode nextsnake = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (nextsnake == NULL)
	{
		perror("SnakeMove:malloc()");
		return;
	}
	switch (ps->_Dir)
	{
	case up:
		nextsnake->y = ps->_pSnake->y - 1;
		nextsnake->x = ps->_pSnake->x;
		break;
	case down:
		nextsnake->y = ps->_pSnake->y + 1;
		nextsnake->x = ps->_pSnake->x;
		break;
	case right:
		nextsnake->x = ps->_pSnake->x + 2;
		nextsnake->y = ps->_pSnake->y;
		break;
	case left:
		nextsnake->x = ps->_pSnake->x - 2;
		nextsnake->y = ps->_pSnake->y;
		break;
	}
	//判断下一个位置是否是食物
	if (nextsnake->x == ps->_pFood->x && nextsnake->y == ps->_pFood->y)
	{
		//如果是食物,就进行头插,把食物的位置当做下一个蛇头的位置
		nextsnake->next = ps->_pSnake;
		ps->_pSnake = nextsnake;
		SetCursorPosition(nextsnake->x, nextsnake->y);
		wprintf(L"%c", SNAKE);
	    //吃到食物,得分增加
	    ps->_Score += ps->_foodWeight;
		//吃完食物后随机生成新的食物
		CreateFood(ps);
	}
	else 
	{
		//如果不是食物,先进行头插,然后释放最后一个结点,并把字符置为空字符
		nextsnake->next = ps->_pSnake;
		pSnakeNode cur = ps->_pSnake;
		while (cur->next->next)
			cur = cur->next;
		SetCursorPosition(cur ->next->x, cur -> next->y);
		wprintf(L"%c", ' ');
		free(cur->next);
		cur->next = NULL;
		ps->_pSnake = nextsnake;
		SetCursorPosition(nextsnake->x, nextsnake->y);
		wprintf(L"%c", SNAKE);
	}
	//当然还要判断下次运动位置是不是围墙和自己
	//如果是,则修改游戏状态,结束游戏
	KillBySelf(ps);
	KillByWall(ps);
}
问题修改1

通过测试我们发现了前面生成食物时的一个bug

这里的判断条件应该改为

复制代码
while(x % 2 != 0)

否则一旦x是偶数时,就会循环重新生成,直到生成奇数,这样就会导致永远也不会得到偶数。

注意,上面的释放尾节点一定要先把原来的宽字符用空字符覆盖掉,否则会出现这种情况

即使空间已经释放掉了,但是数据任然存在,成为垃圾数据。

在给新创结点赋值的时候,一定要时刻提醒自己,坐标是从坐上角往下和右递增的,当蛇方向往上时是行坐标减,下是加;蛇头往左是列坐标减,往右是加,并且是加2

完成了吃食物和基本移动,我们还得判断下一次移动的方向有没有撞到自己活着围墙吧

判断是否撞墙和撞到自己

先分析撞墙判断,我们需要比对墙坐标,判断贪吃蛇的头坐标有没有和围墙的某一个坐标重合,如果有就改变蛇的状态属性

思考一下,如果撞到墙了,蛇的坐标会发生什么变化?

是不是列坐标会等于0或56,行坐标会等于0或26,所以只需判断坐标是否等于这几个就可以

撞墙
撞到自己

撞到自己后均会改变游戏的状态,当游戏状态不为OK时,就是结束游戏,我们根据不同游戏状态,打印不同的结束语,那么这部分应该属于游戏的善后工作。我们先测试这部分的两种撞墙情况的实现是否正常

两种状态均正常,但是运行过程中,光标总是一直在闪烁,十分影响观感,所以我们用SetConsoleCursorInfo来隐藏光标,同时需要先获取手柄权限和在CONSOLE_CURSOR_INFO设置光标信息。

隐藏光标

我们在最初的位置就开始隐藏光标,直至程序结束。

一本正常运行程序已经完成,接下来我们完成游戏的善后工作

结束游戏

游戏结束时我们想在屏幕上打印内容,是正常退出、是撞到自己、是撞墙等,如果是后面两个,就输出原因后给出得分,如果是前者就输出"游戏结束",同时释放掉各个蛇结点的空间。

复制代码
//游戏结束
void EndGame(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake;
	//设置结束语的坐标
	SetCursorPosition(24, 12);
	switch (ps->_Status)
	{
	case END_NOMAL:
		printf("你主动退出游戏\n");
		break;
	case KILL_BY_SELF:
		printf("你撞到了自己,游戏结束\n");
		break;
	case KILL_BY_WALL:
		printf("你撞墙了,游戏结束\n");
		break;
	}
	//释放蛇身结点
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
	ps->_pSnake = NULL;
}

6.结语

至此,贪吃蛇的基本功能都已经实现了,当然里面还有很多可以优化的地方。现在也算是完成了C语言基础语法的学习,对基础进行了简单的自我测试。先将完整代码放于下方

7.完整代码

snake.h文件

复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <conio.h>
#include <windows.h>
#include <stdbool.h>
#include <locale.h>
#include <time.h>
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x8000) ? 1 : 0)
//定义蛇尾初始化的坐标
#define POS_X 22
#define POS_Y 6

//定义围墙字符
#define WALL L'□'
//定义蛇身字符
#define SNAKE L'●'
//定义食物字符
#define FOOD L'★'



//设计蛇的节点
typedef struct SnakeNode
{
	//蛇的节点坐标
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode, *pSnakeNode;

//声明游戏状态
typedef enum GAME_STATE
{
	OK,//正常
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF,//撞到自己
	END_NOMAL//正常退出
}GAME_STATE;
//蛇的方向
typedef enum SNAKE_DIRECTION
{
	up=1,//向上
	down,//向下
	left,//向左
	right//向右
}DIRECTION;


//声明蛇的各种属性
typedef struct Snake
{
	 pSnakeNode _pSnake;//维护整条蛇的指针
	 pSnakeNode _pFood;//维护?物的指针
	 enum DIRECTION _Dir;//蛇头的?向默认是向右
	 enum GAME_STATUS _Status;//游戏状态
	 int _Score;//当前获得分数
	 int _foodWeight;//默认每个?物10分
	 int _SleepTime;//每??步休眠时间
}Snake, * pSnake;
//开始游戏
void GameStart(pSnake ps);
//定义光标的位置
void SetCursorPosition(short x, short y);
//欢迎函数
void WelComeGame();

//初始化蛇身
void InitSnakeBody(pSnake ps);
//生成食物
void CreateFood(pSnake ps);
//给右边空白区域放置帮助信息
void PrintHelpInfo();
//运行游戏
void RunGame(pSnake ps);
//暂停函数
void pause();
//贪吃蛇的移动
void SnakeMove(pSnake ps);
//撞墙
void KillByWall(pSnake ps);
//撞到自己
void KillBySelf(pSnake ps);
//游戏结束
void EndGame(pSnake ps);

snake.c文件

复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
//开始游戏
void GameStart(pSnake ps)
{
	//设置控制台大小为30行100列
	system("mode con cols=100 lines=30");
	//该控制台名字为:贪吃蛇
	system("title 贪吃蛇");
	//打印欢迎界面
	WelComeGame();
	//初始化蛇身
 	InitSnakeBody(ps);
	//生成食物
	CreateFood(ps);
	//给右边空白区域放置帮助信息
	PrintHelpInfo();
}
//运行游戏
void RunGame(pSnake ps)
{
	
	do
	{
		SetCursorPosition(64, 10);
 		printf("得分:%d ", ps->_Score);
		printf("每个食物得分:%d分 ", 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_RIGHT) && ps->_Dir != left)
		{
			ps->_Dir = right;
		}
		else if (KEY_PRESS(VK_LEFT) && ps->_Dir != right)
		{
			ps->_Dir = left;
		}
		else if (KEY_PRESS(VK_SPACE))//暂停
		{
			pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Status = END_NOMAL;
			break;
		}
		else if (KEY_PRESS(VK_F1))//F1为加速,加速就缩短睡眠时间
		{
			if (ps->_SleepTime >= 50)//最快要大于或等于50毫秒
			{
				ps->_SleepTime -= 30;
				ps->_foodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F2))//F2为减速,减速就加长睡眠时间
		{
			if (ps->_SleepTime < 350)//最长不能超过350毫秒
			{
				ps->_SleepTime += 30;
				ps->_foodWeight -= 2;
			}
			//基础分为10分,当速度等于350的时候刚好减到0
			//当然就算速度最慢,单个食物的分也不能为0
			if (ps->_SleepTime == 350)
				ps->_foodWeight = 1;
		}
		//蛇每次移动前休眠一段时间
		Sleep(ps->_SleepTime);
		//然后开始移动
		SnakeMove(ps);
	} while (ps->_Status == OK);
}

//游戏结束
void EndGame(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake;
	//设置结束语的坐标
	SetCursorPosition(24, 12);
	switch (ps->_Status)
	{
	case END_NOMAL:
		printf("你主动退出游戏\n");
		break;
	case KILL_BY_SELF:
		printf("你撞到了自己,游戏结束\n");
		break;
	case KILL_BY_WALL:
		printf("你撞墙了,游戏结束\n");
		break;
	}
	//释放蛇身结点
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
	ps->_pSnake = NULL;
}

//贪吃蛇的移动
void SnakeMove(pSnake ps)
{
	pSnakeNode nextsnake = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (nextsnake == NULL)
	{
		perror("SnakeMove:malloc()");
		return;
	}
	switch (ps->_Dir)
	{
	case up:
		nextsnake->y = ps->_pSnake->y - 1;
		nextsnake->x = ps->_pSnake->x;
		break;
	case down:
		nextsnake->y = ps->_pSnake->y + 1;
		nextsnake->x = ps->_pSnake->x;
		break;
	case right:
		nextsnake->x = ps->_pSnake->x + 2;
		nextsnake->y = ps->_pSnake->y;
		break;
	case left:
		nextsnake->x = ps->_pSnake->x - 2;
		nextsnake->y = ps->_pSnake->y;
		break;
	}
	//判断下一个位置是否是食物
	if (nextsnake->x == ps->_pFood->x && nextsnake->y == ps->_pFood->y)
	{
		//如果是食物,就进行头插,把食物的位置当做下一个蛇头的位置
		nextsnake->next = ps->_pSnake;
		ps->_pSnake = nextsnake;
		SetCursorPosition(nextsnake->x, nextsnake->y);
		wprintf(L"%c", SNAKE);
		//吃到食物,得分增加
		ps->_Score += ps->_foodWeight;
		//吃完食物后随机生成新的食物
		CreateFood(ps);
	}
	else 
	{
		//如果不是食物,先进行头插,然后释放最后一个结点,并把字符置为空字符
		nextsnake->next = ps->_pSnake;
		pSnakeNode cur = ps->_pSnake;
		while (cur->next->next)
			cur = cur->next;
		SetCursorPosition(cur ->next->x, cur -> next->y);
		wprintf(L"%c", ' ');
		free(cur->next);
		cur->next = NULL;
		ps->_pSnake = nextsnake;
		SetCursorPosition(nextsnake->x, nextsnake->y);
		wprintf(L"%c", SNAKE);
	}
	//当然还要判断下次运动位置是不是围墙和自己
	//如果是,则修改游戏状态,结束游戏
	KillBySelf(ps);
	KillByWall(ps);
}

//撞墙
void KillByWall(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake;
	if (cur->x == 0 ||
		cur->x == 56 ||
		cur->y == 0 ||
		cur->y == 26)
	{
		ps->_Status = KILL_BY_WALL;
	}
}
//撞到自己
void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		cur = cur->next;
		if (cur && cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
		{
			ps->_Status = KILL_BY_SELF;
			return;
		}
	}
}

//暂停函数
void pause()
{
	while (1)
	{
		Sleep(300);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

//欢迎函数
void WelComeGame()
{
	//设置光标位置
	SetCursorPosition(40, 13);
	printf("欢迎来到贪吃蛇\n");
	SetCursorPosition(39, 18);
	system("pause");
	//清屏
	system("cls");
	//重新设置光标的坐标
	SetCursorPosition(25, 13);
	printf("用 ↑.↓.←.→ 分别控制蛇的移动, F3为加速,F4为减速\n");
	SetCursorPosition(39, 18);
	system("pause");
	system("cls");
	//打印上、下边界围墙
	for (int x = 0; x <= 56; x += 2)
	{
		SetCursorPosition(x, 0);
		wprintf(L"%c", WALL);
		SetCursorPosition(x, 26);
		wprintf(L"%c", WALL); \
	}
	
	//打印左、右边界
	for (int y = 1; y <= 25; y++)
	{
		SetCursorPosition(0, y);
		wprintf(L"%c", WALL);
		SetCursorPosition(56, y);
		wprintf(L"%c", WALL);
	}
	//system("pause");
}

//定义光标的位置
void SetCursorPosition(short x, short y)
{
	//定义光标位置
	COORD pos = { x, y };
	//获取操作控制台的权限,即获取输出句柄
	HANDLE hOutPut = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置光标位置
	SetConsoleCursorPosition(hOutPut, pos);
}

//初始化蛇身
void InitSnakeBody(pSnake ps)
{
	pSnakeNode snake = NULL;
	//利用头插法初始化五个节点的链表
	for (int i = 0; i < 5; i++)
	{
		snake = (pSnake)malloc(sizeof(SnakeNode));
		if (snake)
		{
			snake->x = POS_X + i * 2;
			snake->y = POS_Y;
			//利用ps的_pSnake来临时存储刚开辟好的空间,以实现头插
			//ps->_pSnake永远指向头结点
			if (ps->_pSnake == NULL)
			{
				snake->next = NULL;
				ps->_pSnake = snake;
			}
			else
			{
				snake->next = ps->_pSnake;
				ps->_pSnake = snake;
			}
		}
		else
		{
			perror("InitSnakeBody()::malloc()");
			return;
		}
	}
			//打印蛇身
			snake = ps->_pSnake;
			while (snake)
			{
				SetCursorPosition(snake->x, snake->y);
				wprintf(L"%c", SNAKE);
				snake = snake->next;
			}
			//设置贪吃蛇的属性
			ps->_Dir = right;//初始方向
			ps->_foodWeight = 10;//一个食物的分数
			ps->_Score = 0;//初始得分
			ps->_SleepTime = 200;//睡眠时间:200毫秒
			ps->_Status = OK;//初始化状态
}

//生成食物
void CreateFood(pSnake ps)
{
	//声明食物的列和行坐标
	int x = 0;
	int y = 0;
	again:
	do
	{
		//食物的范围要在墙内
		x = rand() % 53 + 2;//2~54
		y = rand() % 25 + 1;//1~24

	} while (x % 2 != 0);

	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		//如果与蛇重叠就重新生成
		if (cur->x == x && cur->y == y)
			goto again;
		cur = cur->next;
	}
	
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood)
	{
		pFood->x = x;
		pFood->y = y;
		pFood->next = NULL;
		ps->_pFood = pFood;
		SetCursorPosition(x, y);
		wprintf(L"%c", FOOD);

	}
	else
	{
		perror("CreateFood:malloc()");
		return;
	}
}

//给右边空白区域放置帮助信息
void PrintHelpInfo()
{
	//打印提?信息
	SetCursorPosition(64, 15);
	printf("不能穿墙,不能咬到自己\n");
	SetCursorPosition(64, 16);
	printf("用↑.↓.←.→分别控制蛇的移动.");
	SetCursorPosition(64, 17);
	printf("F1 为加速,F2 为减速\n");
	SetCursorPosition(64, 18);
	printf("ESC :退出游戏.space:暂停游戏.");
}

test.c文件

复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
void test()
{
	HANDLE pOutPut = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO consolecursorinfo;
	consolecursorinfo.bVisible = false;
	consolecursorinfo.dwSize = 50;
	SetConsoleCursorInfo(pOutPut, &consolecursorinfo);
	//声明蛇的属性变量snake
	Snake snake = { ._pSnake = NULL, ._pFood = NULL };
	//开始游戏
	GameStart(&snake);
	//运行游戏
	RunGame(&snake);
	//游戏结束
	EndGame(&snake);
}
int main()
{
	setlocale(LC_ALL, "");
	srand((unsigned)time(NULL));
	test();
	return 0;
}
相关推荐
笨笨饿5 小时前
#79_NOP()嵌入式C语言中内联汇编宏的抽象封装模式研究
linux·c语言·网络·驱动开发·算法·硬件工程·个人开发
weixin_421725265 小时前
C语言中volatile关键字怎么用C语言volatile在多线程中的作用
c语言·数据结构·运算符优先级·变量命名·volatile关键字
星河耀银海6 小时前
C语言与数据库交互:SQLite实战与数据持久化
c语言·数据库·sqlite·交互
05候补工程师7 小时前
【408 从零到一】线性表逻辑特征、存储结构对比与 C/C++ 动态内存分配避坑指南
c语言·开发语言·数据结构·c++·考研
傻瓜搬砖人7 小时前
第五章习题
c语言·谭浩强·绿皮书第三版
华清远见成都中心8 小时前
C 语言内存管理深度解析:malloc/free 与嵌入式堆栈分配策略
java·c语言·算法
努力努力再努力wz8 小时前
【MySQL 进阶系列】拒绝滥用root:从 mysql.user 到权限校验,带你彻底理解用户管理与授权机制!
android·c语言·开发语言·数据结构·数据库·c++·mysql
炸膛坦客8 小时前
嵌入式 - 数据结构与算法:(1-4)数据结构 - 单链表的两个核心缺点(引入循环/双向链表)
c语言·数据结构·链表
上弦月-编程11 小时前
高效编程利器:转移表技术解析
c语言·开发语言·数据结构·算法·排序算法