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

