C语言贪吃蛇小游戏的实现

贪吃蛇

贪吃蛇是一种经典的游戏,玩家操纵一条蛇在一个有边界的区域内移动,通过吃食物来增长长度。

知识储备

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

整体思路

•将游戏拆分成开始、运行和结束三个阶段
•游戏的开始阶段:我们要初始化界面、地图、食物和蛇
•游戏的运行阶段:实现蛇的运动、吃食物、撞墙、吃到自己以及暂停、退出等功能
•游戏的结束阶段:需要根据不同的结束状态给出结束标志,实现循环游玩。
•大体框架如下:

void test()
{
	GameStart();
	GameRun();
	GameEnd();

}

GameStart

•在开始实现各种函数之前,我们需要创建一些全局变量便于函数定义:

enum Direction
{
	UP,
	DOWN,
	RIGHT,
	LEFT
};
enum Snake_State
{
	OK,
	END_NORMAL,
	SELFKILL,
	KILL_WALL,
};
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
typedef struct Snake
{
	pSnakeNode _phead;
	pSnakeNode _pfood;
	int _scroce;
	int _foodweight;
	enum Direction _direction;
	enum Snake_State _state;
	int _SleepTime;
	int MAX;
}Snake,*pSnake;

•上述声明中,snake作为对整个游戏管理的结构体,存储蛇头的坐标、当前得分、蛇头的方向、食物的坐标、食物对应的分数、蛇的状态、蛇运动间歇以及历史最高分
•注意到蛇身我们选择使用单链表构建节点,因为蛇身本身物理结构就与单链表相似,这样打印蛇身事半功倍。
•要打印开始界面,则需要控制光标的位置,否则每次打印都需要大量的换行符和空格,非常费力,接下来我们就实现控制光标位置。

控制光标位置

•要实现控制光标位置的相关功能则需要了解Win32 API的相关知识,在Windows.h下的一系列库函数实现了相关功能。
如:
HANDLE GetStdHandle(DWORD nStdHandle);该函数能获得特定设备的句柄。
BOOL WINAPI GetConsoleCursorInfo( HANDLE hConsoleOutput, PCONSOLE_CURSOR_INFO lpConsoleCursorInfo);能获得句柄对应光标的相关信息。
BOOL WINAPI SetConsoleCursorInfo(HANDLE hConsoleOutput,const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo);设置光标信息。
BOOL WINAPI SetConsoleCursorPosition(HANDLE hConsoleOutput, COORD pos);设置光标位置。
•具体实现如下:

void SetPos(short x, short y)//设置光标位置
{
	COORD pos = { x,y };
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(handle, pos);
}
void GameStart()
{
	HANDLE handle = NULL;
	handle = GetStdHandle(STD_OUTPUT_HANDLE);//获取显示屏的句柄
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(handle, &CursorInfo);//获取光标信息
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(handle, &CursorInfo);//隐藏光标
}

打印初始化界面和地图

•打印初始化界面需要用到上述设置光标位置的函数,并且打印完后程序暂停运行,就需要用到指令pause,调用函数system();

void PrintStart()
{
	SetPos(60, 15);
	printf("欢迎来到贪吃蛇小游戏!");
	SetPos(62, 30);
	system("pause");
	SetPos(50, 15);
	printf("用↑、↓、←、→分别控制蛇的移动,F3为加速,F4为减速\n");
	SetPos(60, 16);
	printf("加速能获得更高的分数哦!");
	SetPos(62, 30);
	system("pause");
}

在这里插入图片描述
在这里插入图片描述

•接下来要打印地图,我们选择用宽字符’□’作为墙体,这样视觉效果更好,既然墙体是宽字符,那么蛇身和食物自然也需要是宽字符。这时我们可以宏定义墙体和蛇身:

#define SNAKENODE L'●'//L是宽字符的声明
#define FOOD L'★'
#define WALL L'□'

仅仅如此还不够,要实现宽字符的打印需要使用setlocale函数,让编译器的C语言功能适应本地环境。

int main()
{
	setlocale(LC_ALL, "");
	test();
	return 0;
}

初始化地图:

void CreaMap()
{
	system("cls");
	SetPos(0, 0);
	int i = 0;
	for (; i <= 57; i += 2)
		wprintf(L"%c", WALL);
	SetPos(0, 26);
	for (i=0; i <= 57; 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);
	}
}

初始化蛇

•往后的实现中肯定需要大量的链表节点,可以先实现节点的创建函数:

pSnakeNode BuyNode()
{
	pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (!node)
	{
		perror("buynode mollc");
		return;
	}
	return node;
}

•创建蛇身就使用链表头插即可。

void CreatSanke(pSnake ps)
{
	for (int i = 0; i < 5; i++)
	{
		pSnakeNode node = BuyNode();
		node->next = NULL;
		node->x = 10 + 2 * i;
		node->y = 5;
		if (ps->_phead == NULL)
		{
			ps->_phead = node;
			SetPos(node->x, node->y);
			wprintf(L"%c", SNAKENODE);
		}
		else
		{
			node->next = ps->_phead;
			ps->_phead = node;
			SetPos(node->x, node->y);
			if (i == 4)
				wprintf(L"%c", L'▲');//区分蛇头和蛇身
			else
				wprintf(L"%c", SNAKENODE);
		}
		ps->_direction = RIGHT;
		ps->_scroce = 0;
		ps->_foodweight = 10;
		ps->_SleepTime = 200;
		ps->_state = OK;
	}
	
}

初始化食物

•食物的生成位置是随机的,这又需要用到我们的老熟人rand()函数。当然,需要先调用srand()函数并且,设置time(NULL)为种子。
•此外,还需注意食物的位置在地图内,并且不能与蛇身重合。

void CreatFood(pSnake ps)
{
	pSnakeNode food = BuyNode();
	food->next = NULL;
again:
	while (1)
	{
		food->x = rand() % 53 + 2;
		food->y = rand() % 25 + 1;
		if (food->x % 2 == 0)
			break;
	}
	pSnakeNode pcup= ps->_phead;
	while (pcup)
	{
		if (pcup->x == food->x && pcup->y == food->y)
			goto again;
		pcup = pcup->next;
	}
	ps->_pfood = food;
	SetPos(food->x,food->y);
	wprintf(L"%c", FOOD);
}

那么GameStart函数就完全实现了!

void GameStart(pSnake ps)
{
	srand(time(NULL));
	HANDLE handle = NULL;
	handle = GetStdHandle(STD_OUTPUT_HANDLE);//获取显示屏的句柄
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(handle, &CursorInfo);//获取光标信息
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(handle, &CursorInfo);//隐藏光标
	//打印欢迎界面
	PrintStart();
	//打印地图
	CreaMap();
	//初始化蛇
	CreatSanke(ps);
	//生成第一个食物
	CreatFood(ps);
}

在这里插入图片描述

GameRun

•蛇的运行实现的功能不多,主要包括打印一些得分信息,蛇的运动和吃掉食物、死亡以及退出等功能。

打印静态信息

void PrintHelp(pSnake ps)
{
	SetPos(0, 27);
	printf("操作方式:用↑、↓、←、→分别控制蛇的移动,F3为加速,F4为减速\n");
	SetPos(10, 28);
	printf("Space:暂停,Esc:退出");
	SetPos(5, 29);
	printf("游戏开始时间:%s %s",__TIME__, __DATE__);
	SetPos(5, 30);
	printf("历史最高得分:%d", ps->MAX);
}

打印动态信息

SetPos(64, 10);
		printf("当前得分:%d", ps->_scroce);
		SetPos(64, 11);
		printf("当前食物的分数:%d", ps->_foodweight);

蛇身移动的实现

•在贪吃蛇游戏中,蛇的运动方向是通过控制←、↑、→、↓实现的。那么要实现相关功能,我们就需要一个函数能够获取键盘的按键情况,还是WIN32 API的函数。
•SHORT GetAsyncKeyState(int vKey);
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。GetAsyncKeyState 的返回值是short类型,在上一次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
•由此我们就可以实现蛇身的移动。
如下:

do
	{
		if (KEY_PRESS(VK_UP)&&ps->_direction!=DOWN)
			ps->_direction = UP;
		else if (KEY_PRESS(VK_DOWN) && ps->_direction != UP)
			ps->_direction = DOWN;
		else if (KEY_PRESS(VK_RIGHT) && ps->_direction != LEFT)
			ps->_direction = RIGHT;
		else if (KEY_PRESS(VK_LEFT) && ps->_direction != RIGHT)
			ps->_direction = LEFT;
		else if (KEY_PRESS(VK_SPACE))
		{
			do
			{
				Sleep(10);
			} while (!KEY_PRESS(VK_SPACE));
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_state = END_NORMAL;
			break;
		}
		else if (KEY_PRESS(VK_F3) && ps->_SleepTime >= 50)
		{
			ps->_SleepTime -= 30;
			ps->_foodweight += 2;
		}
		else if (KEY_PRESS(VK_F4) && ps->_foodweight>0)
		{
			ps->_SleepTime += 30;
			ps->_foodweight -= 2;
		}
		Sleep(ps->_SleepTime);
		SnakeMove(ps);
	} while (ps->_state == OK);

GameEnd

•游戏结束部分,我们要实现结束语、销毁节点,保留历史最高分。

结束界面

void GameEnd(pSnake ps)
{
	SetPos(24, 12);
	if (ps->_state == END_NORMAL)
		printf("你正常退出游戏!");
	else if (ps->_state == KILL_WALL)
		printf("你撞墙了!");
	else if (ps->_state == SELFKILL)
		printf("你自杀了!");
	pSnakeNode pcur = ps->_phead;
	while (pcur)
	{
		pSnakeNode del = pcur;
		pcur = del->next;
		free(del);
	}
}

保留历史最高分

•要实现保留历史最高分,那肯定要调用外存,所以需要一定文件操作函数。

void GetMax(pSnake ps)//一开始要载入最高得分
{
	FILE* pf = fopen("data.txt", "rb");
	if (!pf)
	{
		FILE* pf = fopen("data.txt", "wb+");
	}
	if (!pf)
	{
		perror("fopen");
		return 1;
	}
	int i;
	fread(&i, sizeof(int), 1, pf);
	ps->MAX = i;
	if (i < 0)
		ps->MAX = 0;
	fclose(pf);
	pf = NULL;
}
void CopyMax(pSnake ps)//记录最高得分
{
	FILE* pf = fopen("data.txt", "wb");
	if (!pf)
	{
		perror("fopen");
		return 1;
	}
	fwrite(&(ps->_scroce), sizeof(int), 1, pf);
	fclose(pf);
	pf = NULL;
}

整体代码

Snake.h

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<Windows.h>
#include<locale.h>
#include<stdbool.h>
#include<time.h>
#define SNAKENODE L'●'
#define FOOD L'★'
#define WALL L'□'
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1)?1:0)
enum Direction
{
	UP,
	DOWN,
	RIGHT,
	LEFT
};
enum Snake_State
{
	OK,
	END_NORMAL,
	SELFKILL,
	KILL_WALL,
};
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
typedef struct Snake
{
	pSnakeNode _phead;
	pSnakeNode _pfood;
	int _scroce;
	int _foodweight;
	enum Direction _direction;
	enum Snake_State _state;
	int _SleepTime;
	int MAX;
}Snake,*pSnake;
void SetPos(short x, short y);
void PrintStart();
void CreaMap();
void CreatSanke(pSnake ps);
void CreatFood(ps);
void PrintHelp(pSnake ps);
void SnakeMove(pSnake ps);
void EatFood(pSnake ps, pSnakeNode pcup);
void NoFood(pSnake ps, pSnakeNode pcup);
void KillByWall(pSnake ps);
void KillBySelf(pSnake ps);
void GetMax(pSnake ps);
void CopyMax(pSnake ps);

Snake.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"Snake.h"
void SetPos(short x, short y)
{
	COORD pos = { x,y };
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(handle, pos);
}
void PrintStart()
{
	SetPos(60, 15);
	printf("欢迎来到贪吃蛇小游戏!");
	SetPos(62, 30);
	system("pause");
	SetPos(50, 15);
	printf("用↑、↓、←、→分别控制蛇的移动,F3为加速,F4为减速\n");
	SetPos(60, 16);
	printf("加速能获得更高的分数哦!");
	SetPos(62, 30);
	system("pause");
}
void CreaMap()
{
	system("cls");
	SetPos(0, 0);
	int i = 0;
	for (; i <= 57; i += 2)
		wprintf(L"%c", WALL);
	SetPos(0, 26);
	for (i=0; i <= 57; 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);
	}
}
pSnakeNode BuyNode()
{
	pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (!node)
	{
		perror("buynode mollc");
		return;
	}
	return node;
}
void CreatSanke(pSnake ps)
{
	for (int i = 0; i < 5; i++)
	{
		pSnakeNode node = BuyNode();
		node->next = NULL;
		node->x = 10 + 2 * i;
		node->y = 5;
		if (ps->_phead == NULL)
		{
			ps->_phead = node;
			SetPos(node->x, node->y);
			wprintf(L"%c", SNAKENODE);
		}
		else
		{
			node->next = ps->_phead;
			ps->_phead = node;
			SetPos(node->x, node->y);
			if (i == 4)
				wprintf(L"%c", L'▲');
			else
				wprintf(L"%c", SNAKENODE);
		}
		ps->_direction = RIGHT;
		ps->_scroce = 0;
		ps->_foodweight = 10;
		ps->_SleepTime = 200;
		ps->_state = OK;
	}
	
}
void CreatFood(pSnake ps)
{
	pSnakeNode food = BuyNode();
	food->next = NULL;
again:
	while (1)
	{
		food->x = rand() % 53 + 2;
		food->y = rand() % 25 + 1;
		if (food->x % 2 == 0)
			break;
	}
	pSnakeNode pcup= ps->_phead;
	while (pcup)
	{
		if (pcup->x == food->x && pcup->y == food->y)
			goto again;
		pcup = pcup->next;
	}
	ps->_pfood = food;
	SetPos(food->x,food->y);
	wprintf(L"%c", FOOD);
}
void PrintHelp(pSnake ps)
{
	SetPos(0, 27);
	printf("操作方式:用↑、↓、←、→分别控制蛇的移动,F3为加速,F4为减速\n");
	SetPos(10, 28);
	printf("Space:暂停,Esc:退出");
	SetPos(5, 29);
	printf("游戏开始时间:%s %s",__TIME__, __DATE__);
	SetPos(5, 30);
	printf("历史最高得分:%d", ps->MAX);
}
void EatFood(pSnake ps, pSnakeNode pcup)
{
	pcup->next = ps->_phead;
	ps->_phead = pcup;
	pcup = ps->_phead;
	int i = 1;
	while (pcup->next)
	{
		if (i)
		{
			SetPos(pcup->x, pcup->y);
			wprintf(L"%c", L'▲');
			i = 0;
		}
		else
		{
			SetPos(pcup->x, pcup->y);
			wprintf(L"%c", SNAKENODE);
		}
		pcup = pcup->next;
	}
	ps->_scroce += ps->_foodweight;
	CreatFood(ps);
}
void NoFood(pSnake ps, pSnakeNode pcup)
{
	pcup->next = ps->_phead;
	ps->_phead = pcup;
	pcup = ps->_phead;
	int i = 1;
	while (pcup->next->next)
	{
		if (i)
		{
			SetPos(pcup->x, pcup->y);
			wprintf(L"%c", L'▲');
			i = 0;
		}
		else
		{
			SetPos(pcup->x, pcup->y);
			wprintf(L"%c", SNAKENODE);
		}
		pcup = pcup->next;
	}
	SetPos(pcup->next->x, pcup->next->y);
	printf("  ");
	free(pcup->next);
	pcup->next = NULL;
}
void KillByWall(pSnake ps)
{
	if (ps->_phead->x == 0 ||
		ps->_phead->x == 56 ||
		ps->_phead->y == 0 ||
		ps->_phead->y == 26)
	{
		ps->_state = KILL_WALL;
		return 1;
	}
	return 0;
}
void KillBySelf(pSnake ps)
{
	pSnakeNode pcup = ps->_phead;
	pSnakeNode pnext = pcup->next;
	while (pnext->next)
	{
		if (pcup->x == pnext->x && pcup->y == pnext->y)
		{
			ps->_state = SELFKILL;
			return;
		}
		pnext = pnext->next;
	}
}
void SnakeMove(pSnake ps)
{
	pSnakeNode pcup = BuyNode();
	if (ps->_direction == RIGHT)
	{
		pcup->x = ps->_phead->x + 2;
		pcup->y = ps->_phead->y;
	}
	else if (ps->_direction == LEFT)
	{
		pcup->x = ps->_phead->x - 2;
		pcup->y = ps->_phead->y;
	}
	else if (ps->_direction == UP)
	{
		pcup->x = ps->_phead->x;
		pcup->y = ps->_phead->y - 1;
	}
	else if (ps->_direction == DOWN)
	{
		pcup->x = ps->_phead->x;
		pcup->y = ps->_phead->y + 1;
	}
	if(pcup->x==ps->_pfood->x&&pcup->y==ps->_pfood->y)
		EatFood(ps,pcup);
	else
		NoFood(ps,pcup);
	KillByWall(ps);
	KillBySelf(ps);
}
void GetMax(pSnake ps)
{
	FILE* pf = fopen("data.txt", "rb");
	if (!pf)
	{
		FILE* pf = fopen("data.txt", "wb+");
	}
	if (!pf)
	{
		perror("fopen");
		return 1;
	}
	int i;
	fread(&i, sizeof(int), 1, pf);
	ps->MAX = i;
	if (i < 0)
		ps->MAX = 0;
	fclose(pf);
	pf = NULL;
}
void CopyMax(pSnake ps)
{
	FILE* pf = fopen("data.txt", "wb");
	if (!pf)
	{
		perror("fopen");
		return 1;
	}
	fwrite(&(ps->_scroce), sizeof(int), 1, pf);
	fclose(pf);
	pf = NULL;
}

test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"Snake.h"
void GameStart(pSnake ps)
{
	srand(time(NULL));
	system("mode con cols=150 lines=40");
	system("title 贪吃蛇");
	HANDLE handle = NULL;
	handle = GetStdHandle(STD_OUTPUT_HANDLE);//获取显示屏的句柄
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(handle, &CursorInfo);//获取光标信息
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(handle, &CursorInfo);//隐藏光标
	//获取历史最高得分;
	GetMax(ps);
	//打印欢迎界面
	PrintStart();
	//打印地图
	CreaMap();
	//初始化蛇
	CreatSanke(ps);
	//生成第一个食物
	CreatFood(ps);
}
void GameRun(pSnake ps)
{
	//打印静态信息
	PrintHelp(ps);	
	do
	{
		//打印动态信息
		SetPos(64, 10);
		printf("当前得分:%d", ps->_scroce);
		SetPos(64, 11);
		printf("当前食物的分数:%d", ps->_foodweight);
		if (KEY_PRESS(VK_UP)&&ps->_direction!=DOWN)
			ps->_direction = UP;
		else if (KEY_PRESS(VK_DOWN) && ps->_direction != UP)
			ps->_direction = DOWN;
		else if (KEY_PRESS(VK_RIGHT) && ps->_direction != LEFT)
			ps->_direction = RIGHT;
		else if (KEY_PRESS(VK_LEFT) && ps->_direction != RIGHT)
			ps->_direction = LEFT;
		else if (KEY_PRESS(VK_SPACE))
		{
			do
			{
				Sleep(10);
			} while (!KEY_PRESS(VK_SPACE));
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_state = END_NORMAL;
			break;
		}
		else if (KEY_PRESS(VK_F3) && ps->_SleepTime >= 50)
		{
			ps->_SleepTime -= 30;
			ps->_foodweight += 2;
		}
		else if (KEY_PRESS(VK_F4) && ps->_foodweight>0)
		{
			ps->_SleepTime += 30;
			ps->_foodweight -= 2;
		}
		Sleep(ps->_SleepTime);
		SnakeMove(ps);
	} while (ps->_state == OK);

}
void GameEnd(pSnake ps)
{
	//保留最大得分
	if(ps->_scroce>ps->MAX)
		CopyMax(ps);
	SetPos(24, 12);
	if (ps->_state == END_NORMAL)
		printf("你正常退出游戏!");
	else if (ps->_state == KILL_WALL)
		printf("你撞墙了!");
	else if (ps->_state == SELFKILL)
		printf("你自杀了!");
	pSnakeNode pcur = ps->_phead;
	while (pcur)
	{
		pSnakeNode del = pcur;
		pcur = del->next;
		free(del);
	}
}
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' || ch == 'y');
	system("cls");
	SetPos(60, 20);
	printf("汗流浃背了吧,老弟!");
	while (1)
		Sleep(100);
}
int main()
{
	setlocale(LC_ALL, "");
	test();
	return 0;
}

That`s all!

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值