一站式数据表导出流程 for Unity

作者:inspoy
2018-01-16
17 21 2

写在前面

本文首发自 inspoy 的杂七杂八,欢迎关注

前言

游戏多少都会会有点数值信息,为了尽量实现数据驱动,方便策划调整游戏体验,势必需要选择一种合适的配表方式。

看了看网上主流的解决方案,主要以 csv 和 json 为主,纯文本表示在调试和版本控制中非常方便,小规模的数据读取起来也不是很慢,主要缺点在于策划编辑起来不太方便,csv 不支持公式,不支持格式,json 则非常难以编辑。

嗯,决定手动造个轮子,把这套工作流优化一下。

工作流程

编辑 Excel 表

准备一个空白的文件夹用于存放所有的 Excel 表格,每个数据表一个 xlsx 文档,文档中只有一个 Sheet 页,除了第一个以外的 Sheet 页将会被忽略,所以可以用这个机制来实现临时或参考用表。另外,文件夹中所有以下划线 _ 开头的文件将会被导出工具所忽略。

每当策划需要创建一个新表,需要手动将空白表的模板(_ 模板.xlsx)复制一份出来,以保证表结构一致,空白表如图:
fig. 1

从 A 列开始,数据表的每个字段对应一列。

每列的第1行是该列的注释,用于说明这一行是做什么的,可留空但不建议。

每列的第2行是该列的名称,建议以 Pascal 命名法命名,即使用首字母大写的单词组合

每列的第3行是该列的数据类型,目前支持4种类型:int-整数,text-字符串,ints-半角逗号隔开的整数,在 C#中

以以数组访问到,texts-同 ints,是以半角逗号隔开的字符串。

从第4行开始是真正的数据内容。

导出数据库和代码

使用工具 DataTableExporter 导出,该工具使用 Python 编写,实现细节见下文。

导出的数据库文件要存放在 Unity 的 StreamingAssets 目录下,目的是让 Unity 在 Build 的时候不要把这个数据库文件打包,这样才能在代码中访问,也方便更新。
同时为了节省程序和工作量和避免手动写代码可能会出现的手滑,工具还可以根据 Excel 表中描述的表结构来生成 C#中的数据结构。

在 Unity 中使用

在上一步中工具已经自动生成了相应的代码,在需要获取某个数据时,只需调用 XxxConfig.GetWithId(id)方法即可获得对应 ID 的一行数据,如下图的示例:
fig. 2
如果有自定义的需求,比如通过某个参数来获取若干条记录,也可以自由添加,重新导出时并不会覆盖。
如果想在数据加载时就进行统一的预处理,比如把某个字符串解析成游戏中特定的数据结构,也可以通过重载CustomProcess() 方法来实现想要的效果。
不过当然,虽然我们使用了 sqlite 数据库,但是并不能使用 SQL 语句来获取数据,只能通过唯一的数字 ID 来索引,好处是一次缓存之后每次访问的速度很快,并且无需多余的 GC。

实现细节

从 Excel 读取信息

使用 Python 库 xlrd 读取 Excel 文档

wb = xlrd.open_workbook(full_path)
table = wb.sheet_by_index(0)

首先读取表结构,针对每一列读取其注释,字段名和数据类型,并拼接用于创建表结构的 SQL 字符串

col_count = table.ncols
create_sql = "DROP TABLE IF EXISTS `%s`;;;\n" \
             "CREATE TABLE `%s`(" % (table_name, table_name)
for i in range(col_count):
    field_comment = table.row_values(0)[i]
    field_name = table.row_values(1)[i]
    field_type = table.row_values(2)[i]
    arr_new_structure.append({"key": field_name, "type": field_type, "comment": field_comment})
    types.append(field_type)
    if field_type != "int" and field_type != "bool":
        field_type = "text"
    create_sql += "\n`%s` %s," % (field_name, field_type)

然后就是从表中读取数据了,从第4行开始读,对于每一行根据每一个字段的值拼接 SQL 字符串。然后执行这些写 SQL 字符串,就完成了一个表的导出。

row_count = table.nrows - 3
insert_items = []
for i in range(row_count):
row_id = i + 3
insert_sql = "INSERT INTO `%s` VALUES (" % table_name
for col_id in range(col_count):
data = table.row_values(row_id)[col_id]
if types[col_id] == "int":
if data == "":
data = 0
data = int(float(data))  # 整数被读取出来是 float,要转成 int
insert_sql += "%d," % data
else:  # text & ints & texts
insert_sql += "'%s'," % data
insert_items.append(insert_sql[:len(insert_sql) - 1] + ")")


生成 C#代码

为了减少程序的工作量同时避免潜在的手滑,也应该准备一套自动生成对应数据结构代码的工具。表结构从上面的代码中已经可以获取到 arr_new_structure。根据这个表结构就可以生成类似下面的代码了:

public class GameCommonConfig : BaseConfig
{
    /// <summary>
    /// 唯一 ID
    /// </summary>
    public int nId = 0;
    /// <summary>
    /// 整数值
    /// </summary>
    public int nIntVal = 0;
    protected override void InitWithData(SqlData data)
    {
        nId = data.GetInt("Id");
        nIntVal = data.GetInt("IntVal");
    }
    /// <summary>
    /// 从数据库中读取全部记录,仅初始化缓存用
    /// </summary>
    /// <returns></returns>
    public static Dictionary<int, GameCommonConfig> CreateAll()
    {
        string sql = "SELECT * FROM `GameCommon`";
        var data = ConfigManager.instance.GetData(sql);
        var ret = new Dictionary<int, GameCommonConfig>();
        do
        {
            var item = new GameCommonConfig();
            item.InitWithData(data);
            ret.Add(item.nId, item);
        } while (data.Next());
        data.Close();
        return ret;
    }

    /// <summary>
    /// 根据 ID 获取一条记录
    /// </summary>
    /// <param name="nId">指定 ID</param>
    /// <returns></returns>
    public static GameCommonConfig GetWithId(int nId)
    {
        var ret = ConfigManager.instance.GetSingleConfig<GameCommonConfig>(nId);
        return ret;
    }
}


ConfigManager

在游戏启动时使用 CreateAll()方法从数据库加载所有记录,这里使用一个叫 ConfigManager 的类来统一调用,为了实现自动化,所以也需要在对应的方法里添加:

/// <summary>
/// 加载全部数据表,初始化用
/// </summary>
private void LoadAll()
{
    m_dictEmptyConfig[typeof(GameCommonConfig)] = new GameCommonConfig();
    m_dictConfigData[typeof(GameCommonConfig)] = GameCommonConfig.CreateAll();
    // 更多的表...
}

执行脚本时,打开 ConfigManager 的代码文件,找到 private void LoadAll()这行字符串,把函数体替换成相应的文本(根据具体导出了多少表)

改善空间

1. 现在想要改一个配置数据就必须先在 Excel 中修改并使用导出工具导出到数据库,在游戏中才能生效,我觉得这样在开发阶段是有点麻烦的,而且多人改表容易出现数据库冲突,之后可以考虑改成在 Unity 的 Editor 模式中直接读取 xlsx 文件,build 打包之后再使用数据库中的数据。关于在 Unity 中读取 Excel 的数据,我在做这个项目的关卡编辑器时有找到了一个功能强大的 Excel 文档操作的库 EPPlus,支持读取和写入,甚至可以使用公式,调整格式和创建图表。

2. 现在所有配置数据都是在游戏启动时统一全部加载的,现在数据不多还好,当游戏规模变大之后,这个过程可能会比较消耗时间(也可能会占用较多内存),这个暂时还没有更好的解决方案,毕竟加载的时间省不掉,我们也不知道什么时候会用到这些数据,而且反正加载是一次性的,就让他在启动时花点时间,姑且先这么用吧(doge

3. 之后可以支持更灵活的注释,比如注释表中的一整行(一整列的注释暂时没必要,即使导出了,程序不去使用它就好了,就是不知道规模大了之后每条记录多了这一个字段会有多大的影响)。以特定字符串开头的行将会被忽略,不会导出到数据库。

本文为用户投稿,不代表 indienova 观点。

近期点赞的会员

 分享这篇文章

您可能还会对这些文章感兴趣

参与此文章的讨论

  1. Eran 2018-01-16

    https://gitee.com/eran/ExcelToJson

    自己写的 用了很久. 自荐一下.

  2. wing 2021-08-24 微信会员

    我也自荐一下
    支持多种语言及导出方式:
    https://github.com/wingcd/go-xlsx-exporter

您需要登录或者注册后才能发表评论

登录/注册