Mosaic Game 的数据配置

前面介绍的typed_matrix就是一个基础的带数据格式的excel数据导出方案,mosaic_game在这个方案的基础上做了一下二次开发。首先是导出项目的配置,在data/xslx/export_config.json中我们使用json格式来指定哪些excel的哪些sheet需要导出的文件名是什么:

{
	"files": {
		"场景表": {
			"场景表": "space.json",
			"场景类型表": "space_type.json",
			"初始场景表": "init_spaces.json"
		},
		"属性表": {
			"玩家属性表": "sect_attrs.json",
			"怪物属性表": "monster_attrs.json",
			"全局属性表": "global_attrs.json"
		},
    }
}

上面的files字段就是每个单一sheet的导出列表。在mosaic_game的实际开发中发现,副本数据用这个列表来配置每个sheet有点繁琐。因为每个副本的目录结构和excel结构都一样,不同副本的导出配置只有其前缀不同:

qian@qian-desktop:~/Github/mosaic_game/data/xlsx/spaces$ ls ./s1
怪物表.xlsx  流程表.xlsx  陷阱表.xlsx
qian@qian-desktop:~/Github/mosaic_game/data/xlsx/spaces$ ls ./s2
怪物表.xlsx  流程表.xlsx  陷阱表.xlsx
qian@qian-desktop:~/Github/mosaic_game/data/xlsx/spaces$ ls ./s3
怪物表.xlsx  流程表.xlsx  陷阱表.xlsx

只是前缀不同的情况下每次都重复相同的配置是很无聊的,所以在export_config.json中,添加了folder字段,代表自动扫描目录下所有的子目录,将这些excel导出到带子目录前缀的文件夹中:

{
"folders": {
		"spaces": {
			"陷阱表": {
				"陷阱表": "trap.json"
			},
			"怪物表": {
				"怪物表": "monster.json"
			},
			"流程表": {
				"流程表": "quest.json"
			}
		}
	}
}

这样就可以自动创建子文件夹并做导出了:

qian@qian-desktop:~/Github/mosaic_game/data/export/xlsx/spaces$ ls ./s1
monster.json  quest.json  trap.json
qian@qian-desktop:~/Github/mosaic_game/data/export/xlsx/spaces$ ls ./s2
monster.json  quest.json  trap.json
qian@qian-desktop:~/Github/mosaic_game/data/export/xlsx/spaces$ ls ./s3
monster.json  quest.json  trap.json

然后为了加快导出速度,做了一下增量导出的功能,即记录每个导出json所依赖的excel的上次修改时间戳到export_timestamps.json中。

{"spaces/s1/怪物表":"2023_11_16_23_18_47","spaces/s1/流程表":"2023_11_16_23_18_47"}

导出某个json时查询对应的excel的最近修改时间戳,如果与这个export_timestamps.json中记录的时间戳不一样时才会去生成这个json。如果想触发全量导表则只需要清空这个时间戳文件里的内容即可。

导出的数据格式是json格式,其顶层为一个map,内部包含四个key:

{
	"headers": [
	[
		"idx",
		"类型",
		"uint"
	],
	[
		"hp_per_str",
		"每点力量增长血量",
		"float"
	],
	[
		"mp_per_int",
		"每点智力增长蓝量",
		"float"
	],
	[
		"armor_per_dex",
		"护甲敏捷系数",
		"float"
	],
	[
		"base_magic_defence",
		"基础魔法抗性",
		"float"
	]
],
	"shared_json_table": [
	null,
	1,
	20,
	10,
	0.4,
	15
],
	"extras" : [],
	"row_matrix": [
		[
			1, // (idx, 1)
			2, // (hp_per_str, 20)
			3, // (mp_per_int, 10)
			4, // (armor_per_dex, 0.4)
			5 // (base_magic_defence, 15)
		]
	]
}
  1. headers 字段存储了导出数据的表头信息,这里又包括三个字符串值,按照顺序分别是表头英文名、表头中文名、表头格式typed_string_desc对应的字符串
  2. shared_json_table 这里是一个数组,里面存储了整个sheet里所有cell导出的json值集合,其作用相当于之前提到的excel文件格式中的共享字符串表
  3. row_matrix 这是一个二维数组,存储的是导出的matrix中每个cell对应的json值在shared_json_table中的索引。注意这里采用了带注释的json,这样就可以很方便的看出当前cell对应的表头英文名字和json
  4. extras 这个是用来支持手动编辑导出数据文件功能的,是一个数组,数组内每个元素都是(row_key, column_key, cell_value)这样的三元组,这个三元组用来覆盖原始的导出的数据值,其作用相当于rows[row_key].columns[column_key]=cell_value。使用extras字段可以在开发期方便的对导出数据进行改动,避免每次都要去修改对应的excel然后再重新导出,这样加快调试迭代速度。

游戏里的配置数据是很庞大的,全加载的话需要大量的CPU和内存资源,但是实际运行时只会访问其中很小的一部分。典型例子就是道具系统和时装系统,这些系统会有上百万行数据,但是一个角色自身只会有上百个道具和几十种时装,没必要将所有的数据都加载进来,所以一般来说配置表都是按需加载部分数据。在mosaic_game中为了执行按需加载,构造了一个typed_matrix_data_manager来提供按需加载的get接口

class typed_matrix_data_manager
{
private:
    std::unordered_map<std::string, std::unique_ptr<const typed_matrix::typed_matrix>> m_datas;
public:
    const std::string m_dir;
    
private:
    typed_matrix_data_manager(const std::string& dir)
    : m_dir(dir)
    {

    }
    static typed_matrix_data_manager* m_the_instance;
public:
    static bool init(const std::string& dir)
    {
        if(m_the_instance)
        {
            return false;
        }
        if(dir.empty())
        {
            return false;
        }
        m_the_instance = new typed_matrix_data_manager(dir);
        return true;
    }
    static typed_matrix_data_manager* instance()
    {
        return m_the_instance;
    }
    const typed_matrix::typed_matrix* get(const std::string& name);


};

其数据成员m_datas就存储了所有已经加载的数据表,这里我们只把按需加载控制在表这个级别,并没有增加更加高级的缓存淘汰策略,因此数据表加载之后就不会卸载,只有进程彻底退出的时候通过std::unique_ptr<const typed_matrix::typed_matrix>来自动释放资源。

当业务代码要读取某张表时,直接根据数据表的路径从typed_matrix_data_manager::get得到typed_matrix,然后通过下面的两个接口来通过行对应的key来查询指定行的数据handler

class typed_row
{
    const typed_matrix* m_matrix;
    std::uint16_t m_row_index;
    friend class typed_matrix;
    typed_row(const typed_matrix* matrix, std::uint16_t row_index);
};

typed_row get_row(const std::string& cur_row_key) const;
typed_row get_row(const std::uint32_t& cur_row_key) const;

由于整个typed_matrixcell数据是以二维数组存在的,所以支持在得到了指定行之后,获取一个指定列的数据可以通过下面的两个接口:

const json& get_cell(typed_matrix::column_index column_idx) const;
const json& get_cell(const std::string& cur_column_key) const;

这里的column_index其实就是封装了一个不可变的uint16_t,这样通过(typed_row, column_index)可以快速定位到一个cell并获取其值。

class column_index
{
    std::uint16_t m_value;

};
typed_matrix::column_index typed_row::get_column_idx(const std::string& cur_column_key) const
{
	if (!m_matrix || !m_row_index)
	{
		return {};
	}
	else
	{
		return m_matrix->get_column_idx(cur_column_key);
	}
}

const json& typed_row::get_cell(typed_matrix::column_index column_idx) const
{
    static const json invalid_result;
    if (!m_matrix)
    {
        return invalid_result;
    }
    return m_matrix->get_cell(*this, column_idx);
}
const json& typed_matrix::get_cell(const typed_row& row_idx, const typed_matrix::column_index col_idx) const
{
    if (row_idx.m_matrix != this)
    {
        return m_cell_json_values[0];
    }
    if (row_idx.m_row_index == 0)
    {
        return m_cell_json_values[0];
    }
    if (!col_idx.valid())
    {
        return m_cell_json_values[0];
    }
    std::uint16_t cur_row_idx = row_idx.m_row_index - 1;
    std::uint16_t cur_column_idx = col_idx.value() - 1;
    if (cur_row_idx >= m_row_sz)
    {
        return m_cell_json_values[0];
    }
    if (cur_column_idx >= m_column_sz)
    {
        return m_cell_json_values[0];
    }
    return get_cell_safe(cur_row_idx, cur_column_idx);
}

而使用string作为参数的版本需要传入此列表头英文名字,再转换为column_index,所以执行时慢一点:

const json& typed_row::get_cell(const std::string& cur_column_key) const
{
    static const json invalid_result;
    if (!m_matrix)
    {
        return invalid_result;
    }
    return m_matrix->get_cell(*this, cur_column_key);
}
const json& typed_matrix::get_cell(const typed_row& row_idx, const std::string& cur_column_key) const
{
    if (row_idx.m_matrix != this)
    {
        return m_cell_json_values[0];
    }
    if (row_idx.m_row_index == 0)
    {
        return m_cell_json_values[0];
    }
    auto cur_iter = m_column_indexes.find(cur_column_key);
    if (cur_iter == m_column_indexes.end())
    {
        return m_cell_json_values[0];
    }
    std::uint16_t cur_row_idx = row_idx.m_row_index - 1;
    std::uint16_t cur_column_idx = cur_iter->second;
    if (cur_row_idx >= m_row_sz)
    {
        return m_cell_json_values[0];
    }

    return get_cell_safe(cur_row_idx, cur_column_idx);
}

如果只是读取单行的指定列的数据,这两个接口其实是等价的,因为column_index总归是要传入一个string来做查询。如果是遍历多行的指定列,则推荐先预先获取这个列对应的column_index,再执行行遍历时get_cell使用column_index的版本,这样可以避免在循环内重复执行column_index的计算。例如在场景初始化时需要遍历其中的怪物表monsters.json来创建怪物,这里使用monster_sysd_columns这样的结构来存储所需的列的column_index:

struct monster_sysd_columns
{
    typed_matrix::typed_matrix::column_index spawn;
    typed_matrix::typed_matrix::column_index pos;
    typed_matrix::typed_matrix::column_index sid;
    typed_matrix::typed_matrix::column_index yaw;
    typed_matrix::typed_matrix::column_index no;
    typed_matrix::typed_matrix::column_index name;
    bool valid() const
    {
        return spawn.valid() && pos.valid() && sid.valid() && no.valid() && name.valid() && yaw.valid();
    }
    bool load(const typed_matrix::typed_matrix* cur_monster_sysd)
    {
        spawn = cur_monster_sysd->get_column_idx("spawn_type");

		pos = cur_monster_sysd->get_column_idx("pos");

		no = cur_monster_sysd->get_column_idx("no");
		sid = cur_monster_sysd->get_column_idx("sid");
		yaw = cur_monster_sysd->get_column_idx("yaw");
		name = cur_monster_sysd->get_column_idx("name");
    }

};

业务逻辑中所期待的数据是强类型的,获取了json之后业务方还需要自己去做检查和转换,会出现很多重复的代码,因此typed_row上提供了一些很方便的强类型数据获取接口:

template <typename T>
bool expect_value(const std::string&  key, T& dest)
{
	auto cur_cell_v = get_cell(key);
	if (cur_cell_v.is_null())
	{
		return false;
	}
	return serialize::decode(cur_cell_v, dest);
}
template <typename T>
bool expect_value(typed_matrix::column_index  col_idx, T& dest)
{
	auto cur_cell_v = get_cell(col_idx);
	if (cur_cell_v.is_null())
	{
		return false;
	}
	return serialize::decode(cur_cell_v, dest);
}

这里的serialize::decode又复用了我们之前提到的自己写的方形轮子any_container,这个库提供了任意基础类型和STL类型到Json格式的互相转换。