从文件加载关卡
搭建项目
- 将地图文件放入到
Res目录下 - 向解决方案添加一个新项目
- 按第3节课配置好环境
分析地图文件
前面我们使用二维数组硬编码了一个关卡,考虑一下游戏中选择关卡,通关后自动跳转到下一关卡这些情形,显然我们需要一个能加载指定关卡的功能。
这里我们设计是所有关卡信息全部存储于Map.data中,共计100关。当然你可以选择每个关卡一个文件,这样实现加载指定关卡会更加容易一些。下面的截图是两个关卡的信息,我们来看一下设计细节。

- 每个关卡以
levels[xx]开头 - 关卡起始于0而不是1,这样第100关就是99
- 前9关的编号是两位(00,01...)而不是1位(0,1,2...),这种设计会保证每个关卡的数据量是相同的。
- 关卡与关卡之间有两个空白行
加载关卡
加载关卡就是从文件中读取到需要的信息。一种方法就是从文件头开始逐行读取,当每读取到levels[xx]这一行后,取出对应的关卡号,与目标关卡号比较,如果一致,就读取关卡信息到二维数组;另一种方式就是跳转到指定关卡处直接读取;显然后者更效,这也是我们为什么要使所有关卡号都是两位数的原因。方案确定后,我们看看整个加载的实现过程。
bool Load_Map(int level)
{
const int cb_newline = 2;
const int cb_level_row = 10 + cb_newline;
const int cb_map_row = 33 + cb_newline;
const int cb_empty_rows = cb_newline * 2;
const int block_size = cb_level_row + cb_map_row * MAP_ROWS + cb_empty_rows;
const int offset = (level - 1) * block_size;
bool loaded = false;
FILE *file = fopen("./res/map.data", "r");
if (file == NULL)
return false;
do
{
fseek(file, offset, SEEK_SET);
if (feof(file))
break;
// 用于调试时查看定位是否正确
//char line[128];
//fgets(line, 128, file);
// 读取关卡编号
int tmp_level;
int sr = fscanf(file, "levels[%d]\r\n", &tmp_level);
if (sr == 0)
break;
// 检测关卡编号是否正确
tmp_level += 1;
if (tmp_level != level)
break;
// 读取地图
char tmp_map[MAP_ROWS][MAP_COLUMNS];
for (int row = 0; !feof(file);)
{
char *m = &tmp_map[row][0];
sr = fscanf(file,
"[%c,%c,%c,%c,%c,%c,%c,%c,%c,%c,%c,%c,%c,%c,%c,%c]\r\n",
m, m + 1, m + 2, m + 3, m + 4, m + 5, m + 6, m + 7,
m + 8, m + 9, m + 10, m + 11, m + 12, m + 13, m + 14, m + 15);
if (sr != 16)
break;
// 将字符0-9转换为数值0-9
for (int i = 0; i < MAP_COLUMNS; i++)
m[i] -= '0';
if (++row == MAP_ROWS)
{
loaded = true;
break;
}
}
if (loaded)
{
g_cur_level = tmp_level;
memcpy(g_map, tmp_map, sizeof(tmp_map));
}
} while (0);
fclose(file);
return loaded;
}
程序先定义一些跳转需要的细节,
变量名前的cb表示count of bytes的意思,即字节数。
- cb_newline:第一个文本都是以
\r\n两个字符结束的,因此行尾有2个字节的不可见字符。 - cb_level_row:表示
levels[xx]这一行的字节数 - cb_map_row:表示关卡每一行的字节数
- cb_empty_rows:表示关卡间2个空行所占用的字节数
- block_size:表示一个关卡总共的字节数
- offset:指出了到指定关卡的字节偏移量
然后读取文件,因为加载时存在错误的情形,这时就需要关闭文件,如:
fseek(file, offset, SEEK_SET);
if (feof(file))
{
fclose(file);
return false;
}
int tmp_level;
int sr = fscanf(file, "levels[%d]\r\n", &tmp_level);
if (sr == 0)
{
fclose(file);
return false;
}
为了避免遗忘fclose(file)并减少过多条件检测下多次类似编码的冗余,我们采用了do..while(0)设计。这样便可在遇到错误时,通过break提前结束循环;或者在成功读取后自然结束循环,而在循环体外统一执行一次关闭文件操作。
do
{
fseek(file, offset, SEEK_SET);
if (feof(file))
break;
int tmp_level;
int sr = fscanf(file, "levels[%d]\r\n", &tmp_level);
if (sr == 0)
break;
} while(0);
fclose(file);
下面是加载第100关的效果
