C语言经典游戏贪吃蛇

 游戏背景

贪吃蛇是久负盛名的游戏,它也和俄罗斯⽅块,扫雷等游戏位列经典游戏的⾏列。 在编程语⾔的教学中,我们以贪吃蛇为例,从设计到代码实现来提升学⽣的编程能⼒和逻辑能⼒。

游戏效果演示

 实现基本的功能

• 贪吃蛇地图绘制

• 蛇吃⻝物的功能(上、下、左、右⽅向键控制蛇的动作)

• 蛇撞墙死亡

• 蛇撞⾃⾝死亡

• 计算得分

• 蛇⾝加速、减速

• 暂停游戏

技术要点

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

Win32 API介绍

Windows这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外,它同时也是⼀个很⼤ 的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程序达到开启 视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application),所以便 称之为ApplicationProgrammingInterface,简称API函数。WIN32API也就是MicrosoftWindows 32位平台的应⽤程序编程接⼝。

由于Win32API内容过多,本篇在此不过多介绍,有兴趣的读者可之后自行去查找,本篇只介绍贪吃蛇需要用到的部分函数。

控制台屏幕上的坐标COORD

 COORD是WindowsAPI中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0)的原点位于缓冲区的顶部左侧单元格。

控制台屏幕坐标如下图所示:

COORD的作用通俗来说就是改变在控制台屏幕上的光标位置。

使用方法

COORD pos = { x,y };//设置光标位置

但要成功改变光标的位置,我们首先要获取控制台屏幕的句柄。

获取句柄GetStdHandle

句柄的类型名为HANDLE,使用方法为

HANDLE put = GetStdHandle(STD_OUTPUT_HANDLE);//标准输出设备(屏幕缓冲区),获得句柄,总之获得句柄就可以操作控制台 

总而言之,获取句柄就能改变控制台屏幕的信息。

改变光标位置SetConsoleCursorPosition

当使用COORD设置完光标信息后,必须使用SetConsoleCursorPosition函数将COORD的信息存放进句柄中才能正确生效。

	SetConsoleCursorPosition(put, pos);//改变光标位置

获取光标信息函数CONSOLE_CURSOR_INFO

CONSOLE_CURSOR_INFO cursur = { 0 };

 创建一个包含光标信息的结构体变量。

改变光标信息GetConsoleCursorInfo

检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息

光标信息生效函数SetConsoleCursorInfo

 设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性。

在改变完光标信息之后,必须调用此函数才可生效。

获取按键信息函数GetAsyncKeyState

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

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

此时我们可以设定一个宏来方便检测按键信息。

#define KEY_PRESS(vk)  ((GetAsyncKeyState(vk)&1)?1:0)

正篇

此时我们就可以正式进入代码环节

头文件

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

贪吃蛇所需的头文件。 

图形定义

这里我们为了方便使用,用宏来简化一些符号

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

蛇身状态和蛇身结点结构体

typedef struct snakenode//蛇身结点
{
	//蛇身坐标
	int x;
	int y;
	//指向下一个结点的指针
	struct snakenode* next;
}snake;

typedef struct SNAKE//贪吃蛇
{
	snake* phead;//指向蛇头的指针
	snake* food;//指向食物的指针
	enum direction dir;//蛇的方向
	enum STATUS status;//蛇的状态
	int onescore;//单个食物的得分
	int score;//总分
	int sleeptime;//休息时间,时间越短,速度越快,时间越长,速度越慢

}sna;

我们创建分别创建两个关于蛇身结点和蛇身状态的结构体,方便进行链表的插入和修改。 

设定光标函数

void GB(short x, short y)//改变光标位置函数 
{
	HANDLE put = GetStdHandle(STD_OUTPUT_HANDLE);//标准输出设备(屏幕缓冲区),获得句柄,总之获得句柄就可以操作控制台 
	//改变光标位置,改完光标位置调用函数才能生效
	COORD pos = { x,y };//设置光标位置
	SetConsoleCursorPosition(put, pos);//改变光标位置
}

我们通过获取句柄来操作控制台屏幕,然后创建一个包含光标信息的结构体,将x和y传入进结构体中,最后再通过SetConsoleCursorPosition函数将pos结构体光标的信息传入句柄之中从而改变光标的位置。 

打印 

void menuone()//初始界面
{
	GB(40, 25);
	wprintf(L"欢迎来到贪吃蛇小游戏");
	GB(40, 28);
	system("pause");//暂停程序
	system("cls");//清理屏幕
	GB(30, 25);
	printf("用箭头来控制上下左右,按F3加速,按F4减速");
	GB(40, 28);
	printf("加速能拿到更高分");
	GB(40, 32);
	system("pause");//暂停程序
	system("cls");
  1. 显示欢迎信息:首先,使用 GB(40, 25); 将光标移动到指定位置(第25行,第40列),然后用 wprintf(L"欢迎来到贪吃蛇小游戏"); 显示欢迎语句。

  2. 程序暂停system("pause"); 执行后,程序将会等待用户按任意键继续。

  3. 清屏system("cls"); 命令被执行后,控制台屏幕上的现有内容将会被清除。

  4. 显示游戏说明:然后,函数再次通过 GB 函数移动光标,使用 printf 在不同的行上打印出游戏的操作说明,如使用箭头键控制蛇的移动,以及按F3加速、按F4减速的信息,和提醒玩家加速可以获得更高的分数。

  5. 再次暂停与清屏:之后,使用另一个 system("pause"); 让玩家有机会阅读这些说明。等待玩家按任意键后,最后使用 system("cls"); 再次清屏,为游戏的开始做准备。

void printgame()//打印游戏中的提示
{
	GB(64, 14);
	wprintf(L"%ls", L"不能穿墙,不能咬到自己");
	GB(64, 15);
	wprintf(L"%ls", L"用 ↑. ↓ . ← . → 来控制蛇的移动");
	GB(64, 16);
	wprintf(L"%ls", L"按F3加速,F4减速");
	GB(64, 17);
	wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
	GB(64, 18);
	wprintf(L"%ls", L"制作");
}

在游戏进行时可在控制台的右侧显示操作信息 

 初始化

void gamestart()//初始化
{
	system("mode con cols=100 lines=50");//设置行和列
	system("title 贪吃蛇");//命名
	HANDLE put = GetStdHandle(STD_OUTPUT_HANDLE);//获得句柄,可修改控制台信息
	CONSOLE_CURSOR_INFO cursur = { 0 };//创建变量,创建一个包含光标信息的结构体变量、
	GetConsoleCursorInfo(put, &cursur);//将光标信息放进cursur,获取和put相关的控制台光标信息,存放进cursur
	//隐藏光标操作
	cursur.bVisible = false;//将光标可见度设为0	
	SetConsoleCursorInfo(put, &cursur);//设置光标信息,改完光标信息调用函数才能生效
	menuone();
	map();
}
  1. 设置控制台大小:通过 system("mode con cols=100 lines=50"); 命令来设置控制台窗口的列数和行数,这里设置的是宽度100字符,高度50行。

  2. 设置控制台标题:使用 system("title 贪吃蛇"); 命令将控制台窗口的标题命名为“贪吃蛇”。

  3. 获取控制台句柄HANDLE put = GetStdHandle(STD_OUTPUT_HANDLE); 这行代码获取标准输出设备的句柄,以便后续修改控制台信息。

  4. 初始化控制台光标信息:定义 CONSOLE_CURSOR_INFO 结构体 cursur 变量,并通过 GetConsoleCursorInfo(put, &cursur); 函数获取当前控制台光标的信息。

  5. 隐藏控制台光标:将 cursur.bVisible 设置为 false,这样光标在控制台就不可见了。然后通过 SetConsoleCursorInfo(put, &cursur); 应用这个设置。

  6. 显示初始菜单:调用 menuone(); 函数向玩家展示游戏的欢迎界面和操作说明。

  7. 绘制游戏地图:最终调用 map(); 函数来绘制游戏中使用的地图。

 

地图

#define WALL L'□'
void map()//地图
{
	int i = 0;
	for (i = 0; i < 29; i++)//上
	{
		
		wprintf(L"%lc", WALL);
	}
	GB(0, 26);
	for (i = 0; i < 29; i++)//下
	{
		wprintf(L"%lc", WALL);
	}
	for (i = 1; i <= 25; i++)//左
	{
		GB(0, i);
		wprintf(L"%lc", WALL);
	}
	for (i = 1; i <= 25; i++)//右
	{
		GB(56, i);
		wprintf(L"%lc", WALL);
	}

}

我们打印墙体是用到的是宽字符,这样我们的地图形状设计才能更加美观,由于普通的字符占用的都是一个字节,打印出的墙体为长方形,此时我们就要用到宽字符打印。而在打印右边的下面的墙体时,我们通过GB函数改变光标的位置,随后再打印墙体。

普通字符和宽字符打印出宽度的展⽰如下:

蛇身

初始化状态,假设蛇的⻓度是5,蛇⾝的每个节点是●,在固定的⼀个坐标处,⽐如(24,5)处开始出现蛇,连续5个节点。 

注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半⼉出现在墙体中, 另外⼀般在墙外的现象,坐标不好对⻬。

void ini(sna* pa)//初始化贪吃蛇
{
	int i = 0;
	snake* new = NULL;
	for (i = 0; i < 5; i++)//创建蛇身
	{
		new = (snake*)malloc(sizeof(snake));//申请空间
		if (new == NULL)
		{
			perror("huang");
			return;
		}
		new->next = NULL;
		new->x = 24 + 2 * i;
		new->y = 5;
		//头插法
		if (pa->phead == NULL)//空链表
		{
			pa->phead = new;
		}
		else//非空链表
		{
			new->next = pa->phead;
			pa->phead = new;

		}
	}
  1. 创建蛇身:在一个 for 循环中,函数连续创建蛇的身体部分。循环变量 i 从 0 开始,它决定了每个蛇身部分在水平方向上的位置。蛇的初始长度被设定为 5 个部分。

  2. 申请空间:对于蛇的每个部分,函数通过调用 malloc 为 snake 结构分配内存。如果内存分配失败,函数会打印错误消息并返回。

  3. 头插法:新创建的蛇身节点通过头插法插入到链表中。头插法是指新的节点始终插入链表的开头,这样新节点就成了新的链表头(pa->phead)。对于第一个节点,由于链表是空的,新节点直接成为链表头。对于后续的节点,它们被插入链表的前端。这样,最后创建的节点成为链表(即蛇的身体)的第一个节点,从而确定了蛇头的位置。

  4. 设置初始坐标:每个新创建的蛇身节点的 x 坐标依次增加,y 坐标保持不变。这确保了蛇初始时呈直线排列。

  5. 绘制蛇身:创建和插入完所有蛇身节点后,函数遍历链表并使用 GB 函数和 wprintf 为每个蛇身节点在游戏界面上绘制对应的符号。

  6. 设置蛇的初始状态:蛇的初始移动方向被设定为向右(RIGHT)。onescore(吃到食物后增加的得分)、score(总分)、sleeptime(控制游戏速度的参数)、status(蛇的状态,比如正常、碰撞等)等都被初始化为特定的值。

 

食物

void food(sna* pa)//生成食物
{
	int x = 0;
	int y = 0;
again:
	do//生成食物,x是2的倍数,如果x不是2的倍数,食物有可能卡住
	{
		x = rand() % 53 + 2;//2-54
		y = rand() % 25 + 1;//1-25
	} while (x % 2 != 0);
	snake* new = pa->phead;
	while (new)
	{
		if (x == new->x && y == new->y)//如果食物与蛇身重合
		{
			goto again;
		}
		new = new->next;
	}

	snake* food = (snake*)malloc(sizeof(snake));
	if (food == NULL)
	{
		perror("food malloc");
		return;
	}
	food->x = x;
	food->y = y;
	food->next = NULL;
	GB(x, y);
	wprintf(L"%lc", FOOD);
	pa->food = food;
}
  1. 初始化两个整数xy用来表示食物在游戏界面上的位置。
  2. 通过循环生成食物的坐标位置,x值在2到54之间,y值在1到25之间。这里特意限制x为2的倍数否则食物可能会“卡住”。循环确保x是2的倍数。
  3. 使用一个while循环遍历蛇的每个节点以检查食物是否生成在蛇的身体上。如果发现食物的位置与蛇身的任何一部分重合,就跳回到标签again,重新生成食物位置。
  4. 一旦确定了食物的位置不与蛇身重合,就分配一个新的snake节点用来表示食物,并将其位置设置为前面生成的xy
  5. 如果malloc调用失败,函数会打印错误信息并返回。
  6. 使用函数GB(x, y)可能会对游戏界面进行更新(该函数没有在代码片段中定义,但通常用来设置指定位置的属性或输出)。然后通过wprintf在对应的位置输出一个字符,这个字符由FOOD宏定义表示,可能是代表食物的特定图案或符号。
  7. 最后,将刚分配的食物节点放入sna结构中,表示食物已被生成并放置在游戏中。

按键识别

这里我们先用一个宏定义来方便检测返回值

#define KEY_PRESS(vk)  ((GetAsyncKeyState(vk)&1)?1:0)

 然后我们使用枚举来列出蛇身方向的状态

enum STATUS//蛇身状态
{
	OK,//正常
	KILLWALL,//撞到墙
	KILLI,//撞到自己
	EXIT,//正常退出
};

 判断蛇身位置

判断下一个结点是否是食物

int nextfood(snake* pa, sna* pb)//判断下一个坐标是不是食物
{
	if (pb->food->x == pa->x && pb->food->y == pa->y)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}
  1. 函数首先比较pb->food->x(食物的x坐标)和pa->x(蛇头的x坐标),以及pb->food->y(食物的y坐标)和pa->y(蛇头的y坐标)是否相同。
  2. 如果这些坐标相同,说明蛇的下一个坐标就是食物的位置,函数返回1,表示下一个坐标是食物。
  3. 如果这些坐标不同,函数返回0,表示下一个坐标不是食物。

 

下一个位置是食物的情况 

void eatfood(snake* pa, sna* pb)//下一个位置是食物
{
	//头插
	pb->food->next = pb->phead;
	pb->phead = pb->food;

	//释放下一个位置的结点
	free(pa);
	pa = NULL;
	//打印蛇
	snake* new = pb->phead;
	while (new)
	{
		GB(new->x, new->y);
		wprintf(L"%lc", BODY);
		new = new->next;
	}
	pb->score += pb->onescore;
	//重新创建食物
	food(pb);
}
  1. 将食物变成蛇的新头部(头插法),表示蛇吃掉了这个食物,并因此增长。
  2. 更新游戏得分,每次吃掉食物时,蛇的得分会增加一个固定的分值(pb->onescore)。
  3. 在蛇吃掉食物后,重新生成食物的位置,保持游戏持续进行。

 

 下一个结点不是食物的情况

void nofood(snake* pa, sna* pb)//下一个结点不是食物
{
	//头插
	pa->next = pb->phead;
	pb->phead = pa;
	snake* new = pb->phead;
	while (new->next->next != NULL)//打印到蛇身倒数第二个结点
	{
		GB(new->x, new->y);
		wprintf(L"%lc", BODY);
		new = new->next;
	}
	GB(new->next->x, new->next->y);//此时蛇身已经移动,将蛇身最后一个结点打印成空格
	printf("  ");
	free(new->next);
	new->next = NULL;
}
  1. 头插法添加新节点:首先,函数通过将新位置 pa(蛇头将要移动到的位置)通过头插法添加到蛇身的开头,以此表示蛇向该方向移动了一个单位。此时,pa成为了蛇的新头部(pb->phead)。

  2. 遍历蛇身体:接着,函数遍历蛇身链表直到倒数第二个节点。在这个过程中,函数调用 GB 函数,并用 wprintf 在每个节点的位置上打印蛇身的字符(由宏 BODY 定义)以在游戏界面上显示蛇的移动。

  3. 更新蛇尾部:在蛇向前移动之后,原本作为蛇尾的节点(即现在链表中的最后一个节点)需要被更新。首先,函数在这个节点的位置上打印空格字符(或者相当于清除该位置的显示),使得该位置在游戏界面上不再显示为蛇身的一部分。

  4. 移除蛇尾节点:最后,函数释放了原蛇尾的节点(即现在的最后一个节点),并将相应的指针设置为 NULL,从而减少蛇身的长度。

 蛇身撞墙的情况

void killwall(sna* pa)//蛇身撞墙
{
	if (pa->phead->x <= 0 || pa->phead->x >= 56 || pa->phead->y <= 0 || pa->phead->y >= 26)
	{
		pa->status = KILLWALL;
	}
}
  1. 检查蛇头的 x 坐标(水平位置)和 y 坐标(垂直位置)。
  2. 若蛇头的 x 坐标小于等于0或者大于等于56,或者蛇头的 y 坐标小于等于0或者大于等于26,说明蛇头已经触碰到了游戏边界,即蛇头撞墙。
  3. 如果检测到蛇头撞墙,函数将 pa->status 更新为 KILLWALL,表示蛇已经死亡。

 

 蛇撞到自己的情况

void killmy(sna* pa)//蛇身撞到自己
{
	snake* new = pa->phead->next;//指向蛇头的第二个位置,如果是第一个位置就会直接死亡
	while (new)
	{
		if (new->x == pa->phead->x && new->y == pa->phead->y)
		{
			pa->status = KILLI;
			break;
		}
		new = new->next;
	}
}
  1. 初始化:函数开始通过设置一个指针 new,该指针指向蛇头 (pa->phead) 的下一个节点,也就是蛇身的第二个部分。注意:蛇头是蛇身的第一个部分,因此它的下一个节点是蛇身第二个部分。

  2. 循环检测:函数进入一个 while 循环,循环的条件是 new 指针非空。在这个循环中,函数检查当前 new 指向的蛇身部分的坐标 (xy) 是否与蛇头的坐标相同。

  3. 撞击检测:如果蛇头的坐标和任何其他蛇身部分的坐标相同,那么就意味着蛇头撞到了自己的身体。此时,设置 pa->status 为 KILLI,也就是游戏中的"自杀"状态。

  4. 游戏状态更新:之后,函数跳出 while 循环。这时,游戏逻辑会检查 pa->status,如果是 KILLI,就可以处理游戏结束的逻辑。

 判断蛇身的运动状态

void snakemove(sna* pa)//蛇的运动状态
{
	snake* new = (snake*)malloc(sizeof(snake));//蛇的下一个位置
	if (new == NULL)
	{
		perror("snakemove");
		return;
	}
	switch (pa->dir)//推算蛇头的下一个位置
	{
	case UP://x,y-1
		new->x = pa->phead->x;
		new->y = pa->phead->y - 1;
		break;
	case DOWN://x,y+1
		new->x = pa->phead->x;
		new->y = pa->phead->y + 1;
		break;
	case LEFT://x-2,y
		new->x = pa->phead->x - 2;
		new->y = pa->phead->y;
		break;
	case RIGHT://x+2,y
		new->x = pa->phead->x + 2;
		new->y = pa->phead->y;
		break;
	}
	if (nextfood(new, pa))//检查下一个坐标是否是食物
	{
		eatfood(new, pa);//下一个结点是食物
	}
	else
	{
		nofood(new, pa);//下一个结点不是食物
	}
	killwall(pa);
	killmy(pa);
}
  1. 函数首先尝试分配一个新的snake类型的节点,用于蛇头的下一个位置。

  2. 如果内存分配失败,函数会打印出一个错误信息,并直接返回。

  3. 使用 switch 语句按照蛇当前的移动方向(存储在 pa->dir 中)来计算蛇头的下一个位置。根据蛇的方向(上、下、左、右),更新新节点 new 的 x 和 y 坐标。

    • 如果向上移动(UP),y 坐标减1。
    • 如果向下移动(DOWN),y 坐标加1。
    • 如果向左移动(LEFT),x 坐标减2(这里的减2可能是因为在游戏的显示界面中,蛇的每次移动可能是两个字符单位的宽度)。
    • 如果向右移动(RIGHT),x 坐标加2。
  4. 调用 nextfood 函数来检查蛇的下一个位置是否是食物。如果是食物,调用 eatfood 函数处理蛇吃食物的动作;如果不是食物,则调用 nofood 函数让蛇继续移动而不增长。

  5. 函数还调用了 killwall 函数,用于检查蛇是否撞墙,以及调用 killmy 函数检查蛇是否咬到自己。

暂停

void stope()//暂停
{
	while (1)
	{
		Sleep(200);
		if (KEY_PRESS(VK_SPACE))//再按一次空格取消暂停
		{
			break;
		}
	}
}
  1. 开始一个无限循环while(1) 使得函数会一直循环运行,直到明确的中断(即 break)语句出现。

  2. 休眠Sleep(200) 使得当前线程暂停 200 毫秒,这个延时操作让代码短暂休息,避免了持续无间断的运行造成的不必要的计算机资源消耗。

  3. 检测按键输入:如果玩家按下空格键 KEY_PRESS(VK_SPACE),在循环运行的过程中就会break退出循环,从而结束函数的运行。换句话说,暂停状态会一直保持,直到玩家再次按下空格

游戏运行问题 

void gamerun(sna* pa)//游戏运行问题
{
	printgame();
	do
	{
		GB(64, 10);
		printf("总分数:%d\n", pa->score);
		GB(64, 11);
		printf("当前食物的分数:%2d\n", pa->onescore);//用2d保证食物的分数不会打印错误
		if (KEY_PRESS(VK_UP) && pa->dir != DOWN)
		{
			pa->dir = UP;
		}
		else if (KEY_PRESS(VK_DOWN) && pa->dir != UP)
		{
			pa->dir = DOWN;
		}
		else if (KEY_PRESS(VK_LEFT) && pa->dir != RIGHT)
		{
			pa->dir = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && pa->dir != LEFT)
		{
			pa->dir = RIGHT;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			stope();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			pa->status = EXIT;
		}
		else if (KEY_PRESS(VK_F3))
		{
			//加速
			if (pa->sleeptime > 30)//休眠时间大于80才能继续加速
			{
				pa->sleeptime = pa->sleeptime - 30;
				pa->onescore += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			//减速
			if (pa->onescore > 2)//分数大于2是才能继续减速
			{
				pa->sleeptime += 30;
				pa->onescore -= 2;
			}
		}
		snakemove(pa);//蛇运动的状态
		Sleep(pa->sleeptime);//设置休眠时间

	} while (pa->status == OK);//在状态为OK下运行

}

 

  1. 打印游戏界面:调用 printgame() 函数来显示游戏界面。

  2. 运行一个无限循环:使用 do...while 语句使游戏循环运行,直到游戏状态不再是 OK。

  3. 打印分数和当前食物分数:在指定位置显示总分数和当前食物的得分。

  4. 检测按键输入

    • 如果玩家按下上键且蛇当前不是向下移动,则改变蛇的方向为上。
    • 如果玩家按下下键且蛇当前不是向上移动,则改变蛇的方向为下。
    • 如果玩家按下左键且蛇当前不是向右移动,则改变蛇的方向为左。
    • 如果玩家按下右键且蛇当前不是向左移动,则改变蛇的方向为右。
    • 如果玩家按下空格键,则调用 stope() 函数,该函数用于暂停游戏。
    • 如果玩家按下 escape 键,则将游戏状态设置为 EXIT,表示退出游戏。
    • 如果玩家按下 F3 键,则游戏加速,休眠时间减少,食物分数增加。
    • 如果玩家按下 F4 键,则游戏减速,休眠时间增加,食物分数减少。
  5. 移动蛇:调用 snakemove(pa) 函数来根据蛇的当前方向移动蛇。

  6. 延时:使用 Sleep() 函数根据 pa->sleeptime 设置的休眠时间暂停游戏,这是为了控制游戏的速度。

源码

#define  _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<locale.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>
#include<time.h>

#define KEY_PRESS(vk)  ((GetAsyncKeyState(vk)&1)?1:0)

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

enum direction//枚举方向
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT,
};

void GB(short x, short y)//改变光标位置函数 
{
	HANDLE put = GetStdHandle(STD_OUTPUT_HANDLE);//标准输出设备(屏幕缓冲区),获得句柄,总之获得句柄就可以操作控制台 
	//改变光标位置,改完光标位置调用函数才能生效
	COORD pos = { x,y };//设置光标位置
	SetConsoleCursorPosition(put, pos);//改变光标位置
}

enum STATUS//蛇身状态
{
	OK,//正常
	KILLWALL,//撞到墙
	KILLI,//撞到自己
	EXIT,//正常退出
};

typedef struct snakenode//蛇身结点
{
	//蛇身坐标
	int x;
	int y;
	//指向下一个结点的指针
	struct snakenode* next;
}snake;

typedef struct SNAKE//贪吃蛇
{
	snake* phead;//指向蛇头的指针
	snake* food;//指向食物的指针
	enum direction dir;//蛇的方向
	enum STATUS status;//蛇的状态
	int onescore;//单个食物的得分
	int score;//总分
	int sleeptime;//休息时间,时间越短,速度越快,时间越长,速度越慢

}sna;

void ini(sna* pa)//初始化贪吃蛇
{
	int i = 0;
	snake* new = NULL;
	for (i = 0; i < 5; i++)//创建蛇身
	{
		new = (snake*)malloc(sizeof(snake));//申请空间
		if (new == NULL)
		{
			perror("huang");
			return;
		}
		new->next = NULL;
		new->x = 24 + 2 * i;
		new->y = 5;
		//头插法
		if (pa->phead == NULL)//空链表
		{
			pa->phead = new;
		}
		else//非空链表
		{
			new->next = pa->phead;
			pa->phead = new;

		}
	}
	while (new)
	{
		GB(new->x, new->y);
		wprintf(L"%lc", BODY);
		new = new->next;
	}
	//蛇身的初始化
	pa->dir = RIGHT;
	pa->onescore = 4;
	pa->score = 0;
	pa->sleeptime = 200;
	pa->status = OK;
}

void food(sna* pa)//生成食物
{
	int x = 0;
	int y = 0;
again:
	do//生成食物,x是2的倍数,如果x不是2的倍数,食物有可能卡住
	{
		x = rand() % 53 + 2;//2-54
		y = rand() % 25 + 1;//1-25
	} while (x % 2 != 0);
	snake* new = pa->phead;
	while (new)
	{
		if (x == new->x && y == new->y)//如果食物与蛇身重合
		{
			goto again;
		}
		new = new->next;
	}

	snake* food = (snake*)malloc(sizeof(snake));
	if (food == NULL)
	{
		perror("food malloc");
		return;
	}
	food->x = x;
	food->y = y;
	food->next = NULL;
	GB(x, y);
	wprintf(L"%lc", FOOD);
	pa->food = food;
}

void menuone()//初始界面
{
	GB(40, 25);
	wprintf(L"欢迎来到贪吃蛇小游戏");
	GB(40, 28);
	system("pause");//暂停程序
	system("cls");//清理屏幕
	GB(30, 25);
	printf("用箭头来控制上下左右,按F3加速,按F4减速");
	GB(40, 28);
	printf("加速能拿到更高分");
	GB(40, 32);
	system("pause");//暂停程序
	system("cls");
}

void map()//地图
{
	int i = 0;
	for (i = 0; i < 29; i++)//上
	{
		//printf("1");
		//wprintf(L"%lc", L'▢');
		wprintf(L"%lc", WALL);
	}
	GB(0, 26);
	for (i = 0; i < 29; i++)//下
	{
		wprintf(L"%lc", WALL);
	}
	for (i = 1; i <= 25; i++)//左
	{
		GB(0, i);
		wprintf(L"%lc", WALL);
	}
	for (i = 1; i <= 25; i++)//右
	{
		GB(56, i);
		wprintf(L"%lc", WALL);
	}

}

void gamestart()//初始化
{
	system("mode con cols=100 lines=50");//设置行和列
	system("title 贪吃蛇");//命名
	HANDLE put = GetStdHandle(STD_OUTPUT_HANDLE);//获得句柄,可修改控制台信息
	CONSOLE_CURSOR_INFO cursur = { 0 };//创建变量,创建一个包含光标信息的结构体变量、
	GetConsoleCursorInfo(put, &cursur);//将光标信息放进cursur,获取和put相关的控制台光标信息,存放进cursur
	//隐藏光标操作
	cursur.bVisible = false;//将光标可见度设为0	
	SetConsoleCursorInfo(put, &cursur);//设置光标信息,改完光标信息调用函数才能生效
	menuone();
	map();
}
void printgame()//打印游戏中的提示
{
	GB(64, 14);
	wprintf(L"%ls", L"不能穿墙,不能咬到自己");
	GB(64, 15);
	wprintf(L"%ls", L"用 ↑. ↓ . ← . → 来控制蛇的移动");
	GB(64, 16);
	wprintf(L"%ls", L"按F3加速,F4减速");
	GB(64, 17);
	wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
	GB(64, 18);
	wprintf(L"%ls", L"制作");
}
void stope()//暂停
{
	while (1)
	{
		Sleep(200);
		if (KEY_PRESS(VK_SPACE))//再按一次空格取消暂停
		{
			break;
		}
	}
}

int nextfood(snake* pa, sna* pb)//判断下一个坐标是不是食物
{
	if (pb->food->x == pa->x && pb->food->y == pa->y)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}

void eatfood(snake* pa, sna* pb)//下一个位置是食物
{
	//头插
	pb->food->next = pb->phead;
	pb->phead = pb->food;

	//释放下一个位置的结点
	free(pa);
	pa = NULL;
	//打印蛇
	snake* new = pb->phead;
	while (new)
	{
		GB(new->x, new->y);
		wprintf(L"%lc", BODY);
		new = new->next;
	}
	pb->score += pb->onescore;
	//重新创建食物
	food(pb);
}

void nofood(snake* pa, sna* pb)//下一个结点不是食物
{
	//头插
	pa->next = pb->phead;
	pb->phead = pa;
	snake* new = pb->phead;
	while (new->next->next != NULL)//打印到蛇身倒数第二个结点
	{
		GB(new->x, new->y);
		wprintf(L"%lc", BODY);
		new = new->next;
	}
	GB(new->next->x, new->next->y);//此时蛇身已经移动,将蛇身最后一个结点打印成空格
	printf("  ");
	free(new->next);
	new->next = NULL;
}

void killwall(sna* pa)//蛇身撞墙
{
	if (pa->phead->x <= 0 || pa->phead->x >= 56 || pa->phead->y <= 0 || pa->phead->y >= 26)
	{
		pa->status = KILLWALL;
	}
}
void killmy(sna* pa)//蛇身撞到自己
{
	snake* new = pa->phead->next;//指向蛇头的第二个位置,如果是第一个位置就会直接死亡
	while (new)
	{
		if (new->x == pa->phead->x && new->y == pa->phead->y)
		{
			pa->status = KILLI;
			break;
		}
		new = new->next;
	}
}
void snakemove(sna* pa)//蛇的运动状态
{
	snake* new = (snake*)malloc(sizeof(snake));//蛇的下一个位置
	if (new == NULL)
	{
		perror("snakemove");
		return;
	}
	switch (pa->dir)//推算蛇头的下一个位置
	{
	case UP://x,y-1
		new->x = pa->phead->x;
		new->y = pa->phead->y - 1;
		break;
	case DOWN://x,y+1
		new->x = pa->phead->x;
		new->y = pa->phead->y + 1;
		break;
	case LEFT://x-2,y
		new->x = pa->phead->x - 2;
		new->y = pa->phead->y;
		break;
	case RIGHT://x+2,y
		new->x = pa->phead->x + 2;
		new->y = pa->phead->y;
		break;
	}
	if (nextfood(new, pa))//检查下一个坐标是否是食物
	{
		eatfood(new, pa);//下一个结点是食物
	}
	else
	{
		nofood(new, pa);//下一个结点不是食物
	}
	killwall(pa);
	killmy(pa);
}

void gamerun(sna* pa)//游戏运行问题
{
	printgame();
	do
	{
		GB(64, 10);
		printf("总分数:%d\n", pa->score);
		GB(64, 11);
		printf("当前食物的分数:%2d\n", pa->onescore);//用2d保证食物的分数不会打印错误
		if (KEY_PRESS(VK_UP) && pa->dir != DOWN)
		{
			pa->dir = UP;
		}
		else if (KEY_PRESS(VK_DOWN) && pa->dir != UP)
		{
			pa->dir = DOWN;
		}
		else if (KEY_PRESS(VK_LEFT) && pa->dir != RIGHT)
		{
			pa->dir = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && pa->dir != LEFT)
		{
			pa->dir = RIGHT;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			stope();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			pa->status = EXIT;
		}
		else if (KEY_PRESS(VK_F3))
		{
			//加速
			if (pa->sleeptime > 30)//休眠时间大于80才能继续加速
			{
				pa->sleeptime = pa->sleeptime - 30;
				pa->onescore += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			//减速
			if (pa->onescore > 2)//分数大于2是才能继续减速
			{
				pa->sleeptime += 30;
				pa->onescore -= 2;
			}
		}
		snakemove(pa);//蛇运动的状态
		Sleep(pa->sleeptime);//设置休眠时间

	} while (pa->status == OK);//在状态为OK下运行

}
void gameend(sna* pa)//游戏结束工作
{
	GB(30, 35);
	switch (pa->status)
	{
	case EXIT:
		printf("游戏正常结束");
		break;
	case KILLWALL:
		printf("你撞到墙了,你死了");
		break;
	case KILLI:
		printf("你撞到你自己了,你死了");
		break;
	}
	//释放链表
	snake* new = pa->phead;
	while (new)
	{
		snake* del = new->next;
		free(new);
		new = del;
	}

}
int main()
{
	//设置适配本地环境
	setlocale(LC_ALL, "");//打印宽字符
	srand((unsigned int)time(NULL));
	gamestart();
	sna tou = { 0 };
	ini(&tou);
	food(&tou);
	gamerun(&tou);
	GB(50, 40);
	system("pause");
	gameend(&tou);
	return 0;
}

 

  • 25
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值