前言:
对于C语言的初学者来说,可能会觉得文件操作是一个比较陌生的领域,但实际上无论编写怎样的程序文件操作都是必需的。在操作系统中,所有的外围设备(包括键盘和显示器)都被看作是文件系统中的文件,因此所有的输入/输出都要通过读文件/写文件完成。启动一个C语言程序时,操作环境负责打开标准输入,标准输出,标准错误3个文件,并将这3个文件的指针提供给该程序。而我们熟知的 getchar 函数 putchar 函数其实就是通过这些文件指针实现的。这样说大家应该对文件操作的重要性有了一个初步的了解,接下来我们就运用文件操作的相关知识,为贪吃蛇游戏加上存档读档功能。
如何存储画面的信息呢,在贪吃蛇游戏中,我们只需要存储蛇的坐标、食物的坐标以及蛇的前进方向。我们可以利用标准库里的fopen,fwrite,fread三个函数完成这一要求。如果大家记不住这几个函数,请自行百度。
PS:本篇博客中将要实现的游戏存档读档功能是基于 《C语言实现贪吃蛇(三)—-结构+链表实现》 这个贪吃蛇版本的。如果大家不是从那篇博客过来的话,务必回去看看。
一、游戏存档:
在编写存档函数 save() 前,我们先来看看新增的一些需要用到的变量定义:
FILE * mem; //游戏存档文件句柄
char dir = UP; //因为在save函数里要用到,把原来定义在主函数里的dir移出来,并初始化为向上
void save(void); //游戏存档函数
save() 游戏存档函数:
void save(void){
snake_node * p = NULL;
//打开存档文件
if((mem = fopen("memory.txt","w+")) == NULL){
fprintf(stderr,"Can't open memory.txt!\n");
exit(1);
}
//保存蛇长度
fwrite(&len,sizeof(len),1,mem);
//保存方向
fwrite(&dir,sizeof(dir),1,mem);
//保存食物坐标(结构体是值类型)
fwrite(&food,sizeof(food),1,mem);
//保存蛇坐标
for(p = snake_head;p != NULL;p = p->next){
fwrite(&(p->cor),sizeof(p->cor),1,mem);
}
fclose(mem);
}
二、游戏读档:
编写 load() 读档函数前只需声明一下自己即可:
//读档
void load(void);
load() 游戏读档函数:
void load(void){
snake_node * p,* q; //这里的 p、q 无非跟之前的 last、current 一样
int m = 1,i,j;
//读取游戏存档
if((mem = fopen("memory.txt","r")) == NULL){
fprintf(stderr,"No Game Cache Here!\n");
exit(2);
}
//读取蛇长度
fread(&len,sizeof(len),1,mem);
//读取方向
fread(&dir,sizeof(dir),1,mem);
//读取食物坐标(结构体是值类型)
fread(&food,sizeof(food),1,mem);
//读取蛇头坐标
snake_head = (snake_node *)malloc(sizeof(snake_node));
fread(&(snake_head->cor),sizeof(COORD),1,mem);
snake_head->next = NULL;
m ++; //准备读取第二节
//获取蛇身体坐标
p = snake_head;
while(m <= len){
q = (snake_node *)malloc(sizeof(snake_node));
fread(&(q->cor),sizeof(COORD),1,mem);
q->next = NULL;
p->next = q;
p = q;
m ++;
}
//打印围墙
for (j = 0; j < 17; j ++) {
for (i = 0; i < 17; i ++)
if (i == 0 || i == 16 || j == 0 || j == 16)
{
putchar('#');
}
else
{
putchar(' ');
}
putchar('\n');
}
//打印蛇身
for (p = snake_head; p != NULL; p = p->next) {
gotoxy(p->cor);
putchar('*');
}
//打印食物
gotoxy(food);
putchar('$');
fclose(mem);
}
三、存档与读档:
万事俱备、只欠东风。现在我们已经有了存档读档函数了,接下来就是考虑什么时候需要存储游戏记录了。
显然,只有在游戏中途退出的时候需要存储。我们可以在游戏进行的时候通过按 ESC 键中途退出游戏,并保存游戏进度。
在此之前,我们需要对 ESC 键定义:
#define ESC 27
对 get_dir() 函数进行稍微的改进以达到该目的:
int get_dir(int old_dir){
int new_dir = old_dir;
//用kbhit()与getch()组合实现键盘响应
//kbhit() 检查当前是否有键盘输入,若有则返回一个非0值,否则返回0
//getch() 用ch=_getch();会等待你按下任意键之后,把该键字符所对应的ASCII码赋给ch,再执行下面的语句。
if(_kbhit()){
//按ESC存档并退出
if (_getch() == ESC){
save(); //存档
free_node(snake_head);
system("cls");
printf("Caching...\n");
exit(0);
};
new_dir = _getch(); //getch()函数要使用两次,原因是因为第一次返回的值指示该键扩展的字符,第二次调用才返回实际的键代码
//如果蛇身长度大于1,则不能回头,如果摁回头方向,则按原来方向走
if(len > 1 && (abs(new_dir - old_dir) == 2 || abs(new_dir - old_dir) == 8)){
new_dir = old_dir;
}
}
return new_dir;
}
注意这里ESC应该放在第一个getch()处检测。ESC不同于方向键,第一次便返回键值,方向键需要两次。
既然加入了存档与读档功能,那么就必须得有个 新建游戏 与 继续游戏 的选择界面。我们在主函数的循环开始前加入choose函数实现这个界面。
在实现 choose() 函数前,先来看看需要的变量等的声明
//新建游戏与继续游戏选项的显示位置
COORD s_option = { 5,5 },c_option = { 5,7 };
//按回车键选择
#define ENTER 13
//选择是新建游戏还是继续游戏
void choose(void);
choose() 函数代码:
void choose(void)
{
int key, option = 1; //默认是新建游戏
gotoxy(s_option);
printf("->NEW GAME");
gotoxy(c_option);
printf(" CONTINUE");
while ((key = _getch()) != ENTER) {
if (key == UP && option > 1)
{
option --;
}
if (key == DOWN && option < 2)
{
option ++;
}
if (option == 1) {
gotoxy(s_option);
printf("->");
gotoxy(c_option);
printf(" ");
}
else {
gotoxy(c_option);
printf("->");
gotoxy(s_option);
printf(" ");
}
}
system("cls");
if (option == 1)
{
init_game(); //初始化新游戏
}
else
{
load(); //加载存档
}
}
option的值n代表第n个选项。
由于添加了存档读档功能,因此主函数也得有相应的改变,改变如下:
int main(void) {
choose(); //决定是新建游戏还是继续游戏
while(1){
dir = get_dir(dir); //获取方向(我们摁下的方向)
move_snake(dir); //移动蛇身
if(!isalive()){ //判断蛇的生命状态
break;
}
}
//清除屏幕
system("cls");
printf("Game Over!\n");
//释放申请的内存空间
free_node(snake_head);
return 0;
}
到了这一步我们的程序已经算是完成了。
但是在选择是新建游戏还是继续游戏那里,你会发现光标会一直在闪烁,其实这个情况跟前面我们遇到的贯标一直跟着蛇尾是一样的,前面我们的解决方法是将光标定位到左下角:
//避免光标一直跟着蛇尾(或食物 )
COORD foot = {0,17}; //将光标置于左下角
gotoxy(foot);
在这里原作者给出了一个更好的解决方法:直接将光标隐藏掉!
为了实现隐藏光标,我们需要修改 gotoxy() 函数:
void gotoxy(COORD pt)
{
//句柄
HANDLE hout;
//GetStdHandle函数获取一个指向特定标准设备的句柄,包括标准输入,标准输出和标准错误。
//STD_OUTPUT_HANDLE正是代表标准输出(也就是显示屏)的宏
hout = GetStdHandle(STD_OUTPUT_HANDLE);
//SetConsoleCursorPosition函数用于设置控制台光标的位置
SetConsoleCursorPosition(hout, pt);
//隐藏光标
CONSOLE_CURSOR_INFO cursor_info = { 1,0 };
SetConsoleCursorInfo(hout, &cursor_info);
}
本博客参考自C语言实现贪吃蛇之文件操作篇 。
好了,到了这里这个程序已经很不错了。但是原作者似乎还不够过瘾,毕竟命令行黑乎乎的。于是乎原作者就搞了个贪吃蛇图形界面版。下一篇博客中我将带着大家来看看图形界面版怎么实现。