资讯动态

AssetRipper 数据存储体系拆解:配置与元信息如何被管好

发布时间:2026/9/20 16:23:01 来源:尧图企业网站定制
AssetRipper 数据存储体系拆解配置与元信息如何被管好【免费下载链接】AssetRipperGUI application to analyze game files项目地址: https://gitcode.com/GitHub_Trending/as/AssetRipper每次启动 AssetRipper它都要记住上次的导入设置——脚本反编译开到哪一级、导出目录放在哪里。这份记忆没有走任何关系型数据库而是由工具内置的 AssetRipper 数据存储层Source/AssetRipper.Configuration/承担。作为解析游戏文件的 Unity 资产提取工具AssetRipper 在这里存放两类数据单值配置项与列表形式的资产元信息。下文按一条配置数据的完整生命周期讲清它从写入、序列化落盘到读回、查询的全过程。先看场景改了导入设置后重启工具发生了什么CoreConfiguration.cs 是这套存储的消费方它只暴露两个容器public class CoreConfiguration { public SingletonDataStorage SingletonData { get; } new(); public ListDataStorage ListData { get; } new(); public ImportSettings ImportSettings { get SingletonData.GetStoredValueImportSettings(nameof(ImportSettings)); set SingletonData.SetStoredValue(nameof(ImportSettings), value); } }构造函数里注册了默认值以nameof(ImportSettings)为 key放入一个JsonDataInstanceImportSettings序列化器来自System.Text.Json的源生成上下文ImportSettingsContext.Default.ImportSettings。此后业务代码只操作ImportSettings属性完全感知不到底下存的是什么格式ResetToDefaultValues()则通过两个容器的Clear()一次性还原所有条目。为什么配置层要分四层先把每层职责划清楚打开 AssetRipper.Configuration 会看到一小组类DataEntry、DataInstance、DataSet、DataStorageT及各自的 String / Parsable / Json 变体。分层不是凑数量每层只解决一个问题DataEntry是公共契约只有一个Clear()重置自身而不删除。容器级的Clear()就是遍历所有条目逐个调用它。DataInstance表达单个值。抽象成员Text可读写字符串是它与外界的唯一接口泛型子类DataInstanceT持有Value和一个DataSerializerTText的 get/set 分别走序列化与反序列化。DataSet表达一组值。基类同时实现IEnumerable并暴露StringAccessor——一个IReadOnlyListstring视图把任意类型集合按逐个转字符串的方式读出来。泛型子类DataSetT内部就是一张ListT加一个序列化器。DataStorageTT : DataEntry是索引层一张Dictionarystring, T提供Keys、下标访问、TryGetValue/GetValue/Add/Clear。它不关心值的内部结构只管按 key 找条目。SingletonDataStorage和ListDataStorage只是把T钉死为DataInstance或DataSet再加几个按类型取值的便捷方法。也就是说值的形态单值/列表由子类决定存储形态字典由基类统一新增一种序列化格式不需要动存储层。一条配置数据的一生写入、落盘、读回、查询 写入两个容器钉死两种数据形态DataStorage本体就是个带外壳的字典没有索引、没有锁public class DataStorageT where T : DataEntry { protected readonly Dictionarystring, T data []; public bool TryGetValueTValue(string key, out TValue? value) where TValue : T data.TryGetValue(key, out T? stored) ? (value stored as TValue, value is not null) : (value default, false); public TValue GetValueTValue(string key) where TValue : T TryGetValue(key, out TValue? v) ? v : throw new KeyNotFoundException(); }SingletonDataStorage面向单值额外提供TryGetStoredValueT/GetStoredValueT/SetStoredValueT取出条目后做一次is DataInstanceT类型检查成立才返回Value。ListDataStorage面向列表提供Add(key, Liststring)与AddT(key, ListT)要求T : IParsableT分别包装成StringDataSet或ParsableDataSetT。序列化落盘三种序列化器都说字符串这门语言DataSerializerT只有三个抽象方法Serialize、Deserialize、CreateNew。项目里正好有三种实现对应三种数据方言序列化器约束行为StringDataSerializer无恒等变换Deserialize直接返回原文空值用ParsableDataSerializerTT : IParsableT, new()T.TryParse解析失败回落到CreateNew()JsonDataSerializerTT : new()基于JsonTypeInfoT做完整 JSON 序列化关键设计是字符串作为通用货币任何条目只要能产出并吃进一个string就可以被同一套容器管理、写进同一份落盘文件、甚至在 GUI 里以文本形式展示。格式差异被完全关进序列化器内部。反序列化读取坏数据会被宽容地降级JsonDataSerializer的读取路径是整套系统里最能体现取舍的地方public override T Deserialize(string text) { if (string.IsNullOrEmpty(text)) return CreateNew(); try { return JsonSerializer.Deserialize(text, typeInfo) ?? CreateNew(); } catch { return CreateNew(); // 坏数据静默回落为默认值 } }空文本、null结果、反序列化异常全部退回CreateNew()的默认实例。ParsableDataSerializer的TryParse失败时同样回落。对用户可能手改过配置文件的场景这是合理的——工具宁愿丢一次修改也不能启动即崩代价是错误被吞掉了配置看起来正常其实是默认值。查询Try 与抛异常两套入口对应两种调用姿态读取侧的 API 呈明显的双轨设计TryGetValue/TryGetStoredValue调用方不确定 key 是否存在比如按 key 探测可选配置GetValue/GetStoredValuekey 缺失属于编程错误直接用KeyNotFoundException暴露。列表数据则通过DataSet.Strings拿到StringAccessor视图按IReadOnlyListstring的方式索引、追加、清空底层每个位置都经序列化器转换。以 ImportSettings 为例走完全流程把上面的组件串起来ImportSettings的完整生命周期大致是// ① 首次启动CoreConfiguration 构造时注册默认条目 var config new CoreConfiguration(); // → SingletonData.Add(ImportSettings, // new JsonDataInstanceImportSettings(ImportSettingsContext.Default.ImportSettings)) // ② 用户在界面里调整脚本反编译级别 config.ImportSettings.ScriptContentLevel ScriptContentLevel.Level1; // → SetStoredValue 把新对象写回 DataInstanceImportSettings.Value // ③ 落盘上层取出 instance.Text此时 Value 被序列化为 JSON 字符串写入文件 // ④ 再次启动从文件读回字符串赋给 instance.Text // → Text 的 setter 触发 JsonDataSerializer.Deserialize还原 Value // ⑤ 业务代码照旧只碰属性 bool scriptsOff config.DisableScriptImport; // 内部读 Level0 判断注意 ③④ 两步里存储层只提供了Text这个字符串接口真正读写文件的事由上层决定。这就是存储与格式分离的实际收益换一种落盘格式甚至手工编辑文本都不需要碰DataStorage和CoreConfiguration。列表侧同理ListData.Add(README, [...])存入StringDataSet需要按字符串处理时取GetValueStringDataSet(key).Strings逐元素走的就是恒等序列化器转换代价近乎为零。这套设计的边界与代价适用前提是单机单线程。data是一张裸Dictionary没有任何同步两个线程同时Add或Clear会直接损坏容器。AssetRipper 的 GUI 流程是单线程驱动这个假设成立但把它移植到并发场景前必须自己加锁。查询是 O(n) 起步的。没有二级索引key 即唯一查找路径对几十个配置 key 毫无压力但拿它当资产数据库存上万条记录并不合适——AssetRipper 的主资产数据确实放在独立的 Assets/IO 模块里配置层只放小而慢变的东西。宽容解析是双刃剑。catch { return CreateNew(); }保证工具永远能启动却让配置文件损坏这种事件无声无息。若在自己的项目里照搬至少应保留一条日志通道否则排查问题时会怀疑人生。⚠️Text的即时转换有成本。DataSetT每次经StringAccessor读一个元素都会完整跑一遍Serialize对string或廉价ToString()的类型无所谓对复杂对象则是每访问一次序列化一次——这类集合只适合展示不适合热路径。可迁移到自有项目的核心是两点把字符串定为条目与外界的唯一契约格式差异全部隔离在可替换的序列化器里以及单值/列表/字典索引三个职责严格分层扩展格式时存储层零改动。并发控制、错误上报、索引这些它没做的事则需要在搬走之前按自己的场景补齐。完整源码可从 Source/AssetRipper.Configuration/ 逐文件阅读配合 Source/AssetRipper.Import/Configuration/ 下的CoreConfiguration能看到消费侧的真实用法。【免费下载链接】AssetRipperGUI application to analyze game files项目地址: https://gitcode.com/GitHub_Trending/as/AssetRipper创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价