在Unity中可以使用ScriptableObject作为游戏存档的载体,但需要明确其适用场景和局限性,再结合具体的序列化/反序列化逻辑实现存档功能。

一、先明确:ScriptableObject做存档的优缺点

优点

  1. 编辑时友好:可在Inspector面板直接编辑和预览存档数据,调试方便。
  2. 轻量级:基于Unity的序列化系统,无需手动处理复杂的字段映射。
  3. 可复用:可作为存档模板(如默认存档配置),运行时实例化修改。

局限性

  1. 运行时实例的持久化问题:ScriptableObject本身是资源对象,默认存储在Asset数据库中(.asset文件),运行时创建的ScriptableObject实例无法直接保存为本地文件(需手动序列化其数据)。
  2. 跨平台兼容性:直接保存.asset文件在移动端/打包后无法写入(仅只读),必须将数据提取后用文件形式存储。
  3. 安全性低:纯文本序列化(如JSON)易被篡改,若需防篡改需额外加密。

适用场景:适合单机小游戏的轻量级存档、编辑时的配置存档,或作为存档数据的“容器模板”;不适合大型游戏的复杂存档(如包含大量场景数据、多人存档)。


二、实现思路

核心逻辑:用ScriptableObject定义存档数据的结构,运行时将其数据序列化为JSON/二进制文件存储到本地,读档时反序列化数据并赋值给ScriptableObject实例。

步骤拆解:

  1. 定义ScriptableObject存档类(声明存档数据字段)。
  2. 实现数据的序列化(将ScriptableObject的字段转为JSON/二进制)。
  3. 实现数据的反序列化(从本地文件读取数据并赋值给ScriptableObject)。
  4. 封装存档/读档的工具类。

三、具体实现代码

1. 定义ScriptableObject存档类

创建一个继承自ScriptableObject的类,声明需要存档的字段(如玩家等级、金币、关卡进度等)。

using UnityEngine;

// 标记为可创建Asset文件(可选,用于编辑时的默认存档模板)
[CreateAssetMenu(fileName = "PlayerSaveData", menuName = "Game/SaveData/PlayerSaveData")]
public class PlayerSaveData : ScriptableObject
{
    // 玩家基础数据
    public int playerLevel;          // 等级
    public int gold;                 // 金币
    public float hp;                 // 生命值
    public bool[] unlockedLevels;    // 解锁的关卡
    public Vector3 playerPosition;   // 玩家位置

    // 重置存档数据(可选)
    public void ResetData()
    {
        playerLevel = 1;
        gold = 0;
        hp = 100;
        unlockedLevels = new bool[10] { true, false, false, false, false, false, false, false, false, false };
        playerPosition = Vector3.zero;
    }
}

2. 存档工具类(核心:序列化与反序列化)

封装存档/读档的方法,将PlayerSaveData的字段序列化为JSON文件,存储到Unity的持久化路径Application.persistentDataPath,跨平台可写)。

using UnityEngine;
using System.IO;

public static class SaveSystem
{
    // 存档文件的路径(如:Android的/data/data/包名/files/SaveData.json)
    private static string SavePath => Path.Combine(Application.persistentDataPath, "SaveData.json");

    /// <summary>
    /// 存档:将ScriptableObject的数据序列化为JSON并保存到本地
    /// </summary>
    /// <param name="saveData">要保存的存档数据对象</param>
    public static void Save(PlayerSaveData saveData)
    {
        if (saveData == null)
        {
            Debug.LogError("SaveData is null!");
            return;
        }

        // 将ScriptableObject的字段序列化为JSON字符串
        string json = JsonUtility.ToJson(saveData);

        // 将JSON写入文件
        File.WriteAllText(SavePath, json);

        Debug.Log("Save success! Path: " + SavePath);
    }

    /// <summary>
    /// 读档:从本地文件读取JSON并反序列化为数据,赋值给ScriptableObject
    /// </summary>
    /// <param name="saveData">要赋值的存档数据对象</param>
    /// <returns>是否读档成功</returns>
    public static bool Load(PlayerSaveData saveData)
    {
        if (saveData == null)
        {
            Debug.LogError("SaveData is null!");
            return false;
        }

        // 检查文件是否存在
        if (!File.Exists(SavePath))
        {
            Debug.LogWarning("Save file not found! Create new save data.");
            saveData.ResetData(); // 不存在则重置为默认数据
            return false;
        }

        // 读取文件中的JSON字符串
        string json = File.ReadAllText(SavePath);

        // 将JSON反序列化并赋值给ScriptableObject
        JsonUtility.FromJsonOverwrite(json, saveData);

        Debug.Log("Load success!");
        return true;
    }

    /// <summary>
    /// 删除存档文件
    /// </summary>
    public static void DeleteSave()
    {
        if (File.Exists(SavePath))
        {
            File.Delete(SavePath);
            Debug.Log("Save file deleted!");
        }
        else
        {
            Debug.LogWarning("Save file not found!");
        }
    }
}

3. 测试使用(在游戏逻辑中调用)

创建一个测试脚本,挂载到场景中的GameObject上,测试存档/读档功能。

using UnityEngine;

public class SaveTest : MonoBehaviour
{
    // 可在Inspector面板赋值(编辑时创建的PlayerSaveData.asset文件)
    public PlayerSaveData saveDataTemplate;

    private PlayerSaveData runtimeSaveData; // 运行时的存档实例

    private void Awake()
    {
        // 方式1:使用编辑时创建的Asset模板(只读,需实例化后修改)
        // 注意:直接修改模板会改变Asset文件,因此运行时需创建实例
        runtimeSaveData = Instantiate(saveDataTemplate);

        // 方式2:运行时动态创建ScriptableObject实例(无需提前创建Asset文件)
        // runtimeSaveData = ScriptableObject.CreateInstance<PlayerSaveData>();
        // runtimeSaveData.ResetData();
    }

    private void Update()
    {
        // 按S键存档
        if (Input.GetKeyDown(KeyCode.S))
        {
            // 模拟修改数据
            runtimeSaveData.playerLevel += 1;
            runtimeSaveData.gold += 100;
            runtimeSaveData.playerPosition = transform.position;

            // 调用存档方法
            SaveSystem.Save(runtimeSaveData);
        }

        // 按L键读档
        if (Input.GetKeyDown(KeyCode.L))
        {
            // 调用读档方法
            if (SaveSystem.Load(runtimeSaveData))
            {
                // 读档后更新游戏逻辑(如玩家位置、等级)
                transform.position = runtimeSaveData.playerPosition;
                Debug.Log("Player Level: " + runtimeSaveData.playerLevel);
                Debug.Log("Gold: " + runtimeSaveData.gold);
            }
        }

        // 按D键删除存档
        if (Input.GetKeyDown(KeyCode.D))
        {
            SaveSystem.DeleteSave();
        }
    }
}

四、关键注意事项

  1. ScriptableObject的实例化

    • 编辑时创建的.asset文件是只读资源,运行时不能直接修改(否则打包后会报错),需用Instantiate()创建实例后再修改。
    • 也可通过ScriptableObject.CreateInstance<PlayerSaveData>()在运行时动态创建实例,无需提前创建Asset文件。
  2. 序列化支持的类型

    • JsonUtility仅支持Unity序列化的类型(如intfloatVector3bool[]、自定义类需加[System.Serializable])。
    • 不支持Dictionary,若需存储键值对,可改用List<KeyValuePair<TKey, TValue>>
  3. 跨平台路径

    • 务必使用Application.persistentDataPath(可写路径),不要使用Application.dataPath(打包后只读)。
    • PC端路径示例:C:\Users\用户名\AppData\LocalLow\公司名\游戏名\SaveData.json
  4. 加密与防篡改

    • 若需防止存档被篡改,可对JSON字符串进行AES加密MD5签名验证
    • 简单加密:可将字符串转成字节数组后异或一个密钥,再保存。
  5. 二进制序列化(可选)

    • 若不想用JSON(可读性高但体积大),可改用二进制序列化BinaryFormatterprotobuf-net),体积更小、安全性更高。

五、总结

使用ScriptableObject做存档的核心是将其作为数据结构的定义,而非直接保存ScriptableObject对象本身。通过将其数据序列化为文件存储,既利用了ScriptableObject的编辑友好性,又解决了其运行时持久化的问题。

这种方式适合中小规模的单机游戏存档,若游戏规模较大(如包含大量场景数据、多人存档),建议使用SQLiteAddressables结合数据库的方式。

在Unity中实现游戏存档的方案没有绝对的“最好”,只有最适合项目场景的选择。除了用ScriptableObject作为数据结构+序列化文件的方式外,还有多种更成熟、更适配不同游戏规模/需求的方案。以下是主流的存档方案,按适用场景、优缺点、实现方式逐一分析:


一、按“技术维度”分类的主流存档方案

1. 纯文件序列化(基础款:JSON/二进制/XML)

这是最基础的存档方式,也是ScriptableObject方案的核心底层逻辑,只是可以脱离ScriptableObject直接用普通C#类作为数据载体。

(1)JSON序列化(推荐中小项目)

  • 核心工具:Unity内置JsonUtility、第三方库Newtonsoft.Json(Json.NET)、LitJSON
  • 适用场景:单机小游戏、轻量级存档(玩家数据、关卡进度)。
  • 优点
    • 可读性强,调试方便;
    • 实现简单,无需依赖额外插件;
    • Newtonsoft.Json支持Dictionary、泛型等JsonUtility不支持的类型。
  • 缺点
    • 纯文本易被篡改,需额外加密;
    • 数据量大时,序列化/反序列化效率略低。
  • 实现示例(Newtonsoft.Json)
    using Newtonsoft.Json;
    using System.IO;
    
    // 普通C#类作为数据载体(无需继承ScriptableObject)
    [System.Serializable]
    public class PlayerData
    {
        public int Level;
        public int Gold;
        public Dictionary<string, bool> UnlockedSkills; // 支持Dictionary
    }
    
    public static class JsonSaveSystem
    {
        private static string SavePath => Path.Combine(Application.persistentDataPath, "PlayerData.json");
    
        public static void Save(PlayerData data)
        {
            string json = JsonConvert.SerializeObject(data, Formatting.Indented);
            File.WriteAllText(SavePath, json);
        }
    
        public static PlayerData Load()
        {
            if (!File.Exists(SavePath)) return new PlayerData();
            string json = File.ReadAllText(SavePath);
            return JsonConvert.DeserializeObject<PlayerData>(json);
        }
    }
    

(2)二进制序列化(推荐对性能/体积有要求的项目)

  • 核心工具BinaryFormatter(Unity内置,已标记过时)、protobuf-net(谷歌Protobuf的C#实现)、MessagePack-CSharp(高性能二进制序列化)。
  • 适用场景:中大型单机游戏、需要紧凑数据体积的场景(如移动端)。
  • 优点
    • 数据体积小,序列化/反序列化速度快;
    • 二进制格式比JSON更难篡改(非明文);
    • protobuf-net跨语言兼容(C#/C++/Java等)。
  • 缺点
    • 可读性差,调试不便;
    • 实现比JSON稍复杂。
  • 关键推荐优先用protobuf-net替代BinaryFormatterBinaryFormatter存在安全漏洞且效率低)。

(3)XML序列化(几乎不推荐)

  • 适用场景:仅需与老项目/外部系统兼容的情况。
  • 缺点:体积大、效率低、语法繁琐,已被JSON和二进制替代。

2. 本地数据库(推荐中大型单机游戏)

当存档数据量大、结构复杂、需频繁查询/修改时(如开放世界游戏的任务进度、物品库、NPC关系),文件序列化的方式会变得低效,此时推荐使用本地嵌入式数据库

(1)SQLite(最主流)

  • 核心工具SQLite-net(轻量级ORM)、Unity SQLite(Unity官方提供的插件)。
  • 适用场景:大型单机游戏、需要复杂数据查询的存档(如按条件筛选物品、统计任务进度)。
  • 优点
    • 关系型数据库,支持SQL查询,数据管理高效;
    • 轻量级(无服务器,文件型数据库),跨平台兼容;
    • 数据持久化可靠,支持事务(避免存档损坏)。
  • 缺点
    • 学习成本比文件序列化高;
    • 需手动设计数据表结构。
  • 实现思路
    1. 导入SQLite-net包(NuGet或Unity Package Manager);
    2. 定义数据模型(对应数据库表);
    3. 封装数据库操作类(增删改查)。
    using SQLite;
    
    // 定义数据表
    [Table("Player")]
    public class PlayerTable
    {
        [PrimaryKey, AutoIncrement]
        public int Id { get; set; }
        public int Level { get; set; }
        public int Gold { get; set; }
        public string PlayerName { get; set; }
    }
    
    // 数据库操作类
    public class SQLiteManager
    {
        private SQLiteConnection _conn;
    
        public SQLiteManager()
        {
            string dbPath = Path.Combine(Application.persistentDataPath, "GameDB.db");
            _conn = new SQLiteConnection(dbPath);
            _conn.CreateTable<PlayerTable>(); // 创建表
        }
    
        // 插入/更新数据
        public void SavePlayer(PlayerTable player)
        {
            if (_conn.Find<PlayerTable>(player.Id) != null)
                _conn.Update(player);
            else
                _conn.Insert(player);
        }
    
        // 查询数据
        public PlayerTable LoadPlayer(int id)
        {
            return _conn.Find<PlayerTable>(id);
        }
    }
    

(2)LiteDB(NoSQL型本地数据库,推荐快速开发)

  • 核心特点:NoSQL数据库,类MongoDB的API,无需设计数据表结构,直接存储JSON格式的文档。
  • 优点:比SQLite更易上手,无需写SQL语句,适合快速开发。
  • 适用场景:中小型游戏,需要灵活数据结构的存档。

3. 云端存档(推荐联网游戏)

对于联网游戏或需要跨设备同步存档的场景,本地存档无法满足需求,需结合云端服务实现存档。

(1)Unity官方云服务:Unity Gaming Services (UGS) - Cloud Save

  • 核心优势:与Unity深度集成,无需自建服务器,支持跨平台同步(PC/移动端/主机)。
  • 优点
    • 开箱即用,只需调用API即可实现云端存档;
    • 支持数据加密、版本控制、存档备份。
  • 缺点
    • 有流量/存储费用(免费额度有限);
    • 依赖网络,离线时无法使用。
  • 实现示例
    using Unity.Services.CloudSave;
    using System.Collections.Generic;
    using System.Threading.Tasks;
    
    public async Task SaveToCloud()
    {
        // 初始化UGS(需先在Unity Dashboard配置)
        await UnityServices.InitializeAsync();
    
        // 存储数据到云端
        var data = new Dictionary<string, object>
        {
            { "Level", 10 },
            { "Gold", 1000 }
        };
        await CloudSaveService.Instance.Data.Player.SaveAsync(data);
    }
    
    public async Task LoadFromCloud()
    {
        var data = await CloudSaveService.Instance.Data.Player.LoadAsync(new List<string> { "Level", "Gold" });
        int level = (int)data["Level"];
        int gold = (int)data["Gold"];
    }
    

(2)第三方云服务

  • Firebase Realtime Database/Firestore(谷歌):适合中小型联网游戏,实时同步数据。
  • AWS DynamoDB(亚马逊):适合大型游戏,高可用、高并发。
  • LeanCloud(国内):本地化服务,适配国内网络环境。

4. 混合存档方案(推荐生产环境)

实际项目中,通常会结合本地存档云端存档,兼顾离线体验和跨设备同步:

  1. 本地优先:游戏运行时优先读取本地存档,保证离线可玩;
  2. 云端同步:联网时将本地存档上传到云端,同时从云端拉取最新的存档(解决多设备同步);
  3. 冲突处理:定义存档的版本号/时间戳,解决本地与云端的数据冲突(如以时间戳最新的为准)。

二、不同场景的方案选型建议

游戏类型/规模 推荐方案 核心优势
小型单机小游戏 JSON序列化(Newtonsoft.Json)+ 简单加密 实现快、调试方便
中型单机游戏 Protobuf二进制序列化 + SQLite 效率高、数据管理灵活
大型开放世界单机 SQLite/LiteDB + 分卷存档(按场景/模块拆分) 支持海量数据、查询高效
联网手游/网游 UGS Cloud Save + 本地缓存 跨平台同步、兼顾离线体验
竞技类网游 纯云端存档(服务器存储)+ 本地临时缓存 防止作弊、数据统一管控

三、存档的通用最佳实践

无论选择哪种方案,都需要注意以下关键点,避免踩坑:

1. 数据加密与防篡改

  • 基础加密:对序列化后的字符串/字节数组进行AES对称加密(推荐)、RSA非对称加密(适合联网)。
  • 防篡改:添加MD5/SHA256哈希签名(将数据和签名一起存储,读取时验证签名是否一致)。
  • 避免明文存储:永远不要将敏感数据(如玩家金币、等级)以明文形式存储。

2. 存档损坏处理

  • 备份存档:每次存档时保留上一版本的备份(如SaveData.jsonSaveData.bak),若当前存档损坏则读取备份。
  • 数据校验:读取存档时验证数据的合法性(如等级不能为负数、金币不能超过最大值)。

3. 分卷存档

  • 将大型存档按模块拆分(如玩家数据、关卡数据、物品数据分别存储),避免单个存档文件过大,提高读写效率。

4. 避免频繁存档

  • 不要每帧都存档(如玩家移动时实时存档),而是在关键节点存档(如关卡结束、玩家退出、手动存档),减少IO开销。

四、总结

如果你的项目是小型单机游戏,用ScriptableObject + JSON就足够;如果是中大型游戏,建议升级到二进制序列化 + 本地数据库;如果是联网游戏,优先考虑UGS Cloud Save或Firebase等云端服务。

核心原则是:先满足功能需求,再优化性能和安全性,避免过度设计(比如小型游戏没必要用SQLite)。

在Unity中,当你给存储了数据的ScriptableObject(SO)类增删属性时,会对已存在的.asset文件(SO实例)和运行时的序列化/反序列化逻辑产生直接影响,具体影响程度取决于修改的类型、Unity的序列化规则以及你的使用场景(编辑时/运行时)。以下从影响表现、底层原因、应对方案三个维度详细分析:


一、核心影响:不同修改类型的具体表现

我们将修改分为增属性、删属性、改属性(名/类型) 三类,分别看对编辑时的.asset文件运行时的存档/反序列化的影响:

1. 新增属性

场景 影响表现
编辑时的.asset文件 已存在的SO实例(.asset)中,新增的属性会被赋予默认值(如int为0,string为空,引用为null),原有数据完全保留。
✅ 这是最安全的修改,几乎无风险。
运行时的JSON反序列化(如存档) 若用JsonUtility反序列化旧存档(无新属性的JSON),新属性会被赋予默认值;若用Newtonsoft.Json等库,需确保配置了忽略未知属性(否则可能报错)。

示例
原SO类有playerLevel,新增gold后,旧的PlayerData.assetgold会显示为0,playerLevel保留原有值。

2. 删除属性

场景 影响表现
编辑时的.asset文件 Unity的序列化系统会丢弃已存在的.asset文件中对应属性的数据,且该数据无法恢复(除非回滚代码并重新导入.asset)。
⚠️ 数据永久丢失,需谨慎。
运行时的JSON反序列化(如存档) 若旧存档包含被删除的属性,JsonUtility忽略该属性(无报错);Newtonsoft.Json默认也会忽略,除非开启严格模式。

示例
原SO类有gold,删除后,旧的PlayerData.assetgold的数据被清空,即使重新加回gold,也会恢复为默认值。

3. 修改属性(重命名/改类型)

这是最容易出问题的修改,本质上等价于「删除旧属性 + 新增新属性」。

(1)重命名属性(如playerLevel改为level
场景 影响表现
编辑时的.asset文件 Unity会将playerLevel视为“被删除的属性”(数据丢失),level视为“新增的属性”(默认值)。
⚠️ 原有属性数据直接丢失。
运行时的JSON反序列化 旧存档中的playerLevel会被忽略,level取默认值,数据丢失。
(2)修改属性类型(如int playerLevel改为float playerLevel
场景 影响表现
编辑时的.asset文件 Unity序列化系统无法将旧类型数据转换为新类型,会清空该属性的数据(赋予新类型的默认值)。
⚠️ 数据丢失,且可能导致Inspector显示异常。
运行时的JSON反序列化 JsonUtility会尝试转换类型,失败则赋值默认值(如intfloat可成功,floatint会截断小数,stringint则赋值0)。

特殊情况:若修改的是简单值类型间的兼容转换(如intfloat),Unity可能会保留数据(如int 10转为float 10.0),但这属于未定义行为,不建议依赖


二、底层原因:Unity的序列化规则

Unity对ScriptableObject的序列化基于属性的“序列化名”(而非变量名,默认变量名就是序列化名)和字段的元数据(类型、顺序等),核心规则:

  1. 序列化数据与字段绑定.asset文件中存储的是「序列化名 + 数据」的键值对,而非类的结构。
  2. 字段不匹配时的处理
    • 若新类中有未在.asset中找到的序列化名(新增属性),则赋默认值;
    • .asset中有未在新类中找到的序列化名(删除/重命名属性),则丢弃该数据;
    • 若序列化名存在但类型不匹配,则清空数据。
  3. 序列化名可通过特性自定义:可使用[SerializeField][FormerlySerializedAs]特性显式指定序列化名,避免重命名导致的数据丢失。

三、如何避免/缓解修改属性的负面影响?

针对不同场景,有对应的解决方案,从简单到复杂排序:

1. 使用[FormerlySerializedAs]特性(解决重命名属性的问题)

这是Unity官方提供的最核心的解决方案,用于告诉序列化系统:“新的属性名对应旧的序列化名”,从而保留原有数据。

用法示例

using UnityEngine;

[CreateAssetMenu(fileName = "PlayerData", menuName = "Game/PlayerData")]
public class PlayerDataSO : ScriptableObject
{
    // 原属性名是playerLevel,重命名为level,用特性关联旧序列化名
    [FormerlySerializedAs("playerLevel")]
    public int level;

    // 新增属性,无影响
    public int gold;

    // 原属性名是hp,已删除,无需处理
}

效果:旧的.asset文件中playerLevel的数据会自动映射到level,数据完全保留。

2. 版本控制与数据迁移(解决删/改属性的核心方案)

当需要删除属性、修改类型或做重大结构调整时,需在代码中手动处理数据迁移,核心思路:

  1. 给SO类添加版本号字段(如public int dataVersion = 1;);
  2. 当类结构修改时,递增版本号(如改为2);
  3. 在加载SO时,根据版本号执行不同的迁移逻辑。

示例

using UnityEngine;

[CreateAssetMenu(fileName = "PlayerData", menuName = "Game/PlayerData")]
public class PlayerDataSO : ScriptableObject
{
    // 版本号,用于数据迁移
    public int dataVersion = 2;

    // 原int类型的hp改为float类型的maxHp
    [FormerlySerializedAs("hp")]
    public float maxHp;

    // 新增属性
    public int gold;

    private void OnEnable()
    {
        // 数据迁移逻辑
        if (dataVersion == 1)
        {
            // 版本1的hp是int,版本2的maxHp是float,手动转换
            maxHp = (float)maxHp; // 这里实际是读取旧的int hp值,转为float
            dataVersion = 2; // 升级版本号
            EditorUtility.SetDirty(this); // 标记为脏,确保保存修改(仅编辑时)
        }
    }
}

关键OnEnable()会在SO加载时调用(编辑时和运行时),是执行数据迁移的最佳时机。

3. 编辑时的备份策略

  • 在修改SO类结构前,备份已有的.asset文件(复制到其他文件夹),若修改后数据丢失,可回滚备份;
  • 对于重要的配置表,可使用版本控制工具(如Git)管理.asset文件,方便回滚。

4. 运行时存档的兼容处理

若SO的数据需要序列化为存档文件(如JSON),需在反序列化时处理版本兼容:

  1. 在存档数据中加入版本号;
  2. 加载时根据版本号解析不同的字段结构,手动转换数据。

示例(JSON存档的迁移)

using Newtonsoft.Json;
using System.IO;

public class SaveData
{
    public int saveVersion = 2;
    public float maxHp;
    public int gold;
}

public static class SaveSystem
{
    public static SaveData Load()
    {
        string path = Path.Combine(Application.persistentDataPath, "save.json");
        if (!File.Exists(path)) return new SaveData();

        string json = File.ReadAllText(path);
        var saveData = JsonConvert.DeserializeObject<SaveData>(json);

        // 数据迁移:版本1的hp是int,版本2的maxHp是float
        if (saveData.saveVersion == 1)
        {
            // 读取旧的hp字段(需先定义临时类)
            var oldData = JsonConvert.DeserializeObject<OldSaveData>(json);
            saveData.maxHp = (float)oldData.hp;
            saveData.saveVersion = 2;
            // 重新保存新格式
            Save(saveData);
        }

        return saveData;
    }

    // 旧版本的存档数据类
    private class OldSaveData
    {
        public int saveVersion = 1;
        public int hp;
    }
}

5. 避免频繁修改核心SO结构

  • 在项目初期设计好SO的数据结构,尽量减少后期的重大修改;
  • 将易变的字段和稳定的字段拆分到不同的SO中(如将配置表拆分为基础配置和动态配置),降低修改的影响范围。

四、特殊场景:运行时动态创建的SO实例

对于运行时通过ScriptableObject.CreateInstance<T>()创建的SO实例,增删属性的影响:

  1. 若代码修改后重新运行,旧的运行时实例会被销毁,新实例会使用新的类结构(无数据迁移问题);
  2. 若运行时通过反序列化创建SO(如从旧存档加载),则会遵循上述的序列化规则(新增字段赋默认值,删除字段丢失数据)。

五、总结

修改类型 影响程度 最佳解决方案
新增属性 低(安全) 无需特殊处理,仅需注意默认值是否合理
重命名属性 中(数据丢失) 使用[FormerlySerializedAs]特性关联旧名
修改属性类型 高(数据丢失) 版本号+手动数据迁移
删除属性 高(数据永久丢失) 先备份.asset文件,或通过版本号保留兼容逻辑

核心要点:

  1. [FormerlySerializedAs]是处理重命名的黄金法则
  2. 版本号+数据迁移是处理结构重大变更的通用方案
  3. 修改前备份.asset文件是最稳妥的兜底策略

只要做好这些处理,即使SO类增删属性,也能最大程度避免数据丢失和兼容问题。