贪吃蛇-c语言版本

news2024/12/15 15:28:09

目录

前言

贪吃蛇游戏设计与分析

设计目标:

设计思想:

坐标问题: 

字符问题:

小拓展:C语⾔的国际化特性

本地化头文件:

类项

setlocale函数:

宽字符打印:

地图坐标:

 🐍和🍖:

初始化🐍:

初始化🍖:

数据结构的设计:

游戏主体流程设计:

游戏准备函数-GameStart:

游戏运行函数-GameRun:

游戏结束函数-GameEnd:

代码的具体实现:

创建头文件:

游戏准备函数GameStart():

打印欢迎界面:

创建地图:

初始化蛇身:

创建食物:

游戏运行函数-GameRun:

右侧打印帮助信息

进行蛇的移动:

​编辑

检测下一个是不是食物函数:

注意事项:

吃食物函数:

不吃食物函数:

自己撞自己函数:

实现步骤:

撞墙函数:

游戏结束函数-GameEnd:

全部代码:

Snake.c文件:

test.c文件:

Snake.h文件:

前言

        学习本篇之前建议将上一篇的关于《常用Win32 API的简单介绍》也打开......,同时此篇过长使用电脑观看效果更佳

贪吃蛇游戏设计与分析

设计目标:

设计思想:

坐标问题: 

        我们想在控制台的窗⼝中的指定位置输出我们想要的东西(墙体、食物、蛇、提示信息),我们得知道该位置的坐标,关于控制台窗口的坐标我们做出如下规定:

横向的是X轴,从左向右依次增⻓,纵向是Y轴,从上到下依次增⻓

字符问题:

此外,我们在打印这些想要输出的信息时将会使⽤一些”宽字符“

打印墙体使用宽字符:□

打印蛇使⽤宽字符:●

打印⻝物使⽤宽字符:★

注意事项:普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节

小拓展:C语⾔的国际化特性

        C语⾔最初的假定字符都是英美等以英语为官方语言的国家使用的,所以过去的C语⾔并不适合⾮英语国家使⽤。

        C语⾔字符默认是采⽤ASCII编码的,ASCII字符集采⽤的是单字节编码,且只使⽤了单字节中的低7 位,最⾼位是没有使⽤的,可表⽰为0xxxxxxxx;ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够⽤的。

        但是在其他国家⽐如:在法语中字⺟上⽅会有注⾳符号像é,它就⽆法⽤ ASCII 码表⽰。于是,⼀些欧洲国家就决定,利⽤字节中闲置的最⾼位编⼊新的符号。⽐如,法语中的é的编码为130(⼆进制10000010)。这样⼀来,这些欧洲国家使⽤的编码体系,可以表⽰最多256个符号。但是即使是这样也还是不够满足全球所有国家的需求:130在法语编码中代表了é,在希伯来语编码中却代表了字⺟ג......,⾄于亚洲国家的⽂字,使⽤的符号就更多了,汉字就多达10万左右。⼀个字节只能表⽰256种符号, 肯定是不够的,就必须使⽤多个字节表达⼀个符号。⽐如,简体中⽂常⻅的编码⽅式是 GB231,使⽤两个字节表⽰⼀个汉字,所以理论上最多可以表⽰ 256 x 256 = 65536 个符号。

注意所有编码⽅式中,0--127表⽰的符号是⼀样的,不⼀样的只是128--255的这⼀段

        后来为了使C语⾔的适用范围更广,C语⾔的标准中不断加⼊了国际化的⽀持。加入了宽字符
数据类型、宽字符字符串函数以及宽字符的输入输出函数:
  1. 宽字符数据类型:

    • wchar_t:宽字符类型,用于表示一个宽字符。
    • wint_t:宽整数类型,用于表示一个宽字符或特殊值 WEOF
  2. 宽字符字符串函数:

    • wprintf():用于格式化输出宽字符字符串到标准输出。
    • wscanf():用于从标准输入读取宽字符数据。
    • wcslen():计算宽字符字符串的长度。
    • wcscpy():将一个宽字符字符串复制到另一个宽字符字符串。
    • wcsncpy():将指定数量的宽字符从一个宽字符字符串复制到另一个宽字符字符串。
    • wcscat():将一个宽字符字符串连接到另一个宽字符字符串的末尾。
    • wcsncat():将指定数量的宽字符连接到一个宽字符字符串的末尾。
  3. 宽字符输入输出函数:

    • getwchar():从标准输入读取一个宽字符。
    • putwchar():将一个宽字符输出到标准输出。
    • fgetwc():从指定的文件流读取一个宽字符。
    • fputwc():将一个宽字符写入到指定的文件流。
    • fwscanf():从指定的文件流读取宽字符数据。
    • fwprintf():将格式化的宽字符字符串写入到指定的文件流。

        此外,还加⼊了<locale.h>头⽂件,它是C语言标准库中的一个头文件,提供了与本地化相关的函数和类型定义。本地化是指根据不同的地区和语言环境,对程序进行适应和定制,以便正确显示日期、时间、货币、数字格式等,比如:

英语环境下的日期和钱:10/24/2023    $

中文环境下的日期和钱:2023/10/24   ¥

<locale.h>本地化头文件:
<locale.h>提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分,比如:
数字量的格式
货币量的格式
字符集
⽇期和时间的表⽰形式
类项
        通过修改地区,程序可以改变它的⾏为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中⼀部分可能是我们不希望修改的。所以C语⾔⽀持针对不同的类项进⾏修改,下⾯的⼀个宏, 指定⼀个类项:
LC_COLLATE        影响字符串比较函数 strcoll() strxfrm()
LC_CTYPE            影响字符处理函数的行为
LC_MONETARY    影响货币格式
LC_NUMERIC        影响 printf() 的数字格式
LC_TIME                影响时间格式 strftime() wcsftime()
LC_ALL                  针对所有类项修改,将以上所有类别设置为给定的语言环境  
点击此处查看每个类项的详细说明
setlocale函数:

函数原型:

char * setlocale ( int category, const char * locale);
  • category:选择要修改的类项,如果要选择要修改全部类型请选择LC_ALL
  • locale:选择你想要修改类项的模式是"C"(正常模式)还是“ ”(本地模式)
所有程序开始前默认执行: setlocale (LC_ALL, "C" );
输入: setlocale (LC_ALL, " " ); //切换到本地环境,此时程序支 持宽字符(汉字)等本地字符的输出
这是 c语言官网 中提供的案例:
#define _CRT_SECURE_NO_WARNINGS  //记得加上这行不然localtime会报错显示不安全
/* setlocale example */
#include <stdio.h>      /* printf */
#include <time.h>       /* time_t, struct tm, time, localtime, strftime */
#include <locale.h>     /* struct lconv, setlocale, localeconv */

int main()
{
    time_t rawtime;
    struct tm* timeinfo;
    char buffer[80];

    struct lconv* lc;

    time(&rawtime);
    timeinfo = localtime(&rawtime);

    int twice = 0;

    do {
        printf("Locale is: %s\n", setlocale(LC_ALL, NULL));

        strftime(buffer, 80, "%c", timeinfo);
        printf("Date is: %s\n", buffer);

        lc = localeconv();
        printf("Currency symbol is: %s\n-\n", lc->currency_symbol);

        setlocale(LC_ALL, "");
    } while (!twice++);

    return 0;
}
宽字符打印:
那如果想在屏幕上打印宽字符,怎么打印呢?
#include <stdio.h>
#include<locale.h>
int main() {
	setlocale(LC_ALL, "");
	wchar_t ch1 = L'●';//♥我不会啊┭┮﹏┭┮
	wchar_t ch2 = L'邓';
	wchar_t ch3 = L'紫';
	wchar_t ch4 = L'棋';
	printf("%c\n", 'I');

	wprintf(L"%c\n", ch1);
	wprintf(L"%c\n", ch2);
	wprintf(L"%c\n", ch3);
	wprintf(L"%c\n", ch4);
	return 0;
}
        从输出的结果来看,我们发现⼀个普通字符占⼀个字符的位置,但是打印⼀个汉字字符,占⽤2个字符的位置,那么我们如果要在贪吃蛇中使⽤宽字符,就得处理好地图上坐标的计算。普通字符和宽字符打印出宽度的展⽰如下:

棋盘坐标:
我们将棋盘设置为27行58列的大小:

 🐍和🍖:

初始化🐍:
⻓度为5,蛇⾝的每个节点是●,在固定的⼀个坐标处,⽐如(24, 5)处开始出现,连续5个节点
注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半出现在墙体中
初始化🍖:
在墙体内随机⽣成⼀个坐标,然后打印★
注意:x坐标必须是2的倍数,坐标不能和蛇的⾝体重合

数据结构的设计:

🐍身体的增加:        
        在游戏运⾏的过程中,蛇每次吃⼀个⻝物,蛇的⾝体就会变⻓⼀节,如果我们使⽤链表存储蛇的信息,那么蛇的每⼀节其实就是链表的每个节点,所以组成🐍的链表结构如下:
//定义用与创建结点的链表
typedef struct SnakeNode
{
    int x;//结点横坐标
    int y;//结点纵坐标
    struct SnakeNode* next;
}SnakeNode, * pSnakeNode;//重命名为SnakeNode类型,pSnakeNode指针指向该链表
此外,我们还需要封装⼀个Snake结构体来维护整个游戏中的各项数据:
//包含游戏各项数据的结构体类型
typedef struct Snake
{
    pSnakeNode _pSnake;        //用于维护链表结点的指针(规定它指向链表的第一个结点)
    pSnakeNode _pFood;         //用于维护食物的指针
    enum DIRECTION _Dir;       //_Dir是该枚举类型的变量,可以为其赋值
    //比如:enum DIRECTION _Dir = ok; 后续我们会利用switch语句与之配合
    enum GAME_STATUS _Status;
    int _Score;                //获得总分数
    int _Add;                  //每个食物的分数
    int _SleepTime;            //每进行一次状态转换(切换防线、吃掉食物等)都需要进行短暂的休息
}Snake, * pSnake;//重命名为Snake类型,pSnake指针指向该结构体
蛇的⽅向可以使⽤枚举一一表示:
//定义反应蛇运行⽅向的枚举类型
enum DIRECTION
{
    UP,            //向上
    DOWN,          //向下 
    LEFT,          //向左
    RIGHT          //向右
};
游戏状态可以使⽤枚举一一表示:
//定义反应游戏状态的枚举类型
enum GAME_STATUS
{
    OK,              //游戏正常运⾏
    KILL_BY_WALL,    //撞墙
    KILL_BY_SELF,    //自己撞到自己
    END_NOMAL        //正常结束(自己选择ESC结束游戏)
};
我们来简单演示一下它们该如何使用,有个大致印象即可:
#include <stdio.h>
enum DIRECTION {
    UP,
    DOWN,
    LEFT,
    RIGHT
};

enum GAME_STATUS {
    IN_PROGRESS,
    GAME_OVER
};

struct Snake {
    enum DIRECTION _Dir;
    enum GAME_STATUS _Status;
};

int main() {
    struct Snake snake;
    snake._Dir = UP;
    snake._Status = IN_PROGRESS;

    if (snake._Dir == UP) {
        printf("Snake is moving UP\n");
    }

    if (snake._Status == IN_PROGRESS) {
        printf("Game is in progress\n");
    }

    return 0;
}

游戏主体流程设计:

这里只是大致逻辑,一些更加细节的逻辑设计被放在代码具体实现中进行讲解

游戏准备函数-GameStart:

  1. 设置游戏窗口大小
  2. 设置窗口名字
  3. 隐藏屏幕光标
  4. 打印欢迎界面
  5. 创建地图
  6. 初始化蛇身
  7. 创建食物

游戏运行函数-GameRun:

  1. 右侧打印帮助信息
  2. 打印当前分数和食物分数
  3. 按键获取情况
  4. 根据按键情况移动蛇
  5. 步骤2到步骤4循环,直到游戏为结束状态

游戏结束函数-GameEnd:

  1. 告知游戏结束原因
  2. 释放蛇身结点

ok,基本的设计思路我们已经解释过了,下面我们要开始实操了哦~

代码的具体实现:

创建头文件:

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

#define WALL L'□'     //定义墙体的符号
#define BODY L'●'     //定义蛇身的符号
#define FOOD L'★'    //定义食物的符号
#define POS_X 24      //定义蛇尾的横坐标
#define POS_Y 5       //定义蛇尾的纵坐标

//检测按键是否按下以及按的哪一个键(上一篇的Win32 API中提到过)
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)

//定义反应蛇运行⽅向的枚举类型
enum DIRECTION
{
	UP,            //向上
	DOWN,          //向下 
	LEFT,          //向左
	RIGHT          //向右
};

//定义反应游戏状态的枚举类型
enum GAME_STATUS
{
	OK,              //游戏正常运⾏
	KILL_BY_WALL,    //撞墙
	KILL_BY_SELF,    //自己撞到自己
	END_NOMAL        //正常结束(自己选择ESC结束游戏)
};

//定义用与创建结点的链表
typedef struct SnakeNode
{
	int x;//结点横坐标
	int y;//结点纵坐标
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;//重命名为SnakeNode类型,pSnakeNode指针指向该链表


//包含游戏各项数据的结构体类型
typedef struct Snake
{
	pSnakeNode _pSnake;        //用于维护链表结点的指针(规定它指向链表的第一个结点)
	pSnakeNode _pFood;         //用于维护食物的指针
	enum DIRECTION _Dir;       //_Dir是该枚举类型的变量,可以为其赋值
    //比如:enum DIRECTION _Dir = ok; 后续我们会利用switch语句与之配合
	enum GAME_STATUS _Status;  
	int _Score;                //获得总分数
	int _Add;                  //每个食物的分数
	int _SleepTime;            //每进行一次状态转换(切换防线、吃掉食物等)都需要进行短暂的休息
}Snake,*pSnake;//重命名为Snake类型,pSnake指针指向该结构体

//下面是具体要声明的函数

//游戏准备函数
void GameStart(pSnake ps);

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

    //打开欢迎界面
    void WelcomeToGame();

    //打印地图
    void CreateMap();

    //初始游戏各项数据
    void InitSnake(pSnake ps);

    //创造第⼀个⻝物
    void CreateFood(pSnake ps);
    
//游戏运行函数
void GameRun(pSnake ps);

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

    //游戏暂停
    void pause();

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

    //判断蛇头到达的坐标处是否为食物
    int NextIsFood(pSnake ps, pSnakeNode pnext);

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

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

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

    //撞墙检测
    void KillByWall(pSnake ps);

    //撞自身检测
    void KillBySelf(pSnake ps);

       后续描述中我们将用于创建结点的链表称为链表,将包含各项游戏数据的结构体称为结构体

游戏准备函数GameStart():

//游戏准备函数
void GameStart(pSnake ps)
{
	//控制台窗口设置
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

	//光标隐藏
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); //获取权限
	CONSOLE_CURSOR_INFO CursorInfo; //定义结构体类型变量
	GetConsoleCursorInfo(hOutput, &CursorInfo); // 获取控制台光标信息
	CursorInfo.bVisible = false; // 隐藏控制台光标
	SetConsoleCursorInfo(hOutput, &CursorInfo); // 设置控制台光标状态

	//打开欢迎界面
	WelcomeToGame();

	//打印地图
	CreateMap();

	//初始化游戏各项数据
	InitSnake(ps);

	//创造第⼀个⻝物
	CreateFood(ps);                                               
}

这里的操作就不做细致描述了~

设置光标位置:
// 设置光标的坐标(程序输入时的位置)
void SetPos(short x, short y)
{
	COORD pos = { x, y };
	// 获取标准输出的句柄 ( ⽤来标识不同设备的数值 )
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	// 设置标准输出上光标的位置为 pos
	SetConsoleCursorPosition(hOutput, pos);
}

不做过多解释~

打印欢迎界面:
//欢迎界面
void WelcomeToGame()
{
		//显示一
		SetPos(38, 14);
		printf("欢迎来到贪吃蛇小游戏");
		SetPos(40, 25);//让按任意键继续的出现的位置好看点
		system("pause");//暂停操作
		system("cls");//清屏

		//显示二
		SetPos(20, 14);//重新定义光标位置,从该坐标处开始输入
		printf("使用↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");
		SetPos(40, 25);
		system("pause");
		system("cls");

		//显示三
		SetPos(37, 14);
		printf("加速将能得到更高的分数\n");
		SetPos(40, 25);//让按任意键继续的出现的位置好看点
		system("pause");
		system("cls");
}

实现步骤:

1、利用SetPos设置想要出现文字的位置(光标移动至此)

2、在SetPos指定的位置打印想要输入的文字

3、再次利用SetPos设置“请按任意键出现的位置”即system("pasue")起作用时文字显示的位置

4、利用system("cls")清空屏幕

注意事项:

1、请安任意键的文字是system("pause")后产生的效果请不要自行添加,同时也不能删除该行

2、必须执行system("cls")

最终效果:

创建地图:
//创建地图
void CreateMap()
{
	//地图的四个角的坐标为(0,0)       (56,0)
	//					  (0,1)        (56,1)  //在打印左/右侧墙体时,由于之前上下两行的打印已经将左/右侧墙体的第一个和左后一个打印过了,所以要注意坐标问题,左/右侧每列要少打两个
	//						...             ...
	//					  (0,25)     (56,25)
	//					  (0,26)     (56,26)

	//打印上边界(0,0)至(56,0)
	SetPos(0, 0);
	for (int i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);//wprintf函数的使用方式上面右描述,记得打印宽字符不是%c而是%lc
	}
	//打印下边界(0,26)至(56,26)
	SetPos(0, 26);
	for (int i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//打印左边界(0,1)至(0,25)
	for (int i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//打印右边界(56,1)至(56,25)
	for (int i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
}

实现步骤:

1、按照之前规定好的坐标信息在地图上打印我们的用宽字符”□“围城的墙即可

注意事项:

1、打印宽字符要使用wprintf函数,且格式为wprintf(L"%lc",WALL),

2、上下两侧墙体全部打印完,左右两侧墙体都要少打两个避字符免在四个角落的重复

最终效果:

初始化蛇身:
//初始化游戏各项数据
void InitSnake(pSnake ps)//ps指向结构体
{
	//创建一个指向链表的的指针变量cur,利用该指针创建和连接结点
	pSnakeNode cur;
	//默认创建五个结点
	for (int i = 0; i < 5; i++)
	{
		//令cur指向新开辟的结点内存空间
		cur = (pSnakeNode*)malloc(sizeof(SnakeNode));//关于malloc函数的使用不再过多描述,有疑问可以去看我的《动态内存管理》文章
        
		//开辟失败的报错
		if (cur == NULL)
		{
			perror("InitSnake()-malloc()");
			return;
		}

		//分配完内存空间后就会为该结点分配初始坐标(x,y)
		//2*i实现可以实现横向创建一条蛇身的目的:(26,5)(28,5)(30,5)(32,5)(34,5)
		cur->x = POS_X + 2*i;//定义的变量POS_X和POS_Y便于后期切换初始坐标
		cur->y = POS_Y ;
		cur->next = NULL;//到这里已经完成一个结点的创建,但是该结点还没有连接

		//利用头插法进行蛇身体的链接
        //ps_pSnake相当于一个套娃,ps指向结构体,ps_pSnake指向该结构体中指向链表的指针
		if (ps->_pSnake == NULL)
		{
			ps->_pSnake = cur;//交接工作
		}
		//如果链表不为空则进行头插
		else
		{
			cur->next = ps->_pSnake; 
			ps->_pSnake = cur;
		}
	}

	//链表连接完成后,打印结点
	cur = ps->_pSnake;//令cur指向蛇的第一个结点
	while (cur)//只要cur指向结点不为空就继续循环打印
	{
	 SetPos(cur->x, cur->y);//使用上面分配过的x和y坐标开始从该坐标处打印蛇身
	 wprintf(L"%lc", BODY);//打印我们之前规定的符号●
	 cur = cur->next;//cur指向下一个结点
	}

	//初始化游戏各类所需数据
	ps->_SleepTime = 200; //规定蛇每次移动都需要休息2秒
	ps->_Score = 0;//规定初始得分为0
	ps->_Status = OK;//规定初始游戏状态为ok
	ps->_Dir = RIGHT;//规定蛇开始的运行方向向右
	ps->_Add = 10;//规定吃掉一个食物的得到10分
	
}

实现步骤:

1、创建五个的链表结点,令cur指向创建的一个结点

2、链表为空令新节点作为链表第一个结点进行地址交接工作,链表不为空进行头插操作

这里应该是ps->_pSnake而不是ps_pSnake,写错了懒得改了

3、打印结点,链表的逻辑位置和打印出来的位置相反(注意研究代码逻辑):

这里也写错了

最终效果:

创建食物:
//创建⻝物
void CreateFood(pSnake ps)//食物其实也相当于一个链表结点
{	
	//为食物设置随机的横纵坐标
	int x = 0;
	int y = 0;

	again://利用goto语句实现多次循环
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0); //产⽣的x坐标应该是2的倍数,这样才能与蛇头坐标对⻬
	pSnakeNode cur = ps->_pSnake;//获取链表的第一个结点

	//⻝物的结点不能和此时蛇身的某个结点重合,如果重合则利用goto语句重新分配食物结点的横纵坐标
	while (cur)
	{
		if (cur->x == x && cur->y == y)
			{
				goto again;
			}
			cur = cur->next;//不重合就令cur指向下一个结点
	}

    //为食物结点申请内存空间
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); 
	//创建失败的报错
	if (pFood == NULL)
		{
			perror("CreateFood::malloc()");
			return;
		}
	else
		{
	//创建成功,为结点分配实质的坐标
			pFood->x = x;
			pFood->y = y;
			SetPos(pFood->x, pFood->y);//令光标放在该节点的x和y坐标上
			wprintf(L"%c", FOOD);//打印食物结点
			ps->_pFood = pFood;//令_pFood指向该结点(将该结点的地址交给_pFood)
		}
}

实现步骤:

1、分配合理的横纵坐标

2、申请结点空间

3、令该结点空间的位置是合理分配的坐标处

注意事项:

 1、创建食物结点的过程通俗来讲就是你去上班,老板给你一个空缺的职位,然后为你分配了一块你的办公区域,最后你再在该区域中办公(有职位->分空间->去上任)

2、⻝物的结点不能和此时蛇身的某个结点重合,如果重合则利用goto语句重新分配食物结点的横纵坐标(注意:说此时,是因为蛇身结点在运动中并不会一直霸占某个坐标)

最终效果:

游戏运行函数-GameRun:

//游戏运行函数
void GameRun(pSnake ps)
{
	//打印右侧帮助信息(静态)
	PrintHelpInfo();
	
	do
	{
		//打印计分表(动态),蛇的吃食物、加减速都会引起计分表的变化
		SetPos(64, 8);
		printf("目前得分情况:%d", ps->_Score);
		SetPos(64, 9);
		if (ps->_Add < 10)
		    printf("每个食物的分数: 0%d", ps->_Add);
	    else
		    printf("每个食物的分数:%d", ps->_Add);

		//且按下的方向不能与蛇当前移动方向相反(它头正在向上走你突然让它向下走是不行的)
        //对于如何使用_Dir赋值后的结果我们会在SnakeMove中实现
		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_RIGHT) && ps->_Dir != LEFT)
		{
			ps->_Dir = RIGHT;
		}
		else if (KEY_PRESS(VK_LEFT) && ps->_Dir != RIGHT)
		{
			ps->_Dir = LEFT;
		}
		else if (KEY_PRESS(VK_SPACE))//按下空格键时暂停游戏
		{
			pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))//按下ESC键时主动退出游戏
		{
			ps->_Status = END_NOMAL;  //主动切换游戏状态为END_NOMAL
			break;
		}
		else if (KEY_PRESS(VK_F3))//按下F3加速
		{
			//速度越快得分越高,最多加速五次每次加速食物分数增加2,最高为20
			if (ps->_SleepTime >= 50)
			{
				ps->_SleepTime -= 30;
				ps->_Add += 2;
			}

		}
		else if (KEY_PRESS(VK_F4))//按下F4减速
		{
			//速度越慢得分越低,最多减速四次每次减速食物分数减少2,最低为2
			if (ps->_SleepTime < 320)
			{
				ps->_SleepTime += 30;
				ps->_Add -= 2;
			}
		}

		//蛇每次移动一定进行休眠,休眠时间越短,蛇移动的速度就越快
		Sleep(ps->_SleepTime);  

		//当蛇经历过休眠后,就要开始移动了
		SnakeMove(ps);			
	
	}
	//运动完成后检查游戏状态,游戏状态为OK时才会继续循环
	while (ps->_Status == OK);
}

实现步骤:

1、打印右侧帮助信息

2、打印计分表

3、完成按键检测功能

4、规定蛇每次移动的休眠时间

5、进行蛇的移动

6、循环2到5的过程直至游戏状态不为OK

注意事项:

1、右侧的帮助信息是静态的

2、计分表是动态的,它应该可以随着你蛇的移动后产生的结果(吃食物/加减速)而发生改变

3、在按键检测中:

  • 按下的方向不能与蛇当前移动方向相反
  • 按键检测为空格时游戏会进入程序暂停函数,按键检测为ESC时游戏结束
  • 按键检测为F3时游戏加速每个食物可获得分数增加,为F4时游戏减速可获得分数减少

4、蛇移动函数除了其本身外还会另外包含五个函数:

  • 检测下一个是不是食物函数
  • 吃食物函数
  • 不吃食物函数
  • 自己撞自己函数
  • 撞墙函数

最终效果:

        按上下左右键控制蛇的移动,F3加速蛇,F4减速蛇同时每个食物的分数也相应增加或减少,按下空格游戏暂停再次按下游戏继续,按下ESC游戏结束。

右侧打印帮助信息
//打印右侧帮助信息
void PrintHelpInfo()
{
	//打印提⽰信息
	SetPos(64, 15);
	printf("1、不能穿墙,不能咬到自己");

	SetPos(64, 16);
	printf("2、使用↑、↓、←、→控制蛇的移动");

	SetPos(64, 17);
	printf("3、按F3加速  按F4减速");

	SetPos(64, 18);
	printf("4、ESC:退出游戏  space:暂停游戏");
}

实现步骤:

1、SetPos函数确定要打印提示信息的位置

2、打印提示信息

最终效果:

进行蛇的移动:
//蛇移动函数
void SnakeMove(pSnake ps)
{
	pSnakeNode pNext =(pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNext == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}
	pNext->next = NULL;

	//利用switc判断ps->_Dir的不同方向
	switch (ps->_Dir)
	{
	case UP:
		//如果蛇是向上运动的,那么蛇运动的下一个结点的x轴坐标与蛇头保持一致,y轴坐标为蛇头y轴坐标减一,下面的就不一一写解释了
		pNext->x = ps->_pSnake->x;
		pNext->y = ps->_pSnake->y - 1;
		break;
	case DOWN:
		pNext->x = ps->_pSnake->x;
		pNext->y = ps->_pSnake->y + 1;
		break;
	case LEFT:
		pNext->x = ps->_pSnake->x - 2;
		pNext->y = ps->_pSnake->y;
		break;
	case RIGHT:
		pNext->x = ps->_pSnake->x + 2;
		pNext->y = ps->_pSnake->y;
		break;
	}

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

实现步骤:

1、创建新指针pNext,令其指向申请的蛇头下一个结点的地址

2、利用switch语句判断此时蛇的移动方向,如果前面出现了状态的切换此时switch就需要从原来方向的case语句切换成另一个方向的case语句

3、进行下一个结点是否是食物结点的判断

4、如果是食物就吃掉然后继续向前走

5、如果不是食物就不吃继续向前走

6、在最后还要设置游戏失败的两种方式:自己撞自己和撞墙

注意事项:

 1、在pNext指向下一个结点时,该节点虽然已经有了内存空间但是结点的具体横纵坐标需要经过switc语句的判断后进行分配

2、切换完成后进行的坐标更改如下图所示:

3、!!!注意这里对于x坐标的加减操作数是2,对y坐标的加减操作数是1!!!如果将2写成了1虽然程序正常运行但是由于蛇身是宽字符的原因所以当蛇在水平方向上移动时蛇身结点会重叠同时方向切换时也会有明显的延迟感

4、每个蛇身结点的内存空间所在的坐标一直在变化且不论蛇身有多长的每个结点都有自己的内存空间(在蛇移动函数中已经明确说明了蛇结点的x和y坐标会发生改变,其实就相当于将内存空间不断地移位)

最终效果:

错误的:

正确的:

检测下一个是不是食物函数:
//检查下一个是不是食物
int NextIsFood(pSnake ps, pSnakeNode pnext)
{
	return (pnext->x == ps->_pFood->x) && (pnext->y == ps->_pFood->y);
}
注意事项:

        如果之前创建的食物结点的横纵坐标与蛇运动方向(蛇的第一个结点)相同的下一个结点的横纵坐标相等,那么执行吃食物函数的操作,如果不相等那么就执行不吃食物的操作。

吃食物函数:
//吃掉食物
void EatFood(pSnake ps, pSnakeNode pnext)
{
	//头插法
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;

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

	//原来的食物结点吃掉后,就要释放掉它的内存空间
	free(ps->_pFood);
	//吃掉食物后分数增加
	ps->_Score += ps->_Add;
	//食物吃掉后还要再次创建一个新的食物结点
	CreateFood(ps);
}

实现步骤:

1、利用头插法的原理将吃掉的食物结点作为蛇头(第一个结点)

2、遍历打印蛇身

3、由于我们在蛇移动函数的开头已经创建申请了蛇头的下一个结点的内存空间,在更早一点的时候我们还在创建食物时也申请了一块内存空间,所以当我们蛇头下一个节点为先前创建的食物结点时两块内存空间会在同一坐标下,所以当我们打印完新的蛇身时,需要将该坐标下申请的食物结点的内存空间释放掉,原来申请的蛇头下一个结点的内存空间被保留下来作为蛇头

4、在食物吃完后除了将得分进行增加,还需要创建一个新的食物

不吃食物函数:
//不吃食物
void NoFood(pSnake ps, pSnakeNode pnext)
{
	//头插法
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;

	//打印蛇身
	pSnakeNode cur = ps->_pSnake;
	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、依旧是头插法

2、依旧是打印蛇身

3、将原来最后一个结点的位置打印两个空格后,再释放为该结点申请的内存空间

注意事项:

1、打印蛇身时的while判断条件为cur->next->next,具体原因请看下图:

2、关于“    SetPos(cur->next->x,cur->next->y);”的解释:因为蛇头的下一个结点不为食物结点所以蛇身在向前移动时的结点个数并不会发生改变,但是我们之前已经为蛇头的下一个结点申请了内存空间,该结点也会作为新的蛇头存在此时最后的一个结点就不能存在了否则蛇身就会变长而非不变。通俗来讲就是:前面结点增加一,后面结点就应该减少一个以维持原状,蛇的移动过程如果将每一步都暂停的话其实可以看作是一个在链表头部增加一个新结点后为保持原来节点个数不变所以再在链表尾部删除最后的结点。

3、如果只进行内存释放,虽然地图上打印了多个结点但蛇穿过去后并不会结束游戏

这是因为每次移动后虽然蛇身的最后一个结点的空间已经被释放了,但是他仍然会被打印在屏幕上,所以当我们蛇头穿过那些未被覆盖掉的●时它们其实已经是空有”外表“没有“内核”

4、如果只打印空格而不释放就会:

可以发现此时的打印也并未产生应有的效果......

自己撞自己函数:
//自杀的死亡方式(自己撞自己)
void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake->next;
	while (cur)
	{
		if (ps->_pSnake->x == cur->x && ps->_pSnake->y == cur->y)
		{
			ps->_Status = KILL_BY_SELF;
		}
		cur = cur->next;
	}
}
实现步骤:

1、令cur指向蛇头的下一个结点并开始遍历,直到满足有一次蛇在运动过程中蛇身的某个结点的横纵坐标与cur此时指向的坐标相同那么游戏状态就会被切换至KILL_BY_SELF

(大概思路就是这样具体何时会出现这样的情况,可以画图检验一下)

撞墙函数:
//它杀的死亡方式(撞墙)
void KillByWall(pSnake ps)
{
	if (ps->_pSnake->x == 0 ||
		ps->_pSnake->x == 56 ||
		ps->_pSnake->y == 0 ||
		ps->_pSnake->y == 26)
		ps->_Status = KILL_BY_WALL;
}

这里就不作过多解释了~

游戏结束函数-GameEnd:

//游戏结束函数
void GameEnd(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake;
	SetPos(24, 12);
	switch (ps->_Status)
	{
	case END_NOMAL:
		printf("您主动退出游戏\n");
		break;
	case KILL_BY_SELF:
		printf("您撞上自己了 ,游戏结束!\n");
		break;
	case KILL_BY_WALL:
		printf("您撞墙了,游戏结束!\n");
		break;
	}

	//释放蛇身的节点
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
}

全部代码:

Snake.c文件:

#define _CRT_SECURE_NO_WARRINGS

#include "snake.h"

// 设置光标的坐标(程序输入时的位置)
void SetPos(short x, short y)
{
	COORD pos = { x, y };
	// 获取标准输出的句柄 ( ⽤来标识不同设备的数值 )
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	// 设置标准输出上光标的位置为 pos
	SetConsoleCursorPosition(hOutput, pos);
}

//欢迎界面
void WelcomeToGame()
{
	//显示一
	SetPos(38, 14);
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(40, 25);//让按任意键继续的出现的位置好看点
	system("pause");//暂停操作
	system("cls");//清屏

	//显示二
	SetPos(20, 14);//重新定义光标位置,从该坐标处开始输入
	printf("使用↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");
	SetPos(40, 25);
	system("pause");
	system("cls");

	//显示三
	SetPos(37, 14);
	printf("加速将能得到更高的分数\n");
	SetPos(40, 25);//让按任意键继续的出现的位置好看点
	system("pause");
	system("cls");
}

//创建地图
void CreateMap()
{
	//地图的四个角的坐标为(0,0)       (56,0)
	//					  (0,1)        (56,1)  //在打印左/右侧墙体时,由于之前上下两行的打印已经将左/右侧墙体的第一个和左后一个打印过了,所以要注意坐标问题,左/右侧每列要少打两个
	//						...             ...
	//					  (0,25)     (56,25)
	//					  (0,26)     (56,26)

	//打印上边界(0,0)至(56,0)
	SetPos(0, 0);
	for (int i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);//wprintf函数的使用方式上面右描述,记得打印宽字符不是%c而是%lc
	}
	//打印下边界(0,26)至(56,26)
	SetPos(0, 26);
	for (int i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//打印左边界(0,1)至(0,25)
	for (int i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//打印右边界(56,1)至(56,25)
	for (int i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
}


//初始化游戏各项数据
void InitSnake(pSnake ps)//ps指向结构体
{
	//创建一个指向链表的的指针变量cur,利用该指针创建和连接结点
	pSnakeNode cur;
	//默认创建五个结点
	for (int i = 0; i < 5; i++)
	{
		//令cur指向新开辟的结点内存空间
		cur = (pSnakeNode*)malloc(sizeof(SnakeNode));//关于malloc函数的使用不再过多描述,有疑问可以去看我的《动态内存管理》文章

		//开辟失败的报错
		if (cur == NULL)
		{
			perror("InitSnake()-malloc()");
			return;
		}

		//分配完内存空间后就会为该结点分配初始坐标(x,y)
		//2*i实现可以实现横向创建一条蛇身的目的:(26,5)(28,5)(30,5)(32,5)(34,5)
		cur->x = POS_X + 2 * i;//定义的变量POS_X和POS_Y便于后期切换初始坐标
		cur->y = POS_Y;
		cur->next = NULL;//到这里已经完成一个结点的创建,但是该结点还没有连接

		//利用头插法进行蛇身体的链接
		//ps_pSnake相当于一个套娃,ps指向结构体,ps_pSnake指向该结构体中指向链表的指针
		if (ps->_pSnake == NULL)
		{
			ps->_pSnake = cur;//交接工作
		}
		//如果链表不为空则进行头插
		else
		{
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}

	//链表连接完成后,打印结点
	cur = ps->_pSnake;//令cur指向蛇的第一个结点
	while (cur)//只要cur指向结点不为空就继续循环打印
	{
		SetPos(cur->x, cur->y);//使用上面分配过的x和y坐标开始从该坐标处打印蛇身
		wprintf(L"%lc", BODY);//打印我们之前规定的符号●
		cur = cur->next;//cur指向下一个结点
	}

	//初始化游戏各类所需数据
	ps->_SleepTime = 200; //规定蛇每次移动都需要休息2秒
	ps->_Score = 0;//规定初始得分为0
	ps->_Status = OK;//规定初始游戏状态为ok
	ps->_Dir = RIGHT;//规定蛇开始的运行方向向右
	ps->_Add = 10;//规定吃掉一个食物的得到10分

}


//创建⻝物
void CreateFood(pSnake ps)//食物其实也相当于一个链表结点
{
	//为食物设置随机的横纵坐标
	int x = 0;
	int y = 0;

again://利用goto语句实现多次循环
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0); //产⽣的x坐标应该是2的倍数,这样才能与蛇头坐标对⻬
	pSnakeNode cur = ps->_pSnake;//获取链表的第一个结点

	//⻝物的结点不能和此时蛇身的某个结点重合,如果重合则利用goto语句重新分配食物结点的横纵坐标
	while (cur)
	{
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->next;//不重合就令cur指向下一个结点
	}

	//为食物结点申请内存空间
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	//创建失败的报错
	if (pFood == NULL)
	{
		perror("CreateFood::malloc()");
		return;
	}
	else
	{
		//创建成功,为结点分配实质的坐标
		pFood->x = x;
		pFood->y = y;
		SetPos(pFood->x, pFood->y);//令光标放在该节点的x和y坐标上
		wprintf(L"%c", FOOD);//打印食物结点
		ps->_pFood = pFood;//令_pFood指向该结点(将该结点的地址交给_pFood)
	}
}

//游戏准备函数
void GameStart(pSnake ps)
{
	//控制台窗口设置
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

	//光标隐藏
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); //获取权限
	CONSOLE_CURSOR_INFO CursorInfo; //定义结构体类型变量
	GetConsoleCursorInfo(hOutput, &CursorInfo); // 获取控制台光标信息
	CursorInfo.bVisible = false; // 隐藏控制台光标
	SetConsoleCursorInfo(hOutput, &CursorInfo); // 设置控制台光标状态

	//打开欢迎界面
	WelcomeToGame();

	//打印地图
	CreateMap();

	//初始化游戏各项数据
	InitSnake(ps);

	//创造第⼀个⻝物
	CreateFood(ps);
}


//打印右侧帮助信息
void PrintHelpInfo()
{
	//打印提⽰信息
	SetPos(64, 15);
	printf("1、不能穿墙,不能咬到自己");

	SetPos(64, 16);
	printf("2、使用↑、↓、←、→控制蛇的移动");

	SetPos(64, 17);
	printf("3、按F3加速  按F4减速");

	SetPos(64, 18);
	printf("4、ESC:退出游戏  space:暂停游戏");
}

//暂停游戏函数
void pause()
{
	while (1)
	{
		Sleep(300);//只要开始暂停就会一直休息,这里的Sleep你可以设置为任意值
		if (KEY_PRESS(VK_SPACE))//只有当再次点击空格时游戏才会继续执行(类似于看视频按空格键暂停和继续)
			break;
	}
}

//检查下一个是不是食物
int NextIsFood(pSnake ps, pSnakeNode pnext)
{
	return (pnext->x == ps->_pFood->x) && (pnext->y == ps->_pFood->y);
}

//吃掉食物
void EatFood(pSnake ps, pSnakeNode pnext)
{
	//头插法
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;

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

	//原来的食物结点吃掉后,就要释放掉它的内存空间
	free(ps->_pFood);
	//吃掉食物后分数增加
	ps->_Score += ps->_Add;
	//食物吃掉后还要再次创建一个新的食物结点
	CreateFood(ps);
}

//不吃食物
void NoFood(pSnake ps, pSnakeNode pnext)
{
	//头插法
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;

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

//它杀的死亡方式(撞墙)
void KillByWall(pSnake ps)
{
	if (ps->_pSnake->x == 0 ||
		ps->_pSnake->x == 56 ||
		ps->_pSnake->y == 0 ||
		ps->_pSnake->y == 26)
		ps->_Status = KILL_BY_WALL;
}

//自杀的死亡方式(自己撞自己)
void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake->next;
	while (cur)
	{
		if (ps->_pSnake->x == cur->x && ps->_pSnake->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;

	//利用switc判断ps->_Dir的不同方向
	switch (ps->_Dir)
	{
	case UP:
		//如果蛇是向上运动的,那么蛇运动的下一个结点的x轴坐标与蛇头保持一致,y轴坐标为蛇头y轴坐标减一,下面的就不一一写解释了
		pNext->x = ps->_pSnake->x;
		pNext->y = ps->_pSnake->y - 1;
		break;
	case DOWN:
		pNext->x = ps->_pSnake->x;
		pNext->y = ps->_pSnake->y + 1;
		break;
	case LEFT:
		pNext->x = ps->_pSnake->x - 2;
		pNext->y = ps->_pSnake->y;
		break;
	case RIGHT:
		pNext->x = ps->_pSnake->x + 2;
		pNext->y = ps->_pSnake->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, 8);
		printf("目前得分情况:%d", ps->_Score);
		SetPos(64, 9);
		if (ps->_Add < 10)
			printf("每个食物的分数: 0%d", ps->_Add);
		else
			printf("每个食物的分数:%d", ps->_Add);

		//且按下的方向不能与蛇当前移动方向相反(它头正在向上走你突然让它向下走是不行的)
		//对于如何使用_Dir赋值后的结果我们会在SnakeMove中实现
		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_RIGHT) && ps->_Dir != LEFT)
		{
			ps->_Dir = RIGHT;
		}
		else if (KEY_PRESS(VK_LEFT) && ps->_Dir != RIGHT)
		{
			ps->_Dir = LEFT;
		}
		else if (KEY_PRESS(VK_SPACE))//按下空格键时暂停游戏
		{
			pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))//按下ESC键时主动退出游戏
		{
			ps->_Status = END_NOMAL;  //主动切换游戏状态为END_NOMAL
			break;
		}
		else if (KEY_PRESS(VK_F3))//按下F3加速
		{
			//速度越快得分越高,最多加速五次每次加速食物分数增加2,最高为20
			if (ps->_SleepTime >= 50)
			{
				ps->_SleepTime -= 30;
				ps->_Add += 2;
			}

		}
		else if (KEY_PRESS(VK_F4))//按下F4减速
		{
			//速度越慢得分越低,最多减速四次每次减速食物分数减少2,最低为2
			if (ps->_SleepTime < 320)
			{
				ps->_SleepTime += 30;
				ps->_Add -= 2;
			}
		}

		//蛇每次移动一定进行休眠,休眠时间越短,蛇移动的速度就越快
		Sleep(ps->_SleepTime);

		//当蛇经历过休眠后,就要开始移动了
		SnakeMove(ps);

	}
	//运动完成后检查游戏状态,游戏状态为OK时才会继续循环
	while (ps->_Status == OK);
}


//游戏结束函数
void GameEnd(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake;
	SetPos(24, 12);
	switch (ps->_Status)
	{
	case END_NOMAL:
		printf("您主动退出游戏\n");
		break;
	case KILL_BY_SELF:
		printf("您撞上自己了 ,游戏结束!\n");
		break;
	case KILL_BY_WALL:
		printf("您撞墙了,游戏结束!\n");
		break;
	}

	//释放蛇身的节点
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
}

test.c文件:

这里不做过多解释~

#include "snake.h"
void test()
{
	int ch = 0;
	do
	{
		Snake snake = { 0 };//创建一个Snake结构体类型的变量snake,{0}表示此时获得的总分score、以及每个食物的分数add等成员列表中的各项内容全部为0
		//游戏开始
		GameStart(&snake);
		//游戏运行
		GameRun(&snake);
		//游戏结束
		GameEnd(&snake);
		SetPos(20, 15);
		printf("再来一句吗?(Y/N)");
		ch = getchar();
		getchar();
	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 27);

}
int main()
{
	srand((unsigned int)time(NULL));
	setlocale(LC_ALL, "");
	test();
	return 0;
}

Snake.h文件:

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

#define WALL L'□'     //定义墙体的符号
#define BODY L'●'     //定义蛇身的符号
#define FOOD L'★'    //定义食物的符号
#define POS_X 24      //定义蛇尾的横坐标
#define POS_Y 5       //定义蛇尾的纵坐标

//检测按键是否按下以及按的哪一个键(上一篇的Win32 API中提到过)
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)

//定义反应蛇运行⽅向的枚举类型
enum DIRECTION
{
    UP,            //向上
    DOWN,          //向下 
    LEFT,          //向左
    RIGHT          //向右
};

//定义反应游戏状态的枚举类型
enum GAME_STATUS
{
    OK,              //游戏正常运⾏
    KILL_BY_WALL,    //撞墙
    KILL_BY_SELF,    //自己撞到自己
    END_NOMAL        //正常结束(自己选择ESC结束游戏)
};

//定义用与创建结点的链表
typedef struct SnakeNode
{
    int x;//结点横坐标
    int y;//结点纵坐标
    struct SnakeNode* next;
}SnakeNode, * pSnakeNode;//重命名为SnakeNode类型,pSnakeNode指针指向该链表


//包含游戏各项数据的结构体类型
typedef struct Snake
{
    pSnakeNode _pSnake;        //用于维护链表结点的指针(规定它指向链表的第一个结点)
    pSnakeNode _pFood;         //用于维护食物的指针
    enum DIRECTION _Dir;       //_Dir是该枚举类型的变量,可以为其赋值
    //比如:enum DIRECTION _Dir = ok; 后续我们会利用switch语句与之配合
    enum GAME_STATUS _Status;
    int _Socre;                //获得总分数
    int _Add;                  //每个食物的分数
    int _SleepTime;            //每进行一次状态转换(切换防线、吃掉食物等)都需要进行短暂的休息
}Snake, * pSnake;//重命名为Snake类型,pSnake指针指向该结构体

//下面是具体要声明的函数

//游戏准备函数
void GameStart(pSnake ps);

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

//打开欢迎界面
void WelcomeToGame();

//打印地图
void CreateMap();

//初始游戏各项数据
void InitSnake(pSnake ps);

//创造第⼀个⻝物
void CreateFood(pSnake ps);

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

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

//游戏暂停
void pause();

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

//判断蛇头到达的坐标处是否为食物
int NextIsFood(pSnake ps, pSnakeNode pnext);

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

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

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

//撞墙检测
void KillByWall(pSnake ps);

//撞自身检测
void KillBySelf(pSnake ps);

~over~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1140952.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何创建加载项(1)

《VBA高级应用30例》&#xff08;10178985&#xff09;&#xff0c;是我推出的第十套教程&#xff0c;教程是专门针对高级学员在学习VBA过程中提高路途上的案例展开&#xff0c;这套教程案例与理论结合&#xff0c;紧贴“实战”&#xff0c;并做“战术总结”&#xff0c;以便大…

智安网络|揭秘安全测试和渗透测试的异同点

随着云计算的兴起&#xff0c;企业网络安全面临着新的挑战。为了保护企业的核心资产和数据安全&#xff0c;堡垒机作为一种重要的安全设备被广泛应用。传统的堡垒机在过去几十年中发挥了重要作用&#xff0c;但如今&#xff0c;随着云堡垒机的出现&#xff0c;企业在选择合适的…

BLE基础

文章是视频笔记 蓝牙广播 教程讲义 ESP32之低功耗蓝牙&#xff08;BLE&#xff09;-小鱼创意 蓝牙信道 BLE&#xff08;Bluetooth Low Energy&#xff09;广播使用的是2.4GHz ISM频段&#xff0c;其中包含了40个信道。在BLE广播中&#xff0c;主要使用的是3个不相邻的信道&…

2023年第四届MathorCup大数据竞赛(A题)|坑洼道路检测和识别|数学建模完整代码+建模过程全解全析

当大家面临着复杂的数学建模问题时&#xff0c;你是否曾经感到茫然无措&#xff1f;作为2021年美国大学生数学建模比赛的O奖得主&#xff0c;我为大家提供了一套优秀的解题思路&#xff0c;让你轻松应对各种难题。 希望这些想法对大家的做题有一定的启发和借鉴意义。 让我们来…

普及篇|云备份和云容灾,你用对了吗?

企业常常会因为自然灾害、硬件老化故障、人为误操作等问题导致业务系统中断&#xff0c;该如何进行安全、高效的数据保护&#xff0c;这一问题引起企业越来越多的高度关注。 而对比传统的自建机房对数据进行保护&#xff0c;在云端利用云计算的弹性伸缩、按需扩展特点&#xff…

公众号迁移如何线上办理公证?

公众号账号迁移的作用是什么&#xff1f;只能变更主体吗&#xff1f;1.可合并多个公众号的粉丝、文章&#xff0c;打造超级大V2.可变更公众号主体&#xff0c;更改公众号名称&#xff0c;变更公众号类型——订阅号、服务号随意切换3.可以增加留言功能4.个人订阅号可迁移到企业名…

实现寄生组合继承

寄生组合继承是一种继承方式&#xff0c;它通过组合使用构造函数继承和原型继承的方式&#xff0c;实现了高效而且正确的继承方式。 具体实现步骤如下&#xff1a; ① 定义一个父类&#xff0c;实现其属性和方法&#xff1a; function Person(name) {this.name namethis.age…

Google play 应用下架、封号常见原因:8.3/10.3分发协议及恶意软件政策问题浅析

相信大多数谷歌Android开发者都遭遇过应用下架、账号被封的情况&#xff0c;尤其对于想通过上传马甲包、矩阵方式来获得更多收益的开发者来说&#xff0c;想必应用下架、拒审、账号被封已经是家常便饭了&#xff0c;同时也为此烦恼。 造成这种情况的原因有很多&#xff0c;且每…

腾讯云新用户优惠券领取方法及使用教程

腾讯云作为国内领先的云计算服务提供商&#xff0c;为了吸引更多的新用户&#xff0c;经常会推出各种优惠活动。其中&#xff0c;最吸引新用户的还是腾讯云优惠券&#xff0c;本文将详细介绍腾讯云新用户优惠券的领取方法及使用教程&#xff0c;助力大家轻松上云&#xff01; 一…

特殊类设计(只在堆/栈上创建对象,单例模式),完整版代码+思路

目录 类不能被拷贝 类不能被继承 只在堆上创建对象 只在栈上创建对象 operator new operator delete 只能创建一个对象 设计模式 介绍 常见的设计模式 单例模式 介绍 应用 饿汉模式 介绍 实现 思路 代码 使用 懒汉模式 引入 介绍 实现 思路 代码 使用…

【Javascript】编写⼀个函数,排列任意元素个数的数字数组,按从⼩到⼤顺序输出

目录 sort方法 两个for循环 写法一&#xff1a; 写法二&#xff1a; sort方法 var list[3,6,2,8,1,7];list.sort();console.log(list);使用sort方法有局限&#xff0c;适合元素为个位数 var list[3,6,80,100,78,4];list.sort();console.log(list);如果元素 解决方法&#xf…

VR数字党建:红色文化展厅和爱国主义教育线上线下联动

伴随着党建思想的加深&#xff0c;很多政府单位都有打造VR党建展厅的想法&#xff0c;而党建基地也是激发爱国热情、凝聚人民力量、培养民族精神的重要场所。现如今&#xff0c;伴随着5G、VR等技术的成熟&#xff0c;VR数字党建积极推动运用VR技术&#xff0c;推动红色文化展厅…

linux 实时调度实现

调度入口__schedule() 主要做了几件事: deactivate_task() -> pick_next_task() -> context_switch() pick_next_task 的实现中主要两个步骤: (IN __pick_next_task)put_prev_task_balance(rq, prev, rf);for_each_class(class) {p class->pick_next_task(rq);if…

Java 将数据导出到Excel并发送到在线文档

一、需求 现将列表数据&#xff0c;导出到excel,并将文件发送到在线文档&#xff0c;摒弃了以往的直接在前端下载的老旧模式。 二、pom依赖 <!-- redission --><dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-…

AI 引擎系列 1 - 从 AI 引擎工具开始(2022.1 更新)

AI 引擎系列 1 - 从 AI 引擎工具开始&#xff08;2022.1 更新&#xff09; AI 引擎系列简介 在这篇题为 Versal 自适应 SoC AI 引擎入门的文章中&#xff0c;我介绍了一些 Versal™ 自适应 SoC 器件中存在的 AI 引擎 (AIE) 阵列。本系列是全新的 AI 引擎系列博文&#xff0c;我…

LeetCode--2.两数相加

文章目录 1 题目描述2 解题思路2.1 代码实现 1 题目描述 给你两个 非空 的链表, 表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的, 并且每个节点只能存储 一位 数字 请你将两个数相加, 并以相同形式返回一个表示和的链表 你可以假设除了数字 0 之外, 这两个数都…

maya2023安装

1、解压压缩包&#xff0c;点击setup安装&#xff0c;除修改安装路径外&#xff0c;其他都是都是下一步&#xff0c;安装后最好重启系统 破解步骤 关闭杀毒&#xff0c;防止误删1.安装Autodesk软件&#xff0c;但是不要启动&#xff0c;安装完成后重启电脑 2.安装破解文件夹里…

图神经网络论文笔记(一)——北邮:基于学习解纠缠因果子结构的图神经网络去偏

作者 &#xff1a;范少华 研究方向 &#xff1a;图神经网络 论文标题 &#xff1a;基于学习解纠缠因果子结构的图神经网络去偏 论文链接 &#xff1a;https://arxiv.org/pdf/2209.14107.pdf        https://doi.org/10.48550/arXiv.2209.14107 大多数图神经网络(GNNs)通…

JAVA实现校园二手交易系统 开源

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 二手商品档案管理模块2.3 商品预约管理模块2.4 商品预定管理模块2.5 商品留言板管理模块2.6 商品资讯管理模块 三、实体类设计3.1 用户表3.2 二手商品表3.3 商品预约表3.4 商品预定表3.5 留言表3.6 资讯…

软考系统架构师知识点集锦四:信息安全技术基础知识

一、考情分析 二、考点精讲 2.1信息加解密技术 2.1.1对称加密 概念:对称加密(又称为私人密钥加密/共享密钥加密) : 加密与解密使用同一密钥。特点:加密强度不高&#xff0c;但效率高;密钥分发困难。 (大量明文为了保证加密效率一般使用对称加密) 常见对称密钥加密算法:DES:…