You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 01:01:00