关注小庄 顿顿解馋૮(˶ᵔ ᵕ ᵔ˶)ა 新年快乐呀小伙伴
引言: 小伙伴们应该都有一个做游戏的梦吧?今天让小庄来用C语言简单实现一下我们的童年邪典贪吃蛇,顺便巩固我们的C语言知识,请安心食用~
文章目录
- 贪吃蛇效果
- 一.游戏前工作
- 🏠 控制台程序
- 🏠 光标设置
- 📌 Win32API
- 🏠 绘制地图
- 📌 控制台坐标系
- 📌 控制台坐标的设置
- 📌 墙壁图案
- 🏠 帮助信息
- 🏠 贪吃蛇整体维护
- 📌蛇的初始化
- 📌食物创建
- 📌其他内容初始化
- 二.游戏运行时工作
- 🏠 游戏逻辑
- 🏠 按键检测
- 🏠 蛇的移动
- 📌蛇是否吃到食物
- 📌蛇是否撞墙和咬到自身
- 三.游戏善后工作
- 🏠 贪吃蛇资源的释放
- 🏠 贪吃蛇游戏状态
- 🏠 拓展
贪吃蛇效果
如下是我们将实现的效果 请看vcr
一.游戏前工作
🏠 控制台程序
平时我们运行程序弹出的黑框框就是控制台程序我们平时可能不在意它的字体颜色,大小等等…我们可以根据我们的游戏进行一些调整
- 控制台程序标题
sysytem("title 贪吃蛇"); //包含头文件window.h
我们可以根据自己的需要修改标题
- 控制台程序宽度和高度
ssystem("mode con cols=xxx lines=xxx"); //cols是列长 lines是行长
我们可以根据需求自行调整游戏总界面面长宽
🏠 光标设置
在我们游戏运行时这个光标一直浮动着不太好,我们有什么办法可以隐藏呢?当然可以这里就要介绍我们的Win32API了~
📌 Win32API
什么是API?
我们发现应用程序会有开启视窗、描绘图形、使⽤周边设备等操作,那这是怎么实现的呢 ? 那就是通过我们操作系统应用程序编程接口(Application Programming Interface),这个接口的服务对象是我们的应用程序.不同的API函数能实现不同的操作.
应用程序接口的提供者是运行库,什么样的运行库提供什么样的API,比如Linux下的Glibc库提供POSIX API;Windows的运行库提供Windows API,最常见的32位Windows提供的API又被称为Win32API
GetStdHandle
GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。
HANDLE GetStdHandle(DWORD nStdHandle);//参数是标准设备
参数取值 | 含义 |
---|---|
STD_INPUT_HANDLE | 标准输入设备 |
STD_OUTPUT_HANDLE | 标准输出设备 |
STD_ERROR_HANDLE | 标准错误设备 |
理解:
不同设备可以理解为不同的门,不同门对应不同的锁,而我们的句柄相当于打开门的钥匙,有了钥匙才能破门而入探索新世界~
CONSOLE_CURSOR_INFO
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
这是一个结构体类型
,用于存储控制台光标相关信息
dwSize : 由光标填充的字符单元格的百分⽐光标外观会变化,范围从完全填充单元格到单元底部的⽔平线条,该值范围为0-100超出范围不会报错但不发生变化.
bBisible : 光标的可见性.true为可见,false为不可见.
- GetConsoleCursorInfo
这也是个API函数用于检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息.
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
//第一个参数是我们获得的设备句柄,有了句柄才能操作设备;
//第二个参数是_CONSOLE_CURSOR_INFO结构体,我们获得将获得的光标信息放进这个结构体变量里
SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性。存完光标信息我们就能重新设置光标信息了.
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//获取句柄
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
了解这几个API之后我们就能设置我们的光标了~
🏠 绘制地图
我们的地图大致是这样子
我们知道这里我们就得普及一下控制台程序的坐标系了
📌 控制台坐标系
我们的坐标系向右是x轴,从左向右增长;坐标系向下是y轴,从上至下增长
📌 控制台坐标的设置
知道了坐标系后,我们得清楚我们打印东西是得看这个光标的,从光标的位置开始打印.所以我们得知道相关知识实现"定向射靶".
控制台屏幕上的坐标COORD
COORD
是WindowsAPI中定义的⼀个结构体,表示⼀个字符在控制台屏幕上的坐标
.
SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置.
//设置光标的坐标
void SetPos(short x, short y)
{
COORD pos = { x, y };
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
}
封装成一个函数这样我们就很方便的定向打靶了
📌 墙壁图案
我们地图墙的边界都是由一个个◻组成的,包括我们的蛇身也是由一个个特殊符号组成的,这些符号能直接被打印出来吗?答案是不行,原因是这些宽字符占两个字节
我们的char只占一个字节满足不了我们的需求,所以我们需要用wchar_t 和宽字符的输⼊和输出函数以及本地化来进行字符打印
-
<locale.h>本地化
由于世界文化各有不同,不同地区采用的语言也有所不同,这时我们用C语言输出字符就会出现地区差异,而我们的locale.h头文件提供的函数就很好地为我们解决了这个问题 -
setlocale函数
char* setlocale (int category, const char* locale);
setlocale函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项,以下是部分类项
类项 | 代表意义 |
---|---|
LC_ALL | 所有类别 |
LC_TIME | strftime 和 wcsftime 函数 |
LC_CTYPE | 字符处理函数 |
LC_MONETARY | localeconv 函数返回的货币格式信息 |
第二个参数只有两个取值
1.“”
表示切换到本地模式,此时可以用本地的字符集等
“C”
表示默认的正常方式
本地化后我们就能打印本地区的一些宽字符了
- 宽字符打印
int main()
{
setlocale(LC_ALL, "");
wchar_t ch1 = L'中';
wchar_t ch2 = L'国';
wchar_t ch3 = L'★';
wchar_t ch4 = L'●';
wprintf(L"%lc\n", ch1);
wprintf(L"%lc\n", ch2);
wprintf(L"%lc\n", ch3);
wprintf(L"%lc\n", ch4);
printf("ab\n");
return 0;
}
注意: 宽字符类型前面要加一个
L
表示宽字符,打印输出用wprintf接收wchar_t类型变量,打印格式为L%lc
明白这些后我们就能绘制地图了,代码如下
void Createmap()
{
//上 从(0,0)开始宽字符是普通字符的两倍 不断加2
Setpos(0, 0);
for (int i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//下
Setpos(0, 25);
for (int i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//左
for (int i = 1; i <= 25; i++)
{
Setpos(0,i);
wprintf(L"%lc", WALL);
}
//右
for (int i = 1; i <= 25; i++)
{
Setpos(56, i);
wprintf(L"%lc", WALL);
}
}
🏠 帮助信息
我们可以在加载游戏地图前提供开始界面和帮助信息,注意设置好光标的位置,代码如下
void Welcomeinfo()
{
Setpos(37, 13);
printf("欢迎来到贪吃蛇小游戏ovo");
Setpos(37, 22);
system("pause");
Setpos(33, 13);
printf("用↑ ↓ ← →移动 shift来加速 ctrl来减速");
Setpos(37, 22);
system("pause");
system("cls");
}
大致效果如下
🏠 贪吃蛇整体维护
对于贪吃蛇的整体游戏我们有蛇睡眠时间,贪吃蛇这条蛇本身,游戏分数,食物权重等等信息,这时我们可以想到用我们的结构体来储存我们的信息,同时我们蛇可以看作是链表构成,里面存储坐标以及next指针
//游戏状态
enum Status
{
OK = 1,
ESC,//正常退出
KILL_BYWALL,//撞墙
KILL_BYSELF//自己咬到自己
};
//蛇运动的方向
enum Die
{
UP=1,
DOWN,
LEFT,
RIGHT
};
//定义蛇身结点
typedef int Datatype;
typedef struct SnakeNode
{
//储存坐标
int x;
int y;
struct SnakeNode* next;
}SnakeNode,* pSnakeNode;
//定义整个贪吃蛇要维护的内容
typedef struct Snake
{
pSnakeNode snake;//蛇
pSnakeNode pFood;//食物
int Score;//当前分数
int FoodWeight;//每个食物的权重
int sleeptime;//睡眠时间
enum Status status;//游戏状态
enum Dir dir;//蛇运动方向
}Snake,* pSnake;
📌蛇的初始化
对于蛇的初始化我们可以以链表的形式运用尾插链接蛇,值得注意的是蛇的坐标要设置成2的倍数,不然蛇蛇一半身体会卡到墙里,在创建蛇的结点的同时打印蛇身
#define BODY L'●'
#define POS_X 24
#define POS_Y 5
//初始化蛇
void InitSnake(pSnake ps)
{
//创建蛇 五个结点
pSnakeNode newhead = NULL;
pSnakeNode newtail = NULL;
for (int i = 0; i < 5; i++)
{//创建新结点并初始化化他们的坐标
pSnakeNode cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("malloc failed");
exit(1);
}
cur->x = POS_X + 2*i;//这里要这个两倍不然会发生覆盖
cur->y = POS_Y;
cur->next = NULL;
Setpos(cur->x,cur->y);
wprintf(L"%lc", BODY);
//链表为空新结点作为头结点
if (newhead == NULL)
{
newtail = newhead = cur;
}
else
{
///头插
cur->next = ps->snake;
ps->snake = cur;
}
}
//其他内容的初始化
ps->sleeptime = 200;
ps->Score = 0;
ps->FoodWeight = 10;
ps->status = OK;
ps->dir = UP;
ps->pFood = NULL;
}
📌食物创建
对于食物我们也可以看成链表的一个结点但有几点需要注意:
- 食物的坐标要是随机 可以用rand函数解决
- 食物的坐标不能与墙壁重叠且食物坐标也要是
二的倍数
保证不卡墙- 食物坐标不能与蛇身重叠 可以遍历链表进行比较不满足重新生成
void CreateFood(pSnake ps)
{
//先产生坐标
int x = 0;
int y = 0;
//横坐标要是偶数
again:
do
{
x = rand() % 53 + 2; //2-54 --> 0~52+2
y = rand() % 24 + 1 ; //1~24
} while (x%2!=0);
//不能超出墙 同时不能跟这个蛇的坐标重叠
pSnakeNode cur = ps->snake;
while (cur)
{
if (x == cur->x &&y == cur->y)
{
goto again;//与蛇重叠重新产生
}
cur = cur->next;
}
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("CreateFood()::malloc()");
return;
}
pFood->x = x;
pFood->y = y;
ps->pFood = pFood;
Setpos(x,y);
wprintf(L"%lc", FOOD);
}
📌其他内容初始化
除了蛇之外我们还有游戏状态,蛇睡眠时间以及分数等等
ps->snake = newtail;
//其他内容的初始化
ps->sleeptime = 200;
ps->Score = 0;
ps->FoodWeight = 10;
ps->status = OK;
ps->dir = UP;
ps->pFood = NULL;
二.游戏运行时工作
🏠 游戏逻辑
准备好前置工作后我们就可以着手准备我们的游戏运行工作了,首先我们需要确定游戏的大体逻辑
大体逻辑:玩家根据游戏提示按键—>根据按键判断下步该做什么工作 : 在状态OK下实现蛇的移动 pause状态则是暂停 ESC状态是正常退出 也就是说状态不是OK就是退出程序
(一个循环)
🏠 按键检测
我们在C语言中怎么检测我们的按键情况呢?这要请出我们一个新的API函数GetAsyncKeyState
GetAsyncKeyState
SHORT GetAsyncKeyState
(
int vKey
);
//将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态
返回值:
返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
所以我们可以通过检测函数返回值的最低位是否为1来检测按键情况,可以用一个宏封装
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
基于这个API函数和我们的游戏逻辑就能实现我们游戏运行基本的代码
void pause(pSnake ps)
{
while (1)
{
if (KEY_PRESS(VK_SPACE))
{
return;
}
}
}
void GameRun(pSnake ps)
{
//帮助信息
Helpinfo();
//游戏luoji
do
{
Setpos(64, 10);
printf("得分:%d ", ps->Score);
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_ESCAPE) && ps->dir != DOWN)
{
//退出
ps->status = ESC;
}
else if (KEY_PRESS(VK_SPACE) && ps->dir != DOWN)
{
//暂停
pause(ps);
}
else if (KEY_PRESS(VK_CONTROL) && ps->dir != DOWN)
{
//减速
if (ps->sleeptime < 350)
{
ps->sleeptime += 30;
ps->FoodWeight -= 2;
if (ps->sleeptime == 350)
{
ps->FoodWeight = 1;
}
}
}
else if (KEY_PRESS(VK_SHIFT) && ps->dir != DOWN)
{
//加速 就是减少睡眠时间
if (ps->sleeptime >= 50)
{
ps->sleeptime -= 30;
ps->FoodWeight += 2;
}
}
//休眠
Sleep(ps->sleeptime);
//移动
SnakeMove(ps);
} while (ps->status == OK);
}
注意:
1.要保证按键方向不冲突 2.蛇休眠时间和食物权重不能为负数
🏠 蛇的移动
接下来就是整个游戏的运行的重头戏了
📌蛇是否吃到食物
分析:
对于我们的蛇,它吃到食物蛇长就会增加,也就是结点会增多,那么我们需要判断每次执行按键命令时我们是否会吃到食物。如果吃到食物的话我们就根据移动方向新插入一个结点同时创建新食物;如果没吃到食物我们仍然插入新结点,但尾结点打印空格再释放掉
实现代码如下
int IsFood(pSnake ps,pSnakeNode cur)
{
//判断新结点是否与食物的坐标重合
if (cur->x == ps->pFood->x && cur->y == ps->pFood->y)
return 1;
else
return 0;
}
void Eatfood(pSnake ps,pSnakeNode cur)
{
//吃到食物直接头插
cur->next = ps->snake;
ps->snake = cur;
pSnakeNode del = ps->snake;
while (del)
{
Setpos(del->x, del->y);
wprintf(L"%lc", BODY);
del = del->next;
}
//增加总分
ps->Score += ps->FoodWeight;
//释放食物
free(ps->pFood);
//创建新食物
CreateFood(ps);
}
void NotEatfood(pSnake ps, pSnakeNode cur)
{
//仍然尾插
cur->next = ps->snake;
ps->snake = cur;
//最后一个不打印
pSnakeNode del = ps->snake;
while (del->next->next)
{
Setpos(del->x, del->y);
wprintf(L"%lc", BODY);
del = del->next;
}
Setpos(del->x,del->y);
wprintf(L"%lc", BODY);
//最后一个位置打印空格并释放
Setpos(del->next->x, del->next->y);
printf(" ");
free(del->next);
del->next = NULL;
}
//蛇的移动
void SnakeMove(pSnake ps)
{
//创建 新节点
pSnakeNode cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("malloc failed");
exit(1);
}
cur->next = NULL;
//根据方向确定新结点的坐标
switch (ps->dir)
{
case UP:
cur->x = ps->snake->x;
cur->y = ps->snake->y - 1;
break;
case DOWN:
cur->x = ps->snake->x;
cur->y = ps->snake->y + 1;
break;
case LEFT:
cur->x = ps->snake->x - 2;
cur->y = ps->snake->y;
break;
case RIGHT:
cur->x = ps->snake->x + 2;
cur->y = ps->snake->y;
break;
}
//是否吃到食物
if (IsFood(ps, cur))
{
//吃到食物
Eatfood(ps,cur);
}
else
{
//没吃到食物
NotEatfood(ps,cur);
}
//是否撞墙和咬到自己
Kill_ByWall();
Kill_BySelf();
}
📌蛇是否撞墙和咬到自身
在进行上下左右移动时,我们可能会咬到自己或撞墙,这时游戏就会结束了我们需要进行游戏状态的更新
void Kill_ByWall(pSnake ps)
{
pSnakeNode pcur = ps->snake;
if (pcur->x == 0 || pcur->x == 56 || pcur->y == 0 || pcur->y == 26)
{ //蛇头坐标不能等于这个墙壁的
ps->status = KILL_BYWALL;
return;
}
return;
}
void Kill_BySelf(pSnake ps)
{
//遍历链表看蛇头是不是跟蛇身每个结点的坐标相等
pSnakeNode cur = ps->snake;
while (cur)
{
if (ps->snake->x == cur->x && ps->snake->y == cur->y)
{
ps->status = KILL_BYSELF;
return;
}
cur = cur->next;
}
return;
}
三.游戏善后工作
退出循环后我们需要释放游戏资源同时根据游戏状态确定提示信息
🏠 贪吃蛇资源的释放
//释放游戏资源
pSnakeNode pcur = ps->snake;
while (pcur)
{
pSnakeNode pNext = pcur->next;
free(pcur);
pcur = pNext;
}
🏠 贪吃蛇游戏状态
//根据不同游戏状态进行提示
switch (ps->status)
{
case ESC:
Setpos(12,9);
printf("游戏正常退出..");
Setpos(12, 10);
printf("游戏总分为%d", ps->Score);
break;
case KILL_BYWALL:
Setpos(12, 9);
printf("很遗憾你撞墙了!");
Setpos(12, 10);
printf("游戏总分为%d", ps->Score);
break;
case KILL_BYSELF:
Setpos(12, 9);
printf("很遗憾你咬到自己了!");
Setpos(12, 10);
printf("游戏总分为%d", ps->Score);
break;
}
🏠 拓展
到这里我们基本上我们的游戏了,除此之外我们可以进行拓展,比如把三个过程函数封装进循环里实现多次游玩
void testSnake(void)
{
int input = 0;
do
{
Snake s;
//游戏运行前
GamePre(&s);
//游戏运行后
GameRun(&s);
//游戏
GameEnd(&s);
Setpos(0, 28);
printf("你是否想再来一把(1/0):");
scanf("%d", &input);
} while (input);
Setpos(0, 29);
}
除此之外我们可以运用win提供的颜色API该变蛇的颜色,还有可以加入背景音乐等等…
PlaySound(MAKEINTRESOURCE(IDR_WAVE1), NULL, SND_RESOURCE | SND_ASYNC);
第一个参数:资源文件–>新建–>资源文件–>Accelerator–>导入你的.wav文件
接着打开resource.h就能找到第一个参数啦
最后看下效果~
最后到这里分享内容就结束啦,小伙伴们能学到知识的话不妨来个一键三连,祝大家2024万事顺利~