Unity中如何向二进制格式存档数据添加新变量?
首先得明确问题根源:BinaryFormatter对类结构变化极度敏感——当你给已有类添加新字段/嵌套类时,老版本序列化的数据在反序列化时,会因为字段偏移不匹配,导致原有数据(比如价格、等级)被错误覆盖或丢失,哪怕你做了空值检查也没用,因为问题出在反序列化过程本身,而不是新字段为空。
下面给你分两种方案解决,先讲紧急修复方案,再推荐更长远的替代方案:
一、紧急修复:用BinaryFormatter原生特性兼容版本
1. 给新字段添加[OptionalField]标记
所有后续添加的新字段/嵌套类,都必须加上[OptionalField]特性,告诉BinaryFormatter:这个字段是可选的,老数据里没有也不用报错,直接留默认值就行。
比如你的嵌套类示例:
[Serializable] public class StationData { // 原有字段(老数据里存在的) public int StationBuildingType; public Dictionary<int, int> StationBuildingTypeCosts; // 新添加的字段,必须标记OptionalField [OptionalField] public float CapacityMultiplier = 1f; // 提前设默认值 [OptionalField] public Parameters Parameters; }
2. 手动合并老数据与默认数据,而非直接替换
你之前直接把GameData = gameData会导致老数据被完全覆盖(因为结构变化反序列化后的数据已经损坏),正确的做法是:创建默认数据,然后把老数据的原有字段逐个复制到新对象里,新字段用默认数据填充。
修改你的Load方法,替换成合并逻辑:
public void Load() { GameDataModel loadedData = null; string fullPath = Path.Combine(GameFilePath, GameFileName); if (File.Exists(fullPath)) { var bf = new BinaryFormatter(); // 用using确保文件流正确关闭,避免异常导致资源泄漏 using (var file = File.Open(fullPath, FileMode.Open)) { try { loadedData = (GameDataModel)bf.Deserialize(file); } catch (SerializationException ex) { // 捕获反序列化异常,避免崩溃,同时记录日志 Debug.LogError($"加载存档失败:{ex.Message}"); loadedData = null; } } } // 先创建默认数据作为基础 var defaultData = CreateDefaultGameData(); if (loadedData == null) { GameData = defaultData; return; } // 合并老数据和默认数据,保留原有字段,填充新字段默认值 GameData = MergeGameData(loadedData, defaultData); } // 核心合并方法,递归处理嵌套类 private GameDataModel MergeGameData(GameDataModel oldData, GameDataModel defaultData) { var merged = new GameDataModel(); // 顶级字段合并 merged.WorldName = string.IsNullOrEmpty(oldData.WorldName) ? defaultData.WorldName : oldData.WorldName; merged.Cities = new List<CityData>(); foreach (var oldCity in oldData.Cities) { // 找到对应城市的默认数据(假设按名称匹配) var defaultCity = defaultData.Cities.First(c => c.CityName == oldCity.CityName); var mergedCity = new CityData(); // 保留老城市的核心数据(价格、等级等原有字段) mergedCity.Level = oldCity.Level; mergedCity.Price = oldCity.Price; mergedCity.CityName = oldCity.CityName; // 嵌套对象合并:老数据存在就用老的,否则用默认 mergedCity.FoodStall = oldCity.FoodStall ?? defaultCity.FoodStall; mergedCity.Billboard = oldCity.Billboard ?? defaultCity.Billboard; mergedCity.Station = MergeStation(oldCity.Station, defaultCity.Station); mergedCity.People = MergePeople(oldCity.People, defaultCity.People); mergedCity.Bus = MergeBus(oldCity.Bus, defaultCity.Bus); merged.Cities.Add(mergedCity); } return merged; } // 嵌套类的合并示例:Station private StationData MergeStation(StationData oldStation, StationData defaultStation) { if (oldStation == null) return defaultStation; var merged = new StationData(); // 保留原有字段 merged.StationBuildingType = oldStation.StationBuildingType; merged.StationBuildingTypeCosts = oldStation.StationBuildingTypeCosts ?? defaultStation.StationBuildingTypeCosts; // 新字段:如果老数据是默认值(0),用默认数据的1,否则保留原有值 merged.CapacityMultiplier = oldStation.CapacityMultiplier == 0 ? defaultStation.CapacityMultiplier : oldStation.CapacityMultiplier; merged.Parameters = oldStation.Parameters ?? defaultStation.Parameters; return merged; } // 同理实现MergePeople、MergeBus方法,逻辑一致 private PeopleData MergePeople(PeopleData oldPeople, PeopleData defaultPeople) { if (oldPeople == null) return defaultPeople; var merged = new PeopleData(); merged.DensityMultiplier = oldPeople.DensityMultiplier == 0 ? defaultPeople.DensityMultiplier : oldPeople.DensityMultiplier; merged.Parameters = oldPeople.Parameters ?? defaultPeople.Parameters; // 保留原有核心字段 merged.Population = oldPeople.Population; return merged; }
3. 额外优化:实现ISerializable接口(可选)
如果需要更精细的版本控制(比如区分不同版本的存档),可以让序列化类实现ISerializable接口,手动控制序列化/反序列化的每一步,彻底避免结构变化带来的问题:
[Serializable] public class CityData : ISerializable { public int Level; public int Price; [OptionalField] public float DensityMultiplier = 1f; // 无参构造函数,反序列化必须 public CityData() {} // 反序列化构造函数 protected CityData(SerializationInfo info, StreamingContext context) { // 强制读取原有字段 Level = info.GetInt32(nameof(Level)); Price = info.GetInt32(nameof(Price)); // 读取新字段,不存在则用默认值 if (info.TryGetValue(nameof(DensityMultiplier), out float multiplier)) { DensityMultiplier = multiplier; } else { DensityMultiplier = 1f; } } // 序列化方法 public void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue(nameof(Level), Level); info.AddValue(nameof(Price), Price); info.AddValue(nameof(DensityMultiplier), DensityMultiplier); } }
二、长远方案:替换BinaryFormatter为更可靠的序列化库
BinaryFormatter已经被微软标记为过时(Obsolete),不仅版本兼容性差,还存在严重的安全风险(可能被恶意利用执行代码)。推荐替换成以下两种方案:
方案1:JSON序列化(最易用)
用Newtonsoft.Json或System.Text.Json,它们天生支持版本兼容——老数据里没有的字段会自动用类的默认值填充,不会破坏原有数据。
示例(Newtonsoft.Json):
public void Load() { string fullPath = Path.Combine(GameFilePath, GameFileName); if (File.Exists(fullPath)) { string json = File.ReadAllText(fullPath); GameData = JsonConvert.DeserializeObject<GameDataModel>(json, new JsonSerializerSettings { // 忽略老数据里没有的新字段,避免反序列化失败 MissingMemberHandling = MissingMemberHandling.Ignore, // 空值字段用默认值填充 NullValueHandling = NullValueHandling.Ignore }); } else { GameData = CreateDefaultGameData(); } // 补全嵌套类的默认值(如果需要) foreach (var city in GameData.Cities) { city.FoodStall ??= CreateDefaultGameData().Cities.First(c => c.CityName == city.CityName).FoodStall; } }
方案2:Protobuf序列化(性能最优)
如果追求极致性能和更小的文件体积,用Google.Protobuf,它是二进制序列化,但版本兼容性极佳,支持字段新增/删除,不会破坏老数据。
内容的提问来源于stack exchange,提问作者Just Smile




