目录
前言
在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