用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与各位同行,加油吧各位!
希望大家动动小手帮我点个免费的赞或者关注(感谢感谢),也欢迎大家订阅我的专栏呀~
让我们在接下来的时间里一起成长,一起进步吧!
相关文章:
用C语言实现贪吃蛇游戏!!!(破万字)
前言 大家好呀,我是Humble,不知不觉在CSND分享自己学过的C语言知识已经有三个多月了,从开始的C语言常见语法概念说到C语言的数据结构今天用C语言实现贪吃蛇已经有30余篇博客的内容,也希望这些内容可以帮助到各位正在阅读的小伙伴…...
uniapp 使用echarts做折线图条形图。
提前10天把中烟活动做完了,以为能打酱油到除夕那天,结果又要做什么数据看板,方便烟草领导过年查看数据,还只给5天时间,真实压榨剥削啊,下辈子再也不‘拍黄片’了,不!下份工作我就转前…...
美易平台:诺基亚四季度财报超预期
正文: 近日,诺基亚发布了其四季度财报,显示调整后营业利润达到了8.46亿欧元,超出市场预估的7.627亿欧元。同时,调整后每股收益(EPS)为0.10欧元,符合市场预期。这一成绩表明诺基亚在…...
大数据学习之Flink算子、了解(Source)源算子(基础篇二)
Source源算子(基础篇二) 目录 Source源算子(基础篇二) 二、源算子(source) 1. 准备工作 2.从集合中读取数据 可以使用代码中的fromCollection()方法直接读取列表 也可以使用代码中的fromElements()方…...
抖去推短视频矩阵系统+实景无人直播系统技术源头开发
抖去推爆款视频生成器,通过短视频矩阵、无人直播,文案引流等,打造实体商家员工矩阵、用户矩阵、直播矩阵,辅助商家品牌曝光,团购转化等多功能赋能商家拓客引流。 短视频矩阵通俗来讲就是批量剪辑视频和批量发布视频&a…...
【机器学习】一文读懂统计学与机器学习的区别。
统计学与机器学习的区别 1、机器学习2、统计学3、统计学与机器学习异同性3.1 差异性3.2 相似性 4、总结 1、机器学习 关于机器学习,我想大家都很熟悉,这里我再简单唠叨一些 机器学习是人工智能的一个子领域,主要关注如何通过算法使计算机系统…...
燃烧的指针(二)
🌈个人主页:小田爱学编程 🔥 系列专栏:c语言从基础到进阶 🏆🏆关注博主,随时获取更多关于c语言的优质内容!🏆🏆 😀欢迎来到小田代码世界~ &#x…...
【工具使用-Everything】everything只能搜到文件夹,无法搜到文件
一,问题现象 everything搜索时,只能搜索到文件夹,无法搜索到文件夹下的文件。 二,问题原因 everything搜索设置问题,设置为"文件夹"导致 三,解决方法 将搜索选项设置为“所有”即可&#x…...
手写rpc和redis
rpc框架搭建 consumer 消费者应用 provider 提供的服务 Provider-common 公共类模块 rpc 架构 service-Registration 服务发现 nacos nacos配置中心 load-balancing 负载均衡 redis-trench 手写redis实现和链接 package com.trench.protocol;import com.trench.enumUtil.Redis…...
Unity动画桢事件
1,使用原因 在新项目内部审核的时候,说什么动画节奏不匹配,所以决定用动画桢事件来处理技能释放。当释放技能的时候,先播放技能动画,然后再动画桢所在的时间戳执行技能的逻辑。 2,具体实现 1,…...
搭建Redis集群
一 应用场景 为什么需要redis集群? 当主备复制场景,无法满足主机的单点故障时,需要引入集群配置。 一般数据库要处理的读请求远大于写请求 ,针对这种情况,我们优化数据库可以采用读写分离的策略。我们可以部 署一台…...
C语言sizeof 不是函数吗?
一、问题 sizeof 怎么⽤,它不是函数吗? 二、解答 sizeof 在 C 和 C 中不是一个函数,而是一个运算符。它在编译时计算其操作数所占用的内存大小,并返回一个大小(字节数),这个结果是类型或表达式…...
Mybatis的XML配置
MyBatis 是一个持久层框架,通过 XML 配置文件来定义 SQL 映射和结果的映射规则。以下是关于 MyBatis XML 配置文件的详细说明: 基本结构: XML 配置文件通常包含 <mapper>、<resultMap>、<typeAliases> 等元素。 2. mappe…...
Oracle报错:ORA-08002: sequence CURRVAL is not yet defined in this session
问题 直接查询序列的当前值,然后报了这个错误。 SELECT HR.EMPLOYEES_SEQ.CURRVAL; ORA-08002: sequence CURRVAL is not yet defined in this session解决 ORA-08002错误是Oracle数据库中的一个常见错误,它表示在当前会话中未定义序列的CURRVAL值。这…...
python10-Python的字符串之拼接字符串
如果直接将两个字符串紧挨着写在一起,Python就会自动拼接它们,例如如下代码。 s1 "软件测试划水老师傅,"软件测试老痞print(s1) 上面代码将会输出: 软件测试划水老师傅,软件测试老痞 上面这种写法只是书写字符串的一…...
华为三层交换机之基本操作
Telnet简介 Telnet是一个应用层协议,可以在Internet上或局域网上使用。它提供了基于文本的远程终端接口,允许用户在本地计算机上登录到远程计算机,然后像在本地计算机上一样使用远程计算机的资源。Telnet客户端和服务器之间的通信是通过Telnet协议进行的…...
IntelliJ IDEA 快捷键大全
IntelliJ IDEA 快捷键大全 一、文本编辑二、构建、编译项目 一、文本编辑 CtrlN 查找类 CtrlN 查找文件 CtrlF 查找文本 可以根据需求去选择红框内的选项 CtrlX 剪切 剪切选中文本,如果未选中则剪切当前行CtrlC 复制 复制选中文本,如果未选中则复制当前…...
scrapy的概念作用和工作流程
1. scrapy的概念 Scrapy是一个Python编写的开源网络爬虫框架。它是一个被设计用于爬取网络数据、提取结构性数据的框架。 Scrapy 使用了Twisted[twɪstɪd]异步网络框架,可以加快我们的下载速度。 Scrapy文档地址:http://scrapy-chs.readthedocs.io/zh_…...
首页热卖推荐商品显示axios异步请求数据动态渲染实现
flex-wrap属性: 默认情况下,项目都排在一条线(又称“轴线”)上。flex- wrap属性定义,如果一条轴线 排不下,如何换行? flex-wrap:wrap 该样式用于设置 换行。 .product_name{white-space: nowrap…...
【C++11并发】mutex 笔记
简介 在多线程中往往需要访问临界资源,C11为我们提供了mutex等相关类来保护临界资源,保证某一时刻只有一个线程可以访问临界资源。主要包括各种mutex,他们的命名大都是xx_mutex。以及RAII风格的wrapper类,RAII就是一般在构造的时…...
洛谷 P5635 【CSGRound1】天下第一
原址链接 P5635 【CSGRound1】天下第一 先看标签 搜索?模拟?用不着这么复杂 创建函数a(int x,int y,int p) a(int x,int y,int p){if(x<0){return 1;}x (xy)%p;if(y<0){return 2;}y (xy)%p;return a(x,y,p); }写入主函数 #include<iostrea…...
如何通过Navicat远程访问宝塔面板安装的MySQL数据库
Navicat报错信息: 错误代码 1045 Access denied for user ‘root’’219.144.205.81’ (using password:YES) —— 没有权限的访问的报错 1.宝塔面板 > 放行端口:3306 2.阿里云安全组 > 放行端口:3306 3.配置mysql3306端口 4.使用Xshell 链接服务器 m…...
【硅谷甄选】导航守卫(进度条,网页标题,路由鉴权)
import setting from /setting import router from /router // 任意路由切换实现进度条业务: nprogress插件 import nprogress from nprogress // js插件在ts中的报错 // 引入进度条样式 import nprogress/nprogress.css // 表示在加载进度条时不显示加载小图标 np…...
OpenHarmony—TypeScript到ArkTS约束说明
对象的属性名必须是合法的标识符 规则:arkts-identifiers-as-prop-names 级别:错误 在ArkTS中,对象的属性名不能为数字或字符串。通过属性名访问类的属性,通过数值索引访问数组元素。 TypeScript var x { name: x, 2: 3 };c…...
蓝桥杯——每日一练(简单题)
题目 有n个整数,使前面各数顺序向后移m个位置,最后m个数变成前面m个数。写一函数:实现以上功能,在主函数中输入n个数和输出调整后的n个数。 解析 一、list()函数配合map()函数获得…...
css设置不可点击
文章目录 一、前言二、MDN三、使用四、注意五、总结六、最后 一、前言 在网页开发中,经常会遇到一种情况,就是需要将某个元素的点击事件屏蔽,使其在用户点击时没有任何反应。这时候,我们可以通过CSS的pointer-events属性设置为no…...
fastapi学习
fastapi框架 fastapi,一个用于构建 API 的现代、快速(高性能)的web框架。 fastapi是建立在Starlette和Pydantic基础上的,Pydantic是一个基于Python类型提示来定义数据验证、序列化和文档的库。Starlette是一种轻量级的ASGI框架/工…...
【代码随想录-数组】长度最小的子数组
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学习,不断总结,共同进步,活到老学到老导航 檀越剑指大厂系列:全面总结 jav…...
多表查询练习题
student表: score表: 向student表插入记录: 向score表插入记录: 1.查询student表的所有记录 SELECT * FROM student;2.查询student表的第2条到4条记录 SELECT * FROM student LIMIT 1,3;3.从student表查询所有学生的学号(id)、姓名(name&…...
SpringBoot之时间数据前端显示格式化
背景 在实际我们通常需要在前端显示对数据操作的时间或者最近的更新时间,如果我们只是简单的使用 LocalDateTime.now()来传入数据不进行任何处理那么我们就会得到非常难看的数据 解决方式: 1). 方式一 在属性上加上注解,对日期进行格式…...
营销型网站建设需要多少钱/深圳防疫措施优化
获取当前文件夹下的所有空文件夹路径信息 1usingSystem;2usingSystem.Drawing;3usingSystem.Collections;4usingSystem.ComponentModel;5usingSystem.Windows.Forms;6usingSystem.Data;7usingSystem.IO;89namespace删除空文件夹10{ 11 /**//// <summary> 12 /// Fo…...
wordpress if include/郑州网站建设外包
这是我STM32F103C8T6开发笔记专栏的一部分。可以到专栏中查阅更多内容。STM32F103C8T6开发笔记整理zhuanlan.zhihu.com【写在前面】:这篇笔记的内容,一是要实现STM32中串口的收发功能,二是要解析来自上位机发送的命令,从而控制单…...
wordpress自定义用户头像/汕头疫情最新消息
基本上每个WordPress主题文件都包含一个header.php文件,“header.php” 是一个全局性的文件(WordPress产生的每个页面都会包含header.php里的代码),显示页面的题头(header)和导航菜单,还包含了 HTML文件的h…...
做旅游网站毕设任务书/免费注册个人网站
实现学生信息管理功能模块 1、编写查询全部学生记录功能 编写get_all_students()、display_all_students()函数 启动程序,查看效果 2、编写增加学生记录功能 编写add_student()函数代码 启动程序,查看效果 3.编写删除学生记录 运行程序࿰…...
展示型网站制作公司/百度问答seo
郑重承诺:林哥团队15年分析经验,为大家做推荐,只为能和大家分享自己看好的比赛。希望大家多多关注,底部扫码添加好友,获取更多推荐。公推雷霆VS红队红队赢指没有命中,红队仅以两分优势险胜雷霆赢球输指。NB…...
兰亭集势的网站平台建设/域名解析查询工具
为了让jmeter的结果报告更有内容,往往需要提供测试时服务器端的性能数据 我知道的有几种方式1. jmeter的perfmon插件 我只知道它可以在GUI下运行,如果非GUI是什么效果?同时它也需要在服务器上安装agent2. nmon监控数据 运行…...