C语言实现贪吃蛇游戏

目录

一、游戏背景及相关知识介绍

1.1游戏背景

1.2游戏效果展示

1.3游戏涉及知识

二、游戏涉及相关Win32API

2.1Win32API

2.2控制台程序

2.3控制台屏幕上的坐标COORD

2.4GetStdHandle

2.5GetConsoleCursorInfo

 2.5.1CONSOLE_CURSOR_INFO

2.6SetConsoleCursorInfo

2.7SetConsoleCursorPosition

2.8GetAsyncKeyState

三、游戏分析与代码实现

3.1游戏需实现功能

3.2地图的绘制

3.2.1本地化

3.2.2类项

3.2.3setlocale函数

3.2.4宽字符

3.2.5欢迎界面及介绍界面

3.2.6墙体绘制

3. 3蛇的数据结构设计

3.3.1蛇的节点

3.3.2蛇的状态信息

3.3.3蛇的方向

3.3.4游戏状态

3.3.5初始化蛇身

3.4蛇吃食物的功能

3.4.1创建食物

3.4.2蛇身移动

3.4.2.1判断下一个位置是否是食物

3.4.2.2是食物——吃掉食物

3.4.2.3不是食物——继续移动

3.4.2.4撞墙死亡

3.4.2.5撞到自己身体死亡

3.4.3蛇身移动完整实现

3.5暂停游戏

四、游戏模块

4.1游戏流程设计

4.2游戏主逻辑 

4.2.1GameStart

4.2.2GameRun

4.2.2.1需要的虚拟按键

4.2.2.2打印帮助信息

4.2.2.3具体代码实现

4.2.3GameEnd

五、参考代码

5.1snake.c

5.2snake.h

5.3test.c


一、游戏背景及相关知识介绍

1.1游戏背景

贪吃蛇(也叫贪食蛇)游戏是一款休闲益智类游戏,有PC和手机等多平台版本。游戏通过控制蛇头方向吃食物,从而使蛇变得越来越长。

我们可以通过控制台程序实现简易的贪吃蛇游戏。

1.2游戏效果展示

1.3游戏涉及知识

C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32API等

二、游戏涉及相关Win32API

2.1Win32API

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

2.2控制台程序

平常我们运行起来的黑框程序其实就是控制台程序。

我们可以使用cmd命令来设置控制台窗口的长宽(eg: mode 命令)

mode con cols=100 lines =30//设置控制台窗口的大小:30行,100列

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

title 贪吃蛇//将控制台命名为贪吃蛇

这些能在控制台窗口执行的命令,也可以调用C语言函数system执行

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

2.3控制台屏幕上的坐标COORD

COORD是Windows API中定义的一个结构体,标识一个字符在控制台屏幕缓冲区上的坐标,坐标系的远点位于缓冲区的顶部左侧单元格,x轴正方向水平向右,y轴正方向竖直向下

COORD类型的声明:

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

给坐标赋值:

COORD pos = { 10, 15};

2.4GetStdHandle

用于从一个特定的标准设备(标准输入、标准输出或者标准错误)中取得一个句柄(用来标识不同设备的数据),这个句柄可以操作设备

HANDLE GetStdHandle(DWORD nStdHandle);

实例:

HANDLE houtput = NULL;
//获得标准输出设备的句柄
houtput = GetStdHandle(STD_OUTPUT_HANDLE);

2.5GetConsoleCursorInfo

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

BOOL WINAPI GetConsoleCursorInfo(
      HANDLE hConsoleOutput,
      PCONCOLE_CURSOR_INFO lpConsoleCursorInfo
);
PCONCOLE_CURSOR_INFO是指向CONSOLE_CURSOR_INFO结构的指针,
该结构接受有关主机游标

实例:

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

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

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

2.6SetConsoleCursorInfo

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

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.7SetConsoleCursorPosition

设置指定控制台缓冲区中的光标位置,我们将想要设置的坐标信息放在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)
{
	//获得标准输出设备的句柄
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	//定位光标的位置
	COORD pos = { x,y };
	SetConsoleCursorPosition(houtput, pos);
}

2.8GetAsyncKeyState

获取按键状态,将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态

SHORT GetAsyncKeyState ( int vKey);

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

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

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

键盘上的每一个按键都对应着一个虚拟键码 虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn

三、游戏分析与代码实现

3.1游戏需实现功能

  • 贪吃蛇地图绘制
  • 蛇吃食物的功能(上、下、左、右方向键控制蛇的动作)
  • 蛇撞墙死亡
  • 蛇撞自身死亡
  • 计算得分
  • 蛇身加速、减速
  • 暂停游戏、结束游戏

3.2地图的绘制

在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符:●,打印食物使用宽字符★

3.2.1<locale.h>本地化

由于C语言最开始假定字符都是单字节的,ASCII编码只能包含128个字符,对于很多非英语国家(地区)不适用,为了使C语言适应国际化,C语言标准中不断加入了国际化的支持。比如:加入了宽字符的类型wchar_t和宽字符的输入输出函数,加入了<locale.h>头文件,其中提供了允许程序员针对特定地区调整程序行为的函数。

<locale.h>提供的函数用于控制C标准库中对于不同地区会产生不一样行为的部分

在标准中,依赖地区的部分有以下几项:

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

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

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

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

setlocale的第一个参数可以是前说明的类项中的一个,修改一个类项,也可以是LC_ALL,影响所有的类项

C标准给第二个参数仅定义了两种可能取值:“C(正常模式)”和“(本地模式)”

在任意程序执行开始时都会隐式执行调用:

setlocale (LC_ALL, "C");
3.2.4宽字符

普通字符占一个字节,宽字符占两个字节

要在屏幕上打印宽字符,需要在字符的字面量前面加上“L”,否则C语言会将字面量当作窄字符类型来处理。前缀“L”在单引号前面,标识宽字符,对应wprintf()的占位符为"%lc",在双引号前面,表示宽字符串,对应wprintf()的占位符为"%ls"

实例:

wprintf(L"%lc", L'□');
wprintf(L"蛇");
3.2.5欢迎界面及介绍界面

首先设置一下控制台界面的大小,将控制台命名为贪吃蛇,隐藏光标

void GameStart(pSnake ps)
{
	//设置窗口的大小、名称
	system("mode con lines=40 cols=120");
	system("title 贪吃蛇");
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	//隐藏光标
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(houtput, &CursorInfo);
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(houtput, &CursorInfo);
}

封装一个函数打印功能提醒和欢迎界面

void WelcomeToGame()
{
	SetPos(50, 18);
	wprintf(L"欢迎来到贪吃蛇小游戏\n");
	SetPos(52, 38);
	system("pause");
	system("cls");
	SetPos(40, 16);
	wprintf(L"用↑.↓.←.→来控制蛇的移动,按F3加速,F4减速\n");
	SetPos(50, 18);
	wprintf(L"加速能够得到更高的分数\n");
	SetPos(52, 38);
	system("pause");
	system("cls");
}
3.2.6墙体绘制

通过CreateMap()函数创建一个地图

将光标分别定位在要打印墙体的位置,用wprintf()函数打印宽字符(注意算好坐标)

#define WALL L'□'
void CreatMap()
{
	int i = 0;
	for (i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL); 
		//wprintf(L"%lc", L'□');
	}
	for (i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	for (i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
	SetPos(0, 26);
	for (i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL);
	}
}

3. 3蛇的数据结构设计

在游戏运行过程中,蛇每吃一个食物,蛇的身体就会变长一节,我们可以使用链表存储蛇的信息,蛇的每一节就是链表的一个节点。每个节点对应一个在屏幕上的坐标,通过打印节点来实现打印蛇

3.3.1蛇的节点
typedef struct SnakeNode
{
    int x;
    int y;
    struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
3.3.2蛇的状态信息
typedef struct Snake
{
	pSnakeNode _pSnake;//指向蛇头的指针
	pSnakeNode _pFood;//指向食物的指针
	enum DIRECTION _dir;//蛇的方向
	enum GAME_STATUS _status;//游戏的状态
	int _food_weight;//一个食物的分数
	int _score;//总成绩
	int _sleep_time;//休息时间
}Snake, * pSnake;
3.3.3蛇的方向

由于只有四个方向,可以一一列举,使用枚举类型

enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};
3.3.4游戏状态

同样的,游戏的状态也是可以一一列举的,使用枚举类型实现

enum GAME_STATUS
{
	OK,//正常运行
	KILL_BY_WALL,//撞到墙
	KILL_BY_SELF,//撞到自己
	END_NORMAL//正常结束
};
3.3.5初始化蛇身

蛇身开始的长度可以自行设置,这里设置的是三节。

循环创建三个链表节点,将每个节点存放在链表中进行管理,创建完蛇身之后,将蛇的每一节打印在屏幕上。设置游戏的状态、蛇移动的速度、默认的方向、初试成绩、每个食物的分数。

  • 游戏状态:OK
  • 蛇的移动速度:200毫秒
  • 蛇的默认方向
  • 初试成绩:0
  • 每个食物的分数:10

#define BODY L'●'
void InitSnake(pSnake ps)
{
	int i = 0;
	pSnakeNode cur = NULL;
	for (i = 0; i < 3; i++)
	{
		//创建蛇身节点
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("malloc");
			return;
		}

		//设置坐标
		cur->next = NULL;
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;

		//头插法
		if (ps->_pSnake == NULL)
		{
			ps->_pSnake = cur;
		}
		else
		{
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}

	//打印蛇的身体
	cur = ps->_pSnake;
	while (cur != NULL)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	//初始化贪吃蛇数据
	ps->_dir = RIGHT;
	ps->_score = 0;
	ps->_food_weight = 10;
	ps->_sleep_time = 200;//单位是毫秒
	ps->_status = OK;
}

3.4蛇吃食物的功能

3.4.1创建食物

随机生成食物的坐标,要注意,x坐标必须是2的倍数,食物的坐标不能和蛇身的节点坐标重合

#define FOOD L'★'

 使用rand()函数,生成一个食物的坐标,遍历蛇身链表,确保食物的坐标不与蛇身的坐标重合,将光标定位到食物坐标处,打印食物

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 != NULL)
	{
		if (x == cur->x && y == cur->y)
		{
			goto again;
		}
		cur = cur->next;
	}

	//创建食物
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood == NULL)
	{
		perror("malloc");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	//pFood->next = NULL;

	SetPos(x, y);
	wprintf(L"%lc", FOOD);
	ps->_pFood = pFood;
}
3.4.2蛇身移动

我们创建的蛇身链表,只需要根据移动方向和蛇头的坐标,头插一个新的节点,释放尾节点,就可以实现蛇身移动。

首先创建下一个位置,判断该位置上是否是食物,如果是就吃掉,不是就前进一步

蛇身移动后,需要判断此次移动是否会造成撞墙或者撞上自己蛇身,从而影响游戏的状态

3.4.2.1判断下一个位置是否是食物
int ChargeFood(pSnakeNode pn, pSnake ps)
{
	return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}
3.4.2.2是食物——吃掉食物

如果该节点是食物,吃掉食物之后蛇身需要增长一节,对应的得分增加,食物消失(在食物的位置上打印空格),重新生成食物

void EatFood(pSnakeNode pn, pSnake ps)
{
	//头插法创建新的节点
	ps->_pFood->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;
	
	free(pn);
	pn = NULL;

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

	//重新创建新的食物
	CreateFood(ps);
}
3.4.2.3不是食物——继续移动

 将下一个节点插入蛇的身体,经之前蛇身的最后一个节点打印为空格,释放掉蛇身的最后一个节点

注意将最后一个节点释放掉之后,新的尾节点的next指针要置为空

void NoFood(pSnakeNode pn, pSnake ps)
{
	//头插蛇的节点
	pn->next = ps->_pSnake;
	ps->_pSnake = pn;

	//打印蛇
	pSnakeNode cur = ps->_pSnake;
	while (cur->next->next != NULL)
	{
		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.4.2.4撞墙死亡

判断蛇头的坐标是否和墙的坐标冲突

void KillByWall(pSnake ps)
{
	if (ps->_pSnake->x == 0 || ps->_pSnake->y == 0 ||
		ps->_pSnake->x == 56 || ps->_pSnake->y == 26)
	{
		ps->_status = KILL_BY_WALL;
	}
3.4.2.5撞到自己身体死亡

判断蛇头坐标是否和蛇身体坐标冲突

void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake->next;
	while (cur != NULL)
	{
		if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
		{
			ps->_status = KILL_BY_SELF;
			break;
		}
		cur = cur->next;
	}
}
3.4.3蛇身移动完整实现
void SnakeMove(pSnake ps)
{
    //创建下一个节点
	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNextNode == NULL)
	{
		perror("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 (ChargeFood(pNextNode, ps))
	{
		EatFood(pNextNode, ps);
	}
	else
	{
		NoFood(pNextNode, ps);
	}

	//检测蛇是否撞墙
	KillByWall(ps);
	//检测蛇是否撞到自己
	KillBySelf(ps);
}

3.5暂停游戏

当检测到按空格键后,暂停游戏

void Pause()
{
	while (1)
	{
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
		Sleep(200);
	}
}

四、游戏模块

4.1游戏流程设计

4.2游戏主逻辑 

程序开始就设置程序支持本地模式,然后进入游戏的主逻辑

主逻辑分为三个过程

  1. 游戏开始(GameStart)完成游戏的初始化
  2. 游戏运行(GameRun)完成游戏运行逻辑的实现
  3. 游戏结束(GameEnd)完成游戏结束的说明,实现资源释放
4.2.1GameStart

该模块完成游戏的初始化任务

  • 控制台窗口大小的设置
  • 控制台窗口名字的设置
  • 鼠标光标的隐藏
  • 打印欢迎界面
  • 创建地图
  • 初始化蛇
  • 创建第一个食物
void GameStart(pSnake ps)
{
	//设置窗口的大小、名称
	system("mode con lines=40 cols=120");
	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);
	//创建食物
	CreateFood(ps);
	//设置游戏的相关信息
}
4.2.2GameRun

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

根据游戏状态检查游戏是否结束,如果状态是OK,游戏继续,否则游戏结束

如果游戏继续,需要检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏

4.2.2.1需要的虚拟按键
  • 上: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)
4.2.2.2打印帮助信息

在界面右侧显示游戏规则、得分情况、按键信息等

void PrintHelpInfo()
{
	SetPos(64, 2);
	printf("按空格键开始");
	SetPos(64,4);
	printf("不能穿墙,不能咬到自己");
	SetPos(64, 6);
	printf("用↑.↓.←.→来控制蛇的移动");
	SetPos(64, 8);
	printf("按F3加速,F4减速");
	SetPos(64, 10);
	printf("按ESC退出游戏,按空格暂停游戏");
}
4.2.2.3具体代码实现
void GameRun(pSnake ps)
{
	PrintHelpInfo();
	do 
	{
		//打印总分数和食物的分值
		SetPos(64, 15);
		printf("总分数:%2d\n", ps->_score);
		SetPos(64, 17);
		printf("当前食物的分值:%2d\n", ps->_food_weight);

		//蛇的方向
		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_NORMAL;
		}
		else if (KEY_PRESS(VK_F3))
		{
			if (ps->_sleep_time > 80)
			{
				ps->_sleep_time -= 30;
				ps->_food_weight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			if (ps->_food_weight > 2)
			{
				ps->_sleep_time += 30;
				ps->_food_weight -= 2;
			}
		}
		SnakeMove(ps);
		Sleep(ps->_sleep_time);
	} while (ps->_status == OK);
}
4.2.3GameEnd

游戏状态不再是OK时,告知游戏结束的原因,并且释放蛇身节点

void GameEnd(pSnake ps)
{
	SetPos(20, 15);
	switch (ps->_status)
	{
	case END_NORMAL:
		printf("成功结束游戏\n");
		break;
	case KILL_BY_WALL:
		printf("您撞到了墙上,游戏结束\n");
		break;
	case KILL_BY_SELF:
		printf("您撞到了自己,游戏结束\n");
		break;
	}
	
	//释放蛇身的链表
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
}

五、参考代码

5.1snake.c

#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)
#include "snake.h"
//定位光标
void SetPos(short x, short y)
{
	//获得标准输出设备的句柄
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	//定位光标的位置
	COORD pos = { x,y };
	SetConsoleCursorPosition(houtput, pos);
}
//欢迎界面
void WelcomeToGame()
{
	SetPos(50, 18);
	wprintf(L"欢迎来到贪吃蛇小游戏\n");
	SetPos(52, 38);
	system("pause");
	system("cls");
	SetPos(40, 16);
	wprintf(L"用↑.↓.←.→来控制蛇的移动,按F3加速,F4减速\n");
	SetPos(50, 18);
	wprintf(L"加速能够得到更高的分数\n");
	SetPos(52, 38);
	system("pause");
	system("cls");
}
//打印地图
void CreatMap()
{
	int i = 0;
	for (i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL); 
		//wprintf(L"%lc", L'□');
	}
	for (i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	for (i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
	SetPos(0, 26);
	for (i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL);
	}
}

//初始化蛇
void InitSnake(pSnake ps)
{
	int i = 0;
	pSnakeNode cur = NULL;
	for (i = 0; i < 3; i++)
	{
		//创建蛇身节点
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("malloc");
			return;
		}

		//设置坐标
		cur->next = NULL;
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;

		//头插法
		if (ps->_pSnake == NULL)
		{
			ps->_pSnake = cur;
		}
		else
		{
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}

	//打印蛇的身体
	cur = ps->_pSnake;
	while (cur != NULL)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	//初始化贪吃蛇数据
	ps->_dir = RIGHT;
	ps->_score = 0;
	ps->_food_weight = 10;
	ps->_sleep_time = 200;//单位是毫秒
	ps->_status = OK;
}

//创建食物
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 != NULL)
	{
		if (x == cur->x && y == cur->y)
		{
			goto again;
		}
		cur = cur->next;
	}

	//创建食物
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood == NULL)
	{
		perror("malloc");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	//pFood->next = NULL;

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

//打印帮助信息
void PrintHelpInfo()
{
	SetPos(64, 2);
	printf("按空格键开始");
	SetPos(64,4);
	printf("不能穿墙,不能咬到自己");
	SetPos(64, 6);
	printf("用↑.↓.←.→来控制蛇的移动");
	SetPos(64, 8);
	printf("按F3加速,F4减速");
	SetPos(64, 10);
	printf("按ESC退出游戏,按空格暂停游戏");
}

//暂停游戏
void Pause()
{
	while (1)
	{
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
		Sleep(200);
	}
}

//判断下一个坐标是否是食物
int ChargeFood(pSnakeNode pn, pSnake ps)
{
	return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}

//下一个位置是食物,吃掉食物
void EatFood(pSnakeNode pn, pSnake ps)
{
	//头插法创建新的节点
	ps->_pFood->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;
	
	free(pn);
	pn = NULL;

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

	//重新创建新的食物
	CreateFood(ps);
}

//下一个位置不是食物
void NoFood(pSnakeNode pn, pSnake ps)
{
	//头插蛇的节点
	pn->next = ps->_pSnake;
	ps->_pSnake = pn;

	//打印蛇
	pSnakeNode cur = ps->_pSnake;
	while (cur->next->next != NULL)
	{
		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->y == 0 ||
		ps->_pSnake->x == 56 || ps->_pSnake->y == 26)
	{
		ps->_status = KILL_BY_WALL;
	}
}

//检测蛇是否撞到自己
void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake->next;
	while (cur != NULL)
	{
		if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
		{
			ps->_status = KILL_BY_SELF;
			break;
		}
		cur = cur->next;
	}
}

//初始化游戏
void GameStart(pSnake ps)
{
	//设置窗口的大小、名称
	system("mode con lines=40 cols=120");
	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);
	//创建食物
	CreateFood(ps);
	//设置游戏的相关信息
}

//蛇的移动
void SnakeMove(pSnake ps)
{
	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNextNode == NULL)
	{
		perror("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 (ChargeFood(pNextNode, ps))
	{
		EatFood(pNextNode, ps);
	}
	else
	{
		NoFood(pNextNode, ps);
	}

	//检测蛇是否撞墙
	KillByWall(ps);
	//检测蛇是否撞到自己
	KillBySelf(ps);
}

//运行游戏
void GameRun(pSnake ps)
{
	PrintHelpInfo();
	do 
	{
		//打印总分数和食物的分值
		SetPos(64, 15);
		printf("总分数:%2d\n", ps->_score);
		SetPos(64, 17);
		printf("当前食物的分值:%2d\n", ps->_food_weight);

		//蛇的方向
		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_NORMAL;
		}
		else if (KEY_PRESS(VK_F3))
		{
			if (ps->_sleep_time > 80)
			{
				ps->_sleep_time -= 30;
				ps->_food_weight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			if (ps->_food_weight > 2)
			{
				ps->_sleep_time += 30;
				ps->_food_weight -= 2;
			}
		}
		SnakeMove(ps);
		Sleep(ps->_sleep_time);
	} while (ps->_status == OK);
}

//结束游戏
void GameEnd(pSnake ps)
{
	SetPos(20, 15);
	switch (ps->_status)
	{
	case END_NORMAL:
		printf("成功结束游戏\n");
		break;
	case KILL_BY_WALL:
		printf("您撞到了墙上,游戏结束\n");
		break;
	case KILL_BY_SELF:
		printf("您撞到了自己,游戏结束\n");
		break;
	}
	
	//释放蛇身的链表
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
}

5.2snake.h

#define _CRT_SECURE_NO_WARNINGS
#pragma once

#include <windows.h>
#include <stdio.h>
#include <stdbool.h>
#include <locale.h>
#include <time.h>

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

#define POS_X 24
#define POS_Y 5

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

//蛇的状态
enum GAME_STATUS
{
	OK,
	KILL_BY_WALL,
	KILL_BY_SELF,
	END_NORMAL
};

//蛇身的节点类型
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 _food_weight;//一个食物的分数
	int _score;//总成绩
	int _sleep_time;//休息时间
}Snake, * pSnake;

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

//定位光标
void SetPos(short x, short y);
//欢迎界面
void WelcomeToGame();
//打印地图
void CreatMap();
//创建蛇
void InitSnake(pSnake ps);
//创建食物
void CreateFood(pSnake ps);

//运行游戏
void GameRun(pSnake ps);
//打印帮助信息
void PrintHelpInfo();
//暂停游戏
void Pause();
//蛇的移动
void SnakeMove(pSnake ps);
//判断下一个坐标是否是食物
int ChargeFood(pSnakeNode pn, pSnake ps);
//下一个位置是食物,吃掉食物
void EatFood(pSnakeNode pn, pSnake ps);
//下一个位置不是食物
void NoFood(pSnakeNode pn, pSnake ps);
//检测蛇是否撞墙
void KillByWall(pSnake ps);
//检测蛇是否撞到自己
void KillBySelf(pSnake ps);

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

5.3test.c

#include "snake.h"
void game()
{
	char ch = 0;
	srand((unsigned)time(NULL));
	do 
	{
		system("cls");
		//创建贪吃蛇
		Snake snake = { 0 };
		//初始化游戏
		GameStart(&snake);
		//运行游戏
		GameRun(&snake);
		///结束游戏
		GameEnd(&snake);
		
		SetPos(20, 13);
		printf("再来一局吗?(Y/N):");
		ch = getchar();
		while(getchar()!='\n');
	} while (ch == 'Y' || ch == 'y');
	SetPos(1, 35);
}
int main()
{
	//设置适配本地环境
	setlocale(LC_ALL, "");
	game();
	return 0;
}
  • 36
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值