简单说,ScriptableObject是Unity用来存储数据的容器。
通过它我们可以创建并存储自己的数据资源,而不需要依托于Prefab/GameObject。
除了用于存放基本数据结构之外,还可以通过Serializable来自定义可序列化的类型。关于序列化的知识可以看这里。
虽然Unity的序列化有诸多限制,可存储的数据类型也局限于Array和一维的List,但对于数据存储而言这已经足够。类似字典和哈希表的数据类型,最简单的办法是直接存成k-v一维列表,在载入游戏之后再存成字典/哈希表。我们可以很自由的组合我们想要存储的结构,如果你愿意你可以存成树的形式。
当然,如果你觉得默认的序列化已经满足不了你的需求,你可以自定义序列化方式,通过实现ISerializationCallbackReceiver接口来自定义你的序列化行为。我觉得通常情况我们不需要到这一步就可以实现大部分需求,所以不详细叙述自定义序列化。
通过Unity强大的编辑器扩展功能,我们依据需求,实现自己的资源编辑器,对一些复杂的数据来说,专门的编辑器能有效的提高工作效率。另外,Unity中能很容易的实现图形化的编辑方式,所以一些表格/折线等信息也能很好的编辑。
在很多时候,纯文本的表格依然是存放数据的第一选择。简单的csv/json文件,在大量数据编辑中依然十分实用,因为它们本身就具备非常良好的可读性和可编辑性。
既然有文本文件,那么我们就需要去解析它们。而有了ScriptableObject,我们可以把文本解析的过程从runtime提前到打包之前。只需要在编辑器中把文本文件解析并存储到ScriptableObject,在游戏中直接使用这些ScriptableObject,从而不需要再次解析这些文本文件。
这么做有什么好处呢?
以最简单的csv文件为例,通常我们在游戏中使用一个csv文本文件,第一步读入文本字符串,第二步解析文本字符串并存放到Csv文件类实体中,然后我们实际使用的是这个CSVObject。而使用ScriptableObject的情况下,我们只需要一步,直接读入解析好的CSVObject即可,少了两个步骤自然性能更好。
由于我们把解析的过程从Runtime中移到了编辑阶段,那么解析过程的效率就变得不那么重要了。所以我们可以自由的使用各种反射/LINQ这些平常在runtime中,需要小心避开的东西。
功能强大的反射能够让我们能够更加方便的处理数据,例如我有一个weapon.csv的表格
name | type | damage | requirement | skills | price |
Great Axe | melee | 100 | 0 | N/A | 100 |
Shot Gun | range | 25 | 1 | fast-shot | 2000 |
weaponObject的数据结构
class WeaponObject
{
string name;
string type;
int damage;
int requirement;
string skills;
int price;
}
处理数据的时候,只需要读取表头,通过反射将表头中的列与weaponObject中的成员一一对应,即可把一个weapon.csv直接变成我们需要的List
还觉得不够灵活?如果需要更多的灵活性,我们可以通过使用attribute的方式,在解析过程中重写数据类型的处理方式。例如weaponObject改为
class WeaponObject
{
string name;
[EnumConvert]
EWeaponType type;
int damage;
int requirement;
[ListConvert]
List skills;
int price;
}
通过获取类成员的attribute,然后根据不同的attribute把文本转换成想要的格式并存放在数据成员中,同样也是用一种一劳永逸的方式解决了类似数据结构的解析。
对Json,Unity提供了原生的解析支持。
当然我想说的是,反射功能是非常强大的,但runtime时使用反射可能会有一些性能问题,而在编辑阶段我们就可以自由施展了。
人总是会犯错误的,所以代码需要错误处理。数据资源也会出错,例如前面的weapon.csv中的skills,如果其中的type是非法的类型或者skills中引用了一个根本不存在的skill会发生什么?
如果是直接使用csv文本资源,那么我们或许要等到在游戏中载入这个csv文件时才会发现错误,或者是真正使用到它的时候才发现错误,那么或许测试人员需要找到程序,然后程序定位问题,再找到策划修改数据。如果我们在编辑阶段,就把csv文件解析了,那么我们就可以在解析过程中完成错误检测,然后告诉策划人员你的数据哪里出了错误,你不应该把它上传,整个过程就简单多了。
当然ScriptableObject也有其局限性。首先你需要花一些时间去写一些编辑器扩展,相比之下随处可得的文本解析工具库似乎就来得简单方便得多了,所以需要的时候我们确实应该付出一定的时间成本,但不必要的情况下,选择最为简单方便的途径才是正确的。
另外Unity序列化有其局限性,所以使用上可能不是那么舒服。
此外ScriptableObject是二进制格式的,所以无法Merge也无法diff,万一程序出现什么错误,造成的文件损坏可能是毁灭性的。
所以我的观点是,ScriptableObject更多时候是作为一个自动生成的中间文件来使用,开发时的数据还是存放在文本文件中,需要的时候再打包。如果必要时,甚至可以同时有两条路径,开发版本直接使用文本文件,release版本则使用文本文件转换成的ScriptableObject,当然这需要更多的精力去完善代码构架。
另外就是,如果文件要与其他系统共享(例如后台服务器),那么使用unity格式的文件显然是不合适的。
最后贴一篇论坛上的帖子。
https://forum.unity.com/threads/correct-way-to-maintain-hundreds-of-objects-for-a-procedural-system.514378/
其中一句"XML is pointless"简直是掷地有声:)
xml作为一种对人类非常不友好的文件格式我实在是想不到为什么要去直接编辑xml。当然有工具的情况下除外,因为xml对机器还是非常友好的。