Unity存储系统——基于Newtonsoft.Json

前言

在Unity中用于保存玩家数据到本地的方法有很多种,这是一套基于Newtonsoft.Json的存储系统,Newtonsoft.Json相对于JsonUtility可以直接序列化字典或者列表,但是相对的也多了其他的限制,这个会在下面有所提到


一、构建框架

在游戏中,我们有很多的数据需要保存,这些数据在不同的类里面,为了存储这些类里面的数据,我们可以用一个接口将这个数据传到用于保存数据的父类中(里氏替换),然后在存储系统的业务中心中通过一个列表存储获得数据的接口,通过循环接口中获得的数据,将其序列化存储在本地。

二、创建存储数据类Data

public class Data
{
    //后面需要存储什么数据在里面加 
}

三、创建数据接口ISavable

public interface ISavable 
{
    void RegisterSaveData()=> SaveLoadManager.RegisterSaveData(this);

    void UnRegisterSaveData()=> SaveLoadManager.UnRegisterSaveData(this);

    void GetSaveData(Data data);//获得数据的方法
    void LoadData(Data data);//加载数据的方法
}

四、创建业务中心SaveLoadManager

1、在C#中,我们在使用一个类时,需要new一个对象
2、存储时需要一个保存文件的路径
3、一个将数据传入savableList 的方法,一个将数据移出savableList
4、一个保存方法Save,一个加载方法Load,一个读取存储文件的方法ReadSaveData

public class SaveLoadManager : SingletonScript<SaveLoadManager>
{
	public SaveLoadPanel saveLoadPanel;
	private List<ISavable> savableList = new List<ISavable>();
    private Data saveData;
    private string jsonFolder;
    protected override void Awake()
    {
        base.Awake();
        saveData = new Data();
		jsonFolder = Application.persistentDataPath + SaveLoadPath.Save_Folder;
		ReadSavedData();
    }
	public static void RegisterSaveData(ISavable savableSave)
    {
        if (!Instance.savableList.Contains(savableSave))
        {
            Instance.savableList.Add(savableSave);
            Debug.Log("存档列表中的数据个数:" + Instance.savableList.Count);
        }
    }

    public static void UnRegisterSaveData(ISavable savable)
    {
        if (Instance.savableList.Contains(savable))
        {
            Instance.savableList.Remove(savable);
            Debug.Log("移除存档列表成功:" + savable.ToString());
        }
    }
    public static void Save()
    {
        try
        {
            foreach (var savable in Instance.savableList)
            {
                savable.GetSaveData(Instance.saveData);//循环接口获得的数据
            }
            var saveData = JsonConvert.SerializeObject(Instance.saveData);//序列化数据为json文件
			var resultPath = Instance.jsonFolder + SaveLoadPath.Save_File;//存储路径
            Directory.CreateDirectory(Instance.jsonFolder);//创建文件夹
            File.WriteAllText(resultPath, saveData);//将数据写入本地

            Debug.Log("存档成功:" + resultPath);
        }
        catch (Exception ex)
        {
            Debug.LogError("存档失败" + ex.Message);
        }
    }
	public static void Load()
    {
    	//读取的方法与加载类似,都是先循环ISavabel接口获得的数据
       	try
        {
            foreach (var savable in Instance.savableList)
            {
                savable.LoadData(Instance.saveData);
                Debug.Log("加载存档成功:" + savable.ToString());
            }
        }
        catch (Exception ex)
        {
            Debug.LogError("加载失败" + ex.Message);
        }
    }
	public static void ReadSavedData()//在游戏启动时先读取是否有存档,如果有则将数据传给saveData ,然后通过Load方法读取
    {
        var resultPath = Instance.jsonFolder + SaveLoadPath.Save_File;//存储路径
        if (File.Exists(resultPath))
        {
            var stringData = File.ReadAllText(resultPath);
            Instance.saveData = JsonConvert.DeserializeObject<Data>(stringData);//将数据传给saveData

            Debug.Log("读取存档成功" + resultPath);
        }
    }
  }
public class SaveLoadPath
{
    public const string Save_Folder = "/SAVE DATA/";
    public const string Save_File = "data.save";
}

Tips:SingletonScript是一个泛型单例,不了解的朋友可以先去了解一下。

至此,这个存储系统实现了简单的数据存储加载功能(开始游戏,继续游戏),但它仍然不支持多存档功能,同一角色不同属性的保存功能,删除存档的功能

五、实现多存档功能

我们在游玩游戏时,如果打开它的存档面板就会发现,存档可以不止有一个,当我们选择某一存档选择加载时,便会加载对应的存档,接下来我们在这个存档系统中实现它。
1、搭建存档面板(这个大家自行发挥)
在这里插入图片描述
当我们点击生成存档时,会保存当前数据,然后生成一个存档按钮,但选择存档按钮时,点击加载存档,会加载当前所选择的存档,点击删除存档,会删除当前所选择的存档,点击清空存档,会清除所有存档,这么多的存档,据此我们需要一个专门保存存档目录的列表,在每次启动或者关闭游戏时自动调用这个方法,根据保存在存档目录中的信息将对应的存档按钮实例化出来
2、创建用于管理面板上组件的代码SaveLoadPanel

using UnityEngine;
using UnityEngine.UI;

public class SaveLoadPanel : MonoBehaviour
{
    public Transform instanceSaveBtnPosParent;//生成存档按钮的父级
    public SaveButton saveButtonPre;//存档按钮的预制体
    public LoadButton loadButton;//加载按钮
    public DeletButton deletButton;//删除按钮
    public List<SaveButtonData> saveButtonDataList = new List<SaveButtonData>();


    public void InstanceSaveBtn(string saveName,string savePath)//生成按钮的方法
    {
        SaveLoadManager.Instance.saveDataCatalogue.saveButtonDataList = saveButtonDataList;

        SaveButton saveBtn = Instantiate(saveButtonPre, instanceSaveBtnPosParent);

        saveBtn.saveText.text = saveName;
        saveBtn.savePath = savePath;
        saveBtn.saveToggle.group = instanceSaveBtnPosParent.GetComponent<ToggleGroup>();
        
        saveButtonDataList.Add(saveBtn.GetSaveButtonData());
    }
}

3、创建存档按钮预制体及代码SaveButton;

using UnityEngine;
using UnityEngine.UI;
public class SaveButton : MonoBehaviour
{
    public Text saveText;
    public Toggle saveToggle;
    [Tooltip("生成按钮时传入的路径")]public string savePath;

    private void Awake() 
    {
       saveToggle.onValueChanged.AddListener(OnClickSaveToggle);
    }

    private void OnClickSaveToggle(bool isSelected)
    {
        if(isSelected)
        {
            saveToggle.isOn = true;

            SaveLoadManager.Instance.saveLoadPanel.loadButton.savePath = null;
            SaveLoadManager.Instance.saveLoadPanel.loadButton.savePath = savePath;//将路径信息传递给LoadButton的savePath

            SaveLoadManager.Instance.saveLoadPanel.deletButton.savePath = null;
            SaveLoadManager.Instance.saveLoadPanel.deletButton.savePath = savePath;    
        }
        Debug.Log("当前选择存档:" + saveText.text);
        
    }
	public SaveButtonData GetSaveButtonData()
    {
        return new SaveButtonData
        {
            saveName = this.saveText.text,
            savePath = this.savePath
        };
    }
}
[System.Serializable]
public class SaveButtonData
{
    public string saveName;
    public string savePath;
}

在这段代码中有一个存档的名字和一个存档路径,在业务中心SaveLoadManager中有一个用于读取路径的方法ReadSaveData,这是发现,我们只需要点击当前存档时将这个路径传给这个方法,然后加载时调用Load方法,就可以实现加载对应存档的功能,同样的删除所选存档的功能,也可以据此去实现。

Tips:Toggle这个组件的功能与Button类似,后续的使用中发现这个在做多选择时的选中效果时比Button好用,便替换成了此,使用Toggle时需要给父级设置一个ToggleGroup。可能会有人疑问为什么要新建一个类将数据SaveButton中的一些数据转换成这个类,这是因为UnityEngine.UI中的数据在使用Newtonsoft.Json序列化时,会出现循环自引用的问题,从而出现无法存档的问题,所以要进行一次数据的转换

六、优化更改SaveLoadManager

1、因为存档目录与其他要保存的数据不同,它需要在游戏每次启动或者关闭游戏时自动加载,所以需要专门去处理这个功能。
在Data类中创建一个新类SaveDataCatalogue用于保存SaveButtonData

[System.Serializable]
public class SaveDataCatalogue
{
    public List<SaveButtonData> saveButtonDataList;
}

2、在SaveLoadManager中new一个对象

	public SaveDataCatalogue saveDataCatalogue;

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

3、创建生成目录的方法SaveCatalogue,在SaveLoadPath中加入一个目录文件的常量,用于当目录文件的名称

public class SaveLoadPath
{
    public const string Save_Folder = "/SAVE DATA/";
    public const string Save_File = "data.save";
    public const string Save_Catalogue = "data.catalogue";
}
	private static void SaveCatalogue()//推荐在Disable时生成存档目录
    {
        var resultPath = Instance.jsonFolder  + SaveLoadPath.Save_Catalogue;
        var saveDataCatalogue = JsonConvert.SerializeObject(Instance.saveDataCatalogue);

        Directory.CreateDirectory(Instance.jsonFolder);
        File.WriteAllText(resultPath, saveDataCatalogue); 

        Debug.Log("保存存档目录成功");       
    }

4、创建读取目录的方法LoadDataCatalogue并传入一个SaveDataCatalogue参数

private void LoadDataCatalogue(SaveDataCatalogue catalogueData)//启动时读取目录,在Start中执行
    {
        var resultPath = Instance.jsonFolder  + SaveLoadPath.Save_Catalogue;

        if (File.Exists(resultPath))
        {
            var stringcatalogueData = File.ReadAllText(resultPath);
            catalogueData = JsonConvert.DeserializeObject<SaveDataCatalogue>(stringcatalogueData);

            
            if(catalogueData != null)
            {
                saveLoadPanel.saveButtonDataList = catalogueData.saveButtonDataList;
                
                saveDataCatalogue.saveButtonDataList = catalogueData.saveButtonDataList;

                if(saveDataCatalogue.saveButtonDataList != null)//需要确保反序列化后的数据存在saveButtonDataList,否则会报空
                {
                    SaveButton saveButton = null;
                    for (int i = 0; i < saveDataCatalogue.saveButtonDataList.Count; i++)
                    {
                        saveButton = Instantiate(saveLoadPanel.saveButtonPre,saveLoadPanel.instanceSaveBtnPosParent);
                        //确保父级包含ToggleGroup
                        if(saveLoadPanel.instanceSaveBtnPosParent.GetComponent<ToggleGroup>() == null)
                        {
                            saveLoadPanel.instanceSaveBtnPosParent.AddComponent<ToggleGroup>();
                            saveButton.saveToggle.group = saveLoadPanel.instanceSaveBtnPosParent.GetComponent<ToggleGroup>();
                        }
                        else
                        {
                            saveButton.saveToggle.group = saveLoadPanel.instanceSaveBtnPosParent.GetComponent<ToggleGroup>();
                        }
                        saveButton.saveText.text = saveDataCatalogue.saveButtonDataList[i].saveName;
                        saveButton.savePath = saveDataCatalogue.saveButtonDataList[i].savePath;
                        
                    }
                }
                
            }

            Debug.Log("读取存档目录成功" + resultPath);
        }
  
    }

5、优化ReadSaveData方法

 public static void ReadSavedData(string savePath)
    {
        var resultPath = savePath;

        if (File.Exists(resultPath))
        {
            var stringData = File.ReadAllText(resultPath);
            Instance.saveData = JsonConvert.DeserializeObject<Data>(stringData);

            Debug.Log("读取存档成功" + resultPath);
        }
    }

6、优化后ReadSaveData的方法需要在点击加载存档时传递过来的savePath,所以我们需要一个LoadButton类挂载在加载按钮上,用于专门处理点击事件,和处理点击SaveButton时传递过来的savePath

using UnityEngine;
using UnityEngine.UI;

public class LoadButton : MonoBehaviour
{
    public Button loadBtn;
    public string savePath;

    private void Awake() 
    {
        loadBtn.onClick.AddListener(OnLoad);
    }

    private void OnLoad()
    {
        SaveLoadManager.ReadSavedData(savePath);
    }
}

7、优化Save方法
在保存时,需要一个当前存档的名字,我们可以用一个时间戳来表示,点击生成存档时,需要生成一个保存当前路径的按钮

public static void Save()
    {
        try
        {
            foreach (var savable in Instance.savableList)
            {
                savable.GetSaveData(Instance.saveData);
            }
            
            
            System.DateTime time = System.DateTime.Now;

            var resultPath = Instance.jsonFolder + time.ToString("yyyy_MMdd_HHmmss") + SaveLoadPath.Save_File;
            Instance.saveLoadPanel.InstanceSaveBtn(time.ToString("yyyy_MMdd_HHmmss"),resultPath);//以当前的时间戳为名
            var saveData = JsonConvert.SerializeObject(Instance.saveData);

            Directory.CreateDirectory(Instance.jsonFolder);
            File.WriteAllText(resultPath, saveData);

            Debug.Log("存档成功:" + resultPath);
        }
        catch (Exception ex)
        {
            Debug.LogError("存档失败" + ex.Message);
        }

    }

至此,已经实现了多存档系统的存储加载,那么如何使用它?有两种常见的方法(事件中心,SO事件的方法),这里笔者选择的是利用的是后者。

七、使用SO存储事件方法

using UnityEngine;
using UnityEngine.Events;

[CreateAssetMenu(menuName = "Event/VoidEvent_SO")]
public class VoidEvent_SO : ScriptableObject 
{
    public UnityAction OneventRaised;

    public void RaiseEvent()
    {
        OneventRaised?.Invoke();
    }
}

八、将Save,Load注册为事件

1、在SaveLoadManager中获得者两个无返回值的SO

 public VoidEvent_SO saveEvent;
 public VoidEvent_SO loadEvent;

2、注册事件

private void OnEnable() 
{
     saveEvent.OneventRaised += Save;
     loadEvent.OneventRaised += Load;
  }

private void OnDisable() 
 {
      saveEvent.OneventRaised -= Save;
      loadEvent.OneventRaised -= Load;
     

      SaveCatalogue();
  }

这样便已经将Save和Load方法注册成了事件
3、赋值
创建两个SO文件,并赋值
在这里插入图片描述
4、使用
Save按钮直接使用
在这里插入图片描述

Load按钮在代码中先获取SO,然后在点击时使用
在这里插入图片描述

注意:由于所有要存储的数据都需要通过SaveLoadManager的注册和注销的方法获取和移除,所以这个类需要在其他类之前执行,有两种方法实现这种效果
1、[DefaultExecutionOrder(-100)]在SaveLoadManager上加入这个特性
2、在这里插入图片描述
这两种方法其实都是更改Unity中代码执行的优先级

九、结语

好了,目前这个存档系统已经实现了多存档功能,至于删除指定存档和删除所以存档的功能,它的原理与读取时类似,留着各位读者去自己实现吧,另外如果在使用过程中发现无法存储的数据的很话(一般是Unity自身的数据),可以先试着自行转换,无法实现的话可以评论区讯问哦。


如果觉得文档太乱的话可以自行下载学习:https://github.com/immortal5205/SampleDialogue,这个里面不止有当前的存储系统,还有之前的对话系统已及任务系统,大家也可以对照着之前的文档学习。

如果想查看具体在游戏中的使用效果的话:https://www.bilibili.com/video/BV1wt421M7Lg/?vd_source=cbcb01112bcf2b5e16c5a97933138e45

  • 15
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值