贪吃蛇(C语言详解)

贪吃蛇游戏运行画面-CSDN直播

目录

贪吃蛇游戏运行画面-CSDN直播

[1. 实验目标](#1. 实验目标)

[2. Win32 API介绍](#2. Win32 API介绍)

[2.1 Win32 API](#2.1 Win32 API)

[2.2 控制台程序(Console)](#2.2 控制台程序(Console))

[2.3 控制台屏幕上的坐标COORD](#2.3 控制台屏幕上的坐标COORD)

[2.4 GetStdHandle](#2.4 GetStdHandle)

[2.5 GetConsoleCursorlnfo](#2.5 GetConsoleCursorlnfo)

[2.5.1 CONSOLE_CURSOR_INFO](#2.5.1 CONSOLE_CURSOR_INFO)

[2.6 SetConsoleCursorlnfo](#2.6 SetConsoleCursorlnfo)

[2.7 SetConsoleCursorPosition](#2.7 SetConsoleCursorPosition)

[2.8 GetAsyncKeyState](#2.8 GetAsyncKeyState)

[3. 贪吃蛇准备阶段](#3. 贪吃蛇准备阶段)

[3.1 地图](#3.1 地图)

[3.1.1 本地化](#3.1.1 本地化)

[3.1.2 类型](#3.1.2 类型)

[3.1.3 setlocale函数](#3.1.3 setlocale函数)

[3.14 宽字符的打印](#3.14 宽字符的打印)

[3.1.5 地图坐标](#3.1.5 地图坐标)

[3.2 蛇身和食物](#3.2 蛇身和食物)

[3.3 链表定义蛇身](#3.3 链表定义蛇身)

[3.4 结构体维护贪吃蛇游戏](#3.4 结构体维护贪吃蛇游戏)

[3.5 枚举定义蛇的方向和游戏状态](#3.5 枚举定义蛇的方向和游戏状态)

[3.6 确定游戏流程设计](#3.6 确定游戏流程设计)

[4. 游戏开始(GameStart)](#4. 游戏开始(GameStart))

[4.1 设置游戏窗口大小和名字以及隐藏光标](#4.1 设置游戏窗口大小和名字以及隐藏光标)

[4.2 打印欢迎界面](#4.2 打印欢迎界面)

[4.3 绘制地图](#4.3 绘制地图)

[4.4 初始化蛇身](#4.4 初始化蛇身)

[4.5 创建食物](#4.5 创建食物)

[5. 游戏运行(GameRun)](#5. 游戏运行(GameRun))

[5.1 打印帮助信息(PrintHelpInfo)](#5.1 打印帮助信息(PrintHelpInfo))

[5.2 按键判断与打印得分](#5.2 按键判断与打印得分)

[5.3 蛇身移动(SnakeMove)](#5.3 蛇身移动(SnakeMove))

[5.3.1 判断移动过程中是否遇到食物(NextIsFood)](#5.3.1 判断移动过程中是否遇到食物(NextIsFood))

[5.3.1.1 吃食物(EatFood)](#5.3.1.1 吃食物(EatFood))

[5.3.1.2 不吃食物(NoFood)](#5.3.1.2 不吃食物(NoFood))

[5.3.2 撞到墙游戏结束(KillByWall)](#5.3.2 撞到墙游戏结束(KillByWall))

[5.3.3 咬到自身游戏结束(KillBySelf)](#5.3.3 咬到自身游戏结束(KillBySelf))

[6. 游戏结束(GameEnd)](#6. 游戏结束(GameEnd))

[6.1 总代码](#6.1 总代码)


1. 实验目标

使用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇
实现的基本功能为:

  1. 贪吃蛇地图绘制
  2. 蛇吃食物的功能(上、下、左、右方向键控制蛇的动作)
  3. 蛇撞墙死亡
  4. 蛇撞自身死亡
  5. 计算得分
  6. 蛇身加速、减速
  7. 暂停游戏

要实现这些功能,首先我们必须具备一些知识的储备。例如: C语言函数、枚举、结构体、动态
内存管理、预处理指令、链表、Win32 API等。

2. Win32 API介绍

本次实现贪吃蛇会使用到的一些Win32 API知识,那么就学习一下

2.1 Win32 API

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

其实说人话就是:如果你要基于Windows操作系统来编写一些程序,则Windows会提供各种接口,便于你完成一些功能

2.2 控制台程序(Console)

平常我们运行起来的黑框程序其实就是控制台程序
我们可以使用cmd命令来设置控制台窗口的长宽:设置控制台窗口的大小,30行,100列

cpp 复制代码
mode con cols=100 lines=30

也可以通过命令设置控制台窗口的名字:

cpp 复制代码
title 贪吃蛇

这些能在控制台窗口执行的命令,也可以调用C语言函数system来执行。例如:
注意:system是执行系统命令,使用system需要包含头文件<stdlib.h>

cpp 复制代码
#include <stdlib.h>

int main()
{
	//设置控制台窗口的长度:设置控制台窗口的大小,30行,100列
	system("mode con cols=100 lines=30");
	//设置cmd窗口名称
	system("title 贪吃蛇");
	return 0;
}

运行效果图

执行完后我们会发现窗口大小调制好了,但窗口名却没有,这是因为程序已经结束了。

解决方法:

  1. getchar(); 执行到这行会停下来,等待接收一个字符
  2. system("pause") 执行到这行命令程序会暂停

2.3 控制台屏幕上的坐标COORD

注意:使用COORD需要包含头文件<windows.h>

COORD是WindowsAPI中定义的一个结构体,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0)的原点位于缓冲区的顶部左侧单元格。

cpp 复制代码
typedef struct _COORD {
 SHORT X;
 SHORT Y;
} COORD, *PCOORD;

给坐标赋值:

cpp 复制代码
COORD pos = { 10, 15 };

2.4 GetStdHandle

使用需要包含头文件<windows.h>

GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。

cpp 复制代码
HANDLE GetStdHandle(DWORD nStdHandle);

说人话:要操作特定的控制台程序就要获得它的操作权限,能要识别你在操作谁
实例:

2.5 GetConsoleCursorlnfo

检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息

BOOL WINAPI GetConsoleCursorInfo(

HANDLE hConsoleOutput,

PCONSOLE_CURSOR_INFO lpConsoleCursorInfo

);

PCONSOLE_CURSOR_INFO是指向CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标(光标)的信息

总结:使用GetConsoleCursorlnfo传入的第一个参数是句柄,为何需要句柄?原因是如要隐藏光标首先需要获得当前控制台对应的光标信息。第二个参数是结构体指针

cpp 复制代码
	HANDLE hOutput = NULL;
	//获取标准输出设备的句柄(⽤来标识不同设备的数值)
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//定义结构体变量
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
	//调用完这个函数后就能把hOutput对应的光标信息填充到这个结构体变量中去

2.5.1 CONSOLE_CURSOR_INFO

这个结构体,包含有关控制台光标的信息

cs 复制代码
typedef struct _CONSOLE_CURSOR_INFO {
    DWORD dwSize;
    BOOL  bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
  • dwSize,由光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的水平线条。

  • bVisible,游标的可见性。 如果光标可见,则此成员为 TRUE。

    cpp 复制代码
    CursorInfo.bVisible = false; //隐藏控制台光标

注意:使用false需要包含头文件<stdbool.h>

2.6 SetConsoleCursorlnfo

设置指定控制台屏幕缓冲区的光标的大小和可见性。

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

实例:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <stdbool.h>

int main()
{
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//隐藏光标操作
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(hOutput, &CursorInfo);//获得控制台光标信息
	CursorInfo.bVisible = false;//隐藏控制台光标,false需包含头文件stdbool.h
	SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
	return 0;
}

调试:


可以看出当执行完CursorInfo.bVisible = false;的时候光标还是会显示,只有执行完SetConsoleCursorInfo函数后,光标才会隐藏。
在举一个例子:修改光标可见大小

运行结果:

2.7 SetConsoleCursorPosition

设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。

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

第一个参数传入的是句柄,第二个参数传入的是坐标信息,也就是COORD类型的结构体变量。

实例:

cpp 复制代码
#include <stdio.h>
#include <windows.h> 

int main()
{
	COORD pos = { 10,5 };
	HANDLE hOutput = NULL;
	//获得标准输出的句柄(用来标识不同设备的数值)
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置标准输出上光标的位置为pos
	SetConsoleCursorPosition(hOutput, pos);
	printf("haha\n");
	return 0;
}

运行结果:

如果我们不去设置指定光标位置,那么haha就会在这里被输出

由于日后我们可能会多次使用设置指定光标位置,所以我们不妨封装一个设置光标位置的函数Setpos

cpp 复制代码
//设置光标位置
void SetPos(short x, short y)
{
	COORD pos = { x,y };
	HANDLE hOutput = NULL;
	//获得标准输出的句柄(用来标识不同设备的数值)
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置标准输出上光标的位置为pos
	SetConsoleCursorPosition(hOutput, pos);
}

2.8 GetAsyncKeyState

获取按键情况,GetAsyncKeyState的函数原型如下:

cpp 复制代码
SHORT GetAsyncKeyState(
 int vKey
);

这个函数需要你传一个虚拟键值进去,然后该函数会检测,传进去的虚拟键值所代表的按键是否被按过,函数通过返回值来分辨按键的状态。(返回值类似是short)

如果返回的这个数据的二进制位的最高位为1,则代表按键状态是按下

如果返回的这个数据的二进制位的最高位为0,则代表按键状态是抬起

如果返回的这个数据的二进制位的最低位为1,则代表该键被按过

如果返回的这个数据的二进制位的最低位为0,则代表该键没被按过

如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1

cpp 复制代码
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

举个例子

虚拟键值表如下

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

3. 贪吃蛇准备阶段

3.1 地图

我们最终的贪吃蛇大纲要是这个样子,那我们的地图如何布置呢?



这里不得不讲一下控制台窗口的一些知识,如果想在控制台的窗口中指定位置输出信息,我们得知道该位置的坐标,所以首先介绍一下控制台窗口的坐标知识。
控制台窗口的坐标如下所示,横向的是X轴,从左向右依次增长,纵向是Y轴,从上到下依次增长。

在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★
普通的字符是占一个字节的,这类宽字符是占用2个字节。
这里再简单的讲一下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。
C语言最初假定字符都是自己的。但是这些假定并不是在世界的任何地方都适用。


C语言字符默认是采用ASCII编码的,ASCII字符集采用的是单字节编码,且只使用了单字节中的低7位,最高位是没有使用的,可表示为0xxxxxxx;可以看到,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>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。

3.1.1 <locale.h>本地化

<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。
在标准可以中,依赖地区的部分有以下几项:

  • 数字量的格式
  • 货币量的格式
  • 字符集
  • 日期和时间的表示形式

3.1.2 类型

通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个宏,指定一个类项:

  • LC_COLLATE:影响字符串比较函数 strco1l()和 strxfrm()。
  • LC_CTYPE:影响字符处理函数的行为。
  • LC_MONETARY:影响货币格式。
  • LC_NUMERIC:影响printf()的数字格式。
  • LC_TIME:影响时间格式strftime()和wcsftime()。
  • LC_ALL-针对所有类项修改,将以上所有类别设置为给定的语言环境。

3.1.3 setlocale函数

使用setlocale函数需要包含头文件<locale.h>

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

setlocale 函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。
setlocale 的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有的类项。
C标准给第二个参数仅定义了2种可能取值:"C"和""。"C"是正常模式,C语言默认的模式;""则是本地模式
在任意程序执行开始,都会隐藏式执行调用:

cpp 复制代码
setlocale(LC_ALL, "C");

当地区设置为"C"时,库函数按正常方式执行,小数点是一个点。
当程序运行起来后想改变地区,就只能显示调用setlocale函数。用""作为第2个参数,调用setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。

cpp 复制代码
setlocale(LC_ALL, "");//切换本地环境

扩展:

  1. setlocale的返回值是一个字符串指针,表示已经设置好的格式。如果调用失败,则返回空指针NULL。
  2. setlocale()可以用来查询当前地区,这时第二个参数设为NULL就可以了。
cpp 复制代码
#include <locale.h>

int main()
{
	char* loc;
	loc = setlocale(LC_ALL, NULL);
	printf("默认的本地信息:%s\n", loc);

	loc = setlocale(LC_ALL, "");
	printf("设置后的本地信息:%s\n", loc);
	return 0;
}

运行结果:

3.14 宽字符的打印

那如果想在屏幕上打印宽字符,怎么打印呢?宽字符的字面量必须加上前缀L,否则C语言会把字面量当作窄字符类型处理。前缀L在单引号前面,表示宽字符,宽字符的打印使用wprintf,对应wprintf()的占位符为%lc;在双引号前面,表示宽字符串,对应wprintf()的占位符为%ls

cpp 复制代码
#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"%lc\n", ch1);
	wprintf(L"%lc\n", ch2);
	wprintf(L"%lc\n", ch3);
	wprintf(L"%lc\n", ch4);
	return 0;
}

运行结果:


从输出的结果来看,我们发现一个普通字符占一个字符的位置但是打印一个汉字字符,占用2个字符的位置,那么我们如果要在贪吃蛇中使用宽字符,就得处理好地图上坐标的计算。
普通字符和宽字符打印出宽度的展示如下:

3.1.5 地图坐标

我们假设实现一个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙,
如下:

注意:

  1. 列:最好是2的倍数,因为宽字符占2位
  2. 我们可以看一个正常字符占的大小,我们不难发现,一个字符占的大小的宽度是比较窄的,但是高度是比较长的

3.2 蛇身和食物

初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现蛇,连续5个节点。注意:蛇的每个节点的x坐标必须是2的倍数,否则可能会出现蛇的一个节点有一半出现在墙体中,另外一半在墙外的现象,坐标不好对齐。
关于食物,就是在墙体内随机生成成一个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。

3.3 链表定义蛇身

在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信
息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行,所以蛇节点结构如下:

cpp 复制代码
typedef struct SnakeNode
{
    int x;
    int y;
    struct SnakeNode* next;
}SnakeNode, * pSnakeNode;

3.4 结构体维护贪吃蛇游戏

要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇:

cpp 复制代码
typedef struct Snake //定义贪吃蛇
{
 pSnakeNode pSnake;//维护整条蛇的指针
 pSnakeNode pFood;//维护⻝物的指针
 enum DIRECTION Dir;//蛇头的⽅向默认是向右
 enum GAME_STATUS Status;//游戏状态
 int Socre;//当前获得分数
 int foodWeight;//默认每个⻝物10分
 int SleepTime;//每⾛⼀步休眠时间(蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间越长,蛇的速度越慢)
}Snake, * pSnake;

3.5 枚举定义蛇的方向和游戏状态

蛇的方向,可以一一列举,使用枚举

cpp 复制代码
//方向
enum DIRECTION
{
 UP = 1,
 DOWN,
 LEFT,
 RIGHT
};

游戏状态,可以一一列举,使用枚举

cpp 复制代码
//游戏状态
enum GAME_STATUS
{
 OK,//正常运⾏
 KILL_BY_WALL,//撞墙
 KILL_BY_SELF,//咬到⾃⼰
 ESC//强制退出游戏
};

3.6 确定游戏流程设计

4. 游戏开始(GameStart)

首先我们先创建3个文件

snake.h ----> 贪吃蛇游戏中类型的声明,函数的声明

snake.c ----> 函数的实现

test.c ----> 贪吃蛇游戏的测试

游戏主逻辑(test.c)

cpp 复制代码
#include "snake.h"

void test()
{
	srand((unsigned int)time(NULL));
	
	int ch = 0;
	do
	{	
		Snake ps = { 0 };
		//游戏开始前的初始化
		GameStart(&ps);
		//游戏玩的过程
		GameRun(&ps);
		//游戏结束
		GameEnd(&ps);
		SetPos(16,13);
		printf("再来一局吗?(Y/N):");
		scanf(" %c", &ch);
	} while (ch == 'Y' || ch == 'y');
}

int main()
{
	//适配本地中文环境
	setlocale(LC_ALL, "");
	test();
	SetPos(0, 27);
	return 0;
}

4.1 设置游戏窗口大小和名字以及隐藏光标

cpp 复制代码
//游戏开始
void GameStart(pSnake ps)
{
	//设置控制台的信息,窗口大小,窗口名
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");
	//隐藏光标
	HANDLE hOutput = NULL;
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(hOutput, &CursorInfo);
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(hOutput, &CursorInfo);
	//打印欢迎信息
	WelcomeToGame();
}

首先我们要让窗口大小100行,30列,有人是不是会疑惑为什么不是58行,27列,我们不是之前给的图片就是这样的嘛?

那是因为这只是地图的大小,地图大小外还有提示的信息,就比如下图一样,所以我们给的大小就会比地图大小还要大一些

4.2 打印欢迎界面

效果图:

要实现这个效果图,首先肯定要用到COORD和SetConsoleCursorPosition来设置光标位置

cpp 复制代码
//定位光标位置
void SetPos(int x, int y)
{
	COORD pos = { x,y };
	HANDLE hOutput = NULL;
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//在屏幕上设置指定光标位置
	SetConsoleCursorPosition(hOutput, pos);
}

接下来就是打印欢迎信息

cpp 复制代码
//打印欢迎信息
void WelcomeToGame()
{
	//欢迎信息
	SetPos(35,10);
	printf("欢迎来带贪吃蛇小游戏\n");
	SetPos(38, 20);
	//暂停
	system("pause");
	//清屏
	system("cls");
	//功能介绍
	SetPos(15, 10);
	printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");
	SetPos(15, 11);
	printf("加速将得到更高的分数。");
	SetPos(38, 20);
	system("pause");
	system("cls");
}

4.3 绘制地图

创建地图就是将墙打印出来,因为是宽字符打印,所以使用wprintf函数,打印格式串前使用L
打印地图的关键是要算好坐标,才能在想要的位置打印墙体。
因为打印墙体字符L'□'我们后续会经常使用,所以我们把它封住成宏

cpp 复制代码
#define WALL L'□'

打印墙体代码:

cpp 复制代码
//绘制地图
void CreateMap()
{
	SetPos(0, 0);
	int i = 0;
	//一个宽字符占2位,所以i加的是2
	//i到56就行了,因为输出WALL就能把56个57空间给占了
	//上框框
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//下框框
	SetPos(0, 26);
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//左框框
	//这里i只要加1的原因是宽字符和正常字符的高度是一样的
	//只有宽字符和正常字符的宽度有差2倍
	for (i = 1; i < 26; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//右框框
	for (i = 1; i < 26; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
}

效果图:

4.4 初始化蛇身

我们可以按照下面这张图来进行初始化蛇身与食物
蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。
创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,将蛇的每一节打印在屏幕上。
再设置当前游戏的状态,蛇移动的速度,默认的方向,初始成绩,蛇的状态,每个食物的分数。
蛇身打印的宽字符(由于我们后面可能会多次打印蛇身宽字符L'●',所以我们索性把它封装成宏):

cpp 复制代码
#define BODY L'●'

1.打印蛇身

cpp 复制代码
//初始化蛇身
//ps是维护整条蛇的地址
void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	//按照图片上的指示创建5个节点
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake():malloc()");
			return;
		}
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;
		cur->next = NULL;
		//创建好后就进行头插
		cur->next = ps->pSnake;
		ps->pSnake = cur;
	}

我把行和列封装成一个宏,这样的好处是方便后续修改

cpp 复制代码
#define POS_X 24
#define POS_Y 5

头插法的解析

2.创建好后打印蛇身

cpp 复制代码
	//创建好后就打印蛇的身体
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

3.初始化贪吃蛇的其他数据

cpp 复制代码
	//初始化贪吃蛇的数据
	ps->SleepTime = 200;
	ps->Socre = 0;
	ps->Status = OK;
	ps->Dir = RIGHT;
	ps->foodWeight = 10;
	ps->pFood = NULL;

4.初始化蛇身总代码

cpp 复制代码
//初始化蛇身
void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	//按照图片上的指示创建5个节点
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake():malloc()");
			return;
		}
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;
		cur->next = NULL;
		//创建好后就进行头插
		cur->next = ps->pSnake;
		ps->pSnake = cur;
	}
	//创建好后就打印蛇的身体
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//初始化贪吃蛇的数据
	ps->SleepTime = 200;
	ps->Socre = 0;
	ps->Status = OK;
	ps->Dir = RIGHT;
	ps->foodWeight = 10;
	ps->pFood = NULL;
}

4.5 创建食物

首先创建食物我们需要考虑的问题

  • 食物是随机出现的,所以坐标就是随机的(但是生成的坐标x必须是2的倍数)
  • 生成的坐标必须在墙内
  • 生成的坐标不能在蛇的身上

由于打印食物宽字符L'★'后续可能会多次用到,所以我们把它封装成一个宏

cpp 复制代码
#define FOOD L'★'

1.创建食物坐标

cpp 复制代码
	int x = 0;
	int y = 0;
agin:
	//使食物坐标必须要在墙内,并且x的坐标必须要是2的倍数
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);
	//判断坐标是否在蛇的身上
	pSnakeNode cur = ps->pSnake;
	while (cur)
	{
		if (x == cur->x && y == cur->y)
		{
			goto agin;
		}
		cur = cur->next;
	}

x = rand() % 53 + 2;

rand() % 53生成范围是0~52,后面加2,所以x的范围则是2~54

y = rand() % 25 + 1;

rand() % 25 + 1;生成范围是0~24,后面加1,所以y的范围则是1到25

首先我们已经保证生成的坐标是在墙内

然后我们还要判断x是否是2的倍数,所以我们用了个do-while循环来进行判断

后续我们又用了while循环来判断生成的食物节点是否与蛇身的某个节点重合,如果一样,我们就用goto语句来进行跳转,使坐标重新生成


|--------------------------------|
| 使用rand(),我们需要包含头文件<stdlib.h> |

为了防止生成的随机数与后续再次执行的程序一样,我用了srand((unsigned int)time(NULL));来修改种子,如果有不懂srand和time的同学可以看我之前写的博客(猜数字游戏),然后使用srand函数需要包含头文件<stdlib.h>;使用time需要包含头文件<time.h>

走到这一步我们生成的x与y坐标就已经符合要求了,接下来就是生成一个节点(pFood),让x与y赋值给pFood中的x与y,最后让维护蛇指针内的pFood指针来指向这个节点

cpp 复制代码
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood == NULL)
	{
		perror("CreateFood()::malloc()");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
	ps->pFood = pFood;

创建食物总代码

cpp 复制代码
//创建食物
void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;
agin:
	//使食物坐标必须要在墙内,并且x的坐标必须要是2的倍数
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);
	//判断坐标是否在蛇的身上
	pSnakeNode cur = ps->pSnake;
	while (cur)
	{
		if (x == cur->x && y == cur->y)
		{
			goto agin;
		}
		cur = cur->next;
	}
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood == NULL)
	{
		perror("CreateFood()::malloc()");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
	ps->pFood = pFood;
}

5. 游戏运行(GameRun)

首先我们再次看一遍游戏流程设计

  1. 游戏运行期间,右侧打印帮助信息,提示玩家
  2. 根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
  3. 如果游戏继续,就是检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。
  4. 确定了蛇的方向和速度,蛇就可以移动了。

5.1 打印帮助信息(PrintHelpInfo)

首先我们可以看到程序运行起来后右侧的帮助信息

因为我们设置游戏窗口的时候设置的是100列,30行,所以设置帮助信息我是一开始是把光标定位到62列17行,大家可以按自己的想法来,不一定要和我一样

cpp 复制代码
//打印帮助信息
void PrintHelpInfo()
{
	SetPos(62, 17);
	printf("1.不能穿墙,不能咬到自己");
	SetPos(62, 18);
	printf("2.用↑.↓.←.→分别控制蛇的移动。");
	SetPos(62, 19);
	printf("3.F3是加速,F4是减速");
	SetPos(62, 20);
	printf("ESC:退出游戏 SPACE:暂停游戏");
}

5.2 按键判断与打印得分

首先,我们需要知道每个键的虚拟键值,如下:

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

然后我整理出了本程序需要用到的虚拟键值

然后我们就要判断我们是否有按过这些键,就可以用到前面GetAsyncKeyState中定义的宏

cpp 复制代码
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

游戏玩的过程总代码

cpp 复制代码
//游戏玩的过程
void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();
	do
	{
		//打印当前得分
		SetPos(62, 10);
		printf("得分:%d", ps->Socre);
		SetPos(62, 11);
		printf("每个食物得分:%-2d", 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_SPACE))
		{
			//进行休眠
			pause();
		}
		else if (KEY_PRESS(VK_F3))
		{
			//加速
			if (ps->SleepTime >= 50)
			{
				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_ESCAPE))
		{
			//按ESC键退出
			ps->Status = ESC;
            break;
		}
		//蛇每走一步都需要休眠,休眠时间越短,蛇移动速度就越快
		Sleep(ps->SleepTime);
		SnakeMove(ps);
		//判断是否撞墙了
		KillByWall(ps);
		//判断是否撞到自己了
		KillBySelf(ps);
	} while (ps->Status == OK);
}

当我们按下了上下左右的时候,我们还要与当前蛇行走的方向进行判断

  1. 当我们按上的时候,蛇当前运动方向是不能向下的
  2. 当我们按下的时候,蛇当前运动方向是不能向上的
  3. 当我们按左的时候,蛇当前运动方向是不能向右的
  4. 当我们按右的时候,蛇当前运动方向是不能向左的
  5. 当我们按ESC键的时候,我们就把当前游戏状态进行修改,然后直接退出循环
  6. 当我们按下F3就会加速,休眠时间变短,但时间不可能减成负数,索性我们就规定当休眠时间大于等于50的时候,我们才能加速,当然休眠时间变短一次吃食物分数就会变多
  7. 当我们按下F4就会减速,休眠时变长,一次吃的食物分数变少,但是我们不能把分数减到0吧,不能吃一个食物一分都不得,所以我们规定只有食物分数大于2时我们按了F4才有效果

注意事项(打印每个食物得分):

打印每个食物的得分有个小细节就是要用%2d(或者%-2d,%3d,%-4d都可以),就是不要用%d来打印,因为一开始默认每个食物的得分是10分,后续我们是可以用F3和F4来控制蛇移动的速度,速度决定每吃一个食物所得分数。如果我们速度慢下来了,食物所得分数就会变少,由原来的10分变成8分或者更低,但因为一开始我们默认打印了10分,然后减了一次速度按理来说是要打印8分,但用%d打印的结果是80,原因是10后面的0没有被覆盖掉

5.3 蛇身移动(SnakeMove)

当然我们在蛇身移动前我们可以让蛇先休眠一下

蛇身移动的主要思想:

  1. 先创建下一个节点,根据移动方向和蛇头的坐标,蛇移动到下一个位置的坐标。
  2. 确定了下一个位置后,看下一个位置是否是食物(NextIsFood),是食物就做吃食物处理
    (EatFood),如果不是食物则做前进一步的处理(NoFood)。
  3. 蛇身移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上自己蛇身(KillBySelf),从而影响游戏的状态。

移动过程:

向上和向下

  1. 向上走:首先创建的新节点是在蛇头上面,由图可以看出新节点y坐标就是蛇头坐标y减1,而新节点x坐标则是和蛇头x坐标一样
  1. 向下走:首先创建的新节点是在蛇头下面,由图可以看出新节点y坐标就是蛇头坐标y加1,而新节点x坐标则是和蛇头x坐标一样
    向左和向右
  1. 向左走:首先创建的新节点是在蛇头左边,由图可以看出新节点x坐标是蛇头节点x坐标减2(减2的原因是宽字符打印宽度占2位),新节点y坐标则是和蛇头节点y坐标一样
  2. 向右走:首先创建的新节点是在蛇头右边,由图可以看出新节点x坐标是蛇头节点x坐标加2(加2的原因是宽字符打印宽度占2位),新节点y坐标则是和蛇头节点y坐标一样
cpp 复制代码
//蛇移动过程
void SnakeMove(pSnake ps)
{
	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNextNode == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}
	switch (ps->Dir)
	{
	case UP:
		pNextNode->x = ps->pSnake->x;
		pNextNode->y = ps->pSnake->y - 1;
		break;
	case DOWN:
		pNextNode->x = ps->pSnake->x;
		pNextNode->y = ps->pSnake->y + 1;
		break;
	case LEFT:
		pNextNode->x = ps->pSnake->x - 2;
		pNextNode->y = ps->pSnake->y;
		break;
	case RIGHT:
		pNextNode->x = ps->pSnake->x + 2;
		pNextNode->y = ps->pSnake->y;
		break;
	}
	//移动的过程中,判断是否遇到食物
	if (NextIsFood(pNextNode, ps))
	{
		//遇到食物
		EatFood(pNextNode, ps);
	}
	else
	{
		//没遇到食物
		NotEatFood(pNextNode, ps);
	}
}

5.3.1 判断移动过程中是否遇到食物(NextIsFood)

当然在蛇移动的过程中,可能移动的下一个节点就是食物,所以我们还要判断是否遇到了食物

cpp 复制代码
​
//判断移动过程中是否碰到食物
int NextIsFood(pSnakeNode pNextNode, pSnake ps)
{
	return (pNextNode->x == ps->pFood->x && pNextNode->y == ps->pFood->y);
}

相等返回1,不相等返回0

5.3.1.1 吃食物(EatFood)

如果返回值是1,则我们遇到了食物,那么我们就把新节点头插到贪吃蛇身上,然后打印蛇身

同时,我们之前定义食物的时候还动态malloc了一块空间,我们既然用新节点的空间头插到贪吃蛇上面,那我们就应该将原本食物的节点给销毁(free)

吃掉了食之后,我们原先在地图上的食物就被覆盖了,那么此时我们就应该再创建一个食物

同时,我们吃掉了一个食物之后,我们的分数也应该变高,就让原先的分数加上一个食物的分数

cpp 复制代码
//下一步要走的位置处就是食物,就吃掉食物
void EatFood(pSnakeNode pNextNode, pSnake ps)
{
	pNextNode->next = ps->pSnake;
	ps->pSnake = pNextNode;
	pSnakeNode cur = ps->pSnake;
	ps->Socre += ps->foodWeight;
	//打印蛇身
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	free(ps->pFood);
	CreateFood(ps);
}

首先我不知道看到这里的小伙伴会不会有这个疑问:

我们知道食物和新节点是不同的地址(两次malloc开辟出来的),但是他们两个空间重合了(食物和下一个节点在地图上存放空间重合),所以会不会不理解释放食物为什么不会把下一个节点空间也释放了?

原因:

创建出来的下一个节点和食物这个节点只是x和y坐标一样,节点是完全不同的两个节点,所以释放食物对创建出来的下一个节点没有影响

5.3.1.2 不吃食物(NoFood)

当下一个节点不是食物的时候,我们就先将下一个节点头插到贪吃蛇上面

同时我们需要知道,本来贪吃蛇是已经被打印出来了的(初始化蛇身的时候),所以我们只需要将新的头节点打印出来,同时将尾节点打印成空格并释放,我们就能在视觉上达到贪吃蛇走一步的效果

至于找到尾结点对我们来说可是很轻松的,用while循环就能办到

**注意:**SetPos到尾结点位置之后要打印两个空格,注意,是两个空格,因为尾结点宽字符占2位

cpp 复制代码
//pSnakeNode pNextNode 是下⼀个节点的地址
//pSnake ps 维护蛇的指针
//下一步要走的位置处不是食物,就不吃食物
void NotEatFood(pSnakeNode pNextNode, pSnake ps)
{
	pNextNode->next = ps->pSnake;
	ps->pSnake = pNextNode;
	pSnakeNode 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;
}

我不知道有没有小伙伴没看我前面介绍有这一个疑问,反正当时我是有这个疑问的:


原因是:这里就是不进行打印的,使用的是上一次打印留下的图形,走到cur->next->next是为了释放下一个节点,所以这个函数没有进行清屏,而是用printf(" ");来把最后一个图形覆盖掉

5.3.2 撞到墙游戏结束(KillByWall)

判断蛇是否撞到墙,我们只需要判断贪吃蛇的头节点是否在墙壁所圈定的范围之内,如果不在这个范围内,那就证明蛇已经撞到墙了

接着,我们需要将游戏的状态更改为 KILL_BY_WALL

当蛇向后走了一步时,判断到状态不为 OK ,就会跳出循环,游戏结束

cpp 复制代码
//判断是否撞墙了
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;
}

5.3.3 咬到自身游戏结束(KillBySelf)

我们要判断蛇是否会撞到自己,我们只需要将头节点和蛇身的每一个坐标一一比对,当发现有相同的时候,就说明蛇已经咬到自己了

接着,我们需要将游戏的状态更改为 KILL_BY_SELF

当蛇向后走了一步时,判断到状态不为 OK ,就会跳出循环,游戏结束

cpp 复制代码
//检测是否撞自己
void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->pSnake->next;
	while (cur)
	{
		if (ps->pSnake->x == cur->x && ps->pSnake->y == cur->y)
		{
			ps->Status = KILL_BY_SELF;
			return;
		}
		cur = cur->next;
	}
}

6. 游戏结束(GameEnd)

游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇身节点与食物节点。

至于删除蛇身,我们可以定义两个指针,一个指向要删除的节点,一个指向下一个节点

这是因为如果我们将该节点的空间释放掉之后,我们就找不到下一个节点了,所以我们才需要两个节点,而循环的条件就是当指针cur指向 NULL 的时候,循环停止

在释放完之后,不忘释放食物的空间

cpp 复制代码
/游戏结束
void GameEnd(pSnake ps)
{
	SetPos(18, 10);
	switch (ps->Status)
	{
	case KILL_BY_WALL:
		printf("很遗憾撞墙了,游戏结束");
		break;
	case KILL_BY_SELF:
		printf("很遗憾撞到自身了,游戏结束");
		break;
	case ESC:
		printf("按了ESC键,正常退出");
		break;
	}
	//释放蛇身节点和食物节点
	pSnakeNode cur = ps->pSnake;
	pSnakeNode pNextNode = NULL;
	while (cur)
	{
		pNextNode = cur;
		cur = cur->next;
		free(pNextNode);
	}
	free(ps->pFood);
	ps = NULL;
}

6.1 总代码

test.c

cpp 复制代码
 #define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"

void test()
{
	srand((unsigned int)time(NULL));
	
	int ch = 0;
	do
	{	
		Snake ps = { 0 };
		//游戏开始前的初始化
		GameStart(&ps);
		//游戏玩的过程
		GameRun(&ps);
		//游戏结束
		GameEnd(&ps);
		SetPos(16,13);
		printf("再来一局吗?(Y/N):");
		scanf(" %c", &ch);
	} while (ch == 'Y' || ch == 'y');
}

int main()
{
	//适配本地中文环境
	setlocale(LC_ALL, "");
	test();
	SetPos(0, 27);
	return 0;
}

snake.h

cpp 复制代码
#pragma once
#include <stdio.h>
#include <windows.h>
#include <stdbool.h>
#include <locale.h>
#include <stdlib.h>//随机数
#include <time.h>

#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define POS_X 24
#define POS_Y 5
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

//定义蛇身节点
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode, *pSnakeNode;

//定义贪吃蛇
typedef struct Snake
{
	pSnakeNode pSnake;//维护整条蛇的指针
	pSnakeNode pFood;//维护食物的指针
	enum DIRECTION Dir;//蛇头的方向默认是向右
	enum GAME_STATUS Status;//游戏状态
	int Socre;//当前获得分数
	int foodWeight;//默认每个食物10分
	int SleepTime;//每走一步休眠时间
}Snake, * pSnake;

//定义蛇的方向
enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

//游戏状态
enum GAME_STATUS
{
	OK,//正常运行
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF,//咬到自己
	ESC, //按了ESC键退出,正常退出
};

//游戏开始前的准备
void GameStart(pSnake ps);
//打印欢迎信息
void WelcomeToGame();
//定位控制台光标位置
void SetPos(int x, int y);
//绘制地图
void CreateMap();
//初始化蛇身
void InitSnake(pSnake ps);
//创建食物
void CreateFood(pSnake ps);


//游戏运行的整个逻辑
void GameRun(pSnake ps);
//打印帮助信息
void PrintHelpInfo();
//蛇移动过程
void SnakeMove(pSnake ps);
//判断蛇头的下一步要走的位置处是否是食物
int NextIsFood(pSnakeNode pNextNode, pSnake ps);
//下一步要走的位置处就是食物,就吃掉食物
void EatFood(pSnakeNode pNextNode, pSnake ps);
//下一步要走的位置处不是食物,不吃食物
void NotEatFood(pSnakeNode pNextNode, pSnake ps);
//检测是否撞墙
void KillByWall(pSnake ps);
//检测是否撞自己
void KillBySelf(pSnake ps);


//游戏结束,处理善后工作
void GameEnd(pSnake ps);

snake.c

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"

//定位光标位置
void SetPos(int x, int y)
{
	COORD pos = { x,y };
	HANDLE hOutput = NULL;
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//在屏幕上设置指定光标位置
	SetConsoleCursorPosition(hOutput, pos);
}

//打印欢迎信息
void WelcomeToGame()
{
	//欢迎信息
	SetPos(35,10);
	printf("欢迎来带贪吃蛇小游戏\n");
	SetPos(38, 20);
	//暂停
	system("pause");
	//清屏
	system("cls");
	//功能介绍
	SetPos(15, 10);
	printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");
	SetPos(15, 11);
	printf("加速将得到更高的分数。");
	SetPos(38, 20);
	system("pause");
	system("cls");
}

//绘制地图
void CreateMap()
{
	SetPos(0, 0);
	int i = 0;
	//一个宽字符占2位,所以i加的是2
	//i到56就行了,因为输出WALL就能把56个57空间给占了
	//上框框
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//下框框
	SetPos(0, 26);
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//左框框
	//这里i只要加1的原因是宽字符和正常字符的高度是一样的
	//只有宽字符和正常字符的宽度有差2倍
	for (i = 1; i < 26; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//右框框
	for (i = 1; i < 26; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}

}

//初始化蛇身
void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	//按照图片上的指示创建5个节点
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake():malloc()");
			return;
		}
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;
		cur->next = NULL;
		//创建好后就进行头插
		cur->next = ps->pSnake;
		ps->pSnake = cur;
	}
	//创建好后就打印蛇的身体
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//初始化贪吃蛇的数据
	ps->SleepTime = 200;
	ps->Socre = 0;
	ps->Status = OK;
	ps->Dir = RIGHT;
	ps->foodWeight = 10;
	ps->pFood = NULL;
}

//创建食物
void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;
agin:
	//使食物坐标必须要在墙内,并且x的坐标必须要是2的倍数
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);
	//判断坐标是否在蛇的身上
	pSnakeNode cur = ps->pSnake;
	while (cur)
	{
		if (x == cur->x && y == cur->y)
		{
			goto agin;
		}
		cur = cur->next;
	}
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood == NULL)
	{
		perror("CreateFood()::malloc()");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
	ps->pFood = pFood;
}

//游戏开始
void GameStart(pSnake ps)
{
	//设置控制台的信息,窗口大小,窗口名
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");
	//隐藏光标
	HANDLE hOutput = NULL;
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(hOutput, &CursorInfo);
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(hOutput, &CursorInfo);
	//打印欢迎信息
	WelcomeToGame();
	//绘制地图
	CreateMap();
	//初始化蛇身
	InitSnake(ps);
	//创建食物
 	CreateFood(ps);
}

//打印帮助信息
void PrintHelpInfo()
{
	SetPos(62, 17);
	printf("1.不能穿墙,不能咬到自己");
	SetPos(62, 18);
	printf("2.用↑.↓.←.→分别控制蛇的移动。");
	SetPos(62, 19);
	printf("3.F3是加速,F4是减速");
	SetPos(62, 20);
	printf("ESC:退出游戏 SPACE:暂停游戏");
}

//暂停过程
void pause()
{
	while (1)
	{
		Sleep(100);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

//判断移动过程中是否碰到食物
int NextIsFood(pSnakeNode pNextNode, pSnake ps)
{
	return (pNextNode->x == ps->pFood->x && pNextNode->y == ps->pFood->y);
}

//下一步要走的位置处就是食物,就吃掉食物
void EatFood(pSnakeNode pNextNode, pSnake ps)
{
	pNextNode->next = ps->pSnake;
	ps->pSnake = pNextNode;
	pSnakeNode cur = ps->pSnake;
	ps->Socre += ps->foodWeight;
	//打印蛇身
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	free(ps->pFood);
	CreateFood(ps);
}

//下一步要走的位置处不是食物,不吃食物
void NotEatFood(pSnakeNode pNextNode, pSnake ps)
{
	pNextNode->next = ps->pSnake;
	ps->pSnake = pNextNode;
	pSnakeNode 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 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 cur = ps->pSnake->next;
	while (cur)
	{
		if (ps->pSnake->x == cur->x && ps->pSnake->y == cur->y)
		{
			ps->Status = KILL_BY_SELF;
			return;
		}
		cur = cur->next;
	}
}

//蛇移动过程
void SnakeMove(pSnake ps)
{
	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNextNode == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}
	switch (ps->Dir)
	{
	case UP:
		pNextNode->x = ps->pSnake->x;
		pNextNode->y = ps->pSnake->y - 1;
		break;
	case DOWN:
		pNextNode->x = ps->pSnake->x;
		pNextNode->y = ps->pSnake->y + 1;
		break;
	case LEFT:
		pNextNode->x = ps->pSnake->x - 2;
		pNextNode->y = ps->pSnake->y;
		break;
	case RIGHT:
		pNextNode->x = ps->pSnake->x + 2;
		pNextNode->y = ps->pSnake->y;
		break;
	}
	//移动的过程中,判断是否遇到食物
	if (NextIsFood(pNextNode, ps))
	{
		//遇到食物
		EatFood(pNextNode, ps);
	}
	else
	{
		//没遇到食物
		NotEatFood(pNextNode, ps);
	}
}

//游戏玩的过程
void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();
	do
	{
		//打印当前得分
		SetPos(62, 10);
		printf("得分:%d", ps->Socre);
		SetPos(62, 11);
		printf("每个食物得分:%-2d", 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_SPACE))
		{
			//进行休眠
			pause();
		}
		else if (KEY_PRESS(VK_F3))
		{
			//加速
			if (ps->SleepTime >= 50)
			{
				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_ESCAPE))
		{
			//按ESC键退出
			ps->Status = ESC;
			break;
		}
		//蛇每走一步都需要休眠,休眠时间越短,蛇移动速度就越快
		Sleep(ps->SleepTime);
		SnakeMove(ps);
		//判断是否撞墙了
		KillByWall(ps);
		//判断是否撞到自己了
		KillBySelf(ps);
	} while (ps->Status == OK);
}


//游戏结束
void GameEnd(pSnake ps)
{
	SetPos(18, 10);
	switch (ps->Status)
	{
	case KILL_BY_WALL:
		printf("很遗憾撞墙了,游戏结束");
		break;
	case KILL_BY_SELF:
		printf("很遗憾撞到自身了,游戏结束");
		break;
	case ESC:
		printf("按了ESC键,正常退出");
		break;
	}
	//释放蛇身节点和食物节点
	pSnakeNode cur = ps->pSnake;
	pSnakeNode pNextNode = NULL;
	while (cur)
	{
		pNextNode = cur;
		cur = cur->next;
		free(pNextNode);
	}
	free(ps->pFood);
	ps = NULL;
}
相关推荐
单片机学习之路3 分钟前
【C语言】结构
c语言·开发语言·stm32·单片机·51单片机
thesky1234564 分钟前
活着就好20241224
学习·算法
ALISHENGYA9 分钟前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(实战项目二)
数据结构·c++·算法
guogaocai12312 分钟前
连续自成核退火热分级(SSA)技术表征共聚聚丙烯(PP)分子链结构
算法
蜗牛hb12 分钟前
VMware Workstation虚拟机网络模式
开发语言·学习·php
汤姆和杰瑞在瑞士吃糯米粑粑27 分钟前
【C++学习篇】AVL树
开发语言·c++·学习
Biomamba生信基地35 分钟前
R语言基础| 功效分析
开发语言·python·r语言·医药
DARLING Zero two♡35 分钟前
【优选算法】Pointer-Slice:双指针的算法切片(下)
java·数据结构·c++·算法·leetcode
手可摘星河37 分钟前
php中 cli和cgi的区别
开发语言·php
虾球xz40 分钟前
游戏引擎学习第58天
学习·游戏引擎