张银峰的编程课堂

从文件加载关卡

搭建项目

  1. 将地图文件放入到Res目录下
  2. 向解决方案添加一个新项目
  3. 按第3节课配置好环境

分析地图文件

前面我们使用二维数组硬编码了一个关卡,考虑一下游戏中选择关卡,通关后自动跳转到下一关卡这些情形,显然我们需要一个能加载指定关卡的功能。

这里我们设计是所有关卡信息全部存储于Map.data中,共计100关。当然你可以选择每个关卡一个文件,这样实现加载指定关卡会更加容易一些。下面的截图是两个关卡的信息,我们来看一下设计细节。

glimix.com

  • 每个关卡以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关的效果

glimix.com