游戏存档实现
在Unity中可以使用ScriptableObject作为游戏存档的载体,但需要明确其适用场景和局限性,再结合具体的序列化/反序列化逻辑实现存档功能。
一、先明确:ScriptableObject做存档的优缺点
优点
- 编辑时友好:可在Inspector面板直接编辑和预览存档数据,调试方便。
- 轻量级:基于Unity的序列化系统,无需手动处理复杂的字段映射。
- 可复用:可作为存档模板(如默认存档配置),运行时实例化修改。
局限性
- 运行时实例的持久化问题:ScriptableObject本身是资源对象,默认存储在Asset数据库中(.asset文件),运行时创建的ScriptableObject实例无法直接保存为本地文件(需手动序列化其数据)。
- 跨平台兼容性:直接保存.asset文件在移动端/打包后无法写入(仅只读),必须将数据提取后用文件形式存储。
- 安全性低:纯文本序列化(如JSON)易被篡改,若需防篡改需额外加密。
适用场景:适合单机小游戏的轻量级存档、编辑时的配置存档,或作为存档数据的“容器模板”;不适合大型游戏的复杂存档(如包含大量场景数据、多人存档)。
二、实现思路
核心逻辑:用ScriptableObject定义存档数据的结构,运行时将其数据序列化为JSON/二进制文件存储到本地,读档时反序列化数据并赋值给ScriptableObject实例。
步骤拆解:
- 定义ScriptableObject存档类(声明存档数据字段)。
- 实现数据的序列化(将ScriptableObject的字段转为JSON/二进制)。
- 实现数据的反序列化(从本地文件读取数据并赋值给ScriptableObject)。
- 封装存档/读档的工具类。
三、具体实现代码
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();
}
}
}
四、关键注意事项
-
ScriptableObject的实例化
- 编辑时创建的
.asset文件是只读资源,运行时不能直接修改(否则打包后会报错),需用Instantiate()创建实例后再修改。 - 也可通过
ScriptableObject.CreateInstance<PlayerSaveData>()在运行时动态创建实例,无需提前创建Asset文件。
- 编辑时创建的
-
序列化支持的类型
JsonUtility仅支持Unity序列化的类型(如int、float、Vector3、bool[]、自定义类需加[System.Serializable])。- 不支持
Dictionary,若需存储键值对,可改用List<KeyValuePair<TKey, TValue>>。
-
跨平台路径
- 务必使用
Application.persistentDataPath(可写路径),不要使用Application.dataPath(打包后只读)。 - PC端路径示例:
C:\Users\用户名\AppData\LocalLow\公司名\游戏名\SaveData.json。
- 务必使用
-
加密与防篡改
- 若需防止存档被篡改,可对JSON字符串进行AES加密或MD5签名验证。
- 简单加密:可将字符串转成字节数组后异或一个密钥,再保存。
-
二进制序列化(可选)
- 若不想用JSON(可读性高但体积大),可改用二进制序列化(
BinaryFormatter或protobuf-net),体积更小、安全性更高。
- 若不想用JSON(可读性高但体积大),可改用二进制序列化(
五、总结
使用ScriptableObject做存档的核心是将其作为数据结构的定义,而非直接保存ScriptableObject对象本身。通过将其数据序列化为文件存储,既利用了ScriptableObject的编辑友好性,又解决了其运行时持久化的问题。
这种方式适合中小规模的单机游戏存档,若游戏规模较大(如包含大量场景数据、多人存档),建议使用SQLite或Addressables结合数据库的方式。
在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替代BinaryFormatter(BinaryFormatter存在安全漏洞且效率低)。
(3)XML序列化(几乎不推荐)
- 适用场景:仅需与老项目/外部系统兼容的情况。
- 缺点:体积大、效率低、语法繁琐,已被JSON和二进制替代。
2. 本地数据库(推荐中大型单机游戏)
当存档数据量大、结构复杂、需频繁查询/修改时(如开放世界游戏的任务进度、物品库、NPC关系),文件序列化的方式会变得低效,此时推荐使用本地嵌入式数据库。
(1)SQLite(最主流)
- 核心工具:
SQLite-net(轻量级ORM)、Unity SQLite(Unity官方提供的插件)。 - 适用场景:大型单机游戏、需要复杂数据查询的存档(如按条件筛选物品、统计任务进度)。
- 优点:
- 关系型数据库,支持SQL查询,数据管理高效;
- 轻量级(无服务器,文件型数据库),跨平台兼容;
- 数据持久化可靠,支持事务(避免存档损坏)。
- 缺点:
- 学习成本比文件序列化高;
- 需手动设计数据表结构。
- 实现思路:
- 导入
SQLite-net包(NuGet或Unity Package Manager); - 定义数据模型(对应数据库表);
- 封装数据库操作类(增删改查)。
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. 混合存档方案(推荐生产环境)
实际项目中,通常会结合本地存档和云端存档,兼顾离线体验和跨设备同步:
- 本地优先:游戏运行时优先读取本地存档,保证离线可玩;
- 云端同步:联网时将本地存档上传到云端,同时从云端拉取最新的存档(解决多设备同步);
- 冲突处理:定义存档的版本号/时间戳,解决本地与云端的数据冲突(如以时间戳最新的为准)。
二、不同场景的方案选型建议
| 游戏类型/规模 | 推荐方案 | 核心优势 |
|---|---|---|
| 小型单机小游戏 | JSON序列化(Newtonsoft.Json)+ 简单加密 | 实现快、调试方便 |
| 中型单机游戏 | Protobuf二进制序列化 + SQLite | 效率高、数据管理灵活 |
| 大型开放世界单机 | SQLite/LiteDB + 分卷存档(按场景/模块拆分) | 支持海量数据、查询高效 |
| 联网手游/网游 | UGS Cloud Save + 本地缓存 | 跨平台同步、兼顾离线体验 |
| 竞技类网游 | 纯云端存档(服务器存储)+ 本地临时缓存 | 防止作弊、数据统一管控 |
三、存档的通用最佳实践
无论选择哪种方案,都需要注意以下关键点,避免踩坑:
1. 数据加密与防篡改
- 基础加密:对序列化后的字符串/字节数组进行AES对称加密(推荐)、RSA非对称加密(适合联网)。
- 防篡改:添加MD5/SHA256哈希签名(将数据和签名一起存储,读取时验证签名是否一致)。
- 避免明文存储:永远不要将敏感数据(如玩家金币、等级)以明文形式存储。
2. 存档损坏处理
- 备份存档:每次存档时保留上一版本的备份(如
SaveData.json和SaveData.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.asset中gold会显示为0,playerLevel保留原有值。
2. 删除属性
| 场景 | 影响表现 |
|---|---|
编辑时的.asset文件 |
Unity的序列化系统会丢弃已存在的.asset文件中对应属性的数据,且该数据无法恢复(除非回滚代码并重新导入.asset)。⚠️ 数据永久丢失,需谨慎。 |
| 运行时的JSON反序列化(如存档) | 若旧存档包含被删除的属性,JsonUtility会忽略该属性(无报错);Newtonsoft.Json默认也会忽略,除非开启严格模式。 |
示例:
原SO类有gold,删除后,旧的PlayerData.asset中gold的数据被清空,即使重新加回gold,也会恢复为默认值。
3. 修改属性(重命名/改类型)
这是最容易出问题的修改,本质上等价于「删除旧属性 + 新增新属性」。
(1)重命名属性(如playerLevel改为level)
| 场景 | 影响表现 |
|---|---|
编辑时的.asset文件 |
Unity会将playerLevel视为“被删除的属性”(数据丢失),level视为“新增的属性”(默认值)。⚠️ 原有属性数据直接丢失。 |
| 运行时的JSON反序列化 | 旧存档中的playerLevel会被忽略,level取默认值,数据丢失。 |
(2)修改属性类型(如int playerLevel改为float playerLevel)
| 场景 | 影响表现 |
|---|---|
编辑时的.asset文件 |
Unity序列化系统无法将旧类型数据转换为新类型,会清空该属性的数据(赋予新类型的默认值)。 ⚠️ 数据丢失,且可能导致Inspector显示异常。 |
| 运行时的JSON反序列化 | JsonUtility会尝试转换类型,失败则赋值默认值(如int转float可成功,float转int会截断小数,string转int则赋值0)。 |
特殊情况:若修改的是简单值类型间的兼容转换(如int→float),Unity可能会保留数据(如int 10转为float 10.0),但这属于未定义行为,不建议依赖。
二、底层原因:Unity的序列化规则
Unity对ScriptableObject的序列化基于属性的“序列化名”(而非变量名,默认变量名就是序列化名)和字段的元数据(类型、顺序等),核心规则:
- 序列化数据与字段绑定:
.asset文件中存储的是「序列化名 + 数据」的键值对,而非类的结构。 - 字段不匹配时的处理:
- 若新类中有未在
.asset中找到的序列化名(新增属性),则赋默认值; - 若
.asset中有未在新类中找到的序列化名(删除/重命名属性),则丢弃该数据; - 若序列化名存在但类型不匹配,则清空数据。
- 若新类中有未在
- 序列化名可通过特性自定义:可使用
[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. 版本控制与数据迁移(解决删/改属性的核心方案)
当需要删除属性、修改类型或做重大结构调整时,需在代码中手动处理数据迁移,核心思路:
- 给SO类添加版本号字段(如
public int dataVersion = 1;); - 当类结构修改时,递增版本号(如改为2);
- 在加载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),需在反序列化时处理版本兼容:
- 在存档数据中加入版本号;
- 加载时根据版本号解析不同的字段结构,手动转换数据。
示例(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实例,增删属性的影响:
- 若代码修改后重新运行,旧的运行时实例会被销毁,新实例会使用新的类结构(无数据迁移问题);
- 若运行时通过反序列化创建SO(如从旧存档加载),则会遵循上述的序列化规则(新增字段赋默认值,删除字段丢失数据)。
五、总结
| 修改类型 | 影响程度 | 最佳解决方案 |
|---|---|---|
| 新增属性 | 低(安全) | 无需特殊处理,仅需注意默认值是否合理 |
| 重命名属性 | 中(数据丢失) | 使用[FormerlySerializedAs]特性关联旧名 |
| 修改属性类型 | 高(数据丢失) | 版本号+手动数据迁移 |
| 删除属性 | 高(数据永久丢失) | 先备份.asset文件,或通过版本号保留兼容逻辑 |
核心要点:
[FormerlySerializedAs]是处理重命名的黄金法则;- 版本号+数据迁移是处理结构重大变更的通用方案;
- 修改前备份
.asset文件是最稳妥的兜底策略。
只要做好这些处理,即使SO类增删属性,也能最大程度避免数据丢失和兼容问题。