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)
]
]
}
headers字段存储了导出数据的表头信息,这里又包括三个字符串值,按照顺序分别是表头英文名、表头中文名、表头格式typed_string_desc对应的字符串shared_json_table这里是一个数组,里面存储了整个sheet里所有cell导出的json值集合,其作用相当于之前提到的excel文件格式中的共享字符串表row_matrix这是一个二维数组,存储的是导出的matrix中每个cell对应的json值在shared_json_table中的索引。注意这里采用了带注释的json,这样就可以很方便的看出当前cell对应的表头英文名字和json值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_matrix的cell数据是以二维数组存在的,所以支持在得到了指定行之后,获取一个指定列的数据可以通过下面的两个接口:
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格式的互相转换。