杂项功能——数据的保存与加载

文章讲述了在Unity中通过PlayerPrefs和JsonUtility与Newtonsoft.Json进行对象存储的优缺点,以及如何处理继承和类型匹配问题,提供了场景保存和角色背包数据的实例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

方式一:使用 PlayerPrefs + JsonUtility

优点:使用方便   缺点:PlayerPrefs只能保存 int float string 类型

        以下SaveManger类简单实现了Save和Load方法


public class SaveManager : Singleton<SaveManager>
{
    public void Save(Object data,string key)
    {
        var jsonData = JsonUtility.ToJson(data,true);
        PlayerPrefs.SetString(key,jsonData);
        PlayerPrefs.Save();
    }

    public void Load(Object data, string key)
    {
        if(PlayerPrefs.HasKey(key))
        {
            JsonUtility.FromJsonOverwrite(PlayerPrefs.GetString(key), data);
        }
    }

}

方法二: 使用 接口 + Newtonsoft.Json

后续补充:Json在序列化的时候默认不会将对象的类型信息也序列化,因此在使用继承(使用父类来表示子类)的时候要注意。

        解决办法一:调整data数据,不使用上述的继承方式,转虚函数调用,将save 和 load 的实现均放在子类中。

        解决办法二:序列化的过程可以设置参数,将TypeNameHandling 补充上即可。

    private JsonSerializerSettings settings = new JsonSerializerSettings();
    settings.TypeNameHandling = TypeNameHandling.Auto;


    string jsonData = JsonConvert.SerializeObject(saveData, settings);

    saveData = JsonConvert.DeserializeObject<Data>(stringData,settings);

DataDefination:对于可读写(需要存储)的对象,在OnValidata中赋予其一个唯一的Guid作为标识使用。

public class DataDefination : MonoBehaviour
{
    [SerializeField]private PersistentType persistentType;

    public string id;

    private void OnValidate()
    {
        if (persistentType == PersistentType.ReadWrite)
        {
            if (id == string.Empty)
                id = Guid.NewGuid().ToString();
        }
        else
            id = string.Empty;
    }
}

保存接口:

1:提供注册,注销的方法           2:提供保存数据,读取数据的方法

public interface ISavable
{
    string GetDataID();
    
    public void RegisterSaveData()=>DataManager.Instance.RegisterSaveData(this);

    public void UnRegisterSaveData()=>DataManager.Instance.UnRegisterSaveData((this));

    /// <summary>
    /// 将数据保存到data当中
    /// </summary>
    void SaveData(Data data);

    /// <summary>
    /// 从Data中读取数据
    /// </summary>
    void LoadData(Data data);
}

Data类:定义所有需要存储的数据  (包括场景的保存(使用了JsonUtility))

using System.Collections.Generic;
using Newtonsoft.Json;
using UnityEngine;

//记录所有需要存储的数据
public class Data
{
    public string sceneToSave;

    public SerializeVector3 playerPos;
    
    //所有角色的状态信息
    public Dictionary<string, CharacterData_SO> characterStatsData = new();
    
    public List<TaskData_SO> tasks = new ();

    public Dictionary<string, TaskData_SO> taskDict = new();

    #region 背包信息

    public InventoryData_SO consumableInventory = ScriptableObject.CreateInstance<InventoryData_SO>();

    public InventoryData_SO equipmentsInventory = ScriptableObject.CreateInstance<InventoryData_SO>();

    public InventoryData_SO playerEquipmentInventory=ScriptableObject.CreateInstance<InventoryData_SO>();

    public InventoryData_SO actionInventory = ScriptableObject.CreateInstance<InventoryData_SO>();
    
    #endregion
    
    

    public void SaveScene(GameSceneSO gameScene)
    {
        sceneToSave = JsonUtility.ToJson(gameScene);
    }

    public GameSceneSO LoadScene()
    {
         var gameScene = ScriptableObject.CreateInstance<GameSceneSO>();
         JsonUtility.FromJsonOverwrite(sceneToSave, gameScene);
         return gameScene;
    }
    
    
    
}

DataManager类: [DefaultExecutionOrder(-100)] 确保DataManager能更早地执行

1:使用观察者模式保存所用的接口,接口默认实现注册注销(在Enable和Disable时调用)

2:定义存储save文件位置为 jsonFolder = Application.persistentDataPath + "/Save Data/";

3:在Awake中新建Data类型saveData,并且尝试从文件中读取数据

4:Save函数:调用所有接口的SaveData方法,然后将data写入文件

5:Load函数:调用所有接口的LoadData方法

[DefaultExecutionOrder(-100)]
public class DataManager : Singleton<DataManager>
{
    private HashSet<ISavable> savableSet = new HashSet<ISavable>();

    private Data saveData;

    private string jsonFolder;

    protected override void Awake()
    {
        base.Awake();
        saveData = new Data();

        jsonFolder = Application.persistentDataPath + "/Save Data";
        Debug.Log(jsonFolder);
        ReadSaveData();
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.O))
        {
            Save();
        }

        if (Input.GetKeyDown(KeyCode.L))
        {
            Load();
        }
        
    }

    public void RegisterSaveData(ISavable savable)
    {
        if (!savableSet.Contains(savable))
            savableSet.Add(savable);
    }

    public void UnRegisterSaveData(ISavable savable)
    {
        if (savableSet.Contains(savable))
            savableSet.Remove(savable);
    }

    
    //将save列表中的数据全部保存到文件中
    public void Save()
    {
        foreach (var savable in savableSet)
        {
            savable.SaveData(saveData);
        }

        string resultPath = jsonFolder + "saveData.sav";

        
        string jsonData = JsonConvert.SerializeObject(saveData);

        if (!File.Exists(resultPath))
        {
            Directory.CreateDirectory(jsonFolder);
        }
        
        File.WriteAllText(resultPath,jsonData);

    }
    
    //根据save data中的数据调整游戏数据
    public void Load()
    {
        foreach (var savable in savableSet)
        {
            savable.LoadData(saveData);
        }
    }
    
    //将文件中的数据放置到sava data中
    private void ReadSaveData()
    {
        string resultPath = jsonFolder + "saveData.sav";
        if (File.Exists(resultPath))
        {
            string stringData = File.ReadAllText(resultPath);
            saveData = JsonConvert.DeserializeObject<Data>(stringData);
        }
    }
    
}

应用示例:

1:存储角色的数据类型 CharacterData_SO

(1)在Data中新增 字典<角色的GUID,角色的数据> (为所有角色添加DataDefination)

 public Dictionary<string, CharacterData_SO> characterStatsData = new();

(2)在CharacterStats中添加 Isavable接口  并实现

 public string GetDataID()
    {
        return GetComponent<DataDefination>().id;
    }

    public  void SaveData(Data data)
    {
        var saveCharacterData = Instantiate(originalCharacterData);
        saveCharacterData.InitCharacterData(characterData);
        if (data.characterStatsData.ContainsKey(GetDataID()))
            data.characterStatsData[GetDataID()] = saveCharacterData;
        else
            data.characterStatsData.Add(GetDataID(), saveCharacterData);
    }

    public  void LoadData(Data data)
    {
        if (data.characterStatsData.ContainsKey(GetDataID()))
        {
            CharacterData_SO loadCharacterData = data.characterStatsData[GetDataID()];
            float maxHealthChange = loadCharacterData.maxHealth - characterData.maxHealth;
            float maxEnergyChange = loadCharacterData.maxEnergy - characterData.maxEnergy;
            float maxMagicChange = loadCharacterData.maxMagic - characterData.maxMagic;
            characterData.InitCharacterData(loadCharacterData);
            UpdateUIInfo(maxHealthChange, maxEnergyChange, maxMagicChange);
        }
    }

2:存储背包类型

(1)在Data中新增 所有的需要存储的背包内容

 #region 背包信息

    public InventoryData_SO consumableInventory = ScriptableObject.CreateInstance<InventoryData_SO>();

    public InventoryData_SO equipmentsInventory = ScriptableObject.CreateInstance<InventoryData_SO>();

    public InventoryData_SO playerEquipmentInventory=ScriptableObject.CreateInstance<InventoryData_SO>();

    public InventoryData_SO actionInventory = ScriptableObject.CreateInstance<InventoryData_SO>();
    

    #endregion

(2)在InventoryData_SO中提供Save 和 Load 方法(参数为InventoryData_SO)

        这里要注意由于物品ItemData_SO模板是唯一且是拖拽赋值到物品上的,在保存并且重新开始游戏加载后可能会出现Type Mismatch错误(尽管物品还能正确使用(如果正确复制的话))。并且由于在比较物品的时候如果直接根据ItemData_SO(因为它唯一嘛)直接进行比较操作,就会导致我们重新开始游戏(并不是在本局内,拖拽赋值的物品模板的地址发生了改变)并且加载后ItemData_SO对不上。

        虽然问题出在了比较两个引用的值(物品模板会在重新开始游戏时被改变地址),Debug.Log(物品)类型没问题,但是总之就是会出现Type Mismatch的错误,解决方法有两种:

一:使用ItemName进行比较 操作。           

二:设置物品字典(名字->物品模板),然后再加载数据时正确读取当前游戏中的物品模板。

    public void SaveInventory(InventoryData_SO inventoryDataSo)
    {
        var targetItems = inventoryDataSo.items;
        items = new List<InventoryItem>(targetItems.Count);
        
        for (int i = 0; i < targetItems.Count; i++)
        {
            items.Add(new InventoryItem(targetItems[i].itemData, targetItems[i].itemAmount));
        }

        itemCapacity = inventoryDataSo.itemCapacity;
        itemCount = inventoryDataSo.itemCount;
    }

    public void LoadInventory(InventoryData_SO inventoryDataSo)
    {
        var targetItems = inventoryDataSo.items;
        for (int i = 0; i < targetItems.Count; i++)
        {
            if (targetItems[i].itemData != null)
                items[i].itemData = InventoryManager.Instance.GetItemByName(targetItems[i].itemData.itemName);
            else
                items[i].itemData = null;
            items[i].itemAmount = targetItems[i].itemAmount;
        }

        itemCapacity = inventoryDataSo.itemCapacity;
        itemCount = inventoryDataSo.itemCount;
    }

(3)在InventoryManager中 添加 Isavable接口 并且实现

    public void SaveData(Data data)
    {
        //Save所有的背包
        data.consumableInventory.SaveInventory(consumableInventory);
        data.equipmentsInventory.SaveInventory(equipmentsInventory);
        data.playerEquipmentInventory.SaveInventory(playerEquipmentInventory);
        data.actionInventory.SaveInventory(actionInventory);
    }

    public void LoadData(Data data)
    {
        //Load所有的背包
        consumableInventory.LoadInventory(data.consumableInventory);
        equipmentsInventory.LoadInventory(data.equipmentsInventory);
        playerEquipmentInventory.LoadInventory(data.playerEquipmentInventory);
        actionInventory.LoadInventory(data.actionInventory);
        //刷新所有的背包
        RefreshAllContainer();
    }

对于一些无法被 Newtonsort.Json 序列化的类型解决方法举例:

    补充:其实都可以使用JsonUtility作为中间层工具。

1:Vector3:创建一个 SerializeVector3  实现这两个类之间的相互转换

public class SerializeVector3
{
    public float x, y, z;

    public SerializeVector3(Vector3 pos)
    {
        this.x = pos.x;
        this.y = pos.y;
        this.z = pos.z;
    }

    public Vector3 ToVector3()
    {
        return new Vector3(x, y, z);
    }
    
}

2:Sprite:记录资源的地址 使用动态资源加载方式(如Resource 或 AssetReference)

public class SerializeSprite
{
    public string spriteAddress;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值