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

Unity中ScriptableObject引用序列化问题及优化方案问询

问题背景

我用ScriptableObject作为角色派系、武器等内容的模板,以派系为例:FactionSO是CharacterStats的容器,包含移动速度、伤害倍率等属性。Character类对FactionSO的引用如下:

[System.Serializable]
public class Character
{
    public string characterName;
    public int level;
    public FactionSO factionSO;
    public CharacterStats stats;
}

这种方式能快速创建新派系,角色创建时分配FactionSO引用,新游戏启动时从FactionSO的CharacterStats初始化角色属性,且保证FactionSO不被修改。但用ToJSON序列化Character类时,ScriptableObject引用会被序列化为InstanceID(编辑器重启后丢失),构建后则序列化为m_fileID和m_pathID(兼容性差,编辑器与构建版本存档不通用)。

我尝试用Addressables的AssetReference解决,虽然能正常运行,但需要创建单例数据库加载资源,还要单独编写SavableCharacter类替换FactionSO引用,结构繁琐。如果要保存包含WeaponSO引用的WeaponProficiency列表,问题会更复杂:

[System.Serializable]
public class WeaponProficiency{
    public WeaponSO weaponSO;
    public int proficiencyLevel;
}

问题1:是否可通过ISerializationCallbackReceiver让ScriptableObject序列化时自动转为AssetReference,无需手动编写可存档类?

可以实现,核心思路是封装一个通用的可序列化SO引用类,实现ISerializationCallbackReceiver接口,在序列化前将ScriptableObject转换为对应的AssetReference,反序列化后通过AssetReference加载回ScriptableObject。这样就不需要单独编写Savable类,直接在原有Character类中替换引用类型即可。

示例代码如下:

[Serializable]
public class SerializableSOReference<T> : ISerializationCallbackReceiver where T : ScriptableObject
{
    // 编辑器中直接赋值的ScriptableObject引用
    public T SoReference;
    // 用于序列化的AssetReference(自动生成)
    [SerializeField] private AssetReference _assetReference;

    public void OnBeforeSerialize()
    {
#if UNITY_EDITOR
        if (SoReference != null)
        {
            // 获取SO的GUID,转为AssetReference
            string assetPath = UnityEditor.AssetDatabase.GetAssetPath(SoReference);
            string guid = UnityEditor.AssetDatabase.AssetPathToGUID(assetPath);
            _assetReference = new AssetReference(guid);
        }
#endif
    }

    public void OnAfterDeserialize()
    {
        // 运行时通过AssetReference异步加载ScriptableObject
        if (_assetReference != null && _assetReference.RuntimeKeyIsValid())
        {
            _assetReference.LoadAssetAsync<T>().Completed += handle =>
            {
                if (handle.Status == UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationStatus.Succeeded)
                {
                    SoReference = handle.Result;
                }
            };
        }
    }
}

使用方式

将Character类中的FactionSO引用替换为该通用类:

[System.Serializable]
public class Character
{
    public string characterName;
    public int level;
    public SerializableSOReference<FactionSO> factionSO;
    public CharacterStats stats;
}

这样序列化Character时,会自动用AssetReference的GUID进行存储,避免引用丢失问题,同时不需要额外编写Savable类。

问题2:当前方案是否合理?直接用SO引用获取属性,和给所有对象分配ID两种方式哪种更优?

当前方案的合理性

你当前用Addressables+单例数据库的方案是可行的,但存在冗余问题:每个SO类型都需要单独编写数据库和转换逻辑,随着SO类型增多(比如派系、武器、技能等),代码会越来越繁琐。可以通过泛型优化数据库,减少重复代码:

public class GenericAssetDatabase<T> where T : ScriptableObject
{
    private readonly Dictionary<string, T> _guidToSO = new();
    private readonly Dictionary<T, AssetReference> _soToRef = new();

    public void Register(AssetReference assetRef, T so)
    {
        if (!_guidToSO.ContainsKey(assetRef.AssetGUID))
        {
            _guidToSO[assetRef.AssetGUID] = so;
        }
        if (!_soToRef.ContainsKey(so))
        {
            _soToRef[so] = assetRef;
        }
    }

    public T GetSOByGuid(string guid)
    {
        _guidToSO.TryGetValue(guid, out var so);
        return so;
    }

    public AssetReference GetRefBySO(T so)
    {
        _soToRef.TryGetValue(so, out var assetRef);
        return assetRef;
    }
}

// 全局单例数据库
public class GlobalAssetDatabase : MonoBehaviour
{
    public static GlobalAssetDatabase Instance { get; private set; }

    public List<AssetReference> FactionRefs;
    public List<AssetReference> WeaponRefs;

    public GenericAssetDatabase<FactionSO> FactionDB { get; private set; } = new();
    public GenericAssetDatabase<WeaponSO> WeaponDB { get; private set; } = new();

    void Awake()
    {
        if (Instance != null)
        {
            Destroy(gameObject);
            return;
        }
        Instance = this;
        DontDestroyOnLoad(gameObject);

        LoadAssets(FactionRefs, FactionDB);
        LoadAssets(WeaponRefs, WeaponDB);
    }

    private void LoadAssets<T>(List<AssetReference> refs, GenericAssetDatabase<T> db) where T : ScriptableObject
    {
        foreach (var assetRef in refs)
        {
            assetRef.LoadAssetAsync<T>().Completed += handle =>
            {
                if (handle.Status == UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationStatus.Succeeded)
                {
                    db.Register(assetRef, handle.Result);
                }
            };
        }
    }
}

优化后无需为每个SO类型单独编写数据库类,减少代码冗余。

两种方式对比

1. AssetReference方案(基于Addressables)

  • 优点:利用Addressables的资源管理机制,自动处理不同平台的资源路径,用GUID标识资源,编辑器与构建版本的存档兼容性好;无需手动维护ID,新增SO时只需添加到Addressables分组即可。
  • 缺点:需要处理异步加载逻辑(不过可以通过上面的SerializableSOReference封装简化);如果项目未使用Addressables,引入会增加一定的学习成本。

2. 自定义ID方案

  • 优点:同步访问SO,无需处理异步加载;逻辑简单直接,通过ID查SO(比如用全局字典映射ID与SO)。
  • 缺点:需要手动为每个SO分配唯一ID,容易出现重复或遗漏;需要保证编辑器与构建版本的ID完全一致,维护成本高;新增SO时必须手动配置ID,容易出错(可通过编辑器工具自动生成ID缓解,但仍需额外开发)。

选型建议

  • 如果项目已经使用Addressables,优先选择AssetReference+通用序列化引用类的方案,减少手动维护成本,提升存档兼容性。
  • 如果项目未使用Addressables,且不想引入该框架,可以选择自定义ID方案,但必须配套编辑器工具自动生成唯一ID,避免手动分配的错误。

内容的提问来源于stack exchange,提问作者saranw71

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 15:55:06