书接上回,我们把主菜单构建,初始化,开始游戏四个模块完成了,下面我们继续往后面写
我们以开始菜单为分类来写,对于选项4以及选项5只是单纯的输出以及退出程序,故此处我们不做过多解读
进行游戏
在start()函数中,我们调用了一个playGame()的函数来进行游玩游戏时的行为处理
首先我们要获取案件以控制蛇的移动
char key = 'D';
dx = 1; // 初始向右移动
dy = 0;
int gameOver = 0;
// 游戏不能等待用户输入才进行下一步,给以默认值
while (!gameOver && snake.body[0].x >= 1 && snake.body[0].x < high && snake.body[0].y >= 1 && snake.body[0].y < wide)
{ // 外循环与墙碰撞判断,确保在墙内移动
showUI(); // 显示界面
while (_kbhit())
{
key = _getch();
}
switch (key)
{
case 'D':
case 'd':
dx = 1;
dy = 0;
break;
case 'A':
case 'a':
dx = -1;
dy = 0;
break;
case 'W':
case 'w':
dx = 0;
dy = -1;
break;
case 'S':
case 's':
dx = 0;
dy = 1;
break;
case 'q':
case 'Q':
save();
break;
}
}
- 在开头我们给以默认值D代表蛇会在没有进行第一次操作的情况下,默认向右边走
- 随后有一个int类型的gameOver变量,用于判断游戏是否结束,如果游戏结束,则返回1,否则返回0
- 同时我们又定义了一个dx和dy两个变量,代表蛇的偏移坐标,dx 和 dy 的值决定了蛇的移动方向:
向上移动:dx = 0, dy = -1
向下移动:dx = 0, dy = 1
向左移动:dx = -1, dy = 0
向右移动:dx = 1, dy = 0
而在下面的while循环中我们耶获取到用户的输入值,同时按照上诉dx,dy的规则控制蛇的移动
tips:这这里我们有一个监听按键Q然后调用save()函数保存游戏,这个函数我们后面会进行解释
随后又我们要知道蛇的运动会牵扯到尾巴的移动,所以我们定义了lx,ly俩个坐标用来表示蛇尾巴的坐标,并且更新蛇头与蛇尾的状态坐标
// 方向控制
// 先记录尾巴坐标
lx = snake.body[snake.size - 1].x;
ly = snake.body[snake.size - 1].y;
// 移动
for (int i = snake.size - 1; i > 0; i--)
{
snake.body[i].x = snake.body[i - 1].x;
snake.body[i].y = snake.body[i - 1].y;
}
// 更新蛇头位置
snake.body[0].x += dx;
snake.body[0].y += dy;
最后,我们在进行游戏的时候系统要判断是否与墙或者食物/陷阱发生碰撞
// 检查s是否和墙碰撞
if (snake.body[0].x < 1 || snake.body[0].x >= high || snake.body[0].y < 1 || snake.body[0].y >= wide)
{
gameOver = 1;
break;
}
// 检查是否与自己碰撞
for (int i = 1; i < snake.size; i++)
{
if (snake.body[0].x == snake.body[i].x && snake.body[0].y == snake.body[i].y)
{
gameOver = 1;
break;
}
}
// 判断是否与陷阱发生碰撞
for (int i = 0; i < trapNum; i++)
{
if (snake.body[0].x == traps[i].x && snake.body[0].y == traps[i].y)
{
gameOver = 1;
break;
}
}
if (gameOver)
break;
// 蛇和食物碰撞(移动后)
if (snake.body[0].x == food[0] && snake.body[0].y == food[1])
{
// 先保存新身体段的坐标为当前尾巴的位置
snake.body[snake.size].x = lx;
snake.body[snake.size].y = ly;
initFood(); // 刷新食物
snake.size++; // 长度加1
score++;
initTrap(score); // 随着分数增加,生成更多陷阱
}
Sleep(300);//代表帧数
// system("cls");
}
- 与墙碰撞这里我们直接用蛇头的坐标去匹配墙的x/y坐标,如果相撞,gameOver变量则为1,否则不变
- 与自己相撞我们只需遍历蛇的长度,然后得到蛇身体的每一届的坐标,依次与蛇头匹配,相同即代表撞到自己了,gameOver变量则为1,否则不变
- 与陷阱的碰撞大致逻辑与墙一样,都是值得一提的是,我们的陷阱可能会有好多好多个,然后上一篇中我们的trap使用的是结构体存储的,所以我们要通过陷阱数量遍历每一个陷阱的信息然后与蛇头匹配以判断是否发生碰撞
随后我们要对于游戏结束有所处理:
// 游戏结束提示
COORD coord;
coord.X = high / 2 - 8; // X坐标(列)基于high(60)
coord.Y = wide / 2; // Y坐标(行)基于wide(20)
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);
printf("Game Over!");
coord.Y++;
coord.X = high / 2 - 6;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);
printf("你的分数是:%dn", score);
add(score);
printf("请按任意键退回至主菜单...");
_getch();
clearScreen();
main();
Sleep(2000);
// 清除输入缓冲区中的残留字符
while (_kbhit())
{
_getch();
}
查询分数
在游戏结束后,我们通过add函数将分数记录起来
在这里,我们所使用的记录分数采取了链表的方式
typedef struct node
{
int score;
time_t time;
struct node *next;
} node;
int len = 0;
node *head = NULL;
通过链表的声明中不难看出我们的链表中的数据域存储了分数,时间俩个内容,获取分数我们耶采用链表读取数据的方式
// 查询成绩
void query()
{
if (head == NULL)
{
printf("暂无成绩记录!n");
printf("请按任意键返回主菜单...");
_getch();
clearScreen();
// return;
// 不能使用return回调至主菜单,否则是接续上方代码运行,应当直接在方法里面调用main方法
main();
}
printf("历史成绩记录:n");
printf("%-5s %-10s %-20sn", "序号", "分数", "时间");
printf("----------------------------------------n");
node *current = head;
int index = 1;
while (current != NULL)
{
// 解析时间戳
struct tm *tm_info = localtime(¤t->time);
char time_str[20];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info);
printf("%-5d %-10d %-20sn", index, current->score, time_str);
current = current->next;
index++;
}
printf("----------------------------------------n");
printf("共 %d 条记录n", len);
printf("按任意键返回主菜单...");
_getch();
clearScreen();
main();
}
值得一提的是,我们的时间记录是采用时间戳的方式,所以需要解析时间戳才能得到具体的明确时间,此处可以在网络上搜索相关示例,不做过多赘述
保存与读取存档内容
原先并没有设计这俩个功能,因为课题要求所以额外加了这俩个功能,可能还存在一些问题
保存
1.创建文件
void save()
{
FILE *fp;
//如果在同意文件夹下的二级文件夹下的文件哪怕是w只读(如果文件不存在则创建新文件)依旧会返回NULL,故此处判断是否存在,如不存在则创建文件
FILE *check_snake = fopen("./data/snake.ini", "r");
FILE *check_food = fopen("./data/food.ini", "r");
FILE *check_trap = fopen("./data/trap.ini", "r");
if (check_snake == NULL || check_food == NULL || check_trap == NULL) {
// 关闭已打开的文件
if(check_snake) fclose(check_snake);
if(check_food) fclose(check_food);
if(check_trap) fclose(check_trap);
// 创建data目录
system("if not exist data mkdir data");
} else {
// 如果文件都存在,则关闭检查用的文件指针
fclose(check_snake);
fclose(check_food);
fclose(check_trap);
}
}
应当注意的是,如果在同意文件夹下的二级文件夹下的文件哪怕是w只读(如果文件不存在则创建新文件)依旧会返回NULL,所以我们先判断是否存在,不存在那么就创建新的文件,否则覆盖文件
2.保存蛇数据
// 保存蛇
fp = fopen("./data/snake.ini", "w");
if (fp == NULL)
{
printf("保存蛇数据失败!n");
return;
}
fprintf(fp, "[Snake]n");
fprintf(fp, "Size=%dn", snake.size);//大小
fprintf(fp, "Score=%dn", score);//目前分数
fprintf(fp, "X=%dn", dx);//蛇头x坐标
fprintf(fp, "Y=%dn", dy);//蛇头y坐标
fprintf(fp, "[Body]n");//循环读取身体以保存身体数据
for (int i = 0; i < snake.size; i++)
{
fprintf(fp, "Part%d=%d,%dn", i, snake.body[i].x, snake.body[i].y);
}
fclose(fp);
首先我们会有一个if判断文件是否存在,以此来检验前面是否漏创建了相关的ini文件
随后,第一行的[Snake]来告诉我们该文件是蛇的数据文件,后面则为关键的数据记录
3.保存食物,陷阱
食物陷阱的保存大概逻辑和蛇差不多,只不过食物只需保存坐标,食物要保存多个坐标以及数量
// 保存食物
fp = fopen("./data/food.ini", "w");
if (fp == NULL)
{
printf("保存食物数据失败!n");
return;
}
fprintf(fp, "[Food]n");
fprintf(fp, "X=%dn", food[0]);
fprintf(fp, "Y=%dn", food[1]);
fclose(fp);
// 保存陷阱
fp = fopen("./data/trap.ini", "w");
if (fp == NULL)
{
printf("保存陷阱数据失败!n");
return;
}
fprintf(fp, "[Traps]n");
fprintf(fp, "Count=%dn", trapNum);
for (int i = 0; i < trapNum; i++)
{
fprintf(fp, "Trap%d=%d,%dn", i, traps[i].x, traps[i].y);//循环读取陷阱以保存陷阱数据
}
fclose(fp);
printf("游戏数据保存成功!n");
printf("按任意键返回主菜单...");
_getch();
clearScreen();
main();
读取
在读取数据前我们要先检验数据文件是否存在
//判断本地文件是否存在
FILE *snakeD = fopen("./data/snake.ini", "r");
FILE *foodD = fopen("./data/food.ini", "r");
FILE *trapD = fopen("./data/trap.ini", "r");
if (snakeD == NULL || foodD == NULL || trapD == NULL) {
system("color 4");
printf("游戏数据不存在!n");
if(snakeD) fclose(snakeD);
if(foodD) fclose(foodD);
if(trapD) fclose(trapD);
printf("请按任意键返回主菜单...");
_getch();
clearScreen();
system("color 7");
main();
}
然后在输出的时候为了好(zhuang)看(B)采用了彩色输出的模样
在后续代码中的system* (“color “) **均代表输出的颜色
随后救赎读取配置u文件中的数据信息,同时将他们赋值给我们的一些记录元数据的参数,覆盖数据以达到回档的效果
// 读取食物
system("color 2");
FILE *fp = fopen("./data/food.ini", "r");
if (fp == NULL)
{
printf("读取食物数据失败!n按任意键返回!n");
_getch();
system("color 7");
main();
}
fscanf(fp, "%d %d", &food[0], &food[1]);
fclose(fp);
printf("食物已加载...n");
Sleep(500);
// 读取蛇
system("color 6");
fp = fopen("./data/snake.ini", "r");
if (fp == NULL)
{
printf("读取蛇数据失败!n按任意键返回!n");
_getch();
system("color 7");
main();
}
fscanf(fp, "%d %d %d %d", &snake.size, &score, &dx, &dy);
for (int i = 0; i < snake.size; i++)
{
fscanf(fp, "%d %d", &snake.body[i].x, &snake.body[i].y);
}
fclose(fp);
printf("蛇数据已加载...n");
Sleep(500);
// 读取陷阱
system("color 3");
fp = fopen("./data/trap.ini", "r");
if (fp == NULL) {
printf("读取陷阱数据失败!n按任意键返回!n");
_getch();
system("color 7");
main();
}
fscanf(fp, "%d", &trapNum);
printf("陷阱数量已加载...n");
Sleep(300);
for (int i = 0; i < trapNum; i++) {
fscanf(fp, "%d %d", &traps[i].x, &traps[i].y);
}
fclose(fp);
printf("陷阱数据已加载...n");
Sleep(500);
system("color 7");
printf("游戏数据已全部加载完成!n");
// 继续游戏
printf("按任意键继续游戏...");
_getch();
clearScreen();
// 添加墙的初始化
initWall();
playGame();
终
综上,按照代码就能大概写一个贪吃蛇的小游戏出来了
完整代码暂不直接提供,但是源代码已经在github开源,可以参考一下
感谢支持!