求Godot中Newtonsoft.Json序列化Resource的优雅实现方案
问题背景与需求
基于Godot引擎,磁盘资源文件属于Resource类型,需通过ResourceLoader.Load(string path)方法加载。核心需求:
- 序列化包含已加载
Resource的类型时,仅记录资源的类型与路径信息 - 反序列化时通过加载指定路径的资源还原实例,而非创建新的
Resource对象
现有使用Newtonsoft.Json 13.0.3的实现为依赖执行顺序的hack方案,存在以下问题:
- 升级Newtonsoft.Json版本易失效
- 维护性差,逻辑分散在多个类中
- 依赖全局状态,存在线程安全隐患
现有实现代码
public class KnownTypesBinder : ISerializationBinder { public string CurrentResourcePath { get; private set; } public Type BindToType(string assemblyName, string typeName) { Type prospectType = Type.GetType(typeName); if (prospectType == null) { // 当typeName包含'@'时,说明是带路径的资源类型,路径在'@'之后 int strIndex = typeName.IndexOf('@'); Assert.IsTrue(strIndex != -1); // 存储当前反序列化的资源路径,供后续契约使用 CurrentResourcePath = typeName.Substring(strIndex + 1); string realTypeName = typeName.Substring(0, strIndex); return Type.GetType(realTypeName); } else { return Type.GetType(typeName); } } public void BindToName(Type serializedType, out string assemblyName, out string typeName) { if (typeof(Resource).IsAssignableFrom(serializedType)) { // 为资源类型生成带路径的类型名,格式为:"Godot.Resource@res://MyPath/MyResource.tres" // 其中"Godot.Resource"是类型信息,"res://MyPath/MyResource.tres"是Godot资源路径 // BindToName无法直接获取当前序列化对象,因此从契约解析器中获取 assemblyName = null; Resource currentlySerializedResource = (CoreLogic.Instance.JsonSerializingSettings.ContractResolver as ResourceContractResolver).CurrentlySerializedObject as Resource; Assert.IsTrue(currentlySerializedResource != null); string path = currentlySerializedResource.ResourcePath; typeName = serializedType.FullName + "@" + path; } else { string id = serializedType.FullName; assemblyName = null; // 此处可补充程序集信息,不在本次讨论范围内 typeName = id; } } } class ResourceContractResolver : DefaultContractResolver { // 指向即将序列化的对象,当开始序列化对象内部属性时,该值会切换为对应属性的值 public object CurrentlySerializedObject { get; private set; } protected override JsonObjectContract CreateObjectContract(Type objectType) { JsonObjectContract contract = base.CreateObjectContract(objectType); if (typeof(Resource).IsAssignableFrom(objectType)) { // 替换默认创建逻辑,通过加载资源生成实例 contract.DefaultCreator = () => { string currentResourcePath = (CoreLogic.Instance.JsonSerializingSettings.SerializationBinder as KnownTypesBinder).CurrentResourcePath; return ResourceLoader.Load(currentResourcePath); }; } return contract; } protected override List<MemberInfo> GetSerializableMembers(Type objectType) { if (typeof(Resource).IsAssignableFrom(objectType)) { // 不序列化资源的任何成员 return new List<MemberInfo>(); } else { return base.GetSerializableMembers(objectType); } } protected override JsonContract CreateContract(Type objectType) { JsonContract contract = base.CreateContract(objectType); contract.OnSerializingCallbacks.Add(OnSerializingCallback); return contract; } private void OnSerializingCallback(object o, StreamingContext context) { CurrentlySerializedObject = o; } }
序列化示例
容器类型定义
public class ContainerType { public int ValueInt = -1; public float ValueFloat = 3.14f; public Resource Resource = null; // 序列化前已加载路径为res://MyPath/MyResource.tres的资源 }
序列化后的JSON
{ "$type": "ContainerType", "ValueInt": -1, "ValueFloat": 3.14, "Resource": { "$type": "Godot.Resource@res://MyPath/MyResource.tres" } }
更优雅的实现方案:自定义JsonConverter
使用JsonConverter<Resource>替代原有绑定器和契约解析器,直接控制Resource的序列化与反序列化逻辑,避免全局状态依赖,提升线程安全性与版本兼容性。
实现代码
public class ResourceJsonConverter : JsonConverter<Resource> { public override void WriteJson(JsonWriter writer, Resource value, JsonSerializer serializer) { if (value == null) { writer.WriteNull(); return; } // 写入类型信息和资源路径,与原有方案格式兼容 writer.WriteStartObject(); writer.WritePropertyName("$type"); writer.WriteValue($"{value.GetType().FullName}@{value.ResourcePath}"); writer.WriteEndObject(); } public override Resource ReadJson(JsonReader reader, Type objectType, Resource existingValue, bool hasExistingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; // 读取$type字段解析资源路径 JObject obj = JObject.Load(reader); string typePath = obj["$type"]?.Value<string>(); if (string.IsNullOrEmpty(typePath)) throw new JsonSerializationException("Resource $type字段缺失"); int atIndex = typePath.IndexOf('@'); if (atIndex == -1) throw new JsonSerializationException("Resource $type格式无效"); string resourcePath = typePath.Substring(atIndex + 1); return ResourceLoader.Load<Resource>(resourcePath); } // 支持Resource的所有子类 public override bool CanConvert(Type objectType) { return typeof(Resource).IsAssignableFrom(objectType); } }
使用方式
将转换器添加到JsonSerializerSettings中,无需依赖全局状态:
var settings = new JsonSerializerSettings { Converters = new List<JsonConverter> { new ResourceJsonConverter() }, TypeNameHandling = TypeNameHandling.Auto // 保留原有类型序列化逻辑 }; // 序列化 string json = JsonConvert.SerializeObject(container, settings); // 反序列化 ContainerType deserialized = JsonConvert.DeserializeObject<ContainerType>(json, settings);
方案优势
- 无全局状态依赖:每个序列化/反序列化过程独立,解决线程安全问题
- 版本兼容性好:不依赖Newtonsoft.Json内部执行顺序,升级版本不易失效
- 维护性强:逻辑集中在单个Converter类中,职责清晰,易于修改和扩展
- 类型安全:直接针对Resource类型处理,避免类型转换的潜在错误
内容的提问来源于stack exchange,提问作者OeilDeLance
相关产品推荐
相关产品推荐

