目录
前言
学习本篇之前建议将上一篇的关于《常用Win32 API的简单介绍》也打开......,同时此篇过长使用电脑观看效果更佳
贪吃蛇游戏设计与分析
设计目标:
设计思想:
坐标问题:
我们想在控制台的窗⼝中的指定位置输出我们想要的东西(墙体、食物、蛇、提示信息),我们得知道该位置的坐标,关于控制台窗口的坐标我们做出如下规定:
横向的是X轴,从左向右依次增⻓,纵向是Y轴,从上到下依次增⻓
字符问题:
此外,我们在打印这些想要输出的信息时将会使⽤一些”宽字符“:
打印墙体使用宽字符:□
打印蛇使⽤宽字符:●
打印⻝物使⽤宽字符:★
小拓展: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的这⼀段
宽字符数据类型:
wchar_t
:宽字符类型,用于表示一个宽字符。wint_t
:宽整数类型,用于表示一个宽字符或特殊值WEOF
。宽字符字符串函数:
wprintf()
:用于格式化输出宽字符字符串到标准输出。wscanf()
:用于从标准输入读取宽字符数据。wcslen()
:计算宽字符字符串的长度。wcscpy()
:将一个宽字符字符串复制到另一个宽字符字符串。wcsncpy()
:将指定数量的宽字符从一个宽字符字符串复制到另一个宽字符字符串。wcscat()
:将一个宽字符字符串连接到另一个宽字符字符串的末尾。wcsncat()
:将指定数量的宽字符连接到一个宽字符字符串的末尾。宽字符输入输出函数:
getwchar()
:从标准输入读取一个宽字符。putwchar()
:将一个宽字符输出到标准输出。fgetwc()
:从指定的文件流读取一个宽字符。fputwc()
:将一个宽字符写入到指定的文件流。fwscanf()
:从指定的文件流读取宽字符数据。fwprintf()
:将格式化的宽字符字符串写入到指定的文件流。
此外,还加⼊了<locale.h>头⽂件,它是C语言标准库中的一个头文件,提供了与本地化相关的函数和类型定义。本地化是指根据不同的地区和语言环境,对程序进行适应和定制,以便正确显示日期、时间、货币、数字格式等,比如:
英语环境下的日期和钱:10/24/2023 $
中文环境下的日期和钱:2023/10/24 ¥
<locale.h>本地化头文件:
• 数字量的格式• 货币量的格式• 字符集• ⽇期和时间的表⽰形式
类项
• 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"(正常模式)还是“ ”(本地模式)
#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;
}
![](https://i-blog.csdnimg.cn/blog_migrate/c1483673095407c7ae7f733bcd03bf7e.png)
宽字符打印:
#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;
}
![](https://i-blog.csdnimg.cn/blog_migrate/4c45f0995d431b5e795b695ea82e96b0.png)
棋盘坐标:
🐍和🍖:
初始化🐍:
⻓度为5,蛇⾝的每个节点是●,在固定的⼀个坐标处,⽐如(24, 5)处开始出现,连续5个节点
初始化🍖:
在墙体内随机⽣成⼀个坐标,然后打印★
数据结构的设计:
//定义用与创建结点的链表
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指针指向该结构体
//定义反应蛇运行⽅向的枚举类型
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:
- 设置游戏窗口大小
- 设置窗口名字
- 隐藏屏幕光标
- 打印欢迎界面
- 创建地图
- 初始化蛇身
- 创建食物
游戏运行函数-GameRun:
- 右侧打印帮助信息
- 打印当前分数和食物分数
- 按键获取情况
- 根据按键情况移动蛇
- 步骤2到步骤4循环,直到游戏为结束状态
游戏结束函数-GameEnd:
- 告知游戏结束原因
- 释放蛇身结点
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~