C语言——贪吃蛇小游戏

目录

前言

一、贪吃蛇游戏

1.1 游戏背景

1.2 游戏功能

1.3 技术要点

二、Win32 API

2.1  控制台程序

2.2 控制台屏幕上的坐标COORD

2.3 GetStdHandle

2.4 GetConsoleCursorInfo

2.4 CONSOLE_CURSOR_INFO

2.5 SetConsoleCursorInfo

2.6 SetConsoleCursorPosition

2.7 GetAsyncKeyState

三、贪吃蛇游戏设计与分析

3.1 地图

3.1.1 宽字符

3.1.2 本地化

3.1.3 类项

3.1.4 setlocale函数

3.1.5 宽字符的打印

3.1.6 地图坐标

3.2 蛇身和食物

3.3 数据结构设计

3.4 游戏流程

四、核心逻辑实现分析

4.1 游戏主逻辑

4.2 游戏开始(GameStart)

4.2.1 打印欢迎界面

4.2.2 游戏地图

4.2.3 初始化蛇

4.2.4 创建第一个食物

4.3 游戏运行(GameRun)

4.3.1 帮助信息

4.3.2 蛇身移动

4.3.2.1 判断是否是食物

4.3.2.2 吃食物

4.3.2.3 正常走

4.3.2.4 KillByWall

4.3.2.5 KillBySelf

4.4 游戏结束(GameEnd)

五、完整代码

test.c

Snake.h

Snake.c


前言

之前我们用C语言实现了一个扫雷小游戏:扫雷游戏,今天我们通过C语⾔函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等知识,来实现一个贪吃蛇小游戏。


一、贪吃蛇游戏

1.1 游戏背景

贪吃蛇是久负盛名的游戏,它也和俄罗斯方块,扫雷等游戏位列经典游戏的行列。
在编程语⾔中,我们以贪吃蛇为例,从设计到代码实现来提升我们编程能力和逻辑能力。

1.2 游戏功能

使用C语⾔在Windows环境的控制台中模拟实现经典⼩游戏贪吃蛇。
实现基本的功能:
贪吃蛇地图绘制
蛇吃⻝物的功能 (上、下、左、右方向键控制蛇的动作)
蛇撞墙死亡
蛇撞自身死亡
计算得分
蛇身加速、减速
暂停游戏

1.3 技术要点

实现贪吃蛇小游戏,我们需要掌握C语⾔函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等知识。

二、Win32 API

上面提到的中有个Win32 API我们不太熟悉,其实

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

2.1  控制台程序

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

参考:mode命令

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

title 贪吃蛇

参考:title命令

这些能在控制台窗口执行的命令,也可以调用C语⾔函数system来执行。例如:

#include<stdio.h>

int main{
    //设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列
     system("mode con cols=100 lines=30");
     //设置cmd窗⼝名称
     system("title 贪吃蛇"); 
     return 0;
}

2.2 控制台屏幕上的坐标COORD

COORD 是Windows API中定义的⼀个结构体,表示⼀个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0) 的原点位于缓冲区的顶部左侧单元格。
//结构体类型声明
typedef struct _COORD {
     SHORT X;
     SHORT Y;
} COORD, *PCOORD;
如图:

给坐标进行赋值:

COORD pos = { 10, 15 };

2.3 GetStdHandle

GetStdHandle是⼀个Windows API函数。它用于从⼀个特定的 标准设备 (标准输入、标准输出或标准错误)中取得⼀个 句柄 (用来标识不同设备的数值),使用这个句柄可以 操作设备
函数原型:
HANDLE GetStdHandle(DWORD nStdHandle);
HANDLE hOutput = NULL;

//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

2.4 GetConsoleCursorInfo

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

函数原型:

BOOL WINAPI GetConsoleCursorInfo(  
     HANDLE hConsoleOutput,
     PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
//PCONSOLE_CURSOR_INFO 是指向CONSOLE_CURSOR_INFO结构的指针,该结构接收有关主机游标
实例:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
    hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    CONSOLE_CURSOR_INFO CursorInfo;
    GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息

2.4 CONSOLE_CURSOR_INFO

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

typedef struct _CONSOLE_CURSOR_INFO {
     DWORD dwSize;//光标大小
     BOOL bVisible;//光标可见性
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
dwSize,由光标填充的字符单元格的 百分比 (通常为25)。 此值介于1到100之间。 光标外观会变化,范围从    完全填充单元格到单元底部的⽔平线条。
bVisible,游标的可见性。 如果光标可见,则此成员为 TRUE。
 CursorInfo.bVisible = false; //隐藏控制台光标

2.5 SetConsoleCursorInfo

设置指定控制台屏幕缓冲区的光标的大小和可见性。
BOOL WINAPI SetConsoleCursorInfo(
     HANDLE hConsoleOutput,
     const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);

实例:

HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

    //隐藏光标操作
    CONSOLE_CURSOR_INFO CursorInfo;
    GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
    CursorInfo.bVisible = false; //隐藏控制台光标
    SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态

2.6 SetConsoleCursorPosition

设置指定控制台屏幕缓冲区中的 光标位置 ,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
BOOL WINAPI SetConsoleCursorPosition(
     HANDLE hConsoleOutput, //句柄
     COORD pos    //光标位置
);

实例:

COORD pos = { 10, 5};
HANDLE hOutput = NULL;
 //获取标准输出的句柄(⽤来标识不同设备的数值)
     hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
 //设置标准输出上光标的位置为pos
     SetConsoleCursorPosition(hOutput, pos);

我们可以封装成一个函数SetPos,方便后续使用

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

2.7 GetAsyncKeyState

我们要获取按键情况,可以通过GetAsyncKeyState函数,函数原型如下:

SHORT GetAsyncKeyState(int vKey);
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState 的返回值是short类型,在上⼀次调用GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是 按下 ,如果最高是0,说明按键的状态是 抬起 ;如果最低位被置为1则说明,该按键被 按过 ,否则为0。
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的 最低值 是否为1.

我们可以定义成一个宏:

#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

如果返回为1则代表被按过,反之返回0。

参考:虚拟键码 (Winuser.h) - Win32 apps

三、贪吃蛇游戏设计与分析

3.1 地图

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

3.1.1 宽字符
在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★普通的字符是占⼀个字节的,这类宽字符是占用2个字节。这⾥再简单的讲⼀下C语⾔的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。C语言最初假定字符都是单字节的。但是这些假定并不是在世界的任何地方都适用。
C语⾔字符默认是采用ASCII编码的,ASCII字符集用的是单字节编码,且只使用了单字节中的低7位,最高位是没有使用的,可表示为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,比如,在法语中,字母上方有注音符号,它就无法使用 ASCII 码表示。于是,⼀些欧洲国家就决定,利用字节中闲置的 最高位编⼊ 新的符号。比如,法语中的é的编码为130(⼆进制10000010)。这样⼀来,这些欧洲国家使⽤的编码体系,可以表示最多256个符号。但是,这⾥又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不⼀样。比如,130在法语编码中代表了é,在希伯来语编码bs中却代表了字母Gimel在俄语编码中又会代表另⼀个符号。但是不管怎样,所有这些编码方式中,0--127表示的符号是⼀样的,不⼀样的只是128--255的这⼀段。
⾄于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。⼀个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达⼀个符号。比如,简体中文常见的编码方式是GB2312,使用两个字节表示⼀个汉字,所以理论上最多可以表示 256 x 256 = 65536 个符号。
后来为了使C语⾔适应国际化,C语⾔的标准中不断加入了国际化的支持。比如:加入了宽字符的类型wchar_t 和宽字符的输入和输出函数,加入了 <locale.h> 头⽂件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
3.1.2 <locale.h>本地化
<locale.h>提供的函数用于控制C标准库中对于不同的地区会产⽣不⼀样行为的部分。
在标准中,依赖地区的部分有以下几项:
数字量的格式
货币量的格式
字符集
日期和时间的表示形式
3.1.3 类项
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中⼀部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的⼀个宏,
指定⼀个类项:
LC_COLLATE:影响字符串比较函数 strcoll() strxfrm()
LC_CTYPE:影响字符处理函数的行为。
LC_MONETARY:影响货币格式。
LC_NUMERIC:影响 printf() 的数字格式。
LC_TIME:影响时间格式 strftime() wcsftime()
LC_ALL - 针对 所有类项修改 ,将以上所有类别设置为给定的语言环境。

3.1.4 setlocale函数

函数原型:

char* setlocale (int category, const char* locale);
setlocale 函数用于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
setlocale 的第⼀个参数可以是前面说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参数是LC_ALL,就会影响所有的类项。
C标准给第⼆个参数仅定义了2种可能取值: "C" (正常模式)和 " " (本地模式)。
在任意程序执行开始,都会隐藏式执行调用:
setlocale(LC_ALL, "C");
当地区设置为"C"时,库函数按正常方式执行,小数点是⼀个点。
当程序运行起来后想改变地区,就只能显示调用 setlocale函数 。用“ "作为第2个参数,调用 setlocale函数 就可以切换到本地模式,这种模式下程序会适应本地环境。
比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。
 setlocale(LC_ALL, " ");//切换到本地环境
3.1.5 宽字符的打印
那如果想在屏幕上打印宽字符,怎么打印呢?
宽字符的字面量必须加上 前缀“L” ,否则 C 语⾔会把字面量当作窄字符类型处理。前缀“L”在单引
号前面,表示宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前面,表示宽字符串,对应
wprintf() 的占位符为 %ls
#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.6 地图坐标
我们假设实现⼀个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙,如下:

3.2 蛇身和食物

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

3.3 数据结构设计

在游戏运行的过程中,蛇每次吃⼀个⻝物,蛇的身体就会变长⼀节,如果我们使用链表存储蛇的信
息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行,所以蛇节点结构如下:
//蛇身节点
typedef struct SnakeNode {
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇:
//贪吃蛇
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,//咬到⾃⼰
	END_NOMAL//正常结束
};

3.4 游戏流程

游戏流程如下:

四、核心逻辑实现分析

4.1 游戏主逻辑

程序开始就设置程序支持本地模式,然后进入游戏的主逻辑。
主逻辑分为3个过程:
游戏开始(GameStart)完成游戏的初始化
游戏运行(GameRun)完成游戏运行逻辑的实现
游戏结束(GameEnd)完成游戏结束的说明,实现资源释放
#include"Snake.h"

void test() {
	srand((unsigned int)time(NULL));
	int ch = 0;
	//循环支持多次游玩
	do {
		Snake snake = { 0 };
		GameStart(&snake);//游戏开始前的准备
		GameRun(&snake);  //游戏运行
		GameEnd(&snake);  //游戏结束
		SetPos(24, 13);
		printf("是否再来一把(y/n):");
		ch = getchar();
		getchar();//清理\n
	} while (ch == 'Y' || ch == 'y');
}

int main() {
	setlocale(LC_ALL, "");//修改当前地区为本地模式,为了⽀持中⽂宽字符的打印
	test();
	SetPos(24, 28);
	return 0;
}

4.2 游戏开始(GameStart)

这个模块完成游戏的初始化任务:

控制台窗口大小的设置
控制台窗口名字的设置
鼠标 光标的隐藏
打印欢迎界⾯
创建地图
初始化蛇
创建第⼀个食物
//游戏初始化
void GameStart(pSnake ps) {

	//设置控制台窗⼝的⼤⼩,30⾏,100列
	//mode 为DOS命令
	system("mode con cols=100 lines=30");
	//设置cmd窗⼝名称
	system("title 贪吃蛇");

	//隐藏光标

	//获取标准输出的句柄(⽤来标识不同设备的数值)
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//影藏光标操作
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
	CursorInfo.bVisible = false; //隐藏控制台光标
	SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态

	//欢迎界面
	WelcomeToGame();

	//游戏地图
	CreateMap();

	//初始化贪吃蛇
	InitSnake(ps);

	//创造第一个食物
	CreateFood(ps);

}
4.2.1 打印欢迎界面
void WelcomeToGame() {
	SetPos(40, 13);
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(55, 18);
	system("pause");
	system("cls");
	SetPos(25, 13);
	printf("用↑.↓.←.→ 分别控制蛇的移动,F3为加速,F4为减速\n");
	SetPos(25, 14);
	printf("加速将得到更高的分数");
	SetPos(55, 18);
	system("pause");
	system("cls");
}

通过SetPos定位光标的位置,来打印两个欢迎信息。

4.2.2 游戏地图

创建地图就是通过宽字符打印出墙体,关键是计算好坐标才能在想要的位置打印墙体。

我们先来定义个宏表示墙体:

#define WALL L'□'
易错点: 就是坐标的计算
上:(0,0)到(56,0)
下:(0,26)到(56,26)
左:(0,1)到(0,25)
右:(56,1)到(56,25)
打印地图的函数: CreateMap
//游戏地图
void CreateMap() {
	int i = 0;
	//上
	for (i = 0; i < 58; i += 2)
	{
		wprintf(L"%c", WALL);
	}
	//下
	SetPos(0, 26);
	for (i = 0; i < 58; i += 2)
	{
		wprintf(L"%c", WALL);
	}
	//左
	for (i = 1; i < 26; i++)
	{
		SetPos(0, i);
		wprintf(L"%c", WALL);
	}
	//右
	for (i = 1; i < 26; i++)
	{
		SetPos(56, i);
		wprintf(L"%c", WALL);
	}
}

打印效果

4.2.3 初始化蛇
蛇最开始长度为5节,每节对应 链表的⼀个节点 ,蛇⾝的每⼀个节点都有自己的坐标。创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇⾝后,将蛇的每⼀节打印在屏幕上。
蛇的初始位置从 (24,5) 开始。
  再设置当前游戏的状态,蛇移动的速度,默认的方向,初始成绩,每个⻝物的分数。
游戏状态是:OK
蛇的移动速度:200毫秒
蛇的默认方向:RIGHT
初始成绩:0
每个食物的分数:10

蛇头的初始坐标:

//初始蛇的位置
#define POS_X 24
#define POS_Y 5

打印蛇的宽字符:

#define BODY L'●'

初始化蛇的函数:InitSnake

//蛇身
void InitSnake(pSnake ps) {
	pSnakeNode cur = NULL;

	//创建蛇身节点
	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 + i * 2;
		cur->y = POS_Y;
		cur->next = NULL;
		//头插
		if (ps->_pSnake == NULL) {
			ps->_pSnake = cur;
		}
		else {
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}
	//打印蛇身
	cur = ps->_pSnake;
	while (cur) {
		SetPos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}
	//初始化蛇的其他信息
	ps->_SleepTime = 200;
	ps->_Socre = 0;
	ps->_Status = OK;
	ps->_Dir = RIGHT;
	ps->_foodWeight = 10;
}

打印效果:

4.2.4 创建第一个食物
随机生成 食物的坐标
x坐标必须是 2的倍数
食物 的坐标不能和蛇⾝每个节点的坐标 重复
创建食物节点,打印食物
⻝物打印的宽字符:
#define FOOD L'★'
创建食物的函数: CreateFood
//创建食物
void CreateFood(pSnake ps) {
	int x=0;
	int y=0;

	//食物位置必须与蛇头位置对齐,必须是二的倍数
again:
	do {
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} 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 == NULL)
	{
		perror("CreateFood::malloc()");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;
    
    //打印食物
	SetPos(pFood->x, pFood->y);
	wprintf(L"%c", FOOD);
	ps->_pFood = pFood;
}

4.3 游戏运行(GameRun)

游戏运行期间,右侧打印帮助信息,提示玩家,坐标开始位置(64, 15)
根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
如果游戏继续,就是 检测按键情况 ,确定蛇下⼀步的⽅向,或者是否加速减速,是否暂停或者退出游戏。
需要的虚拟按键的罗列:
上:VK_UP
下:VK_DOWN
左:VK_LEFT
右:VK_RIGHT
空格:VK_SPACE
ESC:VK_ESCAPE
F3:VK_F3
F4:VK_F4
确定了蛇的⽅向和速度,蛇就可以移动了。

我们定义一个宏来检测按键情况:

//检查按键是否按
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
//游戏运行
void GameRun(pSnake ps) {

	//打印帮助信息
	PrintfHelp();

	do {
        
        //打印分数情况
		SetPos(62, 10);
		printf("得分:%d", ps->_Socre);
		SetPos(75, 10);
		printf("每个食物分数:%02d", 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_ESCAPE)) {
			//退出游戏
			ps->_Status = END_NOMAL;
			break;
		}
		else if (KEY_PRESS(VK_F3)) {
			//加速,只能加速五次
			if (ps->_SleepTime >= 80) {
				ps->_SleepTime -= 30;
				ps->_foodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4)) {
			//减速,只能减速到食物得分为2
			if (ps->_foodWeight > 2) {
				ps->_SleepTime += 30;
				ps->_foodWeight -= 2;
			}
		}
		//蛇的移动
		SnakeMove(ps);

		Sleep(ps->_SleepTime);

	} while (ps->_Status == OK);

}
4.3.1 帮助信息

右侧打印帮助信息,提示玩家:

//帮助信息
void PrintfHelp() {
	SetPos(68, 14);
	printf("1.不能撞墙,不能咬到自己\n");
	SetPos(68, 15);
	printf("2.用↑.↓.←.→ 分别控制蛇的移动\n");
	SetPos(68, 16);
	printf("3.F3为加速,F4为减速\n");
	SetPos(68, 17);
	printf("ESC:退出游戏  空格: 暂停");
	SetPos(68, 19);
	printf("版权@sparks5210\n");

}

效果如下:

4.3.2 蛇身移动
先创建下⼀个节点,根据移动⽅向和蛇头的坐标,蛇移动到下⼀个位置的坐标。确定了下⼀个位置后,看下⼀个位置 是否是食物(NextIsFood) ,是食物就做 吃食物处理(EatFood) ,如果不是食物则做前进⼀步的处理 (NoFood) 。蛇身移动后,判断此次移动是否会造成 撞墙(KillByWall) 或者撞上 自己蛇⾝(KillBySelf) ,从而影响游戏的状态。
蛇身移动函数: SnakeMove
//蛇的移动
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 {
		NoFood(pNextNode, ps);
	}

	//撞到墙游戏结束
	KillByWall(ps);
	//撞到自己游戏结束
	KillBySelf(ps);
}
4.3.2.1 判断是否是食物

我们需要判断一下下一个位置是否是食物,如果是返回真,不是返回零。

判断食物的函数是:NextIsFood

//判断是否是食物
int NextIsFood(pSnakeNode pNextNode, pSnake ps) {
	return (pNextNode->x == ps->_pFood->x)&& (pNextNode->y == ps->_pFood->y);
}
4.3.2.2 吃食物

如果下一个位置是食物,我们就要进行吃食物的操作,把食物的节点变成新的头,蛇身长度变长。

进行吃操作的函数是:EatFood

//吃食物
void EatFood(pSnakeNode psn, pSnake ps) {
	//头插法
	psn->next = ps->_pSnake;
	ps->_pSnake = psn;

	//打印蛇身
	pSnakeNode cur = ps->_pSnake;
	while (cur) {
		SetPos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}
	ps->_Socre += ps->_foodWeight;

	//释放食物空间
	free(ps->_pFood);
	//创造新的食物
	CreateFood(ps);
}
4.3.2.3 正常走
如果下一个节点不是食物,就将下⼀个节点头插入蛇的身体,并将之前蛇身最后⼀个节点打印为空格,释放掉蛇身的最后⼀个节点。
易错点: 这里最容易错误的是,释放最后⼀个结点后,还得将指向在最后⼀个结点的指针改为NULL,保证蛇尾打印可以正常结束,不会越界访问。
正常走的函数是: NoFood
//下一个位置不是食物
void NoFood(pSnakeNode psn, pSnake ps) {
	//让下一个位置变成新的头
	psn->next = ps->_pSnake;
	ps->_pSnake = psn;

	//释放最后位置
	pSnakeNode cur = ps->_pSnake;
	while (cur->next->next) {
		SetPos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}
    //尾节点打印空格
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	
	free(cur->next);
	cur->next = NULL;
	
}
4.3.2.4 KillByWall
判断蛇头的坐标是否和墙的坐标冲突
//撞到墙游戏结束
void KillByWall(pSnake ps) {
	if (ps->_pSnake->x == 56 ||
		ps->_pSnake->x == 0 ||
		ps->_pSnake->y == 0 ||
		ps->_pSnake->y == 26)
	{
		ps->_Status = KILL_BY_WALL;
		return ;
	}
	return 0;
}

如果撞到墙,更改游戏状态为KILL_BY_WALL游戏结束。

4.3.2.5 KillBySelf
判断蛇头的坐标是否和蛇身体的坐标冲突
//撞到自己
void KillBySelf(pSnake ps) {
	pSnakeNode cur = ps->_pSnake->next;
	
	while (cur) {
		if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y) {
			ps->_Status = KILL_BY_SELF;
			return 1;
		}
		cur = cur->next;
	}
	return 0;

}

如果撞到自己,更改游戏状态为 KILL_BY_SELF游戏结束。

4.4 游戏结束(GameEnd)

游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇身节点。
void GameEnd(pSnake ps) {
	
	SetPos(24, 12);
	switch (ps->_Status)
	{
	case KILL_BY_WALL:
		printf("撞到墙了,游戏结束\n");
		break;
	case KILL_BY_SELF:
		printf("撞到自己了,游戏结束\n");
		break;
	case END_NOMAL:
		printf("游戏正常退出\n");
		break;
	}

	pSnakeNode cur = ps->_pSnake;
	//释放贪吃蛇
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
	//释放食物
	free(ps->_pFood);
}

五、完整代码

下面是游戏的完整代码

test.c

#include"Snake.h"

void test() {
	srand((unsigned int)time(NULL));
	int ch = 0;
	//循环支持多次游玩
	do {
		Snake snake = { 0 };
		GameStart(&snake);//游戏开始前的准备
		GameRun(&snake);  //游戏运行
		GameEnd(&snake);  //游戏结束
		SetPos(24, 13);
		printf("是否再来一把(y/n):");
		ch = getchar();
		getchar();//清理\n
	} while (ch == 'Y' || ch == 'y');
}

int main() {
	setlocale(LC_ALL, "");//修改当前地区为本地模式,为了⽀持中⽂宽字符的打印
	test();
	SetPos(24, 28);
	return 0;
}

Snake.h

#include<stdlib.h>
#include<stdbool.h>
#include<Windows.h>
#include<locale.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)

//蛇的方向
enum DIRECTION {
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};
//游戏状态
enum GAME_STATUS {
	OK,//正常运⾏
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF,//咬到⾃⼰
	END_NOMAL//正常结束
};

//蛇身节点
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;

//游戏初始化
void GameStart(pSnake ps);

//欢迎界面
void WelcomeToGame();

//设置光标的位置
void SetPos(short x, short y);

//游戏地图
void CreateMap();

//初始化贪吃蛇
void InitSnake(pSnake ps);

//食物位置
void CreateFood(pSnake ps);

//打印帮助信息
void PrintfHelp();

//暂停游戏
void pause();

//蛇的移动
void SnakeMove(pSnake ps);

//判断是否是食物
int NextIsFood(pSnakeNode pNextNode, pSnake ps);

//吃食物
void EatFood(pSnakeNode psn, pSnake ps);

//正常走,不是食物
void NoFood(pSnakeNode psn, pSnake ps);

//撞到墙
void KillByWall(pSnake ps);

//撞到自己
void KillBySelf(pSnake ps);

//游戏运行
void GameRun(pSnake ps);

//游戏结束
void GameEnd(pSnake ps);

Snake.c

#include"Snake.h"

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

//欢迎界面
void WelcomeToGame() {
	SetPos(40, 13);
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(55, 18);
	system("pause");
	system("cls");
	SetPos(25, 13);
	printf("用↑.↓.←.→ 分别控制蛇的移动,F3为加速,F4为减速\n");
	SetPos(25, 14);
	printf("加速将得到更高的分数");
	SetPos(55, 18);
	system("pause");
	system("cls");
}

//游戏地图
void CreateMap() {
	int i = 0;
	//上
	for (i = 0; i < 58; i += 2)
	{
		wprintf(L"%c", WALL);
	}
	//下
	SetPos(0, 26);
	for (i = 0; i < 58; i += 2)
	{
		wprintf(L"%c", WALL);
	}
	//左
	for (i = 1; i < 26; i++)
	{
		SetPos(0, i);
		wprintf(L"%c", WALL);
	}
	//右
	for (i = 1; i < 26; i++)
	{
		SetPos(56, i);
		wprintf(L"%c", WALL);
	}
}

//蛇身
void InitSnake(pSnake ps) {
	pSnakeNode cur = NULL;

	//创建蛇身节点
	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 + i * 2;
		cur->y = POS_Y;
		cur->next = NULL;
		//头插
		if (ps->_pSnake == NULL) {
			ps->_pSnake = cur;
		}
		else {
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}
	//打印蛇身
	cur = ps->_pSnake;
	while (cur) {
		SetPos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}
	//初始化蛇的其他信息
	ps->_SleepTime = 200;
	ps->_Socre = 0;
	ps->_Status = OK;
	ps->_Dir = RIGHT;
	ps->_foodWeight = 10;
}

//食物位置
void CreateFood(pSnake ps) {
	int x=0;
	int y=0;

	//食物位置必须与蛇头位置对齐,必须是二的倍数
again:
	do {
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} 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 == NULL)
	{
		perror("CreateFood::malloc()");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;
	SetPos(pFood->x, pFood->y);
	wprintf(L"%c", FOOD);
	ps->_pFood = pFood;
}

//游戏初始化
void GameStart(pSnake ps) {

	//设置控制台窗⼝的⼤⼩,30⾏,100列
	//mode 为DOS命令
	system("mode con cols=100 lines=30");
	//设置cmd窗⼝名称
	system("title 贪吃蛇");

	//隐藏光标

	//获取标准输出的句柄(⽤来标识不同设备的数值)
	HANDLE 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 PrintfHelp() {
	SetPos(68, 14);
	printf("1.不能撞墙,不能咬到自己\n");
	SetPos(68, 15);
	printf("2.用↑.↓.←.→ 分别控制蛇的移动\n");
	SetPos(68, 16);
	printf("3.F3为加速,F4为减速\n");
	SetPos(68, 17);
	printf("ESC:退出游戏  空格: 暂停");
	SetPos(68, 19);
	printf("版权@sparks5210\n");

}

//暂停游戏
void pause() {
	while (1) {
		Sleep(200);
		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 psn, pSnake ps) {
	//头插法
	psn->next = ps->_pSnake;
	ps->_pSnake = psn;

	//打印蛇身
	pSnakeNode cur = ps->_pSnake;
	while (cur) {
		SetPos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}
	ps->_Socre += ps->_foodWeight;

	//释放食物空间
	free(ps->_pFood);
	//创造新的食物
	CreateFood(ps);
}

//下一个位置不是食物
void NoFood(pSnakeNode psn, pSnake ps) {
	//让下一个位置变成新的头
	psn->next = ps->_pSnake;
	ps->_pSnake = psn;

	//释放最后位置
	pSnakeNode cur = ps->_pSnake;
	while (cur->next->next) {
		SetPos(cur->x, cur->y);
		wprintf(L"%c", 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 == 56 ||
		ps->_pSnake->x == 0 ||
		ps->_pSnake->y == 0 ||
		ps->_pSnake->y == 26)
	{
		ps->_Status = KILL_BY_WALL;
		return ;
	}
	return 0;
}

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

}

//蛇的移动
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 {
		NoFood(pNextNode, ps);
	}

	//撞到墙游戏结束
	KillByWall(ps);
	//撞到自己游戏结束
	KillBySelf(ps);
}


//游戏运行
void GameRun(pSnake ps) {

	//打印帮助信息
	PrintfHelp();

	do {

		SetPos(62, 10);
		printf("得分:%d", ps->_Socre);
		SetPos(75, 10);
		printf("每个食物分数:%02d", 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_ESCAPE)) {
			//退出游戏
			ps->_Status = END_NOMAL;
			break;
		}
		else if (KEY_PRESS(VK_F3)) {
			//加速,只能加速五次
			if (ps->_SleepTime >= 80) {
				ps->_SleepTime -= 30;
				ps->_foodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4)) {
			//减速,只能减速到食物得分为2
			if (ps->_foodWeight > 2) {
				ps->_SleepTime += 30;
				ps->_foodWeight -= 2;
			}
		}
		//蛇的移动
		SnakeMove(ps);

		Sleep(ps->_SleepTime);

	} while (ps->_Status == OK);

}

//游戏结束
void GameEnd(pSnake ps) {
	
	SetPos(24, 12);
	switch (ps->_Status)
	{
	case KILL_BY_WALL:
		printf("撞到墙了,游戏结束\n");
		break;
	case KILL_BY_SELF:
		printf("撞到自己了,游戏结束\n");
		break;
	case END_NOMAL:
		printf("游戏正常退出\n");
		break;
	}

	pSnakeNode cur = ps->_pSnake;
	//释放贪吃蛇
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
	//释放食物
	free(ps->_pFood);
}

总结

上述文章,我们通过C语言学习到的知识实现了一个贪吃蛇的小游戏,希望对你有所帮助。

  • 49
    点赞
  • 71
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值