手把手教你实现贪吃蛇

前言

在实现贪吃蛇前,我们需要熟练地掌握C语言知识,对初阶数据结构中的链表有一定的掌握,并且我们还会使用到Win 32 API 的知识,下面我会对需要使用到的API接口函数进行解释。最终的代码我放在后面,有需要的可以自取。

准备阶段

首先我使用的是VS2022版本,在使用VS2022运行贪吃蛇前我们需要进行一些设置,因为我们的贪吃蛇可以设置窗口大小和窗口名字。

运行窗口如下:

如果你是win11操作系统的话:

设置好这两个:

如果你不是Win11的操作系统,可以将Windows控制台主机设计成由Windows决定。

这里的背景颜色和字体颜色都可以自己去调整。

游戏窗口设置

title

这个命令是用来设置窗口名字的

title 贪吃蛇
这里我们会使用到system函数来实现。

	system("title 贪吃蛇");

这样就设置好窗口的名字

窗口大小

mode con cols=100 lines=30
这个命令是用来设置几行几列的,这里注意了,x的两个格子相当于y的一个格子的大小

	system("mode con cols=100 lines=30");

如果觉得自己的窗口比例小,我们可以调节字体的大小来设置窗口的比例~~

本地化配置

由于很多特殊字符是占用两个字节的(这些字符又被称为宽字符),为了能打印出特殊字符,我们需要进行本地化设置。

	setlocale(LC_ALL, "");

这个命令就是自动适配好本地化,需要的头文件是<locale.h>

光标设置

为了能在任意位置上进行输出,我们需要获取光标的位置,然后进行打印,这里我们可以封装一个函数,用来定位光标的位置,方便后续指定位置的输出。

//设置光标位置
void SetPos(short x, short y)
{
	COORD pos = { x, y };
	HANDLE hOutput = NULL;

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

	//设置标准输出上光标的位置为pos 
	SetConsoleCursorPosition(hOutput, pos);
}

这里解释一下里面的函数:

COORD这个函数是用来接收坐标的。

GetStdHandle 是用来接收不同的设备,就是你想操控那个设备,这里由三个参数,大家有兴趣可以去微软的官方文档去阅读,我们需要控制输出设备(就是屏幕),所以使用STD_OUTPUT_HANDLE这个参数

SetConsoleCursorPosition这个函数可以设置光标的位置,需要两个参数,一个就是上面的句柄和坐标。

游戏的实现

游戏准备阶段

游戏开始时,我们要初始化游戏,设置好界面大小,打印欢迎界面,打印按键功能界面,打印游戏界面(墙体,蛇,事物,右侧提示面板)等等操作,这时候我们需要对蛇这个对象进行设置,需要使用到结构体,蛇需要x、y两个坐标,我这里使用到的是单链表的结构,所以蛇的结构体如下:

//创建蛇的结构体
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode;

然后就是这个游戏需要的参数,首先就是蛇,我们需要一个蛇头的指针来找到这条蛇,然后是蛇的方向,速度,总得分,食物的分数,食物的位置,还有蛇的状态(是否死亡),于是我们对这个游戏对象进行结构体的封装:

//创建贪吃蛇对象的结构体
typedef struct SnakeInfo
{
	SnakeNode* snake_head;//蛇头的位置
	int total_score;//总得分
	int food_score;//食物分数
	int snake_speed;//蛇的速度
	SnakeNode* food;//食物的位置
	int snake_diretion;//蛇的方向
	int snake_state;//蛇的状态
}SnakeInfo;

这里不定义食物的结构体是因为食物也是需要x、y的坐标值,所以我们可以借用上面蛇的结构体~~

由于蛇的位置和状态可以一一列举出来,我们就使用枚举来定义,这样也方便我们后面的使用和操作:

//蛇的方向
typedef enum Diretion
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
}Dit;

//蛇的状态
typedef enum SnakeState
{
	OK,
	KILL_BY_ITSELF,
	KILL_BY_WALL,
	END_NORMAL
}State;

为了方便打印字符,我们可以宏定义一下这些字符

#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'

初始化游戏

//初始化游戏
void GameStart(SnakeInfo* sp)
{
	//设置界面大小
	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);//设置控制台光标状态 

	//打印欢迎界面
	Welcome();

	//打印游戏界面
	CreatMap(sp);

	//设置好初始游戏参数
	sp->food_score = 10;
	sp->snake_speed = 200;
	sp->total_score = 0;
	sp->snake_diretion = RIGHT;//开始向右移动
	sp->snake_state = OK;

}

在开始前,光标是会闪烁的,所以为了防止光标对玩家产生影响,我们需要把光标隐藏起来,就是如下代码:

	//获取光标信息
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(hOutput, &CursorInfo);

	CursorInfo.bVisible = false; //隐藏控制台光标 
	SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态 

GetConsoleCursorInfo这个函数是获取光标信息的,CursorInfo.bVisible这里设置成false就可以把光标的能见度调成0,最后使用这个函数SetConsoleCursorInfo来设置光标的能见度。

打印欢迎界面

这里就不多说了,记得设置好光标的位置,然后进行打印信息即可。

//打印欢迎界面
void Welcome()
{
	SetPos(40, 12);
	printf("欢迎来到贪吃蛇小游戏");

	SetPos(42, 18);
	system("pause");

	//清理界面
	system("cls");

	//打印按键功能信息
	SetPos(8, 12);
	printf("↑  ↓  ←  → 分别控制蛇的上下左右移动, F3为加速,F4为减速, Esc 退出, Space 暂停");

	SetPos(35, 15);
	printf("加速后的食物分数会更高!!!");

	SetPos(40, 20);
	system("pause");
	system("cls");
}
游戏地图设置

下面是我们需要编写的函数:

void CreatMap(SnakeInfo* sp)
{
	//打印墙体
	CreatWall();

	//打印右侧面板信息
	PrintInfo();

	//初始化蛇的身体
	sp->snake_head = NULL;
	InitSnake(&(sp->snake_head));

	//打印蛇的身体
	PrintSnake(sp->snake_head);

	//打印食物
	PrintFood(sp);

}
打印墙体和右侧面板信息

我们需要打印墙体和旁边的信息,值得注意的是窗口的坐标分布:

x的两格相当于y的一格,宽字符是占两个字符的,所以一个宽字符是两个x轴的格子加一个y轴的格子。都是从0开始的~~

//打印墙体
void CreatWall()
{
	int i = 0;

	//上墙体
	for (i = 0; i < 30; i++)
	{
		wprintf(L"%lc", WALL);
	}

	//下墙体
	SetPos(0, 24);
	for (i = 0; i < 30; i++)
	{
		wprintf(L"%lc", WALL);
	}

	//左墙体
	for (i = 1; i <= 23; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}

	//右墙体
	for (i = 1; i <= 23; i++)
	{
		SetPos(58, i);
		wprintf(L"%lc", WALL);
	}
}

//打印右侧面板信息
void PrintInfo()
{
	SetPos(64, 8);
	printf("总得分:0    食物分数:10");
	SetPos(64, 12);
	printf("↑  ↓  ←  → 分别控制蛇移动");
	SetPos(65, 14);
	printf("F3为加速,F4为减速");
	SetPos(65, 15);
	printf("Esc 退出  Space 暂停");
	SetPos(65, 18);
	printf("加速后的食物分数会更高!!!");
}
初始化蛇的身体

这里我设置了5个节点进行尾插操作,为了便捷,我还写了一个创建节点的函数:

SnakeNode* CreatSnakeNewNode()
{
	SnakeNode* newnode = (SnakeNode*)malloc(sizeof(SnakeNode));
	if (newnode == NULL)
	{
		perror("CreatSnakeNewNode()::malloc()");
	}
	newnode->x = newnode->y = 0;
	newnode->next = NULL;
	return newnode;
}

//初始化蛇的身体
void InitSnake(SnakeNode** sp)
{
	//五个节点
	if (*sp == NULL)//空链表
	{
		*sp = CreatSnakeNewNode();
		(*sp)->y = 4;
		(*sp)->x = 30;
	}
	SnakeNode* pcur = *sp;
	//尾插
	for (int i = 1; i <= 4; i++)
	{
		SnakeNode* newnode = (SnakeNode*)malloc(sizeof(SnakeNode));
		newnode->y = 4;
		newnode->x = 30 - 2 * i;
		pcur->next = newnode;
		pcur = pcur->next;
	}
	pcur->next = NULL;
}
打印蛇的身体与食物
//打印蛇的身体
void PrintSnake(SnakeNode* sp)
{
	SnakeNode* pcur = sp;
	while (pcur)
	{
		SetPos(pcur->x, pcur->y);
		wprintf(L"%lc", BODY);
		pcur = pcur->next;
	}
}

//打印食物
void PrintFood(SnakeInfo* sp)
{
	//设置食物的位置
	int x, y;

again:
	x = 2 * (rand() % 28 + 1);
	y = rand() % 23 + 1;

	//不能和蛇的身体重叠
	SnakeNode* pcur = sp->snake_head;
	while (pcur)
	{
		if (pcur->x == x && pcur->y == y)
		{
			goto again;
		}
		pcur = pcur->next;
	}

	SnakeNode* newfood = CreatSnakeNewNode();
	sp->food = newfood;
	sp->food->x = x;
	sp->food->y = y;
	sp->food->next = NULL;

	SetPos(x, y);
	wprintf(L"%lc", FOOD);
}

这里需要注意食物的创建,为了使食物随机出现,我们可以使用随机数的知识来创建,我们只需要使用一次随机数的种子,所以我在主函数写了下面一行代码:

//设置随机数种子
srand((unsigned int)time(NULL));

然后就是随机数的判断,不能与蛇身重合,不能超过墙体!!!

游戏的运行

我们需要从玩家的键盘里获取玩家按了什么键,然后做出相关操作,这时候我们需要使用到另外一个API接口函数:

GetAsyncKeyState(VK)

GetAsyncKeyState 的返回值是short类型,在上⼀次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。

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

这时候我们可以写一个宏来便于判断什么键被按过:

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

首先我们要从键盘上进行获取哪些键被按过,然后进行修改游戏参数,由于蛇是由移动速度的,这时候我们可以通过Sleep来调整需要休眠几秒,以此来达到速度的效果。

蛇的移动也是由讲究的,当蛇正在向上移动,玩家按了向下的键是不可以让蛇进行向下移动,同理,向左向右和向下移动也是如此。

如果玩家按了加速键和减速键,我们也要对加速的上限和减速的下限进行限制,不可能一直加速,因为睡眠时间不能为负数,这里大家按自己的喜好进行调整即可。

游戏在进行的时候,就是蛇没有死亡游戏才能进行,所以这里用个da while循环,如果玩家主动退出游戏或者蛇死亡就会跳出游戏,循环的条件就是我们上面结构体中蛇的状态是不是正常~~

//游戏运行
void GameRun(SnakeInfo* sp)
{
	do
	{
		SetPos(64, 8);
		printf("总得分:%2d   食物分数:%2d", sp->total_score, sp->food_score);
		if (KEY_PRESS(VK_UP) && sp->snake_diretion != DOWN)
		{
			//上移
			sp->snake_diretion = UP;
		}
		else if (KEY_PRESS(VK_DOWN) && sp->snake_diretion != UP)
		{
			//下移
			sp->snake_diretion = DOWN;
		}
		else if(KEY_PRESS(VK_RIGHT) && sp->snake_diretion != LEFT)
		{
			//右移
			sp->snake_diretion = RIGHT;
		}
		else if (KEY_PRESS(VK_LEFT) && sp->snake_diretion != RIGHT)
		{
			//左移
			sp->snake_diretion = LEFT;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			//暂停
			Stop();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			//正常退出游戏
			sp->snake_state = END_NORMAL;
		}
		else if (KEY_PRESS(VK_F3))
		{
			//加速
			//每次加速二十
			//可以加速到100ms
			if (sp->snake_speed > 100)
			{
				sp->snake_speed -= 20;
				sp->food_score += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			//减速
			//每次减速二十
			//可以减速到280ms
			if (sp->snake_speed < 280)
			{
				sp->snake_speed += 20;
				sp->food_score -= 2;
			}
		}
		//睡眠时间
		Sleep(sp->snake_speed);
		//蛇的移动
		SnakeMove(sp);
	} while (sp->snake_state == OK);

	//打印死亡或者结束信息
	if (sp->snake_state == END_NORMAL)
	{
		SetPos(30, 10);
		printf("您退出了游戏!!!");
	}
	else if (sp->snake_state == KILL_BY_WALL)
	{
		SetPos(30, 10);
		printf("您的蛇被墙撞到了!!!");
	}
	else
	{
		SetPos(30, 10);
		printf("您的蛇撞到了自己!!!");
	}
}

游戏暂停

游戏暂停很简单,我们写一个死循环的睡眠,当玩家再次按下空格键,就跳出循环。

//暂停
void Stop()
{
	while (1)
	{
		Sleep(200);

		//再次按下空格跳出睡眠
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

蛇的移动

这里的蛇每次移动都是走一步,所以y的坐标每次改变1,x就是每次改变2

这里为了方便,我就直接创建一个新结点进行头插,如果没有吃到食物,最后一个节点就打印成空格(一定要打印空格,因为上一步的时候就打印好蛇的最后一个节点,所以不打印空格,就无法覆盖上一次的节点圆形,造成视觉影响)然后释放空间。如果吃到食物,就直接打印,所有节点保留,我这里设计的是每吃到一个食物蛇就会增加一个节点的长度。

在进行判断是否吃到食物的之前,我们需要对新结点的坐标进行赋值,大家要注意x,y的赋值,别搞错了~~

//蛇的移动
void SnakeMove(SnakeInfo* sp)
{
	//头插新节点
	SnakeNode* newnode = (SnakeNode*)malloc(sizeof(SnakeNode));
	newnode->next = sp->snake_head;
	sp->snake_head = newnode;

	switch (sp->snake_diretion)
	{
	case UP:
		sp->snake_head->y = sp->snake_head->next->y - 1;
		sp->snake_head->x = sp->snake_head->next->x;
		break;
	case DOWN:
		sp->snake_head->y = sp->snake_head->next->y + 1;
		sp->snake_head->x = sp->snake_head->next->x;
		break;
	case LEFT:
		sp->snake_head->x = sp->snake_head->next->x - 2;//注意x轴
		sp->snake_head->y = sp->snake_head->next->y;
		break;
	case RIGHT:
		sp->snake_head->x = sp->snake_head->next->x + 2;
		sp->snake_head->y = sp->snake_head->next->y;
		break;
	}

	//判断新状态
	if (EatFood(sp))
	{
		PrintSnake(sp->snake_head);
		sp->total_score += sp->food_score;
		//释放食物的节点
		free(sp->food);
		//打印新的食物
		PrintFood(sp);
	}
	else 
	{
		NoFood(sp);
		kill_by_wall(sp);
		kill_by_itself(sp);
	}
}

头插和赋值完后我们就来判断是否吃到食物~~当食物的坐标和蛇头的坐标是一样的时候,我们需要释放食物的空间,然后打印新的食物出来。

//吃到食物
//蛇身体增加一节
int EatFood(SnakeInfo* sp)
{
	if ((sp->food->x == sp->snake_head->x) && (sp->food->y == sp->snake_head->y))
	{
		//吃到返回
		return 1;
	}
	//没有吃到返回0
	return 0;
}

如果没有吃到食物,我们先打印蛇身的信息,一定要注意最后一个节点打印成空格,然后再释放掉最后一个节点,没有吃到食物,蛇身长度不增加,然后再判断是否撞墙或者撞到自己~~

//没有吃到食物
void NoFood(SnakeInfo* sp)
{
	SnakeNode* pcur = sp->snake_head;
	SnakeNode* prev = sp->snake_head;
	while (pcur->next)
	{
		prev = pcur;
		SetPos(pcur->x, pcur->y);
		wprintf(L"%lc", BODY);
		pcur = pcur->next;
	}
	SnakeNode* del = pcur;
	SetPos(del->x, del->y);
	printf("  ");
	free(del);
	del = NULL;
	prev->next = NULL;
}

是否撞墙

//是否被墙撞死
void kill_by_wall(SnakeInfo* sp)
{
	if (sp->snake_head->x == 0 || sp->snake_head->x == 58 || sp->snake_head->y == 0 || sp->snake_head->y == 24)
	{
		sp->snake_state = KILL_BY_WALL;
	}
}

是否撞到自己

//是否撞到自己
void kill_by_itself(SnakeInfo* sp)
{
	SnakeNode* pcur = sp->snake_head->next;
	while (pcur)
	{
		if (sp->snake_head->x == pcur->x && sp->snake_head->y == pcur->y)
		{
			sp->snake_state = KILL_BY_ITSELF;
			break;
		}
		pcur = pcur->next;
	}
}

如果在GameRun这个函数走出了循环,我们需要打印一下游戏结束状态的信息。

//打印死亡或者结束信息
if (sp->snake_state == END_NORMAL)
{
	SetPos(30, 10);
	printf("您退出了游戏!!!");
}
else if (sp->snake_state == KILL_BY_WALL)
{
	SetPos(30, 10);
	printf("您的蛇被墙撞到了!!!");
}
else
{
	SetPos(30, 10);
	printf("您的蛇撞到了自己!!!");
}

游戏的结束善后工作

在游戏结束的时候,我们需要释放掉我们申请的空间,避免内存泄漏~~

//游戏结束
void GameEnd(SnakeInfo* sp)
{
	//销毁蛇节点
	SnakeNode* pcur = sp->snake_head;
	while (pcur)
	{
		SnakeNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	sp->snake_head = NULL;

	//销毁食物节点
	free(sp->food);
	sp->food = NULL;
}

最后如果你觉得还想添加一个小设置,就是玩家还要不要再来一把,可以写一个do while循环~~

void test()
{
	int n = 0;
	do 
	{
		system("cls");
		//初始化游戏
		//设置好界面大小
		//打印欢迎界面
		//打印按键功能界面
		//打印游戏界面(墙体,蛇,事物,右侧提示面板)
		SnakeInfo snake = { 0 };
		GameStart(&snake);

		//游戏运行
		GameRun(&snake);

		//游戏结束
		GameEnd(&snake);

		SetPos(10, 15);
		printf("是否再来一局?是输入非0,不需要请输入0:");
		scanf("%d", &n);
	} while (n);
	SetPos(0, 26);
}

如果你不想写,我们可以美化一下游戏,把程序的终止信息打印在最下面,需要设置一下光标位置,所以最后我还写了一行光标的定位代码。

就是最下面这一行的信息:

最终的代码

Sanke.h

#pragma once

#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include <stdbool.h>
#include <time.h>

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

//创建蛇的结构体
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode;

//创建贪吃蛇对象的结构体
typedef struct SnakeInfo
{
	SnakeNode* snake_head;//蛇头的位置
	int total_score;//总得分
	int food_score;//事物分数
	int snake_speed;//蛇的速度
	SnakeNode* food;//食物的位置
	int snake_diretion;//蛇的方向
	int snake_state;//蛇的状态
}SnakeInfo;

//蛇的方向
typedef enum Diretion
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
}Dit;

//蛇的状态
typedef enum SnakeState
{
	OK,
	KILL_BY_ITSELF,
	KILL_BY_WALL,
	END_NORMAL
}State;

//初始化游戏
void GameStart(SnakeInfo* sp);

//初始化蛇的身体
void InitSnake(SnakeNode** sp);

//打印蛇的身体
void PrintSnake(SnakeNode* sp);

//打印食物
void PrintFood(SnakeInfo* sp);

//游戏运行
void GameRun(SnakeInfo* sp);

//蛇的移动
void SnakeMove(SnakeInfo* sp);

//暂停
void Stop();

//吃到食物
//吃到返回 没有吃到返回0
int EatFood(SnakeInfo* sp);

//没有吃到食物
void NoFood(SnakeInfo* sp);

//是否被墙撞死
void kill_by_wall(SnakeInfo* sp);

//是否撞到自己
void kill_by_itself(SnakeInfo* sp);

//游戏结束
void GameEnd(SnakeInfo* sp);

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

Sanke.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 Welcome()
{
	SetPos(40, 12);
	printf("欢迎来到贪吃蛇小游戏");

	SetPos(42, 18);
	system("pause");

	//清理界面
	system("cls");

	//打印按键功能信息
	SetPos(8, 12);
	printf("↑  ↓  ←  → 分别控制蛇的上下左右移动, F3为加速,F4为减速, Esc 退出, Space 暂停");

	SetPos(35, 15);
	printf("加速后的食物分数会更高!!!");

	SetPos(40, 20);
	system("pause");
	system("cls");
}

//打印墙体
void CreatWall()
{
	int i = 0;

	//上墙体
	for (i = 0; i < 30; i++)
	{
		wprintf(L"%lc", WALL);
	}

	//下墙体
	SetPos(0, 24);
	for (i = 0; i < 30; i++)
	{
		wprintf(L"%lc", WALL);
	}

	//左墙体
	for (i = 1; i <= 23; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}

	//右墙体
	for (i = 1; i <= 23; i++)
	{
		SetPos(58, i);
		wprintf(L"%lc", WALL);
	}
}

//打印右侧面板信息
void PrintInfo()
{
	SetPos(64, 8);
	printf("总得分:0    食物分数:10");
	SetPos(64, 12);
	printf("↑  ↓  ←  → 分别控制蛇移动");
	SetPos(65, 14);
	printf("F3为加速,F4为减速");
	SetPos(65, 15);
	printf("Esc 退出  Space 暂停");
	SetPos(65, 18);
	printf("加速后的食物分数会更高!!!");
}

//创建蛇的节点
SnakeNode* CreatSnakeNewNode()
{
	SnakeNode* newnode = (SnakeNode*)malloc(sizeof(SnakeNode));
	if (newnode == NULL)
	{
		perror("CreatSnakeNewNode()::malloc()");
	}
	newnode->x = newnode->y = 0;
	newnode->next = NULL;
	return newnode;
}

//初始化蛇的身体
void InitSnake(SnakeNode** sp)
{
	//五个节点
	if (*sp == NULL)//空链表
	{
		*sp = CreatSnakeNewNode();
		(*sp)->y = 4;
		(*sp)->x = 30;
	}
	SnakeNode* pcur = *sp;
	//尾插
	for (int i = 1; i <= 4; i++)
	{
		SnakeNode* newnode = (SnakeNode*)malloc(sizeof(SnakeNode));
		newnode->y = 4;
		newnode->x = 30 - 2 * i;
		pcur->next = newnode;
		pcur = pcur->next;
	}
	pcur->next = NULL;
}

//打印蛇的身体
void PrintSnake(SnakeNode* sp)
{
	SnakeNode* pcur = sp;
	while (pcur)
	{
		SetPos(pcur->x, pcur->y);
		wprintf(L"%lc", BODY);
		pcur = pcur->next;
	}
}

//打印食物
void PrintFood(SnakeInfo* sp)
{
	//设置食物的位置
	int x, y;

again:
	x = 2 * (rand() % 28 + 1);
	y = rand() % 23 + 1;

	//不能和蛇的身体重叠
	SnakeNode* pcur = sp->snake_head;
	while (pcur)
	{
		if (pcur->x == x && pcur->y == y)
		{
			goto again;
		}
		pcur = pcur->next;
	}

	SnakeNode* newfood = CreatSnakeNewNode();
	sp->food = newfood;
	sp->food->x = x;
	sp->food->y = y;
	sp->food->next = NULL;

	SetPos(x, y);
	wprintf(L"%lc", FOOD);
}

void CreatMap(SnakeInfo* sp)
{
	//打印墙体
	CreatWall();

	//打印右侧面板信息
	PrintInfo();

	//初始化蛇的身体
	sp->snake_head = NULL;
	InitSnake(&(sp->snake_head));

	//打印蛇的身体
	PrintSnake(sp->snake_head);

	//打印食物
	PrintFood(sp);

}

//初始化游戏
void GameStart(SnakeInfo* sp)
{
	//设置界面大小
	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);//设置控制台光标状态 

	//打印欢迎界面
	Welcome();

	//打印游戏界面
	CreatMap(sp);

	//设置好初始游戏参数
	sp->food_score = 10;
	sp->snake_speed = 200;
	sp->total_score = 0;
	sp->snake_diretion = RIGHT;//开始向右移动
	sp->snake_state = OK;

}

//暂停
void Stop()
{
	while (1)
	{
		Sleep(200);

		//再次按下空格跳出睡眠
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

//吃到食物
//蛇身体增加一节
int EatFood(SnakeInfo* sp)
{
	if ((sp->food->x == sp->snake_head->x) && (sp->food->y == sp->snake_head->y))
	{
		//吃到返回
		return 1;
	}
	//没有吃到返回0
	return 0;
}

//没有吃到食物
void NoFood(SnakeInfo* sp)
{
	SnakeNode* pcur = sp->snake_head;
	SnakeNode* prev = sp->snake_head;
	while (pcur->next)
	{
		prev = pcur;
		SetPos(pcur->x, pcur->y);
		wprintf(L"%lc", BODY);
		pcur = pcur->next;
	}
	SnakeNode* del = pcur;
	SetPos(del->x, del->y);
	printf("  ");
	free(del);
	del = NULL;
	prev->next = NULL;
}

//是否被墙撞死
void kill_by_wall(SnakeInfo* sp)
{
	if (sp->snake_head->x == 0 || sp->snake_head->x == 58 || sp->snake_head->y == 0 || sp->snake_head->y == 24)
	{
		sp->snake_state = KILL_BY_WALL;
	}
}

//是否撞到自己
void kill_by_itself(SnakeInfo* sp)
{
	SnakeNode* pcur = sp->snake_head->next;
	while (pcur)
	{
		if (sp->snake_head->x == pcur->x && sp->snake_head->y == pcur->y)
		{
			sp->snake_state = KILL_BY_ITSELF;
			break;
		}
		pcur = pcur->next;
	}
}

//蛇的移动
void SnakeMove(SnakeInfo* sp)
{
	//头插新节点
	SnakeNode* newnode = (SnakeNode*)malloc(sizeof(SnakeNode));
	newnode->next = sp->snake_head;
	sp->snake_head = newnode;

	switch (sp->snake_diretion)
	{
	case UP:
		sp->snake_head->y = sp->snake_head->next->y - 1;
		sp->snake_head->x = sp->snake_head->next->x;
		break;
	case DOWN:
		sp->snake_head->y = sp->snake_head->next->y + 1;
		sp->snake_head->x = sp->snake_head->next->x;
		break;
	case LEFT:
		sp->snake_head->x = sp->snake_head->next->x - 2;//注意x轴
		sp->snake_head->y = sp->snake_head->next->y;
		break;
	case RIGHT:
		sp->snake_head->x = sp->snake_head->next->x + 2;
		sp->snake_head->y = sp->snake_head->next->y;
		break;
	}

	//判断新状态
	if (EatFood(sp))
	{
		PrintSnake(sp->snake_head);
		sp->total_score += sp->food_score;
		//释放食物的节点
		free(sp->food);
		//打印新的食物
		PrintFood(sp);
	}
	else 
	{
		NoFood(sp);
		kill_by_wall(sp);
		kill_by_itself(sp);
	}
}

//游戏运行
void GameRun(SnakeInfo* sp)
{
	do
	{
		SetPos(64, 8);
		printf("总得分:%2d   食物分数:%2d", sp->total_score, sp->food_score);
		if (KEY_PRESS(VK_UP) && sp->snake_diretion != DOWN)
		{
			//上移
			sp->snake_diretion = UP;
		}
		else if (KEY_PRESS(VK_DOWN) && sp->snake_diretion != UP)
		{
			//下移
			sp->snake_diretion = DOWN;
		}
		else if(KEY_PRESS(VK_RIGHT) && sp->snake_diretion != LEFT)
		{
			//右移
			sp->snake_diretion = RIGHT;
		}
		else if (KEY_PRESS(VK_LEFT) && sp->snake_diretion != RIGHT)
		{
			//左移
			sp->snake_diretion = LEFT;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			//暂停
			Stop();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			//正常退出游戏
			sp->snake_state = END_NORMAL;
		}
		else if (KEY_PRESS(VK_F3))
		{
			//加速
			//每次加速二十
			//可以加速到100ms
			if (sp->snake_speed > 100)
			{
				sp->snake_speed -= 20;
				sp->food_score += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			//减速
			//每次减速二十
			//可以减速到300ms
			if (sp->snake_speed < 280)
			{
				sp->snake_speed += 20;
				sp->food_score -= 2;
			}
		}
		//睡眠时间
		Sleep(sp->snake_speed);
		//蛇的移动
		SnakeMove(sp);
	} while (sp->snake_state == OK);

	//打印死亡或者结束信息
	if (sp->snake_state == END_NORMAL)
	{
		SetPos(30, 10);
		printf("您退出了游戏!!!");
	}
	else if (sp->snake_state == KILL_BY_WALL)
	{
		SetPos(30, 10);
		printf("您的蛇被墙撞到了!!!");
	}
	else
	{
		SetPos(30, 10);
		printf("您的蛇撞到了自己!!!");
	}
}

//游戏结束
void GameEnd(SnakeInfo* sp)
{
	//销毁蛇节点
	SnakeNode* pcur = sp->snake_head;
	while (pcur)
	{
		SnakeNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	sp->snake_head = NULL;

	//销毁食物节点
	free(sp->food);
	sp->food = NULL;
}

test.c

#include <locale.h>
#include "Snake.h"

void test()
{
	int n = 0;
	do 
	{
		system("cls");
		//初始化游戏
		//设置好界面大小
		//打印欢迎界面
		//打印按键功能界面
		//打印游戏界面(墙体,蛇,事物,右侧提示面板)
		SnakeInfo snake = { 0 };
		GameStart(&snake);

		//游戏运行
		GameRun(&snake);

		//游戏结束
		GameEnd(&snake);

		SetPos(10, 15);
		printf("是否再来一局?是输入非0,不需要请输入0:");
		scanf("%d", &n);
	} while (n);
	SetPos(0, 26);
}

int main()
{
	//配置好本地化环境,支持宽字符输出
	setlocale(LC_ALL, "");

	//设置随机数种子
	srand((unsigned int)time(NULL));

	//游戏测试
	test();
	return 0;
}

C语言进阶到此完结撒花!!!

如果有机会学习C++我会回来继续更新的!!!

祝老铁们每天快乐!!!知识倍增!!!我们顶峰相见!!!

相关推荐
-一杯为品-4 分钟前
【51单片机】程序实验5&6.独立按键-矩阵按键
c语言·笔记·学习·51单片机·硬件工程
Lenyiin1 小时前
02.06、回文链表
数据结构·leetcode·链表
爪哇学长1 小时前
双指针算法详解:原理、应用场景及代码示例
java·数据结构·算法
爱摸鱼的孔乙己1 小时前
【数据结构】链表(leetcode)
c语言·数据结构·c++·链表·csdn
Dola_Pan1 小时前
C语言:数组转换指针的时机
c语言·开发语言·算法
烦躁的大鼻嘎1 小时前
模拟算法实例讲解:从理论到实践的编程之旅
数据结构·c++·算法·leetcode
IU宝1 小时前
C/C++内存管理
java·c语言·c++
qq_459730032 小时前
C 语言面向对象
c语言·开发语言
C++忠实粉丝2 小时前
计算机网络socket编程(4)_TCP socket API 详解
网络·数据结构·c++·网络协议·tcp/ip·计算机网络·算法
陌小呆^O^3 小时前
Cmakelist.txt之win-c-udp-client
c语言·开发语言·udp