超级详细——手撕贪吃蛇小游戏!

目录

前言

1. Win32 API介绍 

1.1 Win32 API

1.2 控制台程序

1.3 控制台屏幕上的坐标COORD

1.4 GetStdHandle

1.5 GetConsoleCursorInfo

1.6 CONSOLE_CURSOR_INFO

1.7 SetConsoleCursorInfo

1.8 SetConsoleCursorPosition

1.8 GetAsyncKeyState

2.贪吃蛇游戏设计 

 2.1地图

2.1.1 本地化

2.2宽字符的打印

 2.3 蛇身和食物

2.4 数据结构的设计

3.核心逻辑的实现 

3.1 游戏主逻辑

 3.2 游戏开始

3.2.1 隐藏光标

3.2.2 打印欢迎信息

​编辑

​编辑

3.2.3 绘制地图

​编辑

3.2.4 初始化蛇身

3.2.5 创建食物

3.3 游戏运行

3.3.1 KEY_PRESS

3.3.2 PrintHelpInfo

3.3.3 蛇身的移动

3.3.3.1 NextIsFood

3.3.3.2 EatFood

3.3.3.3 NotEatFood

3.3.3.4 KillByWall

3.3.3.5 KillBySelf

3.4 游戏结束

4.完整代码

4.1 test.c

4.2 snake.h

4.3 snake.c

总结


前言

今天这篇贪吃蛇游戏实现满满干货,可以边看边写,或者反复观看。其中需要运用到C语⾔函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等知识,我相信你认真看一遍会学到不少知识。

如果你边看边写,在写一部分的时候需要停下来看效果可以使用getchar()或者system(“pause”)这两段代码其中之一即可。都需要你按键盘上的按钮才会继续。


1. Win32 API介绍 

1.1 Win32 API

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

1.2 控制台程序

平常我们运⾏起来的⿊框程序其实就是控制台程序
我们可以使⽤cmd命令来设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列

mode con cols=100 lines=30

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

title 贪吃蛇

但是,如果你打开cmd是下面这样子,那么你的命令窗口是个终端,需要调整成控制窗口。

超级详细——手撕贪吃蛇小游戏!_第1张图片

按右键,点击设置。点击默认终端应用程序的下划线,选择windows控制台主机。

超级详细——手撕贪吃蛇小游戏!_第2张图片

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

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

1.3 控制台屏幕上的坐标COORD

COORD 是Windows API中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕上的坐标。

超级详细——手撕贪吃蛇小游戏!_第3张图片

如上图,表明原点,X轴和Y轴。给坐标赋值:

COORD pos = { 10, 15 };

1.4 GetStdHandle

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

HANDLE GetStdHandle(DWORD nStdHandle);

简单来说,就是你的命令窗口可以开启多个,为了让系统知道你在哪一个命令窗口执行命令,需要用到GetStdHandle获取该命令窗口的信息。Handle是一个关键字。

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


1.5 GetConsoleCursorInfo

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

BOOL WINAPI GetConsoleCursorInfo(
HANDLE    hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

第一个函数参数是Handle类型的句柄,第二个参数下面会介绍。
 

HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息

1.6 CONSOLE_CURSOR_INFO

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

typedef struct _CONSOLE_CURSOR_INFO {
    DWORD dwSize;
    BOOL  bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
  • dwSize,由光标填充的字符单元格的百分⽐。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的⽔平线条。
  • bVisible,游标的可⻅性。 如果光标可⻅,则此成员为 TRUE。

1.7 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);//设置控制台光标状态

1.8 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);
    SetConsoleCursorPosition(hOutput, pos);
}

1.8 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 )

2.贪吃蛇游戏设计 

 2.1地图

贪吃蛇游戏初始界面大概如下:
超级详细——手撕贪吃蛇小游戏!_第4张图片
超级详细——手撕贪吃蛇小游戏!_第5张图片

超级详细——手撕贪吃蛇小游戏!_第6张图片
在游戏地图上,我们打印墙体使⽤宽字符:■,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★。这些可以在搜狗输入法的符号大全中找到。普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节。

过去C语⾔并不适合⾮英语国家(地区)使⽤。C语⾔最初假定字符都是⾃⼰的。但是这些假定并不是在世界的任何地⽅都适⽤。后来为了使C语⾔适应国际化,C语⾔的标准中不断加⼊了国际化的⽀持。⽐如:加⼊和宽字符的类型wchar_t 和宽字符的输⼊和输出函数,加⼊和头⽂件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语⾔的地理区域)调整程序⾏为的函数。

2.1.1 本地化

提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分。
通过修改地区,程序可以改变它的⾏为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中⼀部分可能是我们不希望修改的。所以C语⾔⽀持针对不同的类项进⾏修改,下⾯的⼀个宏,指定⼀个类项:

  • LC_COLLATE
  • LC_CTYPE
  • LC_MONETARY
  • LC_NUMERIC
  • LC_TIME
  • LC_ALL - 针对所有类项修改

setlocale 函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
setlocale 的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参数是LC_ALL,就会影响所有的类项。C标准给第⼆个参数仅定义了2种可能取值:"C"和" "。
在任意程序执⾏开始,都会隐藏式执⾏调⽤:

setlocale(LC_ALL, "C");

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

setlocale(LC_ALL, " ");//切换到本地环境


2.2宽字符的打印

那如果想在屏幕上打印宽字符,怎么打印呢?

#include 
#include
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"%c\n", ch1);
	wprintf(L"%c\n", ch2);
	wprintf(L"%c\n", ch3);
	wprintf(L"%c\n", ch4);
	return 0;
}

输出结果:

超级详细——手撕贪吃蛇小游戏!_第7张图片

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

一个坐标一个正常字符,两个坐标就是一个宽字符。
超级详细——手撕贪吃蛇小游戏!_第8张图片

我们假设实现⼀个棋盘27行,58列的棋盘(⾏和列可以根据⾃⼰的情况修改),再围绕地图画出墙。

 2.3 蛇身和食物

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

超级详细——手撕贪吃蛇小游戏!_第9张图片
 

2.4 数据结构的设计

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

typedef struct SnakeNode
{
    int x;
    int y;
    struct SnakeNode* next;
}SnakeNode, * pSnakeNode;

 注意:typedef加在结构体面前,第二个命名* pSnakeNode,是吧struct 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//正常结束,按ESC按键
};

3.核心逻辑的实现 

首先,需要创建三个文件,snake.h、snake.c和test.c,其中snake.h里面需要放入数据节后的设计和函数的声明,snake.c里面是贪吃蛇游戏实现的整个核心,test.c是测试代码。如果不熟悉函数文件分写操作,可以点击以下链接,在函数声明与定义中多个文件下有介绍。http://t.csdnimg.cn/E973picon-default.png?t=N7T8http://t.csdnimg.cn/E973p

3.1 游戏主逻辑

在test.c中,先修改为本地模式,封装一个test函数,内部装游戏结束后再来一局的逻辑,ch接受键盘上的输入的字符。SetPos是改变输入文字的位置。

#include "snake.h"
#include 

void test()
{
	int ch = 0;
	do
	{
		Snake snake = { 0 };
		GameStart(&snake);
		GameRun(&snake);
		GameEnd(&snake);
		SetPos(20, 15);
		printf("再来⼀局吗?(Y/N):");
		ch = getchar();
		getchar();//清理\n
	} while (ch == 'Y');
	SetPos(0, 27);
}

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

 3.2 游戏开始

void GameStart(pSnake ps)
{
	//设置控制台的信息,窗口大小,窗口名
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

	//隐藏光标
	HideCursor();

	//打印欢迎信息
	WelcomeToGame();

	//绘制地图
	CreateMap();
	
	//初始化蛇
	InitSnake(ps);
	
	//创建食物
	CreateFood(ps);
}

3.2.1 隐藏光标

void HideCursor()
{
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	//获取光标信息
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(handle, &CursorInfo);
	//修改光标值
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(handle, &CursorInfo);
}

3.2.2 打印欢迎信息

在游戏正式开始之前,做⼀些功能提醒。

void WelcomeToGame()
{
	//欢迎信息
	SetPos(38, 12);
	printf("欢迎来到贪吃蛇小游戏\n");
	SetPos(39, 17);
	system("pause");
	system("cls");

	//功能介绍信息
	SetPos(28, 12);
	printf("用↑ ↓ ← → 来控制蛇的移动,F3加速,F4是减速");
	SetPos(37, 13);
	printf("加速能得到更高的分数");
	SetPos(39, 17);
	system("pause");
	system("cls");//清空屏幕
}

超级详细——手撕贪吃蛇小游戏!_第10张图片

超级详细——手撕贪吃蛇小游戏!_第11张图片

3.2.3 绘制地图

创建地图就是将墙打印出来,因为是宽字符打印,所有使⽤wprintf函数,打印格式串前使⽤L打印地图的关键是要算好坐标,才能在想要的位置打印墙体。

墙体打印的宽字符:

#define WALL L'■'

 创建地图函数CreateMap

void CreateMap()
{
	int i = 0;
	//上(0,0)-(56, 0)
	SetPos(0, 0);
	for (i = 0; i < 58; i += 2)
	{
		wprintf(L"%c", WALL);
	}
	//下(0,26)-(56, 26)

	SetPos(0, 26);
	for (i = 0; i < 58; i += 2)
	{
		wprintf(L"%c", WALL);
	}
	//左
	//x是0,y从1开始增⻓
	for (i = 1; i < 26; i++)
	{
		SetPos(0, i);
		wprintf(L"%c", WALL);
	}
	//x是56,y从1开始增⻓
	for (i = 1; i < 26; i++)
	{
		SetPos(56, i);
		wprintf(L"%c", WALL);
	}
}

超级详细——手撕贪吃蛇小游戏!_第12张图片

3.2.4 初始化蛇身

蛇最开始⻓度为5节,每节对应链表的⼀个节点,蛇⾝的每⼀个节点都有⾃⼰的坐标。创建5个节点,然后将每个节点存放在链表中进⾏管理。创建完蛇⾝后,将蛇的每⼀节打印在屏幕上。

再设置当前游戏的状态,蛇移动的速度,默认的⽅向,初始成绩,蛇的状态,每个⻝物的分数。其中蛇休眠的速度是通过Sleep函数实现,初始值200毫秒。
蛇⾝打印的宽字符:

#define BODY L'●'

 初始化蛇⾝函数:InitSnake

void InitSnake(pSnake ps)
{
	//创建5个蛇身的节点
	pSnakeNode cur = NULL;
	for (int 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;

		//头插法
		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"%lc", BODY);
		cur = cur->next;
	}

	//贪吃蛇其他信息初始化
	ps->dir = RIGHT;
	ps->FoodWeight = 10;
	ps->pFood = NULL;
	ps->Score = 0;
	ps->SleepTime = 200;
	ps->status = OK;

}

3.2.5 创建食物

  • 先随机⽣成⻝物的坐标
  • x坐标必须是2的倍数
  • ⻝物的坐标不能和蛇⾝每个节点的坐标重复
  • 创建⻝物节点,打印⻝物

⻝物打印的宽字符:

#define FOOD L'★'

创建⻝物的函数:CreateFood

void CreateFood(pSnake ps)
{
	srand((unsigned int)time(NULL));
	int x = 0;
	int y = 0;

again:
	do 
	{
		x = rand() % 53 + 2;
		y = rand() % 24 + 1;
	} while(x % 2!= 0);

	//坐标和蛇的身体的每个节点的坐标比较
	pSnakeNode cur = ps->pSnake;
	while (cur)
	{
		if (x == cur->x && y == cur->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;

	ps->pFood = pFood;
	SetPos(pFood->x, pFood->y);
	wprintf(L"%lc", FOOD);
}

3.3 游戏运行

游戏运⾏期间,右侧打印帮助信息,提⽰玩家

  1. 根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
  2. 如果游戏继续,就是检测按键情况,确定蛇下⼀步的⽅向,或者是否加速减速,是否暂停或者退出游戏。
  3. 确定了蛇的⽅向和速度,蛇就可以移动了。
void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();
	
	do
	{   //打印分值信息
		SetPos(62, 10);
		printf("总分:%d", ps->Score);
		SetPos(62, 11);
		printf("食物的分值:%02d", ps->FoodWeight);

		//检测按键
		//上、下、左、右、ESC、空格、F3、F4
		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_ESCAPE))
		{
			ps->status = ESC;
			break;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			//暂停一下
			pause();
		}
		else if (KEY_PRESS(VK_F3))
		{
			if (ps->SleepTime >= 80)
			{
				ps->SleepTime -= 30;
				ps->FoodWeight += 2;
			}
		}         
		else if (KEY_PRESS(VK_F4))
		{
			if (ps->FoodWeight >= 2)
			{
				ps->SleepTime += 30;
				ps->FoodWeight -= 2;
			}
		}
		//走一步
		SnakeMove(ps);
		//睡眠一下
		Sleep(ps->SleepTime);

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

3.3.1 KEY_PRESS

检测按键状态,我们封装了⼀个宏,其中VK为前缀的都是宏代表键盘按键的虚拟键值

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

3.3.2 PrintHelpInfo

void PrintHelpInfo()
{
	SetPos(62, 15);
	printf("1.不能穿墙,不能咬到自己");
	SetPos(62, 16);
	printf("2.用↑↓←→ 来控制蛇的移动");
	SetPos(62, 17);
	printf("3.F3加速,F4是减速");
	SetPos(62, 18);
	printf("4.萧瑟其中版");
}

超级详细——手撕贪吃蛇小游戏!_第13张图片

3.3.3 蛇身的移动

  1. 先创建下⼀个节点,根据移动⽅向和蛇头的坐标,蛇移动到下⼀个位置的坐标。
  2. 确定了下⼀个位置后,看下⼀个位置是否是⻝物(NextIsFood),是⻝物就做吃⻝物处理
  3. (EatFood),如果不是⻝物则做前进⼀步的处理(NoFood)。
  4. 蛇⾝移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上⾃⼰蛇⾝(KillBySelf),从而影响游戏的状态。
void SnakeMove(pSnake ps)
{
	//创建下⼀个节点
	 pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
	 if (pNext == NULL)
	 {
		 perror("SankeMove():malloc()");
		 return 0;
	 }
	 pNext->next = NULL;
	 //确定下⼀个节点的坐标,下⼀个节点的坐标根据,蛇头的坐标和⽅向确定
	switch (ps->dir)
	{
	case UP:
		pNext->x = ps->pSnake->x;
		pNext->y = ps->pSnake->y - 1;
		break;
	case DOWN:
		pNext->x = ps->pSnake->x;
		pNext->y = ps->pSnake->y + 1;
		break;
	case LEFT:
		pNext->x = ps->pSnake->x - 2;
		pNext->y = ps->pSnake->y;
		break;
	case RIGHT:
		pNext->x = ps->pSnake->x + 2;
		pNext->y = ps->pSnake->y;
		break;
	}

	//下一个坐标是否是食物
	if (NextIsFood(ps, pNext))
	{
		EatFood(ps,pNext);
	}
	else
	{
		NotEatFood(ps, pNext);
	}
	//判断是否咬到自己
	KillBySelf(ps);
	//判断是都撞到墙
	KillByWall(ps);
}
3.3.3.1 NextIsFood
int NextIsFood(pSnake ps, pSnakeNode pNext)
{
	if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y)
		return 1;
	else
		return 0;
}
3.3.3.2 EatFood

这里利用链表的头插法。

void EatFood(pSnake ps, pSnakeNode pNext)
{
	pNext->next = ps->pSnake;
	ps->pSnake = pNext;

	pSnakeNode cur = ps->pSnake;
	//打印蛇
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	ps->Score += ps->FoodWeight;

	//释放旧的食物
	free(ps->pFood);
	//新建食物
	CreateFood(ps);
}
3.3.3.3 NotEatFood

将下⼀个节点头插⼊蛇的⾝体,并将之前蛇⾝最后⼀个节点打印为空格,放弃掉蛇⾝的最后⼀个节点。

void NotEatFood(pSnake ps, pSnakeNode pNext)
{
	//头插法
	pNext->next = ps->pSnake;
	ps->pSnake = pNext;

	//释放尾结点
	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;//易错

}
3.3.3.4 KillByWall
void KillByWall(pSnake ps)
{
	if (ps->pSnake->x == 0
		|| ps->pSnake->x == 56
		|| ps->pSnake->y == 0
		|| ps->pSnake->y == 25)
	{
		ps->status = KILL_BY_WALL;
	}
}
3.3.3.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;
		}
		cur = cur->next;
	}
}

3.4 游戏结束

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

void GameEnd(pSnake ps)
{
	SetPos(15, 12);
	switch (ps->status)
	{
	case ESC:
		printf("主动退出游戏,正常退出\n");
		break;
	case KILL_BY_SELF:
		printf("很抱歉,撞墙了,游戏结束\n");
		break;
	case KILL_BY_WALL:
		printf("很抱歉,咬到自己了,游戏结束\n");
		break;
	}
	//释放贪吃蛇的链表资源
	pSnakeNode cur = ps->pSnake;
	pSnakeNode del = NULL;

	while (cur)
	{
		del = cur;
		cur = cur->next;
		free(del);
	}
	
	free(ps->pFood);
	ps = NULL;
}

4.完整代码

完整代码实现,分3个⽂件实现.

4.1 test.c

#include "snake.h"

void test()
{
	int ch = 0;

	do
	{   //创建贪吃蛇
		Snake sanke = { 0 };
		GameStart(&sanke);//游戏开始前的初始化
		GameRun(&sanke);//游戏过程
		GameEnd(&sanke);//善后工作
		SetPos(20, 15);
		printf("再来一局吗?(Y/N):");
		ch = getchar();
		getchar();//清理\n
	} while (ch == 'Y' || ch == 'y');
}

int main()
{
	//修改适配本地中文环境
	setlocale(LC_ALL, "");

	test();//贪吃蛇游戏的测试
	SetPos(0, 26);
	return 0;
}

4.2 snake.h

#pragma once
#include 
#include 
#include 
#include 
#include 
#include 

#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 GAME_STATUS
{
	OK = 1,
	ESC,
	KILL_BY_WALL,
	KILL_BY_SELF
};

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


//贪吃蛇蛇身节点定义
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
//typedef struct SnakeNode* pSnakeNode  //相当于这个

//贪吃蛇
typedef struct Snake
{
	pSnakeNode pSnake;//维护整条蛇的指针
	pSnakeNode pFood;//指向食物的指针
	int Score;//当前累计的分数
	int FoodWeight;//食物的分数
	int SleepTime;//休眠的时间越短,蛇的速度越快,休眠时间越长,蛇的速度越慢
	enum GAME_STATUS status;//游戏当前的状态
	enum DIRECTION dir;//蛇当前走的方向
	//...
}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 GameRun(pSnake ps);

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

//判断蛇头的下一步的位置是不是食物
int NextIsFood(pSnake ps, pSnakeNode pNext);

//是否吃到食物,蛇下一步怎么走
void EatFood(pSnake ps, pSnakeNode pNext); 
void NotEatFood(pSnake ps, pSnakeNode pNext);

//判断什么原因结束
void KillByWall(pSnake ps);
void KillBySelf(pSnake ps);

//善后工作
void GameEnd(pSnake ps);

4.3 snake.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"

void SetPos(short x, short y)
{
	//获得设备句柄
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	//根据句柄设置光标信息
	COORD pos = { x, y };
	SetConsoleCursorPosition(handle, pos);
}

void HideCursor()
{
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	//获取光标信息
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(handle, &CursorInfo);
	//修改光标值
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(handle, &CursorInfo);
}

void WelcomeToGame()
{
	//欢迎信息
	SetPos(38, 12);
	printf("欢迎来到贪吃蛇小游戏\n");
	SetPos(39, 17);
	system("pause");
	system("cls");

	//功能介绍信息
	SetPos(28, 12);
	printf("用↑ ↓ ← → 来控制蛇的移动,F3加速,F4是减速");
	SetPos(37, 13);
	printf("加速能得到更高的分数");
	SetPos(39, 17);
	system("pause");
	system("cls");//清空屏幕
}

void CreateMap()
{
	int i = 0;
	//上(0,0)-(56, 0)
	SetPos(0, 0);
	for (i = 0; i < 58; i += 2)
	{
		wprintf(L"%c", WALL);
	}
	//下(0,26)-(56, 26)

	SetPos(0, 26);
	for (i = 0; i < 58; i += 2)
	{
		wprintf(L"%c", WALL);
	}
	//左
	//x是0,y从1开始增⻓
	for (i = 1; i < 26; i++)
	{
		SetPos(0, i);
		wprintf(L"%c", WALL);
	}
	//x是56,y从1开始增⻓
	for (i = 1; i < 26; i++)
	{
		SetPos(56, i);
		wprintf(L"%c", WALL);
	}
}


void InitSnake(pSnake ps)
{
	//创建5个蛇身的节点
	pSnakeNode cur = NULL;
	for (int 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;

		//头插法
		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"%lc", BODY);
		cur = cur->next;
	}

	//贪吃蛇其他信息初始化
	ps->dir = RIGHT;
	ps->FoodWeight = 10;
	ps->pFood = NULL;
	ps->Score = 0;
	ps->SleepTime = 200;
	ps->status = OK;

}

void CreateFood(pSnake ps)
{
	srand((unsigned int)time(NULL));
	int x = 0;
	int y = 0;

again:
	do 
	{
		x = rand() % 53 + 2;
		y = rand() % 24 + 1;
	} while(x % 2!= 0);

	//坐标和蛇的身体的每个节点的坐标比较
	pSnakeNode cur = ps->pSnake;
	while (cur)
	{
		if (x == cur->x && y == cur->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;

	ps->pFood = pFood;
	SetPos(pFood->x, pFood->y);
	wprintf(L"%lc", FOOD);
}


void GameStart(pSnake ps)
{
	//设置控制台的信息,窗口大小,窗口名
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

	//隐藏光标
	HideCursor();

	//打印欢迎信息
	WelcomeToGame();

	//绘制地图
	CreateMap();
	
	//初始化蛇
	InitSnake(ps);
	
	//创建食物
	CreateFood(ps);
	//system("pause");
}

void PrintHelpInfo()
{
	SetPos(62, 15);
	printf("1.不能穿墙,不能咬到自己");
	SetPos(62, 16);
	printf("2.用↑↓←→ 来控制蛇的移动");
	SetPos(62, 17);
	printf("3.F3加速,F4是减速");
	SetPos(62, 18);
	printf("4.萧瑟其中版");

}

void pause()
{
	while (1)
	{
		Sleep(100);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

int NextIsFood(pSnake ps, pSnakeNode pNext)
{
	if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y)
		return 1;
	else
		return 0;
}

void EatFood(pSnake ps, pSnakeNode pNext)
{
	pNext->next = ps->pSnake;
	ps->pSnake = pNext;

	pSnakeNode cur = ps->pSnake;
	//打印蛇
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	ps->Score += ps->FoodWeight;

	//释放旧的食物
	free(ps->pFood);
	//新建食物
	CreateFood(ps);
}

void NotEatFood(pSnake ps, pSnakeNode pNext)
{
	//头插法
	pNext->next = ps->pSnake;
	ps->pSnake = pNext;

	//释放尾结点
	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 == 25)
	{
		ps->status = KILL_BY_WALL;
	}
}


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;
		}
		cur = cur->next;
	}
}


void SnakeMove(pSnake ps)
{
	//创建下⼀个节点
	 pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
	 if (pNext == NULL)
	 {
		 perror("SankeMove():malloc()");
		 return 0;
	 }
	 pNext->next = NULL;
	 //确定下⼀个节点的坐标,下⼀个节点的坐标根据,蛇头的坐标和⽅向确定
	switch (ps->dir)
	{
	case UP:
		pNext->x = ps->pSnake->x;
		pNext->y = ps->pSnake->y - 1;
		break;
	case DOWN:
		pNext->x = ps->pSnake->x;
		pNext->y = ps->pSnake->y + 1;
		break;
	case LEFT:
		pNext->x = ps->pSnake->x - 2;
		pNext->y = ps->pSnake->y;
		break;
	case RIGHT:
		pNext->x = ps->pSnake->x + 2;
		pNext->y = ps->pSnake->y;
		break;
	}

	//下一个坐标是否是食物
	if (NextIsFood(ps, pNext))
	{
		EatFood(ps,pNext);
	}
	else
	{
		NotEatFood(ps, pNext);
	}
	//判断是否咬到自己
	KillBySelf(ps);
	//判断是都撞到墙
	KillByWall(ps);
}


void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();
	
	do
	{   //打印分值信息
		SetPos(62, 10);
		printf("总分:%d", ps->Score);
		SetPos(62, 11);
		printf("食物的分值:%02d", ps->FoodWeight);

		//检测按键
		//上、下、左、右、ESC、空格、F3、F4
		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_ESCAPE))
		{
			ps->status = ESC;
			break;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			//暂停一下
			pause();
		}
		else if (KEY_PRESS(VK_F3))
		{
			if (ps->SleepTime >= 80)
			{
				ps->SleepTime -= 30;
				ps->FoodWeight += 2;
			}
		}         
		else if (KEY_PRESS(VK_F4))
		{
			if (ps->FoodWeight >= 2)
			{
				ps->SleepTime += 30;
				ps->FoodWeight -= 2;
			}
		}
		//走一步
		SnakeMove(ps);
		//睡眠一下
		Sleep(ps->SleepTime);

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

void GameEnd(pSnake ps)
{
	SetPos(15, 12);
	switch (ps->status)
	{
	case ESC:
		printf("主动退出游戏,正常退出\n");
		break;
	case KILL_BY_SELF:
		printf("很抱歉,撞墙了,游戏结束\n");
		break;
	case KILL_BY_WALL:
		printf("很抱歉,咬到自己了,游戏结束\n");
		break;
	}
	//释放贪吃蛇的链表资源
	pSnakeNode cur = ps->pSnake;
	pSnakeNode del = NULL;

	while (cur)
	{
		del = cur;
		cur = cur->next;
		free(del);
	}
	
	free(ps->pFood);
	ps = NULL;
}


总结

如果你已经坚持看到末尾,我相信你已经了解了个大概。现在可以动手试试,写出属于你的贪吃蛇小游戏,你也可以扩展许多功能,如两个玩家操纵两条蛇,使用你之前学习到的编程知识,获得成就感和乐趣!

创作十分不易,如果喜欢这篇文章,请留下你的三连哦,你的支持的我最大的动力!!!

超级详细——手撕贪吃蛇小游戏!_第14张图片

你可能感兴趣的:(游戏,c语言)