找回密码
 立即注册
查看: 1039|回复: 17

Unity的序列化配表问题用什么方法比较好?

[复制链接]
发表于 2020-12-28 13:17 | 显示全部楼层 |阅读模式
Unity的序列化配表问题用什么方法比较好?
发表于 2020-12-28 13:20 | 显示全部楼层
介绍下个人开源的电子表格数据导出工具套件
跨平台的高性能便捷电子表格导出器

优点
    编写电子表格, 导出. 只需2步, 即可导出数据!跨平台运行, 无第三方依赖, 无需任何的vbs,vba,dll支持文件格式最多的导出器(json, lua, C#+二进制, protobuf text, proto, golang)一次设置, 自动生成索引代码, 支持lua, C#单元格字段列顺序随意调整, 自动检查错误, 精确报错位置强类型, 导出时自动类型检查, 提前暴露表格错误支持中文枚举值, 中文结构体字段, 编写,更直观全中文导出提示,并支持多语言导出提示支持导出Tag匹配,导出需要的部分, 避免客户端混合服务器私密数据支持类型信息导出, 方便无反射的语言(例如C++)使用充分利用CPU多核进行导出, 是已知的现有导出器中速度最快的持续更新, 不断添加新功能, 提高工作效率
商用项目
    Fairy in Wonderland https://itunes.apple.com/us/app/fairy-in-wonderland-parkour/id1128656892?l=zh&ls=1&mt=8Mad Magic https://itunes.apple.com/app/id1146098397消诺克 http://www.taptap.com/app/15881
迭代历程
    2016年8月: 第六代导出器,tabtoy v2 调整为以电子表格为中心的方式, 支持v1 90%常用功能
    增加: 所有导出文件均为1个文件, 提高加载读取速度
    增加: 二进制合并导出(第五代导出器需要使用2个工具才能完成)
    增加: C#源码导出及索引创建,无需protobuf支持
    增加: proto格式导出, 支持v2,v3格式
    重构代码, 导出速度更快2016年3月: 第五代导出器,tabtoy v1 在四代基础上重构,开源,支持并发导出2015年: 第四代导出器,基于Golang导出器,增加ID重复检查,数组格的多重写法, 支持a.b.c栏位导出, 导出速度大大提高2013年: 第三代导出器,在二代基础上做到内容格式与导出器独立,但依然依赖csv前置导出,增加逗号分隔格子内容,导出速度慢2012年: 第二代导出器,基于C++和Protobuf的导出器,内容格式与导出器混合编写,需要vbs导出csv,速度慢2011年: 第一代导出器,基于VBA的表格内建导出器,速度慢,复用困难,容易错,不安全
应用情况
前面多个版本都在本人项目中使用
53个Excel源文件, 格式xlsm, 大小3.8M
导出速度
9.4s 第四代导出器
4.9s 第五代导出器单线程
2.4s 第五代导出器i7-4790 8核并发


现在很多朋友的商业项目、独立游戏项目都在用。从服务器Golang到客户端C#、lua都支持,并且他们表示用起来很爽
本人已经自2009年起没再单独解析过配置文件了, 一直用这套做游戏客户端和服务器
觉得好, 请Star, 谢谢

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2020-12-28 13:27 | 显示全部楼层
介绍一下我们团队目前的做法。我们的做法是使用Excel填表+XML/DataContract序列化+ScriptableObject存储的方式。这种方式是几个项目做下来后归纳下来的最适合我们团队的方式。它的优点包括:
    使用Excel作为数据源,策划的工作可以直接输出为数据
    使用ScriptableObject进行存储,不需要在运行时进行额外的反序列化工作(如XML/Json反序列化);可以在Unity里查看/修改数据可以维持数据之间的引用关系不依赖.net的序列化功能或第三方库,因此可以维持较高的裁剪(strip)等级,缩小发布包绝大部分数据导入操作都是自动化的;理论上可以实现完全自动化数据存在XML形式的中间状态,便于开发外部工具

当然缺点也是不少的:
    依赖外部工具(我们自己开发的一系列工具)ScriptableObject的引用关系非常难伺候工作流简单,但内部逻辑相对复杂


正如前文所说,我们使用Excel作为初始数据源。为此,我们对Excel的表格布局做了一系列约定:
    同类数据(映射到一个相同的class)存储到同一个分页(sheet)中第一行为表头,每一列映射到一个属性属性最好是基本类型、枚举或者它们的数组。更复杂的结构实际上是可以实现的,但会让表格的可读性大大降低分页和列可以存储不需要导入游戏的额外数据(例如策划自己写的批注之类的),但必须在分页名或表头前上加上#符号

下图就是一个简单的数据源表:


表的结构确定后,程序会在一个独立的C#工程(数据assembly)里建立相应的数据类:

可以看出,Excel分页和表头的名称是和代码里相应的DisplayName匹配的。

接下来就要祭出我们的数据导入工具了。这个工具叫axe,我们一般亲切地称之为小斧子(其实叫这个名字只是因为我当时写这个工具时随手用了一个镐的图标= =|||):

这个工具原本是我们的一个多功能编辑器,后来自从加入了导入excel文件的功能后就基本上没人用其他功能了……工具的原理很简单:加载上文所说的包含数据类的assembly,通过反射来将Excel数据生成为对象,然后使用XML序列化导出为XML文件。

到这一步,基本上就已经具备足够的可用性了。将数据assembly作为unity的插件,然后使用XML反序列化即可。我们之前的项目也是这样做的。不过这样有一些不尽完美之处:一是跟unity的结合度不高,无法在unity里查看/修改数据;二是引用关系必须在运行时才能(通过查询Key来)确定;三是依赖于XML序列号乃至DataContract序列化功能,这样一来裁剪等级降低,发布包就要大出一截。除此之外,还有一些诸如必须在运行时反序列化影响性能之类的小瑕疵。因此,我们决定进一步将XML数据处理为Unity的ScriptableObject进行序列化。

这么做的第一步是将数据类导入进Unity,并使其派生ScriptableObject类。为了实现这一步的自动化,我们又写了个代码生成器。这是个命令行工具,它会读取数据assembly,搜索其中需要导入的类(和相关数据类型,例如使用到的枚举),然后生成继承自ScriptableObject的类的代码给Unity使用:

当然,如你所见,这个代码生成器还会做一些额外的工作,比如在上图中,它生成了一个CopyFrom方法,从原数据类拷贝数据。
它还会根据需要生成引用解决代码。例如,在数据类里的这个属性

就会生成相应的解决代码:

这样一来,所有的引用关系就可以在导入数据为ScriptableObject时确定。

代码生成器被嵌入到数据assembly工程的Postbuild里,因此这些代码会自动生成到Unity工程中。

完成这些工作以后,我们将AXE导出的数据存储到Unity工程的Assets下,然后为Unity写了一个AssetPostprocessor。与此相关的内容可以查看Unity的文档:Unity - Scripting API:。这个AssetPostprocessor会在Unity的Assets目录下任何文件发生更改(包括创建和删除)时得到通知,这时候我们只要看看改变的文件是否是AXE导出的文件(我们使用了特殊的扩展名.rdx,并以此判断),如果是的话,将其反序列化成原数据类,并通过之前生成的CopyFrom方法来生成ScriptableObject数据。生成完毕并解决引用关系之后,将其保存为Asset。这些在Unity的文档里都有详述,我就不多说了。

这些步骤里最大的挑战在于维护ScriptableObject的引用关系。比如说技能数据引用Buff数据,那么在导入技能数据时,Buff数据必须已经导入完成。这要求数据必须以正确的先后顺序导入。如果Buff数据发生了变化,还必须同时重新导入技能数据。如果一个数据类型包含自引用,甚至几种数据类型之间存在循环引用,则需要更复杂的处理逻辑。但是,就算你真的完美地处理了这些引用关系问题,ScriptableObject的引用依然是极不稳定的,引用经常会莫名其妙的失效。这里面有一些我们已经排查解决,有一些暂时还不知道原因。不过这些问题都可以通过重导数据来解决。如果有哪位走到了这一步,欢迎探讨。

总之,完成这些步骤以后,数据已经被处理为Unity的ScritableObject asset,可以开心地使用了!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2020-12-28 13:33 | 显示全部楼层
同一个数据 有多种表现形式,妄图 通过一种通用形式数据, 通过修饰器来 呈现不同接口的尝试,都会失败的

excel。数据。导入程序最好用的形式就是 直接生成代码,用python 写个代码模板,把数据生成c#代码,编译dll,直接游戏里面用,连加载都省了。

游戏里面玩家 实体 数据存储为pb,存盘时,直接存pb即可

参考我的火炬之光Demo 做法

还有不要尝试直接把excel 数据转成游戏对象

游戏数据有多种, 绝对静态 例如配置表中数据

动态数据 玩家等级 hp。等,以及根据等级而挑选不同的hp。上限

临时数据 根据攻防计算伤害

excel数据只能算 三层数据架构最底层

修改实体数据就去修改对象内嵌的pb即可

不过pb消息是只读的, 改下pb 代码生成器,生成 可修改 实体代码

我的Demo里自带的pb插件 消息对象可修改
发表于 2020-12-28 13:43 | 显示全部楼层
最近伺候了策划大爷快一个月.

我这里主要还是策划负责一部分数据源的维护,用excel来表现,比如道具,基础技能数据等等.然后用自己编写的工具导出json.
另外一部分有一些引用关系的数据用自己在unity里面写的编辑器来实现.导出json格式

这样有一个好处就是,策划负责的数据可以导出两份不同的数据,服务器和客户端使用同一份数据表,但是导出的json格式可以有所区分.比如服务端可能只要数据的 1,2,4,7列, 而客户端需要1,2,3,4,5,6列.
另外用unity写的编辑器, 很多模型或者图标的选择就很方便, 处理各类引用就很方便,你只需要负责基础的逻辑编写和界面的编写.举个例子,编写关卡数据的时候,地图信息/孵化点信息/障碍物/摄像机镜头的数据就很好获取. 策划在使用编辑器的时候只需要选择什么时候孵化什么怪物,什么时候解锁障碍,什么时候播放过场镜头就可以了.

----------------------------------------------------------------------------------------------------------
下面的图是一般的excel数据,


转换到unity.

包括SVN的上传更新,全部在一个编辑器里面完成

下面是一个关卡的数据编辑器


----------------------------------------------------------------------------------------------------------
上面讲到 ScriptableObject, 我在制作编辑器的时候这个也用到了.如果你的数据比较简单的话可以使用,如果数据很复杂的时候就不要考虑了.主要是维护起来很麻烦, 尤其在团队合作的时候, 处理文件冲突很麻烦.
上面写的编辑器保存的数据都是以json格式保存的,在团队里面,出现冲突的时候很容易就找到地方修改了. 而且数据很容易热更新.
另外, 永远不要低估了策划的脑洞.

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2020-12-28 13:47 | 显示全部楼层
我个人比较喜欢Excel反射为实体类的做法,因为这样在编辑器中开发插件会相对容易些,Unity的序列化和反序列化本身是存在不足的,完全依赖这些特性是冒险的做法,推荐我的一个个人项目:Excel2Unity,它可以解决从策划配表到程序解析的转换,缺点是要自己去维护相应的工作流,因为转换成XML或者是JSON,都无法直接实例化到编辑器中啊,Unity里的黑箱子还是比较多的,以上。

有朋友提及Protobuf,该项目是一个序列化和反序列化的工具,最终会被转换成byte数组,从题主的角度出发,他可能希望得到的是一个可以在编辑器中预览、读取、编辑的方案,肯定会涉及到编辑器相关的内容,当然如果是简单的功能,我可以自己尝试写出来!
发表于 2020-12-28 13:50 | 显示全部楼层
额……
https://github.com/findix/litjson-unity

我自己改的litjson
序列化和反序列化只需
JsonMapper.ToJson(obj)
JsonMapper.ToObject<T>(str)
专为Unity修改……实际项目使用
你可以试试能不能满足,有问题可以给我发issue
发表于 2020-12-28 13:53 | 显示全部楼层
能用配置表 csv 解决的,就不要用 json,因为 json 不方便同时查看多个记录,也没有很好的编辑和查看工具。如果你用 csv 表格的话,直接用excel 就能打开,策划能方便的同时查看多条记录,进行对比和修改。我写了一个 csv2c# 的工具,可以直接生成读取 csv 的代码,这样就不需要手工编写读取 csv 的代码。

玩家信息或地图这类有结构的数据,可以用 json 或 xml 来存取,你碰到的 null 问题可以换个 json 库试试。我是手动编写所有 json 的序列化和反序列化代码的,字段和值都可以自己定义,比起自动序列化的库来的自由些,缺点就是机械活量太大。

ScriptableObject 我一般只存放和程序有关的配置信息,不适合给策划拿来改数值,因为你可能还得自己编写专门的编辑器才能让策划改起来方便,那么为什么不直接让策划用 excel 改呢。
发表于 2020-12-28 13:57 | 显示全部楼层
2020年更新:
最近在Unity AssetStore上发现了一个数据编辑插件非常不错,可以在这个基础上添加自己需要的功能。
Yade sheet | Utilities Tools | Unity Asset Store====================================================================
原答案:
看了一圈我觉得还是这个最好:
Excel辅助开发工具
Excel工具升级版
发表于 2020-12-28 14:06 | 显示全部楼层
//----------------------------------------------
//            NGUI: Next-Gen UI kit
// Copyright  2011-2014 Tasharen Entertainment
//----------------------------------------------

using UnityEngine;
using System.Text;
using System.Collections.Generic;

/// <summary>
/// MemoryStream.ReadLine has an interesting oddity: it doesn't always advance the stream's position by the correct amount:
/// Byte array to String in C#
/// Solution? Custom line reader with the added benefit of not having to use streams at all.
/// </summary>

public class ByteReader
{
        byte[] mBuffer;
        int mOffset = 0;

        public ByteReader (byte[] bytes) { mBuffer = bytes; }
        public ByteReader (TextAsset asset) { mBuffer = asset.bytes; }

        /// <summary>
        /// Whether the buffer is readable.
        /// </summary>

        public bool canRead { get { return (mBuffer != null && mOffset < mBuffer.Length); } }

        /// <summary>
        /// Read a single line from the buffer.
        /// </summary>

        static string ReadLine (byte[] buffer, int start, int count)
        {
#if UNITY_FLASH
                // Encoding.UTF8 is not supported in Flash :(
                StringBuilder sb = new StringBuilder();

                int max = start + count;

                for (int i = start; i < max; ++i)
                {
                        byte byte0 = buffer;

                        if ((byte0 & 128) == 0)
                        {
                                // If an UCS fits 7 bits, its coded as 0xxxxxxx. This makes ASCII character represented by themselves
                                sb.Append((char)byte0);
                        }
                        else if ((byte0 & 224) == 192)
                        {
                                // If an UCS fits 11 bits, it is coded as 110xxxxx 10xxxxxx
                                if (++i == count) break;
                                byte byte1 = buffer;
                                int ch = (byte0 & 31) << 6;
                                ch |= (byte1 & 63);
                                sb.Append((char)ch);
                        }
                        else if ((byte0 & 240) == 224)
                        {
                                // If an UCS fits 16 bits, it is coded as 1110xxxx 10xxxxxx 10xxxxxx
                                if (++i == count) break;
                                byte byte1 = buffer;
                                if (++i == count) break;
                                byte byte2 = buffer;

                                if (byte0 == 0xEF && byte1 == 0xBB && byte2 == 0xBF)
                                {
                                        // Byte Order Mark -- generally the first 3 bytes in a Windows-saved UTF-8 file. Skip it.
                                }
                                else
                                {
                                        int ch = (byte0 & 15) << 12;
                                        ch |= (byte1 & 63) << 6;
                                        ch |= (byte2 & 63);
                                        sb.Append((char)ch);
                                }
                        }
                        else if ((byte0 & 248) == 240)
                        {
                                // If an UCS fits 21 bits, it is coded as 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
                                if (++i == count) break;
                                byte byte1 = buffer;
                                if (++i == count) break;
                                byte byte2 = buffer;
                                if (++i == count) break;
                                byte byte3 = buffer;

                                int ch = (byte0 & 7) << 18;
                                ch |= (byte1 & 63) << 12;
                                ch |= (byte2 & 63) << 6;
                                ch |= (byte3 & 63);
                                sb.Append((char)ch);
                        }
                }
                return sb.ToString();
#else
                return Encoding.UTF8.GetString(buffer, start, count);
#endif
        }

        /// <summary>
        /// Read a single line from the buffer.
        /// </summary>

        public string ReadLine ()
        {
                int max = mBuffer.Length;

                // Skip empty characters
                while (mOffset < max && mBuffer[mOffset] < 32) ++mOffset;

                int end = mOffset;

                if (end < max)
                {
                        for (; ; )
                        {
                                if (end < max)
                                {
                                        int ch = mBuffer[end++];
                                        if (ch != '\n' && ch != '\r') continue;
                                }
                                else ++end;

                                string line = ReadLine(mBuffer, mOffset, end - mOffset - 1);
                                mOffset = end;
                                return line;
                        }
                }
                mOffset = max;
                return null;
        }

        /// <summary>
        /// Assume that the entire file is a collection of key/value pairs.
        /// </summary>

        public Dictionary<string, string> ReadDictionary ()
        {
                Dictionary<string, string> dict = new Dictionary<string, string>();
                char[] separator = new char[] { '=' };

                while (canRead)
                {
                        string line = ReadLine();
                        if (line == null) break;
                        if (line.StartsWith("//")) continue;

#if UNITY_FLASH
                        string[] split = line.Split(separator, System.StringSplitOptions.RemoveEmptyEntries);
#else
                        string[] split = line.Split(separator, 2, System.StringSplitOptions.RemoveEmptyEntries);
#endif

                        if (split.Length == 2)
                        {
                                string key = split[0].Trim();
                                string val = split[1].Trim().Replace("\\n", "\n");
                                dict[key] = val;
                        }
                }
                return dict;
        }

        static BetterList<string> mTemp = new BetterList<string>();

        /// <summary>
        /// Read a single line of Comma-Separated Values from the file.
        /// </summary>

        public BetterList<string> ReadCSV ()
        {
                mTemp.Clear();

                if (canRead)
                {
                        string line = ReadLine();
                        if (line == null) return null;
                        line = line.Replace("\\n", "\n");

                        int wordStart = 0;
                        bool insideQuotes = false;

                        for (int i = 0, imax = line.Length; i < imax; ++i)
                        {
                                char ch = line;

                                if (ch == ',')
                                {
                                        if (!insideQuotes)
                                        {
                                                mTemp.Add(line.Substring(wordStart, i - wordStart));
                                                wordStart = i + 1;
                                        }
                                }
                                else if (ch == '"')
                                {
                                        if (insideQuotes)
                                        {
                                                if (i + 1 >= imax)
                                                {
                                                        mTemp.Add(line.Substring(wordStart, i - wordStart).Replace("\"\"", "\""));
                                                        return mTemp;
                                                }

                                                if (line[i + 1] != '"')
                                                {
                                                        mTemp.Add(line.Substring(wordStart, i - wordStart));
                                                        insideQuotes = false;

                                                        if (line[i + 1] == ',')
                                                        {
                                                                ++i;
                                                                wordStart = i + 1;
                                                        }
                                                }
                                                else ++i;
                                        }
                                        else
                                        {
                                                wordStart = i + 1;
                                                insideQuotes = true;
                                        }
                                }
                        }

                        if (wordStart < line.Length)
                        {
                                mTemp.Add(line.Substring(wordStart, line.Length - wordStart));
                        }
                        return mTemp;
                }
                return null;
        }
}
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-20 09:00 , Processed in 0.071626 second(s), 23 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表