从零开始实现贪吃蛇(C语言版)

游戏介绍

贪吃蛇游戏是耳熟能详的益智小游戏,今天我们就用C语言来实现它!

1.前置知识

今天我们实现的贪吃蛇游戏是运行在控制台程序中(如图),在实现前需要了解一些Win32API以及控制台相关的知识.
游戏截图:
在这里插入图片描述
在这里插入图片描述

1.1 Win32API

API全称:ApplicationProgrammingInterface,API可以简单理解成函数,Win32API也就是Windows32位平台提供给我们的一系列可以帮助应用程序开启视窗、绘制图形等的接口函数,今天我们只需要了解以下函数即可:

GetStdHandle(获取句柄)
GetConsoleCursorInfo(获取光标信息)
CONSOLE_CURSOR_INFO(控制台光标信息)
SetConsoleCursorInfo(设置控制台光标信息)
SetConsoleCursorPosition(设置光标当前位置)
GetAsyncKeyState(获取按键情况)

1.2 控制台程序

我们写C语言程序运行时的黑框框就是控制台程序,使用Win+r键,输入cmd可以打开Windows的控制台程序
在这里插入图片描述
使用cmd命令可以设置控制台窗口的宽高和控制台名称

mode con cols=80 lines=30 //设置控制台宽高
title 贪吃蛇 //设置控制台名称

在这里插入图片描述
在这里插入图片描述
我们也可以使用C语言的system函数来执行上述操作

int main()
{
//设置控制台宽高
system("mode con cols=80 lines=30");
//设置控制台名称
system("title 贪吃蛇");
system("pause");//让程序暂停
return 0;
}

1.3 坐标系统

控制台坐标中每一个普通字符占一个坐标,输出字符时默认从原点开始输出
在这里插入图片描述
COORD是WindowsAPI中定义的结构体,表示字符在控制台上坐标位置

typedef struct _COORD {
SHORT X;//横坐标
SHORT Y;//纵坐标
} COORD, *PCOORD;

1.4 GetStdHandle(获取句柄)

GetStdHandle函数用于从设备(标准输入/输出设备)中获取句柄,可以通过句柄操作设备,句柄可以类比成游戏手柄,通过手柄可以操作游戏中的人物.
函数原型:

HANDLE GetStdHandle(DWORD nStdHandle);

参数取值:STD_INPUT_HANDLE(标准输入句柄),STD_OUTPUT_HANDLE(标准输出句柄)
返回值:HANDLE类型
运用实例

HANDLE handle = GetStdHandle(STD_INPUT_HANDLE);
//STD_INPUT_HANDLE其实就是我们运行后的控制台窗口

1.5 CONSOLE_CURSOR_INFO(控制台光标信息)

CONSOLE_CURSOR_INFO是Win32中定义的一个结构体,结构体中包含了控制台光标的信息

typedef struct _CONSOLE_CURSOR_INFO
{
	DWORD dwSize;
	BOOL bVisible;
}CONSOLE_CURSOR_INFO,*PCONSOLE_CURSOR_INFO;

dwSize表示光标占字符单元格的百分比,默认为25(百分之25),如图左边部分是光标的大小,右边部分是一个字符单元格
在这里插入图片描述
bVisible表示光标的可见性,默认值为true,表示光标可见,如果想隐藏光标,可以将bVisible的值置为false

1.6 GetConsoleCursorInfo(获取光标信息)

获取指定控制台的光标大小和可见性信息,函数原型:

BOOL GetConsoleCursorInfo(HANDLE hConsoleOutput,PCONSOLE_CURSOR_INFO lpConsoleCursorInfo);

第一个参数是设备的句柄,第二个参数是CONSOLE_CURSOR_INFO结构体的指针

运用实例:

	//获取设备句柄
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);

	//定义结构体
	CONSOLE_CURSOR_INFO CursorInfo = { 0 };

	//获取控制台光标信息
	GetConsoleCursorInfo(handle, &CursorInfo);

1.7 SetConsoleCursorInfo(设置控制台光标信息)

获取到光标信息后,如果想修改光标的信息,需要先修改再设置,使用SetConsoleCursorInfo函数可以设置光标信息.
函数原型:

BOOL SetConsoleCursorInfo(HANDLE hConsoleOutput,const CONSOLE_CURSOR_INFO*lpConsoleCursorInfo);

参数类型与GetConsoleCursorInfo函数一致,返回值为布尔类型
运用实例:

	//获取设备句柄
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);

	//定义结构体
	CONSOLE_CURSOR_INFO CursorInfo = { 0 };

	//获取控制台光标信息
	GetConsoleCursorInfo(handle, &CursorInfo);
	
	//修改光标信息
	CursorInfo.bVisible=false;//隐藏光标
	
	//设置光标信息
	SetConsoleCursorInfo(handle,&CursorInfo);

1.8 SetConsoleCursorPosition(设置光标当前位置)

通过SetConsoleCursorPosition函数可以设置光标当前的坐标位置,有了这个函数,我们就可以在任意坐标打印内容,而不是从原点开始一直打印.
函数原型:

BOOL SetConsoleCursorPosition(HANDLE hConsoleOutput,COORD  dwCursorPosition);

第一个参数是句柄,第二个参数是COORD结构体
运用实例:

//封装成设置光标当前位置的函数,x,y表示坐标
void SetPos(short x, short y)
{
	COORD pos = { x, y };

	//获取句柄
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	
	//设置标准输出上光标的位置为pos
	SetConsoleCursorPosition(handle, pos);
}

1.9 GetAsyncKeyState(获取按键情况)

GetAsyncKeyState函数可以获取按键情况,它可以检测程序运行时某个按键是否被按过,某个按键是否正在被按下或者抬起.
函数原型:

SHORT GetAsyncKeyState(int vKey);

参数vKey指的是键盘上按键的虚拟键值,返回值是一个16位的short类型的数.如果这个数最高位是1,表示该按键当前状态是按下,如果是0表示当前状态是抬起;如果这个数最低位是1,表示该按键被按过,如果是0表示未按过.
运用实例:

//定义一个宏判断某个键是否被按过
#define CHECK_KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1)?1:0)

1.10 宽字符

游戏的墙体■,蛇身●,食物★等字符,都是宽字符.宽字符占2个字节,普通字符占一个字节.

1.11 头文件<local.h>与setlocale函数

在不同国家和地区,对时间的表示方式可能不同,对金钱的表示也有不同($和¥).有时我们想打印¥符号或者其他中文字符和宽字符时,显示的却是问号或者乱码.为了使程序能够在不同地区适应,因此有了setlocale函数.它可以改变程序运行时的地区
函数原型:

char* setlocale (int category, const char* locale);

切换到本地环境的方法:

setlocale(LC_ALL, "");

使用setlocale函数需要包含头文件local.h
在屏幕上打印宽字符的方法:

#include<stdio.h>
#include<locale.h>
int main()
{
	setlocale(LC_ALL, "");
	wprintf(L"%lc", L'■');
	return 0;
}

2.游戏设计

2.1地图设计

由于墙体和蛇,食物都是宽字符,一个宽字符横坐标占2个,纵坐标占一个.所以我们可以设计一个58(列)x27(行)的地图,在地图最边缘四周打印字符■表示墙.

2. 2 蛇和食物

蛇头我们用◆表示,身体我们用●表示,食物用★表示.使用单链表的数据结构将每个结点链接起来,每个结点保存了该结点的横纵坐标和下一个结点的地址.游戏开始时蛇的长度是5,食物在地图内随机生成,每个食物的类型和蛇身结点的类型一致,方便蛇吃食物后的连接.

2.3 游戏各属性的维护

属性包括蛇移动方向,蛇的移动速度,单个食物的分数,累计分数,游戏状态(正常运行,正常退出,撞墙,撞到自己),还有蛇头结点,食物.这些信息我们用一个结构体保存

3.逻辑实现

3.1 类型声明

蛇身结点

//一个蛇身的结点
typedef struct SnakeNode
{
	int x;//结点横坐标
	int y;//结点纵坐标
	struct SnakeNode* next;//指向下一个结点的指针
}SnakeNode;
typedef struct SnakeNode* pSnakeNode;//指向SnakeNode的指针类型

游戏属性的维护

typedef struct Snake
{
	pSnakeNode SnakeHead;//蛇头结点
	pSnakeNode Food;//指向食物的指针
	int Score;//游戏总分
	int FoodScore;//一个食物的分数
	int Speed;//蛇的速度(休眠时间)
	enum Status status;//游戏当前状态
	enum Direction dir;//蛇的移动方向
}Snake;

//指向snake类型的指针
typedef struct Snake* pSnake;

蛇的移动方向,使用枚举类型

//蛇当前移动方向
enum Direction
{
	//上下左右
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

游戏状态,使用枚举类型

//游戏状态
enum Status
{
	OK = 1,//正常运行
	EXIT,//退出
	KillByWall,//撞墙
	KillBySelf//撞到自己
};

蛇结点,食物,墙体

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

3.2 主程序

主程序分为3个模块:1.GameStart游戏初始化2.GameRun游戏运行时的逻辑 3.GameEnd游戏结束的工作
GameStart函数的功能:进行游戏的初始化工作,包括:设置控制台的大小,设置控制台名称,打印欢迎界面,打印地图,初始化蛇的移动方向/速度,总分数,单个食物分数,游戏运行的状态等
GameRun函数的功能:实现蛇移动,改变移动方向,按键检测等功能
GameEnd的功能:游戏结束后打印提示信息,释放动态开辟的内存空间
在程序开始前,还需要适配本地环境,防止打印时出现乱码,为了使游戏结束后还能继续选择开始游戏,我们使用do while循环来实现

int main()
{
	//适配本地环境
	setlocale(LC_ALL, "");
	int ch = 0;
	do
	{
		Snake snake = { 0 };//创建贪吃蛇
		GameStart(&snake);//游戏初始化
		GameRun(&snake);//游戏运行时的逻辑
		GameEnd(&snake);//游戏结束后的工作
		SetPos(20, 15);
		printf("输入Y.重新开始  N.退出");
		ch = getchar();
		getchar();
		
	} while (ch=='Y'||ch=='y');
	SetPos(0,27);
	return 0;
}

3.3 地图绘制

在这里插入图片描述
绘制逻辑很简单,最上面一行从(0,0)坐标开始打印■字符.最下面一行从(0,25)坐标开始打印.最左边,横坐标都是0,纵坐标从0开始,每打印一次纵坐标加1.最右边同理.
每次打印前都需要定位一次坐标,为了避免代码重复,坐标定位可以封装成函数:

//控制台坐标定位
void SetPos(int x, int y)
{
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//获取句柄
	//根据句柄设置光标坐标
	COORD pos = { x,y };
	SetConsoleCursorPosition(handle, pos);
}

绘制地图函数:

//绘制地图
void DrawMap()
{
	//上
	SetPos(0, 0);
	for (int i = 0; i <= 56; i+=2)
	{
		wprintf(L"%lc", WALL);
	}
	//下
	SetPos(0, 25);
	for (int i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//左
	for (int i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}

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

}

3.4 欢迎界面及帮助信息

直接定位坐标,然后打印内容

void Welcome()
{
	//欢迎信息
	SetPos(40, 15);
	printf("欢迎来到贪吃蛇小游戏\n");
	SetPos(40, 20);
	system("pause");
	system("cls");
	SetPos(35, 10);
	printf("用↑↓←→控制移动方向,F3加速,F4减速");
	SetPos(35, 11);
	printf("加速得到更多分");
	SetPos(40, 20);
	system("pause");
	system("cls");
}
//打印帮助信息
void PrintHelpInfo()
{
	SetPos(65, 13);
	printf("↑↓←→控制方向,空格暂停,ESC退出,F3加速,F4减速");
	SetPos(65, 17);
	printf("版权@vampire-wpre");
}

3.5 初始化蛇

首先创建5个蛇身结点,设置游戏开始时蛇的位置,设置游戏初始属性

void InitSnake(pSnake ps)
{
	//创建5个初始蛇身结点
	pSnakeNode cur = NULL;
	for (int i = 0; i < 5; i ++ )
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		assert(cur);
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;
		cur->next = NULL;
		if (ps->SnakeHead == NULL)
		{
			ps->SnakeHead = cur;
		}
		else
		{
			//头插法
			cur->next = ps->SnakeHead;
			ps->SnakeHead = cur;
		}
	}

	//打印初始蛇
	cur = ps->SnakeHead;
	SetPos(cur->x, cur->y);
	wprintf(L"%lc", L'◆');
	cur = cur->next;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	
	//其他属性
	ps->dir = RIGHT;
	ps->Food = NULL;
	ps->FoodScore = 10;
	ps->Score = 0;
	ps->Speed = 200;
	ps->status = OK;
}

3.6 生成食物

使用rand函数随机生成食物坐标,食物的坐标必须在地图范围内,并且不能和蛇的位置重合,生成好食物后在对应位置打印食物.因为食物也是宽字符,所以食物的横坐标值必须是2的倍数

//创建食物
void CreatFood(pSnake ps)
{
	//坐标是随机出现的,坐标不能在蛇身体和墙上
	int x = 0;
	int y = 0;	
	srand((unsigned int)time(NULL));//设置随机数种子
reset:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 24 + 1;
	} while (x % 2 != 0);//如果横坐标不是2倍数则重新生成坐标

	pSnakeNode cur = ps->SnakeHead;
	while (cur)//循环遍历蛇的每一个结点
	{
		//如果坐标与蛇重合,使用goto语句重新生成坐标
		if (x == cur->x && y == cur->y)
		{
			goto reset;
		}
		cur = cur->next;
	}

	pSnakeNode pfood = (SnakeNode*)malloc(sizeof(SnakeNode));
	if (pfood == NULL)
	{
		return;
	}
	//设置坐标
	pfood->x = x;
	pfood->y = y;
	ps->Food = pfood;
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
}

3.7 碰撞检测

逻辑很简单,撞墙只需要判断蛇头坐标是否和墙的坐标重合;撞自己则判断蛇头坐标和蛇身每个结点的坐标是否重合,此时需要循环遍历蛇身.

//检测是否撞墙
void CheckKillByWall(pSnake ps)
{
	if ((ps->SnakeHead->x == 0 || ps->SnakeHead->x == 56) || (ps->SnakeHead->y == 0 || ps->SnakeHead->y == 25))
	{
		ps->status = KillByWall;
	}
}

//检测是否撞自己
void CheckKillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->SnakeHead->next;
	while (cur)
	{
		if (cur->x == ps->SnakeHead->x && cur->y == ps->SnakeHead->y)
		{
			ps->status = KillBySelf;
			return;
		}
		cur = cur->next;
	}
}

3.8 蛇的移动

蛇移动分为以下几种情况:
1.蛇直走:
此时,直接改变蛇头的坐标即可.根据蛇的属性(status)判断蛇的方向,如果是向上,蛇头的横坐标不变,向上走一步纵坐标的值就-1;如果是向下,蛇头横坐标不变,向上走一步纵坐标的值+1;如果是向左,蛇头纵坐标不变,向左走一步横坐标-2(宽字符占2个位置);如果是向右,纵坐标不变,向右走一步横坐标+2.
2.蛇转弯:
先定义一个临时结点,该结点的位置在蛇头下一步的位置.
判断蛇头在下一步的位置有没有食物,如果有就吃食物,吃完食物释放掉旧的食物并且重新生成新的食物对应总分也增加,头插法将这个食物结点插入蛇身,然后改变蛇头结点(SnakeHead)的指向;如果不是食物,也将该结点头插,改变蛇头结点(SnakeHead)的指向,接着将最后一个结点删除,并且在最后一个结点的位置打印空格.
我们定义IsFood函数判断下一个位置有没有食物,Eat函数实现吃食物的逻辑,Normal函数实现下一个位置不是食物进行的操作,与此同时还要检测一下有没有撞墙和撞自己.

//吃食物
void Eat(pSnake ps, pSnakeNode next)
{
	next->next = ps->SnakeHead;
	ps->SnakeHead = next;
	//打印蛇
	pSnakeNode cur = ps->SnakeHead;
	SetPos(cur->x, cur->y);
	wprintf(L"%lc", L'◆');
	cur = cur->next;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//增加总分
	ps->Score += ps->FoodScore;
	//free旧的食物
	free(ps->Food);
	//新建食物
	CreatFood(ps);
}
//下一个位置不是食物,正常移动
void Normal(pSnake ps, pSnakeNode next)
{
	//头插法插入next
	next->next = ps->SnakeHead;
	ps->SnakeHead = next;
	//释放尾结点
	pSnakeNode cur = ps->SnakeHead;
	//循环找尾结点的前一个结点
	while (cur->next->next != NULL)
	{
		cur = cur->next;
	}
	//尾结点位置打印成空格
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	//释放尾结点
	free(cur->next);
	cur->next = NULL;

	//打印蛇身
	cur = ps->SnakeHead;
	SetPos(cur->x, cur->y);
	wprintf(L"%lc", L'◆');
	cur = cur->next;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
}
//蛇移动
void Move(pSnake ps)
{
	pSnakeNode next = (pSnakeNode)malloc(sizeof(SnakeNode));//方向改变时,蛇头在下一个坐标的位置,用next结点保存
	if (next == NULL)
	{
		return;
	}
	next->next = NULL;
	switch (ps->dir)
	{

	case UP:
		next->x = ps->SnakeHead->x;
		next->y = ps->SnakeHead->y - 1;
		break;
	case DOWN:
		next->x = ps->SnakeHead->x;
		next->y = ps->SnakeHead->y + 1;
		break;
	case LEFT:
		next->x = ps->SnakeHead->x - 2;
		next->y = ps->SnakeHead->y;
		break;
	case RIGHT:
		next->x = ps->SnakeHead->x + 2;
		next->y = ps->SnakeHead->y;
		break;
	}

	//下一个坐标处是否为食物:
	if (IsFood(ps, next))
	{
		//是食物就吃掉
		Eat(ps,next);
	}
	else
	{
		//不是食物就正常移动
		Normal(ps, next);
	}
	//检测是否撞墙
	CheckKillByWall(ps);

	//检测是否撞自己
	CheckKillBySelf(ps);
}

3.9游戏运行逻辑

使用do while循环,循环条件是游戏状态,如果游戏状态不是OK则游戏结束.游戏运行时,打印对应的帮助信息,同时进行按键检测,如果某个按键被按了,就修改对应的属性.蛇的移动本质上是:蛇移动一步,系统休眠一段时间,休眠时间越短,蛇移动的越快,休眠时间越长,蛇移动的越慢.

//游戏运行逻辑
void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();
	do
	{
		//当前分数情况:
		SetPos(65, 10);
		printf("总分:%02d\n", ps->Score);
		SetPos(65, 11);
		printf("每个食物分数:%5d\n", ps->FoodScore);
		
		//按键监测↑ ↓ ← → ESC 空格 F3加速 F4减速
		if (CHECK_KEY_PRESS(VK_UP) && ps->dir != DOWN)//按上键并且当前方向不是向下
		{
			ps->dir = UP;
		}
		else if (CHECK_KEY_PRESS(VK_DOWN) && ps->dir != UP)//按下键并且当前方向不是向上
		{
			ps->dir = DOWN;
		}
		else if (CHECK_KEY_PRESS(VK_LEFT) && ps->dir != RIGHT)//按左键并且当前方向不是向右
		{
			ps->dir = LEFT;
		}
		else if (CHECK_KEY_PRESS(VK_RIGHT) && ps->dir != LEFT)//按右并且当前方向不是向左
		{
			ps->dir = RIGHT;
		}
		else if (CHECK_KEY_PRESS(VK_ESCAPE))//按ESC退出
		{
			ps->status = EXIT;
			break;
		}
		else if (CHECK_KEY_PRESS(VK_SPACE))//按空格暂停,再按一次继续游戏
		{
			while (1)
			{
				Sleep(100);
				if (CHECK_KEY_PRESS(VK_SPACE))
				{
					break;
				}
			}
		}
		else if (CHECK_KEY_PRESS(VK_F3))//按F3加速,休眠时间变短,食物分数变大
		{
			if (ps->Speed >= 40)//最小速度为40
			{
				ps->Speed -= 20;
				ps->FoodScore += 2;
			}
			
		}
		else if (CHECK_KEY_PRESS(VK_F4))//按F4减速,休眠时间变长,食物分数变小
		{
			if (ps->FoodScore > 4)
			{
				ps->Speed += 20;
				ps->FoodScore -= 2;
			}
		}
		//休眠一下
		Sleep(ps->Speed);
		//蛇移动的逻辑
		Move(ps);
	} while (ps->status==OK);
}

3.10 结束工作

游戏结束后,打印结束的原因(撞墙/撞自己/主动退出),同时释放malloc开辟的空间(蛇的每个结点/食物)

void GameEnd(pSnake ps)
{
	switch (ps->status)
	{
	case EXIT:
		SetPos(24, 12);
		printf("主动退出");
		break;
	case KillByWall:
		SetPos(24, 12);
		printf("撞墙了!");
		break;
	case KillBySelf:
		SetPos(24, 12);
		printf("撞到自己了!");
		break;
	}

	//释放malloc开辟的空间
	pSnakeNode cur = ps->SnakeHead;
	pSnakeNode del = NULL;
	while (cur)
	{
		del = cur;
		cur = cur->next;
		free(del);
	}
	free(ps->Food);
	ps->Food = NULL;
	ps = NULL;
}

4.完整代码

Game.h文件:

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1

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

#define FOOD L'★' //食物
#define BODY L'●' //身体
#define WALL L'■' //墙

//按键监测
#define CHECK_KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1)?1:0)

//游戏开始时,蛇起始位置坐标
#define POS_X 24
#define POS_Y 5

//一个蛇身的结点
typedef struct SnakeNode
{
	int x;//结点横坐标
	int y;//结点纵坐标
	struct SnakeNode* next;//指向下一个结点的指针
}SnakeNode;
typedef struct SnakeNode* pSnakeNode;//指向SnakeNode的指针类型

//游戏状态
enum Status
{
	OK = 1,//正常运行
	EXIT,//退出
	KillByWall,//撞墙
	KillBySelf//撞到自己
};

//蛇当前移动方向
enum Direction
{
	//上下左右
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

//贪吃蛇(整个游戏的维护)
typedef struct Snake
{
	pSnakeNode SnakeHead;//蛇头结点
	pSnakeNode Food;//指向食物的指针
	int Score;//游戏总分
	int FoodScore;//一个食物的分数
	int Speed;//蛇的速度(休眠时间)
	enum Status status;//游戏当前状态
	enum Direction dir;//蛇的移动方向
}Snake;

//指向snake类型的指针
typedef struct Snake* pSnake;

//相关函数
void GameStart(pSnake ps);//游戏初始化

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

void DrawMap();//绘制地图

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

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

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

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

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

int IsFood(pSnake ps, pSnakeNode next);//判断下一步位置处是否为食物

void Eat(pSnake ps, pSnakeNode next);//吃食物

void Normal(pSnake ps, pSnakeNode next);//正常移动

void CheckKillByWall(pSnake ps);//检测是否撞墙

void CheckKillBySelf(pSnake ps);//检测是否撞自己

void GameEnd(pSnake ps);//游戏结束后的善后工作

void SetPos(int x, int y);//控制台光标定位

Game.c文件:

#include"Game.h"

//控制台光标定位
void SetPos(int x, int y)
{
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//获取句柄
	//根据句柄设置光标坐标
	COORD pos = { x,y };
	SetConsoleCursorPosition(handle, pos);
}

//绘制地图
void DrawMap()
{
	//上
	SetPos(0, 0);
	for (int i = 0; i <= 56; i+=2)
	{
		wprintf(L"%lc", WALL);
	}
	//下
	SetPos(0, 25);
	for (int i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//左
	for (int i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}

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

}

//打印欢迎界面
void Welcome()
{
	//欢迎信息
	SetPos(40, 15);
	printf("欢迎来到贪吃蛇小游戏\n");
	SetPos(40, 20);
	system("pause");

	system("cls");
	SetPos(35, 10);
	printf("用↑↓←→控制移动方向,F3加速,F4减速");
	SetPos(35, 11);
	printf("加速得到更多分");
	SetPos(40, 20);
	system("pause");
	system("cls");
}

//打印版权信息
void PrintHelpInfo()
{
	SetPos(65, 16);
	printf("版权@vampire-wpre");
	
}

//创建食物
void CreatFood(pSnake ps)
{
	//坐标是随机出现的,坐标不能在蛇身体和墙上
	int x = 0;
	int y = 0;	

	srand((unsigned int)time(NULL));//设置随机数种子
reset:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 24 + 1;
	} while (x % 2 != 0);

	pSnakeNode cur = ps->SnakeHead;
	while (cur)
	{
		if (x == cur->x && y == cur->y)
		{
			goto reset;
		}
		cur = cur->next;
	}

	pSnakeNode pfood = (SnakeNode*)malloc(sizeof(SnakeNode));
	if (pfood == NULL)
	{
		return;
	}
	//设置坐标
	pfood->x = x;
	pfood->y = y;
	ps->Food = pfood;
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
}

//判断下一个位置是否为食物
int IsFood(pSnake ps, pSnakeNode next)
{
	if (ps->Food->x == next->x && ps->Food->y == next->y)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}

//检测是否撞墙
void CheckKillByWall(pSnake ps)
{
	if ((ps->SnakeHead->x == 0 || ps->SnakeHead->x == 56) || (ps->SnakeHead->y == 0 || ps->SnakeHead->y == 25))
	{
		ps->status = KillByWall;
	}
}

//检测是否撞自己
void CheckKillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->SnakeHead->next;
	while (cur)
	{
		if (cur->x == ps->SnakeHead->x && cur->y == ps->SnakeHead->y)
		{
			ps->status = KillBySelf;
			return;
		}
		cur = cur->next;
	}
}

//初始化蛇
void InitSnake(pSnake ps)
{
	//创建5个初始蛇身结点
	pSnakeNode cur = NULL;
	for (int i = 0; i < 5; i ++ )
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		assert(cur);
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;
		cur->next = NULL;
		if (ps->SnakeHead == NULL)
		{
			ps->SnakeHead = cur;
		}
		else
		{
			//头插
			cur->next = ps->SnakeHead;
			ps->SnakeHead = cur;
		}
	}

	//打印初始蛇
	cur = ps->SnakeHead;
	SetPos(cur->x, cur->y);
	wprintf(L"%lc", L'◆');
	cur = cur->next;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	
	//其他属性
	ps->dir = RIGHT;
	ps->Food = NULL;
	ps->FoodScore = 10;
	ps->Score = 0;
	ps->Speed = 200;
	ps->status = OK;
}

//吃食物
void Eat(pSnake ps, pSnakeNode next)
{
	next->next = ps->SnakeHead;
	ps->SnakeHead = next;
	
	//打印蛇
	pSnakeNode cur = ps->SnakeHead;
	SetPos(cur->x, cur->y);
	wprintf(L"%lc", L'◆');
	cur = cur->next;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	ps->Score += ps->FoodScore;
	
	//free旧的食物
	free(ps->Food);
	//新建食物
	CreatFood(ps);
}

//下一个位置不是食物,正常移动
void Normal(pSnake ps, pSnakeNode next)
{
	//头插法插入next
	next->next = ps->SnakeHead;
	ps->SnakeHead = next;


	//释放尾结点
	pSnakeNode cur = ps->SnakeHead;
	while (cur->next->next != NULL)
	{
		cur = cur->next;
	}
	//尾结点位置打印成空格
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	//释放尾结点
	free(cur->next);
	cur->next = NULL;

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

//蛇移动
void Move(pSnake ps)
{
	pSnakeNode next = (pSnakeNode)malloc(sizeof(SnakeNode));//方向改变时,蛇头在下一个坐标的位置,用next结点保存
	if (next == NULL)
	{
		return;
	}
	next->next = NULL;
	switch (ps->dir)
	{

	case UP:
		next->x = ps->SnakeHead->x;
		next->y = ps->SnakeHead->y - 1;
		break;
	case DOWN:
		next->x = ps->SnakeHead->x;
		next->y = ps->SnakeHead->y + 1;
		break;
	case LEFT:
		next->x = ps->SnakeHead->x - 2;
		next->y = ps->SnakeHead->y;
		break;
	case RIGHT:
		next->x = ps->SnakeHead->x + 2;
		next->y = ps->SnakeHead->y;
		break;
	}

	//下一个坐标处是否为食物:
	if (IsFood(ps, next))
	{
		//是食物就吃掉
		Eat(ps,next);
	}
	else
	{
		//不是食物就正常
		Normal(ps, next);
	}
	//检测是否撞墙
	CheckKillByWall(ps);

	//检测是否撞自己
	CheckKillBySelf(ps);
	

}

//游戏初始化
void GameStart(pSnake ps)
{
	//设置控制台属性
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

	//隐藏光标
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//获取控制台句柄

	CONSOLE_CURSOR_INFO cur_info;
	GetConsoleCursorInfo(handle, &cur_info);//获得控制台光标的信息

	cur_info.bVisible = false;
	SetConsoleCursorInfo(handle, &cur_info);//设置

	//打印欢迎信息
	Welcome();
	
	//地图绘制
	DrawMap();

	//初始化蛇
	InitSnake(ps);
	
	//创建食物
	CreatFood(ps);
}

//游戏运行逻辑
void GameRun(pSnake ps)
{

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

	do
	{
		//当前分数情况:
		SetPos(65, 10);
		printf("总分:%02d\n", ps->Score);
		SetPos(65, 11);
		printf("每个食物分数:%5d\n", ps->FoodScore);
		
		//按键监测↑ ↓ ← → ESC 空格 F3加速 F4减速
		if (CHECK_KEY_PRESS(VK_UP) && ps->dir != DOWN)//按上键并且当前方向不是向下
		{
			ps->dir = UP;
		}
		else if (CHECK_KEY_PRESS(VK_DOWN) && ps->dir != UP)//按下键并且当前方向不是向上
		{
			ps->dir = DOWN;
		}
		else if (CHECK_KEY_PRESS(VK_LEFT) && ps->dir != RIGHT)//按左键并且当前方向不是向右
		{
			ps->dir = LEFT;
		}
		else if (CHECK_KEY_PRESS(VK_RIGHT) && ps->dir != LEFT)//按右并且当前方向不是向左
		{
			ps->dir = RIGHT;
		}
		else if (CHECK_KEY_PRESS(VK_ESCAPE))//按ESC退出
		{
			ps->status = EXIT;
			break;
		}
		else if (CHECK_KEY_PRESS(VK_SPACE))//按空格暂停,再按一次继续游戏
		{
			while (1)
			{
				Sleep(100);
				if (CHECK_KEY_PRESS(VK_SPACE))
				{
					break;
				}
			}
		}
		else if (CHECK_KEY_PRESS(VK_F3))//按F3加速,休眠时间变短,食物分数变大
		{
			if (ps->Speed >= 40)//最小速度为40
			{
				ps->Speed -= 20;
				ps->FoodScore += 2;
			}
			
		}
		else if (CHECK_KEY_PRESS(VK_F4))//按F4减速,休眠时间变长,食物分数变小
		{
			if (ps->FoodScore > 4)
			{
				ps->Speed += 20;
				ps->FoodScore -= 2;
			}
		}

		//休眠一下
		Sleep(ps->Speed);

		//走一步
		Move(ps);
		//蛇移动的逻辑
		
		
	} while (ps->status==OK);

	
}

//游戏善后工作
void GameEnd(pSnake ps)
{
	switch (ps->status)
	{
	case EXIT:
		SetPos(24, 12);
		printf("主动退出");
		break;
	case KillByWall:
		SetPos(24, 12);
		printf("撞墙了!");
		break;
	case KillBySelf:
		SetPos(24, 12);
		printf("撞到自己了!");
		break;
	}

	//释放malloc开辟的空间
	pSnakeNode cur = ps->SnakeHead;
	pSnakeNode del = NULL;
	while (cur)
	{
		del = cur;
		cur = cur->next;
		free(del);
	}
	free(ps->Food);
	ps->Food = NULL;
	ps = NULL;
}

test.c文件:

#include"Game.h"

void test()
{
	int ch = 0;
	do
	{
		Snake snake = { 0 };//创建贪吃蛇
		GameStart(&snake);//游戏初始化
		GameRun(&snake);//游戏运行时的逻辑
		GameEnd(&snake);//游戏结束后的善后工作
		SetPos(20, 15);
		printf("输入Y.重新开始  N.退出");
		ch = getchar();
		getchar();
		
	} while (ch=='Y'||ch=='y');
	
}

int main()
{
	//适配本地环境
	setlocale(LC_ALL, "");
	test();
	SetPos(0,27);
	return 0;
}
  • 23
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 以下是一个简单的贪吃蛇游戏的 C 语言程序代码: ```c #include <stdio.h> #include <stdlib.h> #include <conio.h> #include <windows.h> #define MAX_LENGTH 100 int width = 20; // 游戏窗口宽度 int height = 20; // 游戏窗口高度 int x, y; // 蛇头坐标 int food_x, food_y; // 食物坐标 int score; // 得分 int tail_x[MAX_LENGTH], tail_y[MAX_LENGTH]; // 蛇身坐标 int tail_length; // 蛇身长度 enum direction {STOP = 0, LEFT, RIGHT, UP, DOWN}; // 方向控制 enum direction dir; void init() { x = width / 2; y = height / 2; food_x = rand() % width; food_y = rand() % height; score = 0; dir = STOP; } void draw() { system("cls"); for (int i = 0; i < width+2; i++) printf("#"); printf("\n"); for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { if (j == 0) printf("#"); if (i == y && j == x) printf("O"); else if (i == food_y && j == food_x) printf("F"); else { int flag = 0; for (int k = 0; k < tail_length; k++) { if (tail_x[k] == j && tail_y[k] == i) { printf("o"); flag = 1; } } if (!flag) printf(" "); } if (j == width - 1) printf("#"); } printf("\n"); } for (int i = 0; i < width+2; i++) printf("#"); printf("\n"); printf("Score: %d\n", score); } void input() { if (kbhit()) { switch (getch()) { case 'a': dir = LEFT; break; case 'd': dir = RIGHT; break; case 'w': dir = UP; break; case 's': dir = DOWN; break; case 'x': dir = STOP; break; case 'q': exit(0); break; } } } void logic() { int prev_x = tail_x[0], prev_y = tail_y[0]; int prev2_x, prev2_y; tail_x[0] = x; tail_y[0] = y; for (int i = 1; i < tail_length; i++) { prev2_x = tail_x[i]; prev2_y = tail_y[i]; tail_x[i] = prev_x; tail_y[i] = prev_y; prev_x = prev2_x; prev_y = prev2_y; } switch (dir) { case LEFT: x--; break; case RIGHT: x++; break; case UP: y--; break; case DOWN: y++; break; } if (x >= width) x = 0; else if (x < 0) x = width - 1; if (y >= height) y = 0; else if (y < 0) y = height - 1; for (int i = 0; i < tail_length; i++) { if (tail_x[i] == x && tail_y[i] == y) { score = 0; tail_length = 0; } } if (x == food_x && y == food_y) { score += 10; food_x = rand() % width; food_y = rand() % height; tail_length++; } } int main() { init(); while (1) { draw(); input(); logic(); Sleep(100); } return 0; } ``` 这个程序使用了 Windows.h 头文件,所以只能在 Windows 系统上运行。如果需要在其他系统上运行,需要将 `system("cls")` 改为相应的清屏命令。 ### 回答2: 贪吃蛇是一款经典的游戏,一般由一个蛇和苹果组成。游戏目标是控制蛇吃苹果,每吃到一个苹果蛇身长度增加一节。如果蛇头撞到蛇身或者撞墙,则游戏结束。 以下是一个简单的贪吃蛇C语言程序代码: ```c #include <stdio.h> #include <conio.h> #include <windows.h> #define WIDTH 20 #define HEIGHT 15 int score; int gameOver; int x, y; // 蛇头的坐标 int fruitX, fruitY; // 苹果的坐标 int tailX[100], tailY[100]; // 蛇身的坐标 int tailLength; enum eDirection { STOP = 0, LEFT, RIGHT, UP, DOWN }; enum eDirection dir; void Setup() { gameOver = 0; dir = STOP; x = WIDTH / 2; y = HEIGHT / 2; fruitX = rand() % WIDTH; fruitY = rand() % HEIGHT; score = 0; } void Draw() { system("cls"); for (int i = 0; i < WIDTH + 2; i++) printf("#"); printf("\n"); for (int i = 0; i < HEIGHT; i++) { for (int j = 0; j < WIDTH; j++) { if (j == 0) printf("#"); if (i == y && j == x) printf("O"); else if (i == fruitY && j == fruitX) printf("F"); else { int printTail = 0; for (int k = 0; k < tailLength; k++) { if (tailX[k] == j && tailY[k] == i) { printf("o"); printTail = 1; } } if (!printTail) printf(" "); } if (j == WIDTH - 1) printf("#"); } printf("\n"); } for (int i = 0; i < WIDTH + 2; i++) printf("#"); printf("\n"); printf("Score: %d\n", score); } void Input() { if (_kbhit()) { switch (_getch()) { case 'a': dir = LEFT; break; case 'd': dir = RIGHT; break; case 'w': dir = UP; break; case 's': dir = DOWN; break; case 'x': gameOver = 1; break; } } void Logic() { int prevX = tailX[0]; int prevY = tailY[0]; int prev2X, prev2Y; tailX[0] = x; tailY[0] = y; for (int i = 1; i < tailLength; i++) { prev2X = tailX[i]; prev2Y = tailY[i]; tailX[i] = prevX; tailY[i] = prevY; prevX = prev2X; prevY = prev2Y; } switch (dir) { case LEFT: x--; break; case RIGHT: x++; break; case UP: y--; break; case DOWN: y++; break; } if (x >= WIDTH) x = 0; else if (x < 0) x = WIDTH - 1; if (y >= HEIGHT) y = 0; else if (y < 0) y = HEIGHT - 1; for (int i = 0; i < tailLength; i++) { if (tailX[i] == x && tailY[i] == y) { gameOver = 1; break; } } if (x == fruitX && y == fruitY) { score += 10; fruitX = rand() % WIDTH; fruitY = rand() % HEIGHT; tailLength++; } } int main() { Setup(); while (!gameOver) { Draw(); Input(); Logic(); Sleep(10); // 控制帧率 } return 0; } ``` 这个C语言程序通过控制台绘制了一个贪吃蛇游戏界面。可以使用键盘的ADWS键来控制蛇的移动方向,X键用来退出游戏。贪吃蛇移动时,每吃到一个苹果分数增加10分,同时长度也会增加一节。如果蛇头撞到蛇身或者撞墙,则游戏结束。 ### 回答3: 贪吃蛇是一款经典的游戏,在C语言中编写贪吃蛇的程序代码相当有趣。 首先,我们需要定义一些常量,如窗口宽度、高度、蛇身长度等。然后,我们需要定义蛇的结构体,包括蛇头位置、蛇身数组、蛇的长度等成员变量。 接下来,我们需要实现一些基本的功能函数,如初始化蛇头位置、绘制蛇身、处理键盘输入等。在每次游戏循环中,我们需要不断更新蛇的位置,并处理碰撞事件,如蛇头与边界、自身以及食物的碰撞。 当蛇头与食物碰撞时,蛇的长度增加,然后随机生成一个新的食物位置。同时,我们还需要实现一个函数来判断游戏是否结束,即蛇头与边界或自身碰撞。 在游戏主循环中,我们通过不断更新蛇的位置,并根据用户的输入来改变蛇的方向。通过不断刷新屏幕来实现动画效果,让蛇在窗口中移动。 最后,在主函数中调用以上函数,并通过一个循环来控制游戏的进行。当游戏结束时,展示最终得分,并询问玩家是否重新开始游戏。 总之,贪吃蛇C语言程序代码实现起来并不复杂,主要涉及蛇的移动、碰撞检测以及界面的绘制等基本操作。有了上述的框架和思路,我们就可以编写出一个简单而有趣的贪吃蛇游戏了。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值