贪吃蛇小游戏 --- 基于WIN32API【C语言】

一、前言

本文将用win32提供的API进行贪吃蛇小游戏的开发,用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇。也就是说,你只要会用vs2019或者其他版本的vs即可。不用额外学习esayx等其他软件。win32的API是电脑系统自带的函数接口,通过调用相关函数,我们可以在窗口中实现贪吃蛇小游戏。

使用 Windows API,可以开发可在所有版本的 Windows 上成功运行的应用程序,同时利用每个版本特有的特性和功能。

二、游戏效果演示

这里不方便放视频,直接上图片了

三、实现基本功能

  • 贪吃蛇地图绘制
  • 蛇吃食物的功能(上下左右方向键控制蛇的运动)
  • 蛇撞墙死亡
  • 蛇撞自身死亡
  • 计算得分
  • 蛇的速度随分数变化(蛇吃的越多,跑的越快,蛇身越长,此时难度越大)
  • 暂停游戏

四、WIN32API介绍以及控制台修改

1、修改控制台大小

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

原本大小

修改后大小

2、GetStdHandle 获取控制台权限(从标准输出中取得一个句柄,通过句柄操作设备)

ps:一般控制台就是标准输出 STD_OUPUT表示标准输出

ps:代码栏的TypeScript是随便选的,为了能标示出函数和指针类型

HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

其中HANDLE是个指针类型

3、GetConsoleInfo 获取光标大小和可见性的信息

示例

HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleInfo(hOutput,&CursorInfo); //获取控制台光标信息

4、CONSOLE_CURSOR_INFO 包含控制台光标信息的结构体

5、SetConsoleCursorInfo设置指定控制台屏幕光标的大小和可见性

示例

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

5、COORD结构 光标位置信息

定义控制台屏幕缓冲区中字符单元的坐标。 坐标系 (0,0) 的原点位于控制台的顶部左侧单元格。

typedef struct _COORD {
  SHORT X;
  SHORT Y;
} COORD, *PCOORD;

6、 SetConsoleCursorPosition设置光标在控制台中的指定位置

示例

COORD pos ={10.5};
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标位置为pos
SetConsoleCursorPosition(hOutput,pos);

为了后续方便,我们将设置光标的代码分装成一个函数SetPos

void SetPos(int x,int y)
{
    HANDLE handle  = GetStdHandle(STD_OUTPUT_HANDLE);
    COORD pos = {x,y};
    SetConsoleCursorPosition(handle,pos);
}

7、GetAsyncKeyState 按键获取情况

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

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

虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn

8、控制台知识

控制台窗口的坐标,横向是x轴,向右增长;纵向是y轴,向下增长;左上角为原点,原点坐标为(0,0)

9、关于国际化和本地化

原本的ascii码表只用了一个字节中的前7个比特位,最高位是不使用的。所以ascii码表的取值范围是 0~127

而这只能刚好用来表示英文的字符,对于中文字符来说是远远不够的。为了使c语言适应国际化,在c标准库中加入许多国际化支持。如加入了宽字符类型 wchar_t ,和宽字符的输入输出函数 。加入 <locale.h>文件,提供允许程序员针对特定地区调整程序行为的函数。

10、<locale.h>本地化

<locale.h>提供的函数用于控制c标准库中对于不同地区产生不一样行为的部分。在标准库中依赖地区行为的有

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

11、setlocale函数

char* setlocale(int categary , const char* locale)

setlocale函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。

categary表示类项。类项有

LC_CTYPE 影响字符处理函数行为

LC_MONETARY影响货币格式

LC_ALL 针对所有类项修改 等等。。。

c标准只给第二个参数定义了2种可能取值“C”(正常模式); “ ”(本地模式)

下面切换到本地模式,支持宽字符输入输出(理论上只输入空格也是本地模式)

setlocale(LC_ALL, "zh-CN");

12、宽字符打印

宽字符的定义,其字符面量前必须加L 即单引号前要加L,否则c语言会当作窄字符处理。

其输出用wprintf(“%lc”,ch); 若是宽字符串 则为%ls

宽字符占两个坐标位置,而平常的窄字符只占一个坐标位置

贪吃蛇游戏设计及分析

地图坐标

设计实现一个27行,58列的棋盘当作地图,围绕地图画出墙面

部分示意图如下,围墙的墙由若干小方块组成,一个小方块在x轴占两个坐标位置,在y轴占一个坐标位置。所以棋盘的列数一定是2的倍数,因为一个方块的横坐标就占了两个单位了。

蛇身和食物

初始化状态,假设蛇长度为5,蛇身每个节点是●,在固定坐标点出生,比如(24,5),连续五个节点。

每个节点的x坐标也必须是2的倍数,否则可能出现蛇身的其中一个节点一半在墙外,一半在墙内的情况。坐标不好对齐。

食物应该作为蛇身的一部分,蛇吃到食物就能和蛇的身体融合。食物在墙内随机生成,但是坐标也必须是2的倍数,坐标不能和蛇身重合。用★表示(也可以自己替换其他字符)

游戏主体

关于控制台的调整 -- 不要使用最新版的

设置为windows 控制台主机

一、snake.h文件的初始包含

1.1头文件引用和宏定义

#include<stdio.h>
#include<stdlib.h>  //system所需要的头文件
#include<locale.h>  //支持宽字符的头文件
#include<Windows.h>  //调用win32API需要的头文件
#include<stdbool.h> //使用 false true需要的头文件

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

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

//封装KEY_PRESS 检测vk虚拟键值对应的按键是否被按过
//如果按过返回1 未按过返回0
#define KEY_PRESS(vk) (GetAsyncKeyState(vk) & 0x1 ? 1: 0)  

1.2游戏信息的封装

贪吃蛇蛇身的数据结构设计为链表 --- 此处初始化链表的单个节点

同时创建结构体指针 pSnakeNode 便于后续使用

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

方向信息封装为枚举类型

//方向类型
enum DIRECTION
{
	up = 1,
	down,
	right,
	left
};

游戏状态也封装为枚举类型

//游戏状态类型
enum GameStatus
{
	Normal,   //正常运行
	End_Normal, //正常结束
	KILL_BY_WALL,    //撞墙死亡
	KILL_BY_SELF       // 撞自己死亡
};

贪吃蛇游戏主要信息的封装
//与SnakeNode区分,SnakeNode是管理蛇身和蛇的位置信息的结构体。
//Snake则是存储贪吃蛇行动以及游戏运行信息的结构体

typedef struct Snake
{
	pSnakeNode phead_snake; //指向贪吃蛇头结点的指针
	pSnakeNode _pfood;      //指向食物结点的指针
	int Score;              //总分
	int FoodWeight;         //一个食物的分数
	int SleepTime;          //停顿时间,控制蛇的行进速度
	enum DIRECTION _Dir;         //描述蛇行走方向
	enum GameStatus _Status;        //游戏运行状态 
}Snake,*pSnake;

剩余函数的声明

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

//游戏欢迎界面
void WelcomeToGame();

//打印地图
void CreatMap();

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

//创建食物
void CreatFood(pSnake ps);

二、test.c 和 snake.c文件中主体函数的实现

游戏初始化

2.1 test.c中主函数的调用

#include "snake.h"

void test()
{
	Snake snake = { 0 };   // 创建贪吃蛇游戏信息

	// 1、游戏开始 -- 初始化游戏
	GameStart(&snake);
	// 2、 游戏运行 -- 运行过程
	// GameRun(&snake);
	// 3、 游戏结束 -- 结束后的空间资源释放等
	// GameEnd(&snake);
}
int main()
{
	setlocale(LC_ALL, "zh-CN"); //设置程序适应本地环境
	test();
	return 0;
}

说明:分阶段封装函数。每个阶段都需要对贪吃蛇游戏信息进行使用和更改,所以参数传snake的地址

游戏可大致分为三个阶段,游戏的初始化,游戏的正常运行,游戏的结束。

2.2 snake.c中 GameStart 函数的实现

1)各初始化功能函数的调用及游戏欢迎界面打印

#include "snake.h"

//封装设置光标位置的函数
void SetPos(int x, int y)
{
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); //获取控制台句柄
	COORD pos = { x,y };                             //赋予光标位置
	SetConsoleCursorPosition(handle, pos);           //设置光标位置 -- 确定输入位置信息
}

//游戏启动界面函数
void WelcomeToGame()
{
	//定位光标 -- 在窗口的中间打印信息
	SetPos(40, 14);
	printf("贪 吃 蛇\n");
	SetPos(40, 25);
	system("pause");  //pause是暂停  此命令会在控制台窗口留下 “请按任意键继续”
	system("cls");    //清屏,准备打印下一条信息
	SetPos(40, 14);
	printf("启 动 !\n");
	SetPos(40, 25);
	system("pause");  
	system("cls");
}

//游戏界面初始化
void GameStart(pSnake ps)
{
	//控制台窗口设置
	system("mode con cols=100 lines=45");
	system("title 贪吃蛇");

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

	//打印欢迎界面
WelcomeToGame();
	//创建地图
CreatMap();
	//初始化贪吃蛇
	InitSnake(ps); 
	//创建食物
CreatFood(ps);
}
  • mode con cols=100 lines=45 这段命令要注意 cols=100 是连起来的,如果分开成 cols = 100将无法识别有效命令。 lines=45 也是一样的

2)地图创建

  • 创建地图的易错点 -- 坐标的计算 我们要设计26行58列大小的棋盘。 上方墙壁的最后一个方块的坐标应该是(56,0)因为一个方块的横坐标占2个坐标 于是 上:(0,0)到(56,0) 下:(0,26)到(56,26) 左:(0,1)到(0,25)右:(56,1)到(56,25)
  • 其次在创建左右墙壁时,每打印出一个方块就要重新设置光标位置,所以将SetPos放在循环内
//创建地图
void CreatMap()
{
	//上
	SetPos(0, 0);
	int i = 0;
	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);
	}

	//左
	for (i = 1; i <= 25; i ++)
	{
		SetPos(0, i);
		wprintf(L"%lc\n", WALL);
	}

	//右
	for (i = 1; i <= 25; i ++)
	{
		SetPos(56, i);
		wprintf(L"%lc\n", WALL);
	}
	
} 
  

3)贪吃蛇蛇身创建及相关信息的初始化

  • 贪吃蛇的创建,假设将其出生位置设置在(24,5)连续生成5个节点,那么其各个节点的坐标为(26,5)(28,5)(30,5)(32,5) 使用头插法创建整个蛇身
  • 这条蛇实际长这样,链表的头就是蛇的头,其中方块里的数字表示创建的节点的顺序。3就是第三个创建的节点

void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	int i = 0;
	for (i = 0; i < 5; i++)  //连续创建5个节点
	{
		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->phead_snake == NULL)
		{
			ps->phead_snake = cur;
		}
		else
		{
			cur->next = ps->phead_snake;
			ps->phead_snake = cur;
		}
		
	}
	//打印蛇身
	cur = ps->phead_snake;
	while (cur)
	{
		SetPos(cur->x, cur->y); // 蛇的每个节点都有记录位置信息
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	
	//贪吃蛇游戏的其他信息的初始化
	ps->_Status = Normal;
	ps->Score = 0;
	ps->_pfood = NULL; //食物节点尚未创建,先赋空
	ps->SleepTime = 200;  // 200ms 
	ps->FoodWeight = 10;
	ps->_Dir = right;
}

4)食物创建

  • 食物的创建 -- 坐标应该随机生成,但是不能与蛇的坐标重合,且在墙范围内,其x坐标必须是2的倍数

随机坐标的生成

要先在test.c文件中调用srand函数,设置随机值的起点(随电脑时间变化而变化)

因为墙的横坐标范围是(0,56)所以食物的坐标范围应该是[2,54]

墙纵坐标范围(0,26)食物纵坐标范围[1,25]

	do 
	{
		x = rand() % 53 + 2;   // rand()%53 得到的是0~52  ;+ 2后 2~54
		y = rand() % 25 + 1;   // rand()%25 得到的是0~24  ;+ 1后 1~25
    } while (x % 2 != 0); //x坐标必须是2的倍数

创建食物函数整体

void CreatFood(pSnake ps)
{
	int x = 0;
	int y = 0;
again:
	do 
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0); //x坐标必须是2的倍数

	//食物坐标不能和蛇身冲突
	pSnakeNode cur = ps->phead_snake;
	while(cur)
	{
		//依次比较坐标
		if (cur->x == x && cur->y == y)
		{
			goto again; //与蛇身重复了,重新生成坐标
		}
		cur = cur->next;
	}

	//坐标有效 为食物开辟空间,并将其地址给相应指针
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood == NULL)
	{
		perror("CreatFood()::malloc()"); //检查是否开辟成功
		return;
	}
	//创建成功 更新食物的信息
	pFood->x = x;
	pFood->y = y;
	ps->_pfood = pFood; 

	//打印食物
	SetPos(x, y); //光标定位到随机生成的位置
	wprintf(L"%lc", FOOD);
}

游戏运行

1)游戏边栏协助信息打印

此函数主要是打印不会随游戏进程变化的协助信息

ps:每个函数都要在snake.h声明,此不赘述

void PrintHelpInfo()
{
	SetPos(64, 15);
	printf("1、不能撞墙,不能撞自己哦\n");
	SetPos(64, 16);
	printf("2、使用↑↓←→ 控制蛇的移动\n");
	SetPos(64, 17);
	printf("3、ESC 退出  空格 暂停游戏 再次按空格恢复\n");

}

2)游戏正常运行时的按键接收逻辑

封装个自己的暂停函数,当再次按空格键的时候结束暂停

在接收方向键时,要注意不能蛇往左走,按右键后立马掉到右来,只能先往下(上)再往右,这样掉头。

按下不同方向键后更新蛇前进的方向信息,然后在蛇的行动函数中实现蛇的前进。

相关虚拟键码可从下面链接寻找

虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn

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

void GameRun(pSnake ps)
{
	PrintHelpInfo();

	//游戏的按键接收逻辑
	do
	{
		SetPos(64, 10);
		printf("Score:%05d", ps->Score); 
		SetPos(64, 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))
		{
			my_pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Status = End_Normal;
			break;
		}
        Sleep(ps->SleepTime); // Sleep用来控制蛇的行动速度,相当于游戏难度
		SnakeMove(ps);  

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

3)SnakeMove函数实现

void SnakeMove(pSnake ps)
{
	//创建蛇要移动的位置的下一个节点
	pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNext == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}
	pNext->next = NULL;

	//更新这个节点的位置信息 
	switch (ps->_Dir)
	{
	case up:
		pNext->x = ps->phead_snake->x;
		pNext->y = ps->phead_snake->y -1; //在图像上显示向上,但是在坐标上,y坐标是减小的
		break;
	case down:
		pNext->x = ps->phead_snake->x;
		pNext->y = ps->phead_snake->y + 1; 
		break;
	case left:
		pNext->x = ps->phead_snake->x - 2; //注意x坐标每次变化2个单位
		pNext->y = ps->phead_snake->y ;
		break;
	case right:
		pNext->x = ps->phead_snake->x + 2;
		pNext->y = ps->phead_snake->y;
		break;
	}

	//判断蛇头到达的坐标处是否是食物
if (NextIsFood(ps, pNext))
	{
		//吃掉食物
		EatFood(ps, pNext);
	}
	else
	{
		//不吃食物
		NoFood(ps, pNext);
	}
}

4)判断蛇头要移动的位置是否是食物位置

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

5)吃掉食物

注意吃掉食物时,要释放掉原本食物的空间,因为食物也是malloc生成的,不释放会造成空间污染

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

	//打印蛇身
	pSnakeNode cur = ps->phead_snake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	free(ps->_pfood);//释放掉原本食物的空间
	ps->Score += ps->FoodWeight; //加分

	//蛇身变长,难度增加
	if (ps->SleepTime >= 80)
	{
		ps->SleepTime -= 10;
	}
	if(ps->FoodWeight <= 30)
	{
		ps->FoodWeight += 1;
	}

	CreatFood(ps);
}

6)不吃食物

不吃食物时,蛇向前行动,蛇头节点更新为下一个要走的位置的节点,蛇尾设置为空

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

	//打印蛇身
	pSnakeNode cur = ps->phead_snake;
	
	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;
}

游戏结束的判断

蛇撞墙和撞到自己都是在前进过程中发生的,所以判断蛇是否死掉的函数应该也在行动函数中。

如图,两个判断是否死亡的函数就在判断下一个节点是否为食物的函数的下面

1)撞墙死

只要判断蛇头的位置是否和墙壁重合

void KillByWall(pSnake ps)
{
	if (ps->phead_snake->x == 0 ||
		ps->phead_snake->x == 56 ||
		ps->phead_snake->y == 0 ||
		ps->phead_snake->y == 26)
	{
		ps->_Status = KILL_BY_WALL;
	}
}

2)撞到自己而死

现将蛇头节点的下一个节点给cur,然后用cur遍历蛇身,看是否和蛇头的位置重合

void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->phead_snake->next;
	while (cur)
	{
		if (ps->phead_snake->x == cur->x && ps->phead_snake->y == cur->y)
		{
			ps->_Status = KILL_BY_SELF;
		}
		cur = cur->next;
	}

}


3)GameEnd函数

判断是那种状态下结束游戏的

释放原本开辟的空间


void GameEnd(pSnake ps)
{
	SetPos(20, 12);
	switch (ps->_Status)
	{
	case End_Normal:
		printf("您主动退出了游戏\n");
		break;
	case KILL_BY_WALL:
		printf("笨(~ ̄(OO) ̄)ブ 撞墙啦\n");
		break;
	case KILL_BY_SELF:
		printf("(⊙_⊙)? 怎么还有人能撞到自己啊\n");
		break;
	}


	//释放蛇身节点空间
	pSnakeNode cur = ps->phead_snake;
	while(cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
	ps->phead_snake = NULL;
}

4)封装test()函数

最后结束GameEnd退出到test函数中

实现再开一把,将原本的游戏过程调用函数全放进do while函数中

void test()
{
	
	char ch ;
	do
	{
		Snake snake = { 0 };   // 创建贪吃蛇游戏信息
		// 1、游戏开始 -- 初始化游戏
		GameStart(&snake);
		// 2、 游戏运行 -- 运行过程
		GameRun(&snake);
		// 3、 游戏结束 -- 结束后的空间资源释放等
		GameEnd(&snake);
		system("pause");
		system("cls");
		SetPos(40, 18);
		printf("不服输?接着塔塔开!\n");
		SetPos(40, 19);
		printf(" Y -- 塔塔开\n ");
		SetPos(40, 20);
		printf(" N -- 不了\n");
		ch = getchar();
		while (getchar()!= '\n')//清空缓冲区
		{
			;
		}
	} while (ch == 'y' || ch == 'Y');
	SetPos(0, 27);
}
 

游戏全部代码

snake.h

#include<stdio.h>
#include<stdlib.h>  //system所需要的头文件
#include<locale.h>  //支持宽字符的头文件
#include<Windows.h> //调用win32API需要的头文件
#include<stdbool.h> //使用 false true需要的头文件  
#include<time.h>    //使用时间戳的头文件

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

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

//封装KEY_PRESS 检测vk虚拟键值对应的按键是否被按过
//如果按过返回1 未按过返回0
#define KEY_PRESS(vk) (GetAsyncKeyState(vk) & 0x1 ? 1: 0)  



//蛇身节点
typedef struct SnakeNode
{
	//坐标
	int x;
	int y;
	struct Snakenode* next;
}SnakeNode,*pSnakeNode;

//方向类型
enum DIRECTION
{
	up = 1,
	down,
	right,
	left
};

//游戏状态类型
enum GameStatus
{
	Normal,   //正常运行
	End_Normal, //正常结束
	KILL_BY_WALL,    
	KILL_BY_SELF
};

//与SnakeNode区分,SnakeNode是管理蛇身和蛇的位置信息的结构体。
//Snake则是存储贪吃蛇行动以及游戏运行信息的结构体
typedef struct Snake
{
	pSnakeNode phead_snake; //指向贪吃蛇头结点的指针
	pSnakeNode _pfood;      //指向食物结点的指针
	int Score;              //总分
	int FoodWeight;         //一个食物的分数
	int SleepTime;          //停顿时间,控制蛇的行进速度
	enum DIRECTION _Dir;         //描述蛇行走方向
	enum GameStatus _Status;        //游戏运行状态 
}Snake,*pSnake;

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

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

//游戏欢迎界面
void WelcomeToGame(); 

//打印地图
void CreatMap();

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

//创建食物
void CreatFood(pSnake ps);

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

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

//暂停函数
void my_pause();

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

//判断蛇要移动的位置是否是食物位置
int NextIsFood(pSnake ps, pSnakeNode pNext);

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

//不吃食物
void NoFood(pSnake ps, pSnakeNode pNext);

//撞墙身亡
void KillByWall(pSnake ps);

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

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




snake.c


#include "snake.h"

void SetPos(int x, int y)
{
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); //获取控制台句柄
	COORD pos = { x,y };                             //赋予光标位置
	SetConsoleCursorPosition(handle, pos);           //设置光标位置 -- 确定输入位置信息
}


void WelcomeToGame()
{
	//定位光标 -- 在窗口的中间打印信息
	SetPos(40, 14);
	printf("贪 吃 蛇\n");
	SetPos(40, 25);
	system("pause");  //pause是暂停
	system("cls");    //清屏,准备打印下一条信息
	SetPos(40, 14);
	printf("启 动 !\n");
	SetPos(40, 25);
	system("pause");  
	system("cls");
}


void CreatMap()
{
	//上
	SetPos(0, 0);
	int i = 0;
	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);
	}

	//左
	for (i = 1; i <= 25; i ++)
	{
		SetPos(0, i);
		wprintf(L"%lc\n", WALL);
	}

	//右
	for (i = 1; i <= 25; i ++)
	{
		SetPos(56, i);
		wprintf(L"%lc\n", WALL);
	}
	
}


void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	int i = 0;
	for (i = 0; i < 5; i++)  //连续创建5个节点
	{
		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->phead_snake == NULL)
		{
			ps->phead_snake = cur;
		}
		else
		{
			cur->next = ps->phead_snake;
			ps->phead_snake = cur;
		}
		
	}
	//打印蛇身
	cur = ps->phead_snake;
	while (cur)
	{
		SetPos(cur->x, cur->y); // 蛇的每个节点都有记录位置信息
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	
	//贪吃蛇的其他信息的初始化
	ps->_Status = Normal;
	ps->Score = 0;
	ps->_pfood = NULL; //食物节点尚未创建,先赋空
	ps->SleepTime = 200;  // 200ms 
	ps->FoodWeight = 10;
	ps->_Dir = right;
}

void CreatFood(pSnake ps)
{
	int x = 0;
	int y = 0;
again:
	do 
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0); //x坐标必须是2的倍数

	//食物坐标不能和蛇身冲突
	pSnakeNode cur = ps->phead_snake;
	while(cur)
	{
		//依次比较坐标
		if (cur->x == x && cur->y == y)
		{
			goto again; //与蛇身重复了,重新生成坐标
		}
		cur = cur->next;
	}

	//坐标有效 为食物开辟空间,并将其地址给相应指针
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood == NULL)
	{
		perror("CreatFood()::malloc()"); //检查是否开辟成功
		return;
	}
	//创建成功 更新食物的信息
	pFood->x = x;
	pFood->y = y;
	ps->_pfood = pFood; 

	//打印食物
	SetPos(x, y); //光标定位到随机生成的位置
	wprintf(L"%lc", FOOD);
}

//游戏界面初始化
void GameStart(pSnake ps)
{
	//控制台窗口设置
	system("mode con cols=100 lines=45");
	system("title 贪吃蛇");

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

	//打印欢迎界面
	WelcomeToGame();
	//创建地图
	CreatMap();
	//初始化贪吃蛇
	InitSnake(ps); 
	//创建食物
	CreatFood(ps);
}

void PrintHelpInfo()
{
	SetPos(64, 15);
	printf("1、不能撞墙,不能撞自己哦\n");
	SetPos(64, 16);
	printf("2、使用↑↓←→ 控制蛇的移动\n");
	SetPos(64, 17);
	printf("3、ESC 退出|空格 暂停游戏 \n");

}

void my_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 NoFood(pSnake ps, pSnakeNode pNext)
{
	//头插
	pNext->next = ps->phead_snake;
	ps->phead_snake = pNext;

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

	SetPos(cur_next->x, cur_next->y); //定位到蛇尾巴
	printf("  "); //覆盖
	free(cur->next);
	cur->next = NULL;
}


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

	//打印蛇身
	pSnakeNode cur = ps->phead_snake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	free(ps->_pfood);//释放掉原本食物的空间
	ps->Score += ps->FoodWeight; //加分

	//蛇身变长,难度增加
	if (ps->SleepTime >= 80)
	{
		ps->SleepTime -= 10;
	}
	if(ps->FoodWeight <= 30)
	{
		ps->FoodWeight += 1;
	}

	CreatFood(ps);
}


void KillByWall(pSnake ps)
{
	if (ps->phead_snake->x == 0 ||
		ps->phead_snake->x == 56 ||
		ps->phead_snake->y == 0 ||
		ps->phead_snake->y == 26)
	{
		ps->_Status = KILL_BY_WALL;
	}
}

void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->phead_snake->next;
	while (cur)
	{
		if (ps->phead_snake->x == cur->x && ps->phead_snake->y == cur->y)
		{
			ps->_Status = KILL_BY_SELF;
		}
		cur = cur->next;
	}

}

void SnakeMove(pSnake ps)
{
	//创建蛇要移动的位置的下一个节点
	pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNext == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}
	pNext->next = NULL;

	//更新这个节点的位置信息 
	switch (ps->_Dir)
	{
	case up:
		pNext->x = ps->phead_snake->x;
		pNext->y = ps->phead_snake->y -1; //在图像上显示向上,但是在坐标上,y坐标是减小的
		break;
	case down:
		pNext->x = ps->phead_snake->x;
		pNext->y = ps->phead_snake->y + 1; 
		break;
	case left:
		pNext->x = ps->phead_snake->x - 2; //注意x坐标每次变化2个单位
		pNext->y = ps->phead_snake->y ;
		break;
	case right:
		pNext->x = ps->phead_snake->x + 2;
		pNext->y = ps->phead_snake->y;
		break;
	}

	//判断蛇头到达的坐标处是否是食物
	if (NextIsFood(ps, pNext))
	{
		//吃掉食物
		EatFood(ps, pNext);
	}
	else
	{
		//不吃食物
		NoFood(ps, pNext);
	}

	//撞墙死
	KillByWall(ps);

	//撞自己死
	KillBySelf(ps);
}

void GameRun(pSnake ps)
{
	PrintHelpInfo();

	//游戏的按键接收逻辑
	do
	{
		SetPos(64, 10);
		printf("Score:%05d", ps->Score); 
		SetPos(64, 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))
		{
			my_pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Status = End_Normal;
			break;                   //直接结束循环,不用再等到while来判断
		}

		Sleep(ps->SleepTime); // Sleep用来控制蛇的行动速度,相当于游戏难度
		SnakeMove(ps);

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

}

void GameEnd(pSnake ps)
{
	SetPos(20, 12);
	switch (ps->_Status)
	{
	case End_Normal:
		printf("您主动退出了游戏\n");
		break;
	case KILL_BY_WALL:
		printf("笨(~ ̄(OO) ̄)ブ 撞墙啦\n");
		break;
	case KILL_BY_SELF:
		printf("(⊙_⊙)? 怎么还有人能撞到自己啊\n");
		break;
	}


	//释放蛇身节点空间
	pSnakeNode cur = ps->phead_snake;
	while(cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
	ps->phead_snake = NULL;
}


test.c

#include "snake.h"

void test()
{
	
	char ch ;
	do
	{
		Snake snake = { 0 };   // 创建贪吃蛇游戏信息
		// 1、游戏开始 -- 初始化游戏
		GameStart(&snake);
		// 2、 游戏运行 -- 运行过程
		GameRun(&snake);
		// 3、 游戏结束 -- 结束后的空间资源释放等
		GameEnd(&snake);
		system("pause");
		system("cls");
		SetPos(40, 18);
		printf("不服输?接着塔塔开!\n");
		SetPos(40, 19);
		printf(" Y -- 塔塔开\n ");
		SetPos(40, 20);
		printf(" N -- 不了\n");
		ch = getchar();
		while (getchar()!= '\n')//清空缓冲区
		{
			;
		}
	} while (ch == 'y' || ch == 'Y');
	SetPos(0, 27);
}

int main()
{
	setlocale(LC_ALL, "zh-CN"); //设置程序适应本地环境
	srand((unsigned int)time(NULL));
	test();
	return 0;
}

  • 23
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值