文章目录
一、游戏设计与分析
这个游戏代码我们分成三个文件来完成。
-
Snake.h —— 游戏的头文件
这个文件中包含游戏代码实现所需要的结构体定义以及函数声明。 -
Snake.c —— 游戏的源文件
这个文件中是实现游戏操作的代码。 -
test.c —— 游戏的测试文件
这个文件是用来检验代码能否正常运行。
1、地图
我们最终的贪吃蛇⼤纲要是这个样⼦,那我们的地图如何布置呢?
这⾥不得不讲⼀下控制台窗⼝的⼀些知识,如果想在控制台的窗⼝中指定位置输出信息,我们得知道该位置的坐标,所以⾸先介绍⼀下控制台窗⼝的坐标知识。
控制台窗⼝的坐标如下所⽰,横向的是X轴,从左向右依次增⻓,纵向是Y轴,从上到下依次增⻓。
在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节。
(1)、 <locale.h>本地化
<locale.h>提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分。
在标准中,依赖地区的部分有以下⼏项:
• 数字量的格式
• 货币量的格式
• 字符集
• ⽇期和时间的表⽰形式
类型
通过修改地区,程序可以改变它的⾏为来适应世界的不同区域。但地区的改变可能会影响库的许多部
分,其中⼀部分可能是我们不希望修改的。所以C语⾔⽀持针对不同的类项进⾏修改,下⾯的⼀个宏,
指定⼀个类项:
• LC_COLLATE:影响字符串⽐较函数strcoll() 和strxfrm() 。
• LC_CTYPE:影响字符处理函数的⾏为。
• LC_MONETARY:影响货币格式。
• LC_NUMERIC:影响printf() 的数字格式。
• LC_TIME:影响时间格式strftime() 和wcsftime() 。
• LC_ALL-针对所有类项修改,将以上所有类别设置为给定的语⾔环境。
(2)、 setlocale函数
1 char* setlocale (int category, const char* locale);
setlocale 函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
setlocale 的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参数是LC_ALL,就会影响所有的类项。
C标准给第⼆个参数仅定义了2种可能取值:“C”(正常模式)和""(本地模式)。
1 setlocale(LC_ALL, "C");
当地区设置为"C"时,库函数按正常⽅式执⾏,⼩数点是⼀个点。
当程序运⾏起来后想改变地区,就只能显⽰调⽤setlocale函数。⽤""作为第2个参数,调⽤setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。
⽐如:切换到我们的本地模式后就⽀持宽字符(汉字)的输出等。
1. setlocale(LC_ALL, " ");//切换到本地环境
(3)、宽字符的打印
那如果想在屏幕上打印宽字符,怎么打印呢?
宽字符的字⾯量必须加上前缀“L”,否则C语⾔会把字⾯量当作窄字符类型处理。前缀“L”在单引号前⾯,表⽰宽字符,对应wprintf() 的占位符为 %lc 。在双引号前⾯,表⽰宽字符串,对应wprintf() 的占位符为%ls 。
#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%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、游戏分析
(1)、Snake.h
首先,在Snake.h中创建两个结构体:一个蛇的结构体;一个蛇的节点的结构体。
- 蛇的节点的结构体
整个蛇身可以看作是一个单链表,所有节点一次连接起来的。
蛇的一个节点 要包括该点在控制台坐标系上的X轴值和Y轴值,以及该节点的下一个节点
如下:
//蛇身节点类型
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode, * PSnakeNode;
//PSnakeNode是指向该结构体的指针
//eg: PSnakeNode n 等同于 SnakeNode* n
- 蛇身的结构体
我们可以想象一下,将舍生看作是一个链表,游戏过程中要将蛇运动起来,要想完成贪吃蛇的运动,我们需要再蛇身中添加那些元素呢?
整个蛇身要包含 指向蛇头的指针、指向食物节点的指针、蛇的移动方向、游戏状态、应该食物的分数、总成绩、休息时间。
//贪吃蛇
typedef struct Snake
{
PSnakeNode pSnake; //指向蛇头的指针
PSnakeNode pFood; //指向食物节点的指针
enum DIRECTTON dir; //蛇移动的方向
enum GAME_STATUS status; //游戏状态
int food_weight; //一个食物的分数
int score; //总成绩
int sleep_time; //休息时间,时间越短,速度越快,时间越长,速度越慢
}Snake, * PSnake;
完成结构体后,我们还要继续分析结构体中的元素。
- 蛇的移动方向
//蛇移动的方向
enum DIRECTTON
{
up = 1,
down,
left,
right
};
- 游戏状态
//游戏状态
//正常,撞墙,撞自己,正常退出,
enum GAME_STATUS
{
ok,//正常
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//撞自己
END_NORMAL//正常退出
};
(2)、test.c
在测试文件中,想要打印游戏地图,那要先适应本地化模式,所以我们在main函数中使用setlocale函数适配本地环境。
为了方便之后的代码测试,我们在main函数中调用一个自定义函数test01。
void test01()
{
//创建贪吃蛇
Snake snake = { 0 };
//一、初始化游戏
GameStart(&snake);
//二、运行游戏
GameRun(&snake);
//三、结束游戏——善后工作
GameEnd(&snake);
}
int main()
{
//先适配本地环境
setlocale(LC_ALL, "");
test01();
return 0;
}
(3)、Snake.c
接下来,我们简单的将代码的实现分为三部分:
-
初始化游戏GameStart();
1.打印环境界面和功能介绍 2.绘制地图 3.创建蛇 4.创建食物
-
运行游戏GameRun();
1、键盘上的按键情况 2、蛇每走一步的状态 3、蛇上下左右走 4、判断下一个节点是不是食物 5、判断蛇有没有撞墙或者撞到自己
-
结束游戏——善后工作GameEnd();
二、初始化游戏
1、打印环境界面和功能介绍
我们要先获取控制板上光标的信息,对光标进行相应的操作。
这里所要用到的函数我在上一篇文章中已经详细解释过了,所以我们这里直接使用。
//隐藏光标,将光标设置到指定位置
//设置面板
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//获取标准输出设备的句柄
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定义一个光标信息结构体
CONSOLE_CURSOR_INFO cursor_info = { 0 };
//获取光标信息
GetConsoleCursorInfo(houtput, &cursor_info);
//设置光标可见度
cursor_info.bVisible = false;
//设置光标信息
SetConsoleCursorInfo(houtput, &cursor_info);
我们要定位光标,在相应位置打印相应的文字。
这里可以创建一个函数SetPos,来定位光标
//获取光标位置
void SetPos(int x, int y)
{
//获取标准输出设备的句柄
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置光标位置
COORD pos = { x, y };
SetConsoleCursorPosition(houtput, pos);
}
得到光标以后,就可以进行下一步操作:
void WelcomeToGame()
{
SetPos(35, 12);
wprintf(L"欢迎来到贪吃蛇小游戏\n");
SetPos(37, 20);
system("pause");//暂停
system("cls");//清理屏幕
SetPos(22, 12);
wprintf(L"用↑.←.↓.→来控制蛇的移动方向,按F3加速,按F4减速\n");
SetPos(35, 15);
wprintf(L"加速可以得到更高的分数!\n");
SetPos(37, 20);
system("pause");
system("cls");
}
这样就可以得到我们的游戏介绍
2、绘制地图
我们假设实现⼀个棋盘27⾏,58列的棋盘(⾏和列可以根据⾃⼰的情况修改),再围绕地图画出墙,
如下:
打印墙,要先定位光标,在光标出打印‘□’就可以了。
注:X轴:一个方块占两个X坐标
Y轴:一个方块占一个Y坐标
这里我们在头文件中定义一个墙,以方便我们以后想要把墙换成其他的图案
#define WALL L'□'
- 上面一行,光标定位在(0.0),打印29个‘□’。
- 下面一行,光标定位在(0.26),打印29个‘□’。
- 左边一列,(0.0)处已有‘□’,所以从Y=1处开始打印。以此向下打印26个‘□’,所以光标定位在(0.i)处。
- 右边一列,同左边一样,光标定位在(56.i)处。
//2,绘制地图
void CreateMap()
{
int i = 0;
//上
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
}
//下
SetPos(0, 26);
for (i = 0; i < 29; i++)
{
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);
}
}
这样就得到了墙体。
3、创建蛇
初始时,我们让蛇身有五个节点,我们可以用for循环来申请五个节点的空间,要求五个节点的坐标是挨着的,我们使用头插法将五个节点连接。
节点的Y值一定是相等的,所以我们只需要考虑X坐标,X轴上是一个‘□’占两个坐标,所以每个节点的X坐标值差2。
先给第一个节点一个初始值,先定为(16,5),所以五个节点的坐标为:X:16+2*i; Y:5; 这里也可以将第一个节点的坐标和蛇身图案在Snake.h文件中定义一下,方便以后的修改
#define pos_x 16
#define pos_y 5
#define BODY L'●'
有了上述分析,所以我们就可以用for循环和头插法将初始蛇身打印出来。
void InitSnake(PSnake sk)
{
PSnakeNode cur = NULL;
//循环五次,创建五个节点
for (int i = 0; i < 5; i++)
{
cur = (PSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnake()::malloc");
return;
}
cur->next = NULL;
//坐标
cur->x = pos_x + 2 * i;
cur->y = pos_y;
//头插法插入链表
if (sk->pSnake == NULL) //空链表
{
sk->pSnake = cur;
}
else //非空
{
cur->next = sk->pSnake;
sk->pSnake = cur;
}
}
//打印蛇身
cur = sk->pSnake;
while (cur)//遍历所有节点
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
}
这里我们也要将蛇身的一些属性在InitSnake()函数中设置出来
//设置贪吃蛇的属性
sk->dir = right;//蛇开始时向右走
sk->score = 0;//总成绩开始时为0
sk->food_weight = 10;//一个食物得分为10
sk->status = ok;//游戏起始状态为OK
sk->sleep_time = 200;//单位为毫秒
4、创建食物
食物的坐标要求:x坐标值必需是2的倍数、食物坐标不能和蛇的身体坐标以及墙的坐标冲突、每次打开游戏都要随机生成食物。
首先,根据墙的坐标,我们可以确定食物X坐标值的范围为2 ~ 54,Y坐标值范围为1~25。
生成随机值,我们要用到rand函数,使用此函数要在main函数中包含
srand((unsigned int)time(NULL));
生成随机坐标,并且X 值为2的倍数,可以用do while循环来实现。
again:
do
{
x = rand() % 53 + 2;//用rand生成随机数
y = rand() % 25 + 1;
} while (x % 2 != 0);
然后,遍历已有的蛇身和食物坐标比较,是否重合,如果重合就重新生成随机值,所以需要再do while循环前加again:保证再一次进入循环生成新的坐标。
PSnakeNode cur = sk->pSnake;
while (cur)//遍历所有的节点坐标
{
//如果食物和蛇身体冲突,就重新生成随机值
if (x == cur->x && y == cur->y)
goto again;
cur = cur->next;
}
接下来创造食物节点并打印食物
在头文件中定义一个食物,反比以后需要改变食物图案
#define FOOD L'★'
//创建食物节点
PSnakeNode Food = (PSnakeNode)malloc(sizeof(SnakeNode));
if (Food == NULL)
{
perror("CreateFood()::malloc");
return;
}
Food->x = x;
Food->y = y;
Food->next = NULL;
//打印食物
SetPos(x, y);//定位光标
wprintf(L"%lc", FOOD);
做完这些,不用忘记将创建好的食物放到结构体中的食物中
sk->pFood = Food;//将创建好的食物放到结构体的食物节点中
综上所述,创建食物的完整代码为:
void CreateFood(PSnake sk)
{
int x = 0;//食物的坐标
int y = 0;
//食物的x坐标值必需是2的倍数
//x: 2~54; y:1~25
again:
do
{
x = rand() % 53 + 2;//用rand生成随机数
y = rand() % 25 + 1;
} while (x % 2 != 0);
//食物坐标不能和蛇的身体坐标冲突
PSnakeNode cur = sk->pSnake;
while (cur)//遍历所有的节点坐标
{
//如果食物和蛇身体冲突,就重新生成随机值
if (x == cur->x && y == cur->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;
//打印食物
SetPos(x, y);//定位光标
wprintf(L"%lc", FOOD);
sk->pFood = Food;//将创建好的食物放到结构体的食物节点中
}
三、运行游戏
1、打印帮助信息
我们在地图的右侧打印出游戏的帮助信息
//打印帮助信息
void PrintfHelpInfo()
{
SetPos(64, 14);
wprintf(L"%ls", L"不能穿墙,不能撞到自己");
SetPos(64, 16);
wprintf(L"%ls", L"用↑.←.↓.→.来控制蛇的方向");
SetPos(64, 18);
wprintf(L"%ls", L"按Ctrl加速,按Alt减速");
SetPos(64, 20);
wprintf(L"%ls", L"按ESC结束游戏,按空格键暂停游戏");
}
接下来用一个do while循环,在游戏状态为OK的时候运行游戏
先在循环中打印出食物成绩和总成绩
do
{
//打印总分数和食物分数
SetPos(64, 8);
printf("总分数:%d", sk->score);
SetPos(64, 10);
printf("食物分数:%2d", sk->food_weight );
}while (sk->status == ok);
2、键盘上的按键情况
判断按键情况要用到GetAsyncKeyState函数,在上一篇博客 link中有解释,所以只需要在Snake.c中定义一下就可以
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)
游戏中的按键无非就是上、下、左、右、暂停、结束、加速、减速。上下左右走的时候注意不可以向反方向行走,比如蛇正在向下走,此时只可以让它继续向下、左右走,而不可以让他向上走。
所以根据这个规律来判断按键情况
//判断按键情况
//上
if (KEY_PRESS(VK_UP) == 1 && sk->dir != down)
{
sk->dir = up;
}
//下
else if (KEY_PRESS(VK_DOWN) == 1 && sk->dir != up)
{
sk->dir = down;
}
//左
else if (KEY_PRESS(VK_LEFT) == 1 && sk->dir != right)
{
sk->dir = left;
}
//右
else if (KEY_PRESS(VK_RIGHT) == 1 && sk->dir != left)
{
sk->dir = right;
}
//ESC键 正常退出游戏
else if (KEY_PRESS(VK_ESCAPE) == 1)
{
sk->status = END_NORMAL;
}
//空格 暂停游戏
else if (KEY_PRESS(VK_SPACE) == 1)
{
Pause();
}
除了上面代码中的按键还有加减速按键(所有按键不是唯一,均可以自己喜好调整按键),休息时间越短,速度越快,时间越长,速度越慢
这里的加减速我们给他一个小规则,就是不可以一直加速或减速,加速一次,一个食物的分数会有所增加,相反减速会减少,但是我们需要给加减速一个限制,不可以让分数过大,也不可以让分数减为负数。
调整蛇的速度快慢,只需要调整蛇每走一步的休眠时间就可以了,休眠的初始时间为200毫秒,所以加速时,如果休眠时间小于等于了80毫秒,就不可以在加速了(最快速度为80毫秒),每次加速时间减30毫秒,食物分数加2分;
减速的时候,我们可以用食物分数来判断更方便一些,一个食物分数不能小于等于2分,每次减速时间加30毫秒,食物分数减2分。
代码实现:
//可根据个人喜好调整按键
//Ctrl 加速
else if (KEY_PRESS(0x11) == 1)
{
//规定只能加速四次,加速一次加两分
if (sk->sleep_time > 80)
{
sk->sleep_time -= 30;
sk->food_weight += 2;
}
}
//Alt 减速
else if (KEY_PRESS(0x12) == 1)
{
//只能减速四次,减速一次减二分
if (sk->food_weight > 2)
{
sk->sleep_time += 30;
sk->food_weight -= 2;
}
}
3、蛇每走一步的状态
蛇的行走状态只有上下左右走,以及判断下一个坐标是不是食物。
首先,创建一个新节点作为蛇下一步的节点。
PSnakeNode nextnode = (PSnakeNode)malloc(sizeof(SnakeNode));
if (nextnode == NULL)
{
perror("SnakeMove()::malloc");
return;
}
(1)、上下左右走
对于蛇的行走,只需要改变相应的X轴和Y轴坐标值就可以了,这里我们使用switch函数来实现。
//先让蛇上下左右走
switch(sk->dir )
{
case up:
nextnode->x = sk->pSnake->x;
nextnode->y = sk->pSnake->y - 1;
break;
case down:
nextnode->x = sk->pSnake->x;
nextnode->y = sk->pSnake->y + 1;
break;
case right:
nextnode->x = sk->pSnake->x+2;//两个X坐标为一个‘□’,所以改变X值要加减2
nextnode->y = sk->pSnake->y;
break;
case left:
nextnode->x = sk->pSnake->x-2;//两个X坐标为一个‘□’,所以改变X值要加减2
nextnode->y = sk->pSnake->y ;
break;
}
(2)、判断下一个坐标是不是食物
判断下一个坐标是不是食物,需要行我们前面新创建的节点坐标与实物坐标比较,重合为是食物,不重合就不是食物,创建函数FindIsFood()来判断
//判断下一个坐标是不是食物
int FindIsFood(PSnakeNode pn, PSnake sk)
{
if (sk->pFood->x == pn->x && sk->pFood->y == pn->y)
{
return 1;
}
else
{
return 0;
}
}
返回1是食物, 返回不是1不是食物。
//判断下一个坐标是不是食物
if (FindIsFood(nextnode, sk))
{
//下一个节点是食物
EatFood(nextnode, sk);
}
else
{
//下一个节点不是食物
NoFood(nextnode, sk);
}
(3)、下一个节点是食物
如果下一个节点是食物,那么用头插法将食物与蛇身连接起来,还要注意一定要释放掉原来新创建的节点。然后打印新的蛇身,创建新的食物
//下一个节点是食物
void EatFood(PSnakeNode nextnode, PSnake sk)
{
//头插法
sk->pFood->next = sk->pSnake;
sk->pSnake = sk->pFood;
//新节点和食物重合,蛇与实物连接起来,要释放原来的新节点
free(nextnode);
nextnode = NULL;
//打印蛇
PSnakeNode cur = sk->pSnake;
while (cur)//遍历蛇身
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//加分
sk->score += sk->food_weight;
//创建食物
CreateFood(sk);//直接调用前面创建食物的函数
}
(4)、下一个节点不是食物
如果下一个节点不是食物,就将新创建的节点和蛇身用头插法连接起来,那么我们要注意蛇每向前走一步都要在蛇尾消掉一个节点,并且在蛇尾打印一个空格。
//下一个节点不是食物
void NoFood(PSnakeNode nextnode, PSnake sk)
{
//头插法
nextnode->next = sk->pSnake;
sk->pSnake = nextnode;
PSnakeNode cur = sk->pSnake;
while (cur->next->next != NULL)//找到最后一个节点
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//把最后一个结点打印成空格
SetPos(cur->next->x, cur->next->y);
printf(" ");
//释放最后一个结点
free(cur->next);
//把倒数第二个节点的地址置为NULL
cur->next = NULL;
}
这里如果没有将最后一个节点打印成空格,那么打印中就会将最后一个节点留在原地,如图所示:
此时的贪吃蛇已经可以正常运行起来,接下来就只剩下判断蛇有没有撞墙或者撞到自己,由两个函数来实现
//判断蛇有没有撞墙
KillByWall(sk);
//判断蛇是否撞到自己
KillBySelf(sk);
(5)、判断蛇有没有撞墙
比较蛇头和墙的坐标是否重合
void KillByWall(PSnake sk)
{
if (sk->pSnake->x == 0 || sk->pSnake->x == 56
|| sk->pSnake->y == 0 || sk->pSnake->y == 26)
{
sk->status = KILL_BY_WALL;
}
}
(6)、判断蛇是否撞到自己
判断蛇头是否和蛇身所有节点坐标重合
//判断蛇是否撞到自己
void KillBySelf(PSnake sk)
{
//遍历蛇身,是否与蛇头坐标重合,重合就是撞到了自己
PSnakeNode cur = sk->pSnake->next;
while (cur)
{
if (cur->x == sk->pSnake->x && cur->y == sk->pSnake->y)
{
sk->status = KILL_BY_SELF;
break;
}
cur = cur->next;
}
}
这里我们的贪吃蛇运行就已经完成了,但是要注意蛇正常走路时也要有休眠时间, 要在SnakeMove()函数下面加上睡眠时间的初始值
//蛇每走一步的状态
SnakeMove(sk);
Sleep(sk->sleep_time);
四、结束游戏——善后工作
游戏的结束原因有:自己退出游戏、撞墙、撞到自己;在结束游戏时打印结束原因
void GameEnd(PSnake sk)
{
SetPos(20, 12);
switch (sk->status)//游戏状态
{
case KILL_BY_WALL:
printf("您撞到了墙!游戏结束\n");
break;
case KILL_BY_SELF:
printf("您撞上了自己!游戏结束\n");
break;
case END_NORMAL:
printf("您以结束游戏\n");
break;
}
}
对于这里的游戏善后,我们还可以增加一些其他东西来做修饰,例如:一局游戏结束后不需要退出直接进行下一场游戏······大家可以自由的发挥。
完整代码展示
- Snake.h文件
- Snake.c文件
初始化游戏
运行游戏
在这里插入图片描述
结束游戏
- test.c文件
五、结语
十分感谢您观看我的原创文章。
本文主要用于个人学习和知识分享,学习路漫漫,如有错误,感谢指正。
如需引用,注明地址。