You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Unity中如何向二进制格式存档数据添加新变量?

解决BinaryFormatter序列化数据的版本兼容问题

首先得明确问题根源: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

火山引擎 最新活动