首页 > 其他分享 >C语言实战项目:贪吃蛇游戏(SnakeGame)

C语言实战项目:贪吃蛇游戏(SnakeGame)

时间:2024-07-06 13:02:41浏览次数:3  
标签:cur next SnakeGame 贪吃蛇 Snake C语言 光标 snake

前言:

前面C语言的基础语法和数据结构的顺序表、链表已经学完了,我们就已经有能力去实现一个贪吃蛇项目。我们可以实现一些贪吃蛇的一些功能,例如:食物的随机生成、贪吃蛇的长度、贪吃蛇加速和减速、暂停游戏、贪吃蛇的游戏结束判定等...

如下图所示:

图片仅限参考

真实项目视频:

2024-07-04-20-46-01-CSDN直播

<iframe allowfullscreen="true" data-mediaembed="csdn" frameborder="0" id="UVVyM1bv-1720101344048" src="https://live.csdn.net/v/embed/406448"></iframe>

202407042046

贪吃蛇游戏项目会用到新的知识点WIN32 API,本章前面会讲解WIN32 API,如果您已经了解过WIN32 API 或 只想看源代码可以通过目录的贪吃蛇实现跳过WIN32 API的讲解。

1、游戏背景

贪吃蛇是久负盛名的游戏,它也是俄罗斯方块、扫雷等游戏位列经典游戏行列。

在编程语言的学习中,以贪吃蛇为例,从设计到代码实现提升大家的编程能力和逻辑能力。

2、贪吃蛇基本功能

使用C语言在Windows环境的控制台模拟实现经典小游戏贪吃蛇。

实现基本功能:

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

3、技术要点

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

4、Win32 API的介绍

本次实现贪吃蛇会使用到Win32 API的一些知识,接下来我们就学习一下。

4.1 Win32 API

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

4.2 控制台程序

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

我们可以使用cmd命令来设置控制台窗口的长宽:设置控制台窗口的大小30行、100列:

mode con cols=100 lines=30 

也可以通过命令设置控制台窗口名字:

title 贪吃蛇

我们在程序中需要一个函数来执行控制台窗口。

system这个函数就能执行系统命令,使用该函数需要包含头文件stdlib.h。这些能在控制台窗口执行的命令,也可以调用systme函数来执行:

#include <stdio.h>
#include <stdlib.h>
int main()
{
    system("mode con cols=100 lines=30");//设置窗口大小
    system("title 贪吃蛇");//设置窗口命名
    printf("hello world");//打印看一下窗口的变化
    return 0;
}
4.3 控制台窗口上的坐标COORD

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

COORD类型的声明:

typedef struct _COORD{
    SHORT x;
    SHORT y;
}COORD,*PCOORD;

给坐标赋值:

COORD pos = {10, 15};
4.4 GetStdHandle

GetStdHandle 是一个Windows API函数,它用于从一个特定的标准设备(标准输入、标准输出、标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。

HANDLE GetStdHandle(DWORD nStdHandle);

实例:

HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
含义
STD_INPUT_HANDLE标准输入设备。最初,这是输入缓冲区CONION$的控制台
STD_OUTPUT_HANDLE标准输出设备。最初,这是活动控制台屏幕缓冲区CONOUT$
STD_ERROR_HANDLE标准错误设备。最初,这是活动控制台屏幕缓冲区CONOUT$

简单理解:该函数是为了获得控制台窗口标准设备的控制权,为什么叫句柄呢?我们可以把控制台窗口的标准设备比作一口锅。而我们需要用到锅把来端起这口锅。所以这个锅把就是句柄。

上面的代码就是使用GetStdHandle函数来获取一个标准输出设备。返回类型是HANDLE可控制类型。可以理解为将标准设备封装成可控制设备并返回,然后创建一个HANDLE类型的指针变量,用来接收这个句柄,我们就可以通过该指针变量操作这个标准设备。

4.5 GetConsoleCursorInfo

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

BOOL WINAPI GetConsoleCursorInfo(
    HANDLE               hConsoleOutput,
    PCONCOLE_CURSOR_INFO lpConsoleCursorInfo
);

PCONSOLE_CURSOR_INFO   是指向CONSOLE_CURSOR_INFO结构的指针,该结构接收有关主机游标(光标)的信息

实例:

HANDLE hOutput = NULL;
//获取标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);

GetConsoleCursorInfo函数需要两个参数,一个是句柄,一个是获取主机光标的结构体变量的地址。以上代码调用GetStdHandle函数获取了一个标准输出的句柄。然后又创建了CONSOLE_CURSOR_INFO结构体类型的变量CursorInfo,用来接收光标信息。而GetConsoleCursorInfo函数则是将hOutput句柄的标准设备的光标信息存放在CursorInfo中,Cursorinfo获取到了光标信息就相当于拿到了光标控制权,可以设置光标。

4.5.1 CONSOLE_CURSOR_INFO

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

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

但是并不是这里的光标信息更改了以后光标就会被改,我们还需要一个函数,将句柄和更改后的光标信息传输过去该函数就可以通过这个光标信息来设置句柄的标准设备的光标。

4.6 SetConsoleCursorInfo

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

Bool WINAPI SetConsoleCursorInfo(
    HANDLE hConsoleOutput;
    const CONSOLE_CURSOR_INFO* lpConsoleCursorInfo
);

实例:

HANDLE hOutput = NULL;
//获取标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//接收句柄
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置光标

4.7 SetConsoleCursorPosition

设置指定控制台屏幕缓冲区中光标位置,我们将想要设置的坐标信息放在COORD类型的结构体变量pos中,调用SetConsoleCursorPosition函数将光标的位置设置到指定位置。

BOOL WINAPI SetConsoleCursorPosition(
    HANDLE hConsoleOutput,
    COORD pos
);

实例:

COORD pos = {10, 5};
//获取标准输出的句柄(用来标识不同设备的数值)
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//接收句柄
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput,pos);

我们如果频繁的更改光标位置,例如设置贪吃蛇的食物刷新或贪吃蛇走动这些都是频繁的去更改光标位置。那我们总不能每次都创建一个COORD类型变量,再获取句柄,然后再将他们传给SetConsoleCursorPosition函数,这样使得代码的维护性变差。那我们可以考虑封装一个函数SetPos将以上的更改光标位置步骤封装起来,每次调用该函数时传两个参数作为坐标调用。

void SetPos(short x,short y)
{
    COORD pos = {x, y};
    //获取标准输出的句柄(用来标识不同设备的数值)
    HANDLE hOutput = NULL;
    hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//接收句柄
    CONSOLE_CURSOR_INFO CursorInfo;
    //设置标准输出上光标的位置为pos
    SetConsoleCursorPosition(hOutput,pos);
}

4.8 GetAsyncKeyState

获取按键情况,GetAsyncKeyState的函数原型如下:

SHORT GetAsyncKeyState(
     int vKey
);

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

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

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

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

上、下、左、右键的虚拟键码:

常数Value说明
VK_LEFT0x25LEFT  ARROW  键
VK_UP0x26UP  ARROW  键
VK_RIGHT0x27RIGHT  ARROW  键
VK_DOWN0x28DOWN  ARROW  键

实例:检测数字键

#include <stdio.h>
#include <Windows.h>
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)& 0x1) ? 1 : 0)
int main()
{
    while (1)
    {
       if (KEY_PRESS(VK_UP))
       {
           printf("↑\n");
       }
       else if (KEY_PRESS(VK_LEFT))
       {
           printf("←\n");
       }
       else if (KEY_PRESS(VK_DOWN))
       {
           printf("↓\n");
       }
       else if (KEY_PRESS(VK_RIGHT))
       {
         printf("→\n");
       }
    }

    return 0;
}

5、贪吃蛇的设计与分析

5.1 地图

我们最终的贪吃蛇大纲是这个样子,那地图如何布置呢?

我们需要布置的地图大概就是下图这个样子,这里不得不讲一下控制台窗口的坐标,横轴为x,纵轴为y。也就是说x表示列,y表示行。所以使用设置光标的坐标位置函数需要注意了,第一个参数表示的是列,第二个参数表示的是行,千万不要搞反了。

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

普通的字符是占一个字节的,这类宽字符是占用2个字节。

这里再简单的讲一下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。C语言最初假定字符都是单字节的。但是这些假定并不是在世界任何地方都适用。

C语言字符默认是采用ASCII编码的,ASCII字符集采用的是单字节编码,且只使用了单字节中的低7位,最高位是没有使用的,可表示为0xxxxxxx; 可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,比如,在法语中,字母上方有注音符号,它就无法用ASCII 码表示。于是一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来。这些欧洲国家使用的编码体系,可以表示最多256个符号。但是,这里有出现了新的问题。不同的国家有不同的字符,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法国编码中代表了é,在希伯来语编码中缺点了字母Gimel,在俄语编码中又会代表另一个符号,不管怎样,所有这些编码方式中,0-127表示的符号是一样的,不一样的知识128-255的这一段。

至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示256 x 256 = 65536个符号。

后来为了使C语言适应国际化,C语言的标准中不断加入国际化的支持。比如:加入了宽字符类型wchar_t 和宽字符的输入和输出函数,加入了<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。

5.1.1 <locale.h>本地化

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

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

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

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

  • LC_COLLATE:影响字符串比较函数strcoll( )strxfrm( ) 
  • LC_CTYPE:影响字符处理函数的行为。
  • LC_MONETARY:影响货币格式。
  • LC_NUMERIC:影响printf( ) 的数字格式。
  • LC_TIME:影响时间格式 strftime( ) wcsftime( ) 
  • LC_ALL - 针对所有类型修改,将以上所有类别设置为给定的语言环境。

5.1.3 setlocale函数
char* setlocale(int category, const char* locale);

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

setlocale 的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有类型。

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

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

setlocale(LC_ALL, "C");

当地区设置为"C"时,库函数按正常方式执行,小数点是一个点。

当程序运行起来后项改变地区,就只能显示调用setlocale函数。用" "作为第2个参数,调用setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。

setlocale(LC_ALL, " ");//切换到本地环境

如果想要更加了解setlocale函数的话可以参考cplusplus网站的setlocale的介绍。

setlocale - C++ 参考 (cplusplus.com)

5.1.4 宽字符的打印 

那如果想在屏幕上打印宽字符,怎么打印呢?

宽字符的字面量必须加上前缀"L",否则 C 语言会把字面量当作窄字符类型处理。前缀"L"在单引号前面,表示宽字符,定义 wprintf( ) 的占位符为 %lc ; 在双引号前面,表示宽字符串,对应wprintf( ) 的占位符为 %ls

#include <stdio.h>
#include <locale.h>

int main()
{
    setlocale(LC_ALL, "");//设置本地模式
    //注意,setlocale设置本地模式只需要传一对双引号,双引号中间千万不要加空格
    //否则宽字符输出为乱码
    wchar_t ch1 = L'●';
    wchar_t ch2 = L'林';
    wchar_t ch3 = L'麓';
    wchar_t ch4 = L'★';

    printf("%c%c\n", 'a', 'b');

    wprintf(L"%lc\n", ch1);
    wprintf(L"%lc\n", ch2);
    wprintf(L"%lc\n", ch3);
    wprintf(L"%lc\n", ch4);
    return 0;
}

输出结果:

从输出结果来看,我们发小一个普通字符占一个字符的位置

但是打印一个汉字字符,占用2个字符的位置,那么我们如果要在贪吃蛇中使用宽字符,就得处理好地图上的坐标计算。

5.1.5 地图坐标

我们假设实现一个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改)再围绕地图画出墙,如下图:

5.2 蛇身和食物

初识化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现蛇,连续5个节点。注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半出现在墙体中,另外一半在墙外的现象,坐标不好对齐。

关于食物,就是在墙体内随机生成一个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。

5.3 数据结构设计

在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信息,那么蛇的每一节其实就是链表的每个节点。每个节点只有记录好蛇身节点在地图上的坐标就行,所以蛇节点结构如下:

typedef struct SnakeNode
{
    int x;
    int y;
    struct SnakeNode next;//指向蛇身的下一个节点
}SnakeNode,*pSnakeNode;

要管理整条贪吃蛇,我们需要再封装一个Snake的结构来维护整条贪吃蛇:

enum DIRECTION
{
   UP = 1,
   DOWN,
   LEFT,
   RIGHT
}

enum GAME_STATUS
{
   OK,  //正常运行
   END_NORMAL,  //正常结束
   KILL_BY_WALL,  //撞墙游戏结束
   KILL_BY_SELF  //咬到自身游戏结束
}

typedef struct Snake
{
    pSnakeNode _pSnake;  //维护整条蛇的指针
    pSnakeNode -pFood;   //维护食物的指针
    enum DIRECTION _Dir;  //蛇的方向,默认向右
    enum GAME_STATUS _Status;  //游戏状态
    int _FoodWeight;  //一个食物的分数
    int _Score;  //贪吃蛇累计的总分
    int _SleepTime;  //贪吃蛇移速
}Snake,*pSnake;
5.4 游戏设计

贪吃蛇大致分为三个模块,分别是GameStart(游戏开始) / GameRun(游戏允许) / GameEnd(游戏结束)

  • GameStart模块:初始化贪吃蛇数据、设置控制台窗口大小、创建地图、创建食物。
  • GameRun模块:贪吃蛇的移动方向、移动速度。还有判定是否吃到了食物、是否咬到自身、是否撞到了墙。不同的判定有不同的解决方案。
  • GameEnd模块:判断是因为什么原因结束贪吃蛇游戏的,并打印出原因。

完整代码:

Snake.h 游戏各个模块的声明、自定义类型声明和宏定义:

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

#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define POS_X 26
#define POS_Y 10
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1)?1:0)

typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;

enum DIRECTION
{
	UP = 1,
	LEFT,
	DOWN,
	RIGHT
};

enum GAME_STATUS
{
	OK,
	END_NOMAL,
	KILL_BY_WALL,
	KILL_BY_SELF
};

typedef struct Snake
{
	pSnakeNode _Snake;  //维护贪吃蛇的指针
	pSnakeNode _Food;  //维护食物的指针
	enum DIRECTION _Dir;  //蛇的方向
	enum GAME_STATUS _Status;  //游戏当前状态
	int _FoodWeight;  //食物得分
	int SleepTime;  //移动速度
	int _Score;  //游戏总分
}Snake,*pSnake;

//游戏开始 - 贪吃蛇数据等初始化
void GameStart(pSnake snake);

//打印游戏开始界面
void WelcomeToGame();

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

//创建地图
void CreateMap();

//初始化贪吃蛇
void InitSnake(pSnake snake);

//创建食物节点
void CreateFood(pSnake snake);

//游戏运行 - 贪吃蛇开始移动/吃食物等操作
void GameRun(pSnake snake);

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

//游戏暂停
void space();

//贪吃蛇移动
void SnakeMove(pSnake snake);

//判断是否吃到了食物
int Food(pSnake snake, pSnakeNode next);

//吃到了食物,就加一个节点
void EatFood(pSnake snake, pSnakeNode next);

//未吃到食物就正常移动
void NoFood(pSnake snake, pSnakeNode next);

//撞墙判定
void Kill_by_Wall(pSnake snake);

//判定是否咬到自身
void Kill_by_Self(pSnake snake);

//游戏结束 - 判断状态是因为什么原因导致结束的
void GameEnd(pSnake snake);

Snake.c 游戏模块的实现

#include "Snake.h"

//设置光标位置
void SetPos(short x, short y)
{
	//通过pos设置句柄handle控制的设备光标的位置
	COORD pos = { x,y };
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(handle, pos);
}

//打印欢迎界面
void WelcomeToGame()
{
	SetPos(40, 14);
	printf("欢迎来到贪吃蛇游戏");
	SetPos(40, 20);
	system("pause");//按任意键才继续执行下面程序
	system("cls");
	SetPos(35, 14);
	printf("使用↑ . ↓ . ← . →.来操控蛇的方向");
	SetPos(35, 15);
	printf("通过s3加速,s4减速");
	SetPos(40, 20);
	system("pause");
	system("cls");
}

//创建地图
void CreateMap()
{
	int i = 0;
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	SetPos(0, 26);
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	for (i = 1; i < 26; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	for (i = 1; i < 26; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
}

//初始化贪吃蛇
void InitSnake(pSnake snake)
{
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		pSnakeNode Node = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (Node == NULL)
		{
			perror("InitSnake()::malloc()");
			return;
		}
		Node->x = POS_X + i * 2;
		Node->y = POS_Y;
		Node->next = NULL;
		if (snake->_Snake == NULL)
		{
			snake->_Snake = Node;
		}
		else
		{
			Node->next = snake->_Snake;
			snake->_Snake = Node;
		}
	}
	//打印蛇的身体
	pSnakeNode cur = snake->_Snake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//贪吃蛇的数据初始化
	snake->_Dir = RIGHT;
	snake->_Status = OK;
	snake->_Score = 0;
	snake->SleepTime = 200;
	snake->_FoodWeight = 10;
}

//创建食物节点
void CreateFood(pSnake snake)
{
	int x, y;
	again:
	do{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);
	pSnakeNode cur = snake->_Snake;
	while (cur)
	{
		if (cur->x == x && cur->y == y)
			goto again;
		cur = cur->next;
	}
	pSnakeNode Food = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (Food == NULL)
	{
		perror("CreateFood()::malloc()");
		return;
	}
	Food->x = x;
	Food->y = y;
	Food->next = NULL;
	snake->_Food = Food;
	SetPos(Food->x, Food->y);
	wprintf(L"%lc", FOOD);
}

void GameStart(pSnake snake)
{
	//控制台窗口设置
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");
	//光标隐藏
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(handle, &CursorInfo);
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(handle, &CursorInfo);
	//打印进入游戏的界面
	WelcomeToGame();
	//创建地图
	CreateMap();
	//初始化贪吃蛇
	InitSnake(snake);
	//创建食物节点
	CreateFood(snake);
}

//打印帮助信息
void PrintHelpInfo()
{
	SetPos(60, 12);
	printf("↑ . ↓ . ← . →.来操控蛇的方向");
	SetPos(60, 13);
	printf("点击空格SPACE暂停游戏,点击ESC退出游戏");
	SetPos(60, 14);
	printf("F3加速,F4减速");
	SetPos(60, 15);
	printf("撞墙游戏结束");
	SetPos(60, 20);
	printf("@林麓出品,必属精品");
}

//游戏暂停
void space()
{
	SetPos(35, 23);
	while (1)
	{
		Sleep(100);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

//判断是否吃到了食物
int Food(pSnake snake, pSnakeNode next)
{
	if (snake->_Food->x == next->x && snake->_Food->y == next->y)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}

//如果吃到了食物,就加一个节点
void EatFood(pSnake snake, pSnakeNode next)
{
	next->next = snake->_Snake;
	snake->_Snake = next;
	pSnakeNode cur = snake->_Snake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	snake->_Score += snake->_FoodWeight;
	free(snake->_Food);//释放原先的食物节点
	CreateFood(snake);//重新创造新的食物节点
}

//未吃到食物就正常移动
void NoFood(pSnake snake, pSnakeNode next)
{
	next->next = snake->_Snake;
	snake->_Snake = next;
	pSnakeNode cur = snake->_Snake;
	while (cur->next->next)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	SetPos(cur->x, cur->y);
	wprintf(L"%lc", BODY);
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;
}

//撞墙判定
void Kill_by_Wall(pSnake snake)
{
	if (snake->_Snake->x == 0 ||
		snake->_Snake->x == 56 ||
		snake->_Snake->y == 0 ||
		snake->_Snake->y == 26)
		snake->_Status = KILL_BY_WALL;
}

//判定是否咬到自身
void Kill_by_Self(pSnake snake)
{
	pSnakeNode cur = snake->_Snake->next;
	while (cur)
	{
		if (snake->_Snake->x == cur->x && snake->_Snake->y == cur->y)
		{
			snake->_Status = KILL_BY_SELF;
			break;
		}
		cur = cur->next;
	}
}

//贪吃蛇移动
void SnakeMove(pSnake snake)
{
	pSnakeNode next = (pSnakeNode)malloc(sizeof(SnakeNode));
	next->next = NULL;
	switch (snake->_Dir)
	{
	case UP:
		next->x = snake->_Snake->x;
		next->y = snake->_Snake->y - 1;
		break;
	case DOWN:
		next->x = snake->_Snake->x;
		next->y = snake->_Snake->y + 1;
		break;
	case LEFT:
		next->x = snake->_Snake->x - 2;
		next->y = snake->_Snake->y;
		break;
	case RIGHT:
		next->x = snake->_Snake->x + 2;
		next->y = snake->_Snake->y;
		break;
	}
	if (Food(snake, next))
	{
		EatFood(snake,next);//吃到了食物的情况
	}
	else
	{
		NoFood(snake, next);//未吃到食物的情况,正常移动
	}
	Kill_by_Wall(snake);
	Kill_by_Self(snake);
}

//游戏运行
void GameRun(pSnake snake)
{
	PrintHelpInfo();
	do
	{
		SetPos(60, 9);
		printf("当前总分为:%0d ", snake->_Score);
		printf("每个食物的分数为:%d分",snake->_FoodWeight);
		if (KEY_PRESS(VK_UP) && snake->_Dir != DOWN)
		{
			snake->_Dir = UP;
		}
		else if (KEY_PRESS(VK_LEFT) && snake->_Dir != RIGHT)
		{
			snake->_Dir = LEFT;
		}
		else if (KEY_PRESS(VK_DOWN) && snake->_Dir != UP)
		{
			snake->_Dir = DOWN;
		}
		else if (KEY_PRESS(VK_RIGHT) && snake->_Dir != LEFT)
		{
			snake->_Dir = RIGHT;
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			snake->_Status = END_NOMAL;
			break;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			space();
		}
		else if (KEY_PRESS(VK_F3))
		{
			if (snake->SleepTime >= 60)
			{
				snake->SleepTime -= 20;
				snake->_FoodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			if (snake->_FoodWeight > 2)
			{
				snake->SleepTime += 20;
				snake->_FoodWeight -= 2;
			}
		}
		Sleep(snake->SleepTime);
		SnakeMove(snake);//贪吃蛇自动移动
	} while (snake->_Status == OK);
}

//判断游戏为什么结束
void GameEnd(pSnake snake) 
{
	switch (snake->_Status)
	{
	case END_NOMAL:
		SetPos(35, 5);
		printf("已退出本局游戏\n");
		break;
	case KILL_BY_WALL:
		SetPos(35, 5);
		printf("你撞到墙了,游戏结束\n");
		break;
	case KILL_BY_SELF:
		SetPos(35, 5);
		printf("你咬到自身了,游戏结束\n");
		break;
	}
}

test.c 调用模块

#include "Snake.h"

void game()
{
	int ch = 0;
	do
	{
		Snake snake = { 0 };
		//游戏开始 - 贪吃蛇数据等初始化
		GameStart(&snake);
		//游戏运行 - 贪吃蛇开始移动/吃食物等操作
		GameRun(&snake);
		//游戏结束 - 判断状态是因为什么原因导致结束的
		GameEnd(&snake);
		SetPos(35, 23);
		printf("是否要再来一局?(Y/N)");
		ch = getchar();
		getchar();//用来接收换行符
	} while (ch == 'Y' || ch == 'y');
}
int main()
{
	setlocale(LC_ALL, "");//设置本地模式
	srand((unsigned int)time(NULL));//食物是要随机刷新的,需要更改rand种子
	game();
	return 0;
}

标签:cur,next,SnakeGame,贪吃蛇,Snake,C语言,光标,snake
From: https://blog.csdn.net/2302_78977491/article/details/140123508

相关文章

  • 24.【C语言】getchar putchar的使用
    1.基本作用 用户输入字符,getchar()获取字符(含\n:即键入的Enter)(字符本质上是以ASCII值或EOF(-1)存储的)(与scanf有区别)putchar()打印字符(把得到的ASCII值转换成字符)(相当于printf)由于getcharputchar只操作字符,因此执行效率高例:#include<stdio.h>intmain(){intch=......
  • 使用WebSocket和C语言实现一个简单的计算器
    在现代Web开发中,WebSocket已经成为实时通信的重要工具。本文将介绍如何使用WebSocket与C语言结合,实现一个简单的计算器应用。我们将通过Go语言作为中间层,调用C语言编写的计算函数,并通过WebSocket与前端进行交互。在使用本文章代码开发过程中遇到问题,可参考博主的另外两篇博客......
  • C语言命名规范
    C语言命名规范在C语言中,命名规范对于代码的可读性和可维护性至关重要。以下是一些常见的C语言命名规律和建议变量命名变量名应该具有描述性,清晰地表达变量的用途或含义。变量名使用小写字母和下划线(snake_case)的组合,例如intmy_variable;。避免使用单个字符作为变量名,除非......
  • C语言字节对齐技术在嵌入式、网络与操作系统中的应用与优化
    第一部分:嵌入式系统中的字节对齐嵌入式系统通常对性能和资源有着严格的要求。在这些系统中,字节对齐的正确使用可以显著提高数据访问速度,减少内存占用,并提高系统的整体效率。一、嵌入式系统中的字节对齐挑战嵌入式系统中的微处理器和微控制器通常对数据访问的对齐有特定的要......
  • C语言笔记28 •顺序表经典算法OJ题•
    1.删除数组中指定的元素//算法实现intremoveElement(int*nums,intnumsSize,intval){   intsrc=0;//nums[src]==valsrc++   intdst=0;///nums[src]!=valsrc++ dst++   while(src<numsSize)   {      if(nums[src]==va......
  • 7.5复习C语言
    7.5复习C语言地址传参和值传参的区别1、地址传参是指将函数调用时实参的地址或指针作为形参传递给函数,函数内对形参所指向的内存空间进行操作会改变实参的值也会影响其他使用该实参的地方。2、值传参是指将函数调用时实参的值复制价给形参函数内对形参进行操作不会影响实参的值......
  • 【C语言题目】34.猜凶手
    文章目录作业标题作业内容2.解题思路3.具体代码作业标题猜凶手作业内容日本某地发生了一件谋杀案,警察通过排查确定杀人凶手必为4个嫌疑犯的一个。以下为4个嫌疑犯的供词:A说:不是我。B说:是C。C说:是D。D说:C在胡说已知3个人说了真话,1个人说的是假话。现在请......
  • 【C语言习题】33.杨氏矩阵
    文章目录作业标题作业内容2.解题思路3.具体代码作业标题杨氏矩阵作业内容有一个数字矩阵,矩阵的每行从左到右是递增的,矩阵从上到下是递增的,请编写程序在这样的矩阵中查找某个数字是否存在。要求:时间复杂度小于O(N);2.解题思路我们仔细分析,不难发现,对于杨氏......
  • 【C语言习题】32.字符串旋转结果
    文章目录作业标题作业内容2.解题思路3.具体代码作业标题字符串旋转结果作业内容写一个函数,判断一个字符串是否为另外一个字符串旋转之后的字符串。例如:给定s1=AABCD和s2=BCDAA,返回1给定s1=abcd和s2=ACBD,返回0.AABCD左旋一个字符得到ABCDAAABCD左旋两个字......
  • 【LinuxC语言】手撕Http协议之accept_request函数实现(一)
    文章目录前言accept_request函数作用accept_request实现解析方法根据不同方法进行不同操作http服务器响应格式unimplemented函数实现总结前言在计算机网络中,HTTP协议是一种常见的应用层协议,它定义了客户端和服务器之间如何进行数据交换。在这篇文......