用C语言实现贪吃蛇游戏!!!(破万字)

前言

大家好呀,我是Humble,不知不觉在CSND分享自己学过的C语言知识已经有三个多月了,从开始的C语言常见语法概念说到C语言的数据结构今天用C语言实现贪吃蛇已经有30余篇博客的内容,也希望这些内容可以帮助到各位正在阅读的小伙伴~

本次贪吃蛇项目是Humble继扫雷,通讯录之后分享的第三个项目,大约有五六百行代码的量~

而且不出意外,这应该是HumbleC语言部分的最后一篇博客了,在这之后,Humble未来的规划是分享C++的相关语法内容,C++实现的数据结构与算法,计算机网络,操作系统以及数据库的相关知识,希望未来也请各位多多关照~

好了,因为是一篇具有特殊意义的博客,好像前言说的有些多了(笑),接下来废话不多说,直接进入我们今天的贪吃蛇项目的内容把~

游戏演示

关于贪吃蛇这个游戏,我相信各位并不陌生,小伙伴们也跟我一样在童年的时候都玩过吧~

那么在正式制作之前,先给大家看一下这个即将由我们自己制作的贪吃蛇游戏的演示 ,让各位先对它有个整体的了解之后,之后的制作才会更加容易~

当我们运行起来后,首先看到的是两个初始化界面

然后下面是正式开始游戏的画面

围起来的部分是地图,蛇的初始长度被设置为5个节点,通过按键操作吃掉食物会获得相应得分,同时蛇的节点会增加,其余规则可看界面右方给的文字说明

当我们撞到墙或者自身或者按ESC键自己退出时,会显示游戏结束的界面:

以上就是对这个贪吃蛇小游戏简单的一次总揽,更多细节上的东西我们放在下面再讲

目标

本篇博客的目标:

使用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇
实现基本的功能:

1.初始化以及地图的创建

2.蛇与食物的初始化

3.蛇吃食物

4.撞墙检测

5.装自身检测

6.蛇的移动,加速,减速

7.暂停功能

8.计算得分

....

技术要点

本次贪吃蛇项目涉及的技术要点主要有一下七点:

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

前面5点都是Humble在之前的博客都有分享过的,各位小伙伴可以访问我的主页去阅读哦

今天在讲贪吃蛇这个项目之前,我先对Win32 API  做个介绍 ,对其进行了解才能更丝滑的写接下来的贪吃蛇项目哦~

Win32 API

一.Win32 API概述

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

WIN32API也就是Microsoft Windows32位平台的应用程序编程接口
 

当然,这里大家不要太在乎这个32位平台,我们就把它统称为WIN32 API

二.控制台程序(Console)

控制台程序其实就是平常我们程序运行起来出现的界面

我们可以设置控制台窗口的长宽:设置控制台窗口的大小,30行,100列

mode con cols=100 lines=30

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

title 贪吃蛇

这些命令,我们可以调用C语言函数system来执行。

代码如下:

#include <windows.h>
int main()
{
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
return 0;
}


 

三.控制台屏幕上的坐标COORD

COORD 是Windows API中定义的一个结构体(具体内容我们可以不必深究,主要是为下面的几个函数服务的,咱们会用就行~),表示一个字符在控制台屏幕上的坐标
 

给坐标赋值:COORD pos = { 10, 15 };

四.GetStdHandle

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

它的定义如下:

HANDLE GetStdHandle(DWORD nStdHandle);//返回类型是HANDLE

//括号里只能是标准输入、标准输出或标准错误,这些都有对应的单词,比如标准输出写为STD_OUTPUT_HANDLE

我们直接看一下它怎么用吧~举个例子:
 

HANDLE hOutput = NULL;//我们用一个HANDLE的变量接收函数返回值
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//括号里是标准输出

好,掌握了GetStdHandle()之后,接下来就很简单了,我们看一下下面的几个函数把,使用它们可以帮我们实现对光标和虚拟键的操作

五.GetConsoleCursorInfo

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

实例:

HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出的句柄CONSOLE_CURSOR_INFO CursorInfo;//以CONSOLE_CURSOR_INFO类型定义一个变量~
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息

这里出现了一个CONSOLE_CURSOR_INFO,是我们前面没出现的,也给大家简单介绍一下~

CONSOLE_CURSOR_INFO,它是结构体,包含有关控制台光标的信息

它的结构如下:

typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO;

dwSize,由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完全填充单元格到单元底部的水平线条(这个我们今天用不到)
bVisible,游标的可见性。如果光标可见,则此成员为true,我们接下来要实现的贪吃蛇要隐藏光标,所以要将bVisible设置成false,所以要这样写:

CONSOLE_CURSOR_INFO CursorInfo;

CursorInfo.bVisible = false; //隐藏控制台光标

因为上面的对光标的操作只有使用SetConsoleCursorInfo后才能真正被修改,所以下面来看一下SetConsoleCursorInfo,

六.SetConsoleCursorInfo

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

它有两个参数,这里也不过多赘述,直接看代码:

HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息CursorInfo.bVisible = false; //隐藏控制台光标SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态

经过SetConsoleCursorInfo 函数后,上面对光标的隐藏操作也就在控制台上得以实现啦~

七.SetConsoleCursorPosition

SetConsoleCursorPosition:调用这个函数将光标位置设置到指定的位置
 

代码如下:
 

COORD pos = { 10, 5};//我们将想要设置的坐标信息放在COORD类型的pos中HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);SetConsoleCursorPosition(hOutput, pos);//设置标准输出上光标的位置为pos

当然,为了之后写贪吃蛇代码时使用的更加方便,我们可以将这段代码封装成一个函数SetPos

这样我们之后想改变光标到我们想要的位置就会更加得心应手了~

void SetPos(int x, int y)
{COORD pos = { x, y};HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);SetConsoleCursorPosition(hOutput, pos);}

最后我们再看一个函数GetAsyncKeyState,它是用来实现游戏的交互,即获取按键情况的~

八.GetAsyncKeyState

当我们电脑上的按键被出发时,键盘上每个键的虚拟键值会传递给函数,函数通过返回值来分辨按键的状态


在上一次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中最高位是1,说明按键的状态是按下如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明该按键被按过,没按过的话则为0
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1

我们可以用#define定义:
 

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

当然,关于VK虚拟键的值,在Win API 的官网都有说明,我们可以去翻阅来获得自己想找的值~

说完了Win API及接下来会用到的这些函数的知识,下面我们进入贪吃蛇游戏的设计与分析吧~

贪吃蛇游戏设计与分析

一.地图

前面在演示的时候,我们看过游戏的地图长这个样子~

无论是打印墙体使用的字符□,还是打印蛇使用的字符●,或者打印食物使用的字符★

它们都是名为宽字符的存在

那么什么是宽字符?它与普通字符有什么区别?

其实,从大小上看。普通的字符是占一个字节的,这类宽字符是占用2个字节的
 

这点很重要,因为我们的地图是建立在坐标上的,宽字符的x轴占了2格,而y轴只占1格,我们要考虑好这点,再去设计比如 :地图的大小以及蛇与食物随机生成的坐标的规定等等

知道了这个,我们看一看宽字符是怎么打印的吧~

宽字符的打印

#include <stdio.h
#include<locale.h>int main() 
{
setlocale(LC_ALL, "");
//这个setlocale函数以及它包含的头文件locale.h是为了将模式切换成本地环境,大家直接用就可以,不用深究~wchar_t ch1 = L'●';
wchar_t ch2 = L'原';
wchar_t ch3 = L'神';
wchar_t ch4 = L'★';printf("%c%c\n", 'a', 'b');
wprintf(L"%c\n", ch1);//wprintf是用来打印宽字符的,w即wide,宽的意思
wprintf(L"%c\n", ch2);//宽字符的打印要在%前加上L,别忘了哦~
wprintf(L"%c\n", ch3);
wprintf(L"%c\n", ch4);
return 0;
}

打印结果如下:

我们发现一个普通字符占一个字符的位置.但是打印一个宽字符,占用2个字符的位置

地图坐标

我们假设实现一个棋盘27行,58列的棋盘,再围绕地图画出墙

如下图:

二.蛇身和食物

初始化状态,假设蛇的长度是5,蛇身的每个节点是 ●     ,在固定的一个坐标处开始出现
注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半出现在墙体中,另外⼀般在墙外的现象,坐标不好对⻬
关于食物,就是在墙体内随机生成一个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印 ★
 

三.数据结构设计

在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信
,那么蛇的每一节其实就是链表的每个节点。

每个节点只要记录好蛇身节点在地图上的坐标就行


所以蛇节点结构如下:

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


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

typedef struct Snake
{
pSnakeNode _pSnake;//维护整条蛇的指针
pSnakeNode _pFood;//维护食物的指针
enum DIRECTION _Dir;//蛇头的方向
enum GAME_STATUS _Status;//游戏状态
int _Socre;//当前获得分数
int _foodWeight;//默认每食物10分
int _SleepTime;//每⾛一步休眠时间
}Snake, * pSnake;


蛇的方向,可以一一列举,使用枚举:

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


游戏状态,可以一一列举,使用枚举:

//游戏状态
enum GAME_STATUS
{OK,//正常运行KILL_BY_WALL,//撞墙KILL_BY_SELF,//咬到自己END_NOMAL//正常结束
};


 

四.游戏流程设计

关于游戏流程设计,Humble做了一张思维导图供大家参考~:

核心逻辑实现分析

我们打开VS,创建一个Snake的贪吃蛇工程

在其中创建3个文件:test.c测试文件,snake.c源文件,snake.h头文件

在test.c中我们写游戏的主逻辑:

#define  _CRT_SECURE_NO_WARNINGS
#include "snake.h"void test()
{int ch = 0;srand((unsigned int)time(NULL));do{//设置控制台的信息,窗口大小,窗口名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);Snake snake = { 0 };//创建贪吃蛇GameStart(&snake);GameRun(&snake);GameEnd(&snake);SetPos(20, 15);//设置光标的位置printf("再来一局吗?(Y/N):");ch = getchar();getchar();//清理\n} while (ch == 'Y');SetPos(0, 27);
}int main()
{//修改适配本地中文环境setlocale(LC_ALL, "");test();//贪吃蛇游戏的测试return 0;
}

GameStart

我们先看游戏开始GameStart这个函数,它包含五个函数:


void GameStart(pSnake ps)
{WelcomeToGame();//打印欢迎信息CreateMap();	//绘制地图InitSnake(ps);	//初始化蛇CreateFood(ps);//创建食物PrintHelpInfo();//打印右侧帮助信息
}

先看第一个:打印欢迎信息WelcomeToGame

void WelcomeToGame()
{//欢迎信息SetPos(35, 10);printf("欢迎来到贪吃蛇小游戏\n");SetPos(38, 20);system("pause");system("cls");//清空之前的界面//功能介绍信息SetPos(15, 10);printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");SetPos(15, 11);printf("加速能得到更高的分数");SetPos(38, 20);system("pause");system("cls");
}


 

接下来是创建地图 CreateMap

#define WALL L'□' //宽字符的定义前面要加上L
void CreateMap()
{int i = 0;//上SetPos(0, 0);for (i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//下SetPos(0, 25);for (i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//左for (i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右for (i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);}}

下面是初始化蛇InitSnake

蛇最开始为5节,每节对应链表的一个节点,蛇的每一个节点都自己的坐标
创建5个节点,然后将每个节点存放在链表中进行管理创建完蛇身后,将蛇的每一节打印在屏幕上
再设置当前游戏的状态,蛇移动的速度,默认的方向(向右),初始成绩,蛇的状态,每个食物的分数

 

 #define BODY L'●'  void InitSnake(pSnake ps)
{pSnakeNode cur = NULL;int i = 0;//创建蛇身节点,并初始化坐标//头插法for (i = 0; i < 5; i++){//创建蛇身的节点cur = (pSnakeNode)malloc(sizeof(SnakeNode));if (cur == NULL){perror("InitSnake()::malloc()");return;}//设置坐标cur->next = NULL;cur->x = POS_X + i * 2;cur->y = POS_Y;//头插法if (ps->_pSnake == NULL){ps->_pSnake = cur;}else{cur->next = ps->_pSnake;ps->_pSnake = cur;}}//打印蛇的身体cur = ps->_pSnake;while (cur){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}//初始化贪吃蛇数据ps->_SleepTime = 200;ps->_Socre = 0;ps->_Status = OK; ps->_Dir = RIGHT;ps->_foodWeight = 10;
}

GameStart包含的第四个子函数是:CreateFood

随机生成食物的坐标

下面有一些限制:
 x坐标必须是2的倍数
食物的坐标不能和蛇身每个节点的坐标重复

#define FOOD L'★' //食物打印的宽字符
void CreateFood(pSnake ps)
{int x = 0;int y = 0;
again://产生的x坐标应该是2的倍数,这样才可能和蛇头坐标对齐do{x = rand() % 53 + 2;y = rand() % 25 + 1;} while (x % 2 != 0);pSnakeNode cur = ps->_pSnake;//获取指向蛇头的指针//食物不能和蛇身冲突while (cur){if (cur->x == x && cur->y == y){goto again;}cur = cur->next;}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);wprintf(L"%c", FOOD);ps->_pFood = pFood;}
}


GameStart的最后一个函数是PrintHelpInfo,负责打印文字信息

void PrintHelpInfo()
{SetPos(64, 15);printf("不能穿墙,不能咬到自己\n");SetPos(64, 16);printf("用↑.↓.←.→分别控制蛇的移动.");SetPos(64, 17);printf("F3 为加速,F4 为减速\n");SetPos(64, 18);printf("ESC :退出游戏. 空格键:暂停游戏.");SetPos(64, 20);
}


 

GameRun

下面来看一下游戏运行的函数GameRun吧~

void pause()//暂停
{while (1){Sleep(300);if (KEY_PRESS(VK_SPACE))//空格键break;}
}void GameRun(pSnake ps)
{do{SetPos(64, 10); //每次进来都要对食物分数进行修正printf("得分:%d ", ps->_Socre);printf("每个食物得分:%d分", ps->_foodWeight);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_LEFT) && ps->_Dir != RIGHT){ps->_Dir = LEFT;}else if (KEY_PRESS(VK_RIGHT) && ps->_Dir != LEFT){ps->_Dir = RIGHT;}else if (KEY_PRESS(VK_SPACE)){pause();}else if (KEY_PRESS(VK_ESCAPE)){ps->_Status = END_NOMAL;break;}else if (KEY_PRESS(VK_F3)){if (ps->_SleepTime >= 50){ps->_SleepTime -= 30;ps->_foodWeight += 2;}}else if (KEY_PRESS(VK_F4)){if (ps->_SleepTime < 350){ps->_SleepTime += 30;ps->_foodWeight -= 2;if (ps->_SleepTime == 350){ps->_foodWeight = 1;}}}//蛇每次一定之间要休眠的时间,时间短,蛇移动速度就快Sleep(ps->_SleepTime);SnakeMove(ps);} while (ps->_Status == OK);

虽然这个是三个函数最复杂的部分,但我们也是一个一个点,按照设计的流程来~

关于虚拟键检测按键状态,我们封装一个宏~

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

下面是蛇身的移动,这个很关键~

先创建下一个节点,根据移动方向和蛇头的坐标,蛇移动到下⼀个位置的坐标
确定了下⼀个位置后,判断下一个位置是否是食物:NextIsFood

是食物就做:EatFood,如果不是食物:NoFood
蛇身移动后,判断此次移动是否会造成撞墙:KillByWall或者撞上自己:KillBySelf
 

void SnakeMove(pSnake ps)
{//创建下一个节点pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));//确定下一个节点的坐标,下一个节点的坐标根据,蛇头的坐标和方向确定switch (ps->_Dir){case UP:{pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y - 1;}break;case DOWN:{pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y + 1;}break;case LEFT:{pNextNode->x = ps->_pSnake->x - 2;pNextNode->y = ps->_pSnake->y;}break;case RIGHT:{pNextNode->x = ps->_pSnake->x + 2;pNextNode->y = ps->_pSnake->y;}break;}if (NextIsFood(pNextNode, ps)){EatFood(pNextNode, ps);}else//如果没有食物{NoFood(pNextNode, ps);}KillByWall(ps);KillBySelf(ps);
}

int NextIsFood(pSnakeNode psn, pSnake ps)
{return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}

void EatFood(pSnakeNode psn, pSnake ps)
{//头插法psn->next = ps->_pSnake;ps->_pSnake = psn;pSnakeNode cur = ps->_pSnake;//打印蛇while (cur){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}ps->_Socre += ps->_foodWeight;free(ps->_pFood);CreateFood(ps);
}

 //将下⼀个节点头插⼊蛇的⾝体,并将之前蛇⾝最后⼀个节点打印为空格,弃掉蛇身的最后⼀个节点
//pSnakeNode psn 是下⼀个节点的地址
//pSnake ps 维护蛇的指针
void NoFood(pSnakeNode psn, pSnake ps)
{//头插法psn->next = ps->_pSnake;ps->_pSnake = psn;pSnakeNode cur = ps->_pSnake;//打印蛇while (cur->next->next){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}//最后一个位置打印空格,然后释放节点SetPos(cur->next->x, cur->next->y);printf("  ");free(cur->next);cur->next = NULL;
}

int 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;return 1;}return 0;
}

int 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;return 1;}cur = cur->next;}return 0;
}

呼,终于将最核心的逻辑搞定了~最后来看看游戏结束的函数GameEnd吧~

GameEnd

游戏状态不再是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);}
}

到这,我们整个贪吃蛇游戏的功能就全部实现了(鼓掌鼓掌)

最后是Humble的参考代码,分3个文件

参考代码

test.c

#define  _CRT_SECURE_NO_WARNINGS
#include "snake.h"void test()
{int ch = 0;srand((unsigned int)time(NULL));do{Snake snake = { 0 };//创建贪吃蛇//设置控制台的信息,窗口大小,窗口名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);GameStart(&snake);GameRun(&snake);GameEnd(&snake);SetPos(20, 15);//设置光标的位置printf("再来一局吗?(Y/N):");ch = getchar();getchar();//清理\n} while (ch == 'Y');SetPos(0, 27);
}int main()
{//修改适配本地中文环境setlocale(LC_ALL, "");test();//贪吃蛇游戏的测试return 0;
}

snake.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <stdbool.h>
#include <locale.h>
#include <time.h>//宽字符的定义
#define WALL L'□'
#define BODY L'●' 
#define FOOD L'★'//蛇的初始位置
#define POS_X 24
#define POS_Y 5//虚拟键
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)//方向
enum DIRECTION
{UP = 1,DOWN,LEFT,RIGHT
};//游戏状态
enum GAME_STATUS
{OK,//正常运行KILL_BY_WALL,//撞墙结束KILL_BY_SELF,//咬到自己结束END_NOMAL//按键结束
};//蛇身节点
typedef struct SnakeNode
{int x;int y;struct SnakeNode* next;
}SnakeNode, * pSnakeNode;typedef struct Snake
{pSnakeNode  _pSnake;//维护整条蛇的指针pSnakeNode  _pFood;//维护食物的指针enum DIRECTION  _Dir;//蛇头的方向enum GAME_STATUS  _Status;//游戏状态int  _Socre;//当前获得分数int  _foodWeight;//默认每个食物10分int  _SleepTime;//每走一步休眠时间
}Snake, * pSnake;//设置光标的坐标
void SetPos(int x, int y);//欢迎界面
void WelcomeToGame();//创建地图
void CreateMap();//初始化蛇
void InitSnake(pSnake ps);//创建食物
void CreateFood(pSnake ps);//打印右侧界面的文字说明
void PrintHelpInfo();//暂停响应
void pause();//下一个节点是食物
int NextIsFood(pSnakeNode psn, pSnake ps);//吃食物
void EatFood(pSnakeNode psn, pSnake ps);//不吃食物
void NoFood(pSnakeNode psn, pSnake ps);//撞墙检测
int KillByWall(pSnake ps);//撞自身检测
int KillBySelf(pSnake ps);//蛇的移动
void SnakeMove(pSnake ps);//游戏开始
void GameStart(pSnake ps);//游戏运行
void GameRun(pSnake ps);//游戏结束
void GameEnd(pSnake ps);

snake.c

#define  _CRT_SECURE_NO_WARNINGS
#include "snake.h"//设置光标的位置的函数
void SetPos(int x, int y)
{HANDLE hanlde = GetStdHandle(STD_OUTPUT_HANDLE);	//获得设备句柄COORD pos = { x, y };	//根据句柄设置光标的位置SetConsoleCursorPosition(hanlde, pos);
}void WelcomeToGame()
{//欢迎信息SetPos(35, 10);printf("欢迎来到贪吃蛇小游戏\n");SetPos(38, 20);system("pause");system("cls");//清空之前的界面//功能介绍信息SetPos(15, 10);printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");SetPos(15, 11);printf("加速能得到更高的分数");SetPos(38, 20);system("pause");system("cls");
}void CreateMap()
{int i = 0;//上SetPos(0, 0);for (i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//下SetPos(0, 25);for (i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//左for (i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右for (i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);}}void InitSnake(pSnake ps)
{pSnakeNode cur = NULL;int i = 0;//创建蛇身节点,并初始化坐标//头插法for (i = 0; i < 5; i++){//创建蛇身的节点cur = (pSnakeNode)malloc(sizeof(SnakeNode));if (cur == NULL){perror("InitSnake()::malloc()");return;}//设置坐标cur->next = NULL;cur->x = POS_X + i * 2;cur->y = POS_Y;//头插法if (ps->_pSnake == NULL){ps->_pSnake = cur;}else{cur->next = ps->_pSnake;ps->_pSnake = cur;}}//打印蛇的身体cur = ps->_pSnake;while (cur){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}//初始化贪吃蛇数据ps->_SleepTime = 200;ps->_Socre = 0;ps->_Status = OK; ps->_Dir = RIGHT;ps->_foodWeight = 10;
}void CreateFood(pSnake ps)
{int x = 0;int y = 0;
again://产生的x坐标应该是2的倍数,这样才可能和蛇头坐标对齐do{x = rand() % 53 + 2;y = rand() % 25 + 1;} while (x % 2 != 0);pSnakeNode cur = ps->_pSnake;//获取指向蛇头的指针//食物不能和蛇身冲突while (cur){if (cur->x == x && cur->y == y){goto again;}cur = cur->next;}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);wprintf(L"%c", FOOD);ps->_pFood = pFood;}
}void PrintHelpInfo()
{SetPos(64, 15);printf("不能穿墙,不能咬到自己\n");SetPos(64, 16);printf("用↑.↓.←.→分别控制蛇的移动.");SetPos(64, 17);printf("F3 为加速,F4 为减速\n");SetPos(64, 18);printf("ESC :退出游戏. 空格键:暂停游戏.");SetPos(64, 20);
}void pause()//暂停
{while (1){Sleep(300);if (KEY_PRESS(VK_SPACE))//空格键break;}
}int NextIsFood(pSnakeNode psn, pSnake ps)
{return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}void EatFood(pSnakeNode psn, pSnake ps)
{//头插法psn->next = ps->_pSnake;ps->_pSnake = psn;pSnakeNode cur = ps->_pSnake;//打印蛇while (cur){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}ps->_Socre += ps->_foodWeight;free(ps->_pFood);CreateFood(ps);
}void NoFood(pSnakeNode psn, pSnake ps)
{//头插法psn->next = ps->_pSnake;ps->_pSnake = psn;pSnakeNode cur = ps->_pSnake;//打印蛇while (cur->next->next){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}//最后一个位置打印空格,然后释放节点SetPos(cur->next->x, cur->next->y);printf("  ");free(cur->next);cur->next = NULL;
}int 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;return 1;}return 0;
}int 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;return 1;}cur = cur->next;}return 0;
}void SnakeMove(pSnake ps)
{//创建下一个节点pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));//确定下一个节点的坐标,下一个节点的坐标根据,蛇头的坐标和方向确定switch (ps->_Dir){case UP:{pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y - 1;}break;case DOWN:{pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y + 1;}break;case LEFT:{pNextNode->x = ps->_pSnake->x - 2;pNextNode->y = ps->_pSnake->y;}break;case RIGHT:{pNextNode->x = ps->_pSnake->x + 2;pNextNode->y = ps->_pSnake->y;}break;}if (NextIsFood(pNextNode, ps)){EatFood(pNextNode, ps);}else//如果没有食物{NoFood(pNextNode, ps);}KillByWall(ps);KillBySelf(ps);
}void GameStart(pSnake ps)
{WelcomeToGame();//打印欢迎信息CreateMap();	//绘制地图InitSnake(ps);	//初始化蛇CreateFood(ps);//创建食物PrintHelpInfo();//打印右侧帮助信息
}void GameRun(pSnake ps)
{do{SetPos(64, 10);printf("得分:%d ", ps->_Socre);printf("每个食物得分:%d分", ps->_foodWeight);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_LEFT) && ps->_Dir != RIGHT){ps->_Dir = LEFT;}else if (KEY_PRESS(VK_RIGHT) && ps->_Dir != LEFT){ps->_Dir = RIGHT;}else if (KEY_PRESS(VK_SPACE)){pause();}else if (KEY_PRESS(VK_ESCAPE)){ps->_Status = END_NOMAL;break;}else if (KEY_PRESS(VK_F3)){if (ps->_SleepTime >= 50){ps->_SleepTime -= 30;ps->_foodWeight += 2;}}else if (KEY_PRESS(VK_F4)){if (ps->_SleepTime < 350){ps->_SleepTime += 30;ps->_foodWeight -= 2;if (ps->_SleepTime == 350){ps->_foodWeight = 1;}}}//蛇每次一定之间要休眠的时间,时间短,蛇移动速度就快Sleep(ps->_SleepTime);SnakeMove(ps);} while (ps->_Status == OK);//游戏状态是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);}
}

结语

好了,今天的这个贪吃蛇游戏的实现分享就到这里了

作为C语言部分的最后一篇博客(大概),虽然心情复杂,但不知道该说些什么

总之还是那句话:在学习编程的道路上Humble与各位同行,加油吧各位!

希望大家动动小手帮我点个免费的赞或者关注(感谢感谢),也欢迎大家订阅我的专栏呀~

让我们在接下来的时间里一起成长,一起进步吧!

1d8bd2383fe54a7aa576bdd8d41dc462.png

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Flink实现数据写入MySQL

先准备一个文件里面数据有&#xff1a; a, 1547718199, 1000000 b, 1547718200, 1000000 c, 1547718201, 1000000 d, 1547718202, 1000000 e, 1547718203, 1000000 f, 1547718204, 1000000 g, 1547718205, 1000000 h, 1547718210, 1000000 i, 1547718210, 1000000 j, 154771821…

基于51单片机智能电子秤

实物显示效果&#xff1a; https://www.bilibili.com/video/BV1Wb4y1A7Aw/?vd_source6ff7cd03af95cd504b60511ef9373a1d 功能介绍&#xff1a; &#xff08;1&#xff09;用键盘设计单价&#xff1b; &#xff08;2&#xff09;称重后同时显示该物品的重量、单价和总额&…

记签名机制

签名过程&#xff1a; 首先将数据源通过摘要算法获取到数字摘要 对数字摘要用私钥进行加密得到签名 将原始消息 以及签名发送给消息接收方 接收方用公钥解密得到数字摘要 用同样的摘要算法将原始消息进行计算 比较得到的数字摘要与解密后的是否一致 Android学习笔记——Androi…

【精选推荐】3款强大的API渗透测试工具

1免责声明 请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;作者不为此承担任何责任。工具来自网络&#xff0c;安全性自测。 2前言 给大家介绍三款优秀的…

智能GPT图书管理系统(SpringBoot2+Vue2)、接入GPT接口,支持AI智能图书馆

☀️技术栈介绍 ☃️前端主要技术栈 技术作用版本Vue提供前端交互2.6.14Vue-Router路由式编程导航3.5.1Element-UI模块组件库&#xff0c;绘制界面2.4.5Axios发送ajax请求给后端请求数据1.2.1core-js兼容性更强&#xff0c;浏览器适配3.8.3swiper轮播图插件&#xff08;快速实…

VR拍摄+制作

1.VR制作需要的图片宽高是2:1&#xff0c;需要360✖️180的图片&#xff0c;拍摄设备主要有两种&#xff1a; 1&#xff09;通过鱼眼相机拍摄&#xff0c;拍摄一组图片&#xff0c;然后通过PTGui来合成(拍摄复杂) 2&#xff09;全景相机&#xff0c;一键拍摄直接就能合成需要的…

C# Graphics对象学习

Graphics对象用于进行绘制&#xff1b; 从哪个对象获取的Graphics&#xff0c;然后进行绘制&#xff0c;就绘制到该对象上&#xff1b; 从位图获取Graphics&#xff0c;然后进行绘制&#xff0c;绘制到该位图上&#xff1b; 从某个控件获取Graphics&#xff0c;然后绘制&…

智慧文旅:提升旅游体验与推动经济发展的新动力

一、智慧文旅的定义与意义 智慧文旅&#xff0c;即智慧文化旅游&#xff0c;是一种以当地特色文化元素为核心驱动&#xff0c;利用现代科技手段实现旅游景区全面智慧升级的旅游模式。其意义在于为游客提供高效便捷的旅游信息化服务&#xff0c;提升旅游体验&#xff0c;同时推…

蓝桥杯备战——6.串口通讯

1.分析原理图 由上图我们可以看到串口1通过CH340接到了USB口上&#xff0c;通过串口1我们就能跟电脑进行数据交互。 另外需要注意的是STC15F是有两组高速串口的&#xff0c;而且可以切换端口。 2.配置串口 由于比赛时间紧&#xff0c;我们最好不要去现场查寄存器手册&#x…

Unity应用在车机上启动有概率黑屏的解决方案

问题描述 最近将游戏适配到车机上&#xff08;Android系统&#xff09;&#xff0c;碰到了一个严重bug&#xff0c;启动的时候有概率会遇到黑屏&#xff0c;表现就是全黑&#xff0c;无法进入Unity的场景。 经过查看LogCat日志&#xff0c;也没有任何报错&#xff0c;也没有任…

Python网络爬虫分步走之 – 第一步:什么是网络爬虫?

Python网络爬虫分步走之第一步&#xff1a;什么是网络爬虫&#xff1f; Web Scraping in Python Step by Step – 1st Step, What is Web Crawler? By JacksonML 1. 什么是网络爬虫&#xff1f; 在能够使用Google搜索引擎的场合&#xff0c;你是否尝试过简单搜索&#xff…

经典目标检测YOLO系列(三)YOLOV3的复现(1)总体网络架构及前向处理过程

经典目标检测YOLO系列(三)YOLOV3的复现(1)总体网络架构及前向处理过程 和之前实现的YOLOv2一样&#xff0c;根据《YOLO目标检测》(ISBN:9787115627094)一书&#xff0c;在不脱离YOLOv3的大部分核心理念的前提下&#xff0c;重构一款较新的YOLOv3检测器&#xff0c;来对YOLOv3有…

Go 命令行解析 flag 包之快速上手

本篇文章是 Go 标准库 flag 包的快速上手篇。 概述 开发一个命令行工具&#xff0c;视复杂程度&#xff0c;一般要选择一个合适的命令行解析库&#xff0c;简单的需求用 Go 标准库 flag 就够了&#xff0c;flag 的使用非常简单。 当然&#xff0c;除了标准库 flag 外&#x…

vue3预览pdf文件的几种方法

文章目录 vue3预览pdf集中方法方法一&#xff1a;方法二&#xff1a;展示效果&#xff1a;需要包依赖&#xff1a;代码&#xff1a; 方法三&#xff1a;展示效果&#xff1a;需要包依赖&#xff1a;代码&#xff1a;自己调参数&#xff0c;选择符合自己的 vue3预览pdf集中方法 …

蓝桥杯备战——7.DS18B20温度传感器

1.分析原理图 通过上图我们可以看到DS18B20通过单总线接到了单片机的P14上。 2.查阅DS18B20使用手册 比赛的时候是会提供DS18B20单总线通讯协议的代码&#xff0c;但是没有提供读取温度数据的代码&#xff0c;所以还是需要我们去查看手册&#xff0c;我只把重要部分截下来了 …

一款强大的矢量图形设计软件:Adobe Illustrator 2023 (AI2023)软件介绍

Adobe Illustrator 2023 (AI2023) 是一款强大的矢量图形设计软件&#xff0c;为设计师提供了无限创意和畅行无阻的设计体验。AI2023具备丰富的功能和工具&#xff0c;让用户可以轻松创建精美的矢量图形、插图、徽标和其他设计作品。 AI2023在界面和用户体验方面进行了全面升级…

基于LLaMA-Factory的微调记录

文章目录 数据模型准备基于网页的简单微调基于网页的简单评测基于网页的简单聊天 LLaMA-Factory是一个非常好用的无代码微调框架&#xff0c;不管是在模型、微调方式还是参数设置上都提供了非常完备的支持&#xff0c;下面是对微调全过程的一个记录。 数据模型准备 微调时一般…

Dockerfile里ADD * 保留原来的目录结构

1、问题 给新模块写Dockerfile&#xff0c;很多静态资源分散在各个目录&#xff0c;于是Dockerfile里我直接一句&#xff1a; ADD ./* /dest/镜像出来后&#xff0c;启动容器&#xff0c;进入容器种后发现&#xff1a;文件拷贝成功&#xff0c;但原来的目录结构都不在了&…

SAP EXCEL上传如何实现指定读取某一个sheet页(ALSM_EXCEL_TO_INTERNAL_TABLE)

如何读取指定的EXCEL sheet 页签&#xff0c;比如要读取下图中第二个输出sheet页签 具体实现方法如下&#xff1a; 拷贝标准的函数ALSM_EXCEL_TO_INTERNAL_TABLE封装成一个自定义函数ZCALSM_EXCEL_TO_INTERNAL_TABLE 在自定义函数导入参数页签新增一个参数SHEET_NAME 在源代码…

云原生离线工作流编排利器 -- 分布式工作流 Argo 集群

作者&#xff1a;庄宇 在现代的软件开发和数据处理领域&#xff0c;批处理作业&#xff08;Batch&#xff09;扮演着重要的角色。它们通常用于数据处理&#xff0c;仿真计算&#xff0c;科学计算等领域&#xff0c;往往需要大规模的计算资源。随着云计算的兴起&#xff0c;阿里…