前言
这次做一个针对大型游戏,更加复杂和全面的存储系统,解决存储数据比较多的情况。其实这个功能在之前的实战项目中已经做过,在这里我只是单独提取出来,感兴趣可以回头去看看:
【制作100个unity游戏之26】unity2d横版卷轴动作类游戏1(附带项目源码)
存储位置信息
新增VoidEventSO,定义保存数据事件ScriptableObject
[CreateAssetMenu(menuName = "Event/VoidEventSO")] public class VoidEventSO : ScriptableObject { public UnityAction OnEventRaised; public void RaiseEvent(){ OnEventRaised?.Invoke(); } }
新增Data
public class Data { /// <summary> /// 存储角色位置信息的字典,键为角色名称,值为对应的位置坐标(Vector3)。 /// </summary> public Dictionary<string, Vector3> characterPosDict = new Dictionary<string, Vector3>(); }
新增DataManager,为了保证Data Manager可以优先其他代码执行,为它添加特性[DefaultExecutionOrder(-100)]。很多小伙伴没有留意后面会提到的这个内容,发现有ISaveable的注册报错。[DefaultExecutionOrder(-100)] 是 Unity 中的一个属性,用于指定脚本的默认执行顺序。参数 -100 表示该脚本的执行顺序优先级,数值越小,优先级越高,即越先执行。
新输入系统获取键盘的输入,按下L按键读取一下进度。
using System.Collections.Generic; using UnityEngine; using UnityEngine.InputSystem; //指定脚本的默认执行顺序,数值越小,优先级越高 [DefaultExecutionOrder(-100)] public class DataManager : MonoBehaviour { public static DataManager instance; [Header("事件监听")] public VoidEventSO saveDataEvent; // 保存数据事件 /// <summary> /// 存储需要保存数据的 ISaveable 实例的列表。 /// </summary> private List<ISaveable> saveableList = new List<ISaveable>(); /// <summary> /// 保存数据到 Data 对象中。 /// </summary> private Data saveData; private void Awake() { if (instance == null) { instance = this; } else { Destroy(gameObject); } saveData = new Data(); } private void Update() { // 按L 加载测试 if(Keyboard.current.lKey.wasPressedThisFrame){ Debug.Log("加载"); Load(); } } /// <summary> /// 注册需要保存数据的 ISaveable 实例。 /// </summary> /// <param name="saveable">需要保存数据的 ISaveable 实例。</param> public void RegisterSaveData(ISaveable saveable) { if (!saveableList.Contains(saveable)) { saveableList.Add(saveable); } } public void UnRegisterSaveData(ISaveable saveable){ if (saveableList.Contains(saveable)) { // 如果在,就从列表中移除 saveableList.Remove(saveable); } } private void OnEnable() { saveDataEvent.OnEventRaised += Save; // 监听保存数据事件 } private void OnDisable() { saveDataEvent.OnEventRaised -= Save; // 取消监听保存数据事件 } /// <summary> /// 保存数据。 /// </summary> public void Save() { foreach (var saveable in saveableList) { saveable.GetSaveData(saveData); } } /// <summary> /// 加载数据并应用到相应的 ISaveable 实例中。 /// </summary> public void Load() { foreach (var saveable in saveableList) { saveable.LoadData(saveData); } } }
挂载配置
新增接口ISaveable
public interface ISaveable { DataDefination GetDataID(); /// <summary> /// 将该实例注册到数据管理器以便保存数据。 /// </summary> void RegisterSaveData() => DataManager.instance.RegisterSaveData(this); /// <summary> /// 将该实例从数据管理器中注销,停止保存数据。 /// </summary> void UnRegisterSaveData() => DataManager.instance.UnRegisterSaveData(this); /// <summary> /// 获取需要保存的数据并存储到指定的 Data 对象中。 /// </summary> /// <param name="data">保存数据的 Data 对象。</param> void GetSaveData(Data data); /// <summary> /// 从指定的 Data 对象中加载数据并应用到该实例中。 /// </summary> /// <param name="data">包含加载数据的 Data 对象。</param> void LoadData(Data data); }
那么如果有三个野猪的名字完全一样,我们怎么区分每一只野猪具体存储的位置呢,所以接下来我们要创建一个唯一的标识,我们可以直接使用c#为我们设置好的全局唯一标识符,GUID就是个16位的串码,保证它的唯一性
新增枚举
/// <summary> /// 指示数据定义的持久化类型。 /// </summary> public enum PersistentType { /// <summary> /// 可读写的持久化类型,数据会被持久化保存。 /// </summary> ReadWrite, /// <summary> /// 不持久化类型,数据不会被持久化保存。 /// </summary> DoNotPerst }
新增DataDefination
public class DataDefination : MonoBehaviour { /// <summary> /// 持久化类型,指示数据定义的持久化方式。 /// </summary> public PersistentType persistentType; /// <summary> /// 数据定义的唯一标识符。 /// </summary> public string ID; /// <summary> /// 当编辑器中的属性值发生更改时调用,用于自动设置默认的ID值。 /// </summary> private void OnValidate() { if (persistentType == PersistentType.ReadWrite) { if (ID == string.Empty) { ID = System.Guid.NewGuid().ToString(); } } else { ID = string.Empty; } } }
配置挂载脚本,比如我们放在人物身上,生成唯一的UID,记得每个UID都要唯一,如果是复制出来的对象记得刷新一下UID
修改PlayerController,调用接口
public class PlayerController : MonoBehaviour, ISaveable { //... private void OnEnable() { ISaveable saveable = this; saveable.RegisterSaveData(); } private void OnDisable() { ISaveable saveable = this; saveable.UnRegisterSaveData(); } // 获取数据ID,用于唯一标识当前对象的位置信息 public DataDefination GetDataID() { return GetComponent<DataDefination>(); } // 将对象的位置信息保存到数据中 public void GetSaveData(Data data) { // 检查数据中是否已经存在当前对象的位置信息 if (data.characterPosDict.ContainsKey(GetDataID().ID)) { // 如果已经存在,则更新位置信息 data.characterPosDict[GetDataID().ID] = transform.position; } else { // 如果不存在,则添加新的位置信息 data.characterPosDict.Add(GetDataID().ID, transform.position); } } // 从数据中加载对象的位置信息 public void LoadData(Data data) { // 检查数据中是否存在当前对象的位置信息 if (data.characterPosDict.ContainsKey(GetDataID().ID)) { // 如果存在,则将位置信息设置为对应的数值 transform.position = data.characterPosDict[GetDataID().ID]; } } }
修改SavePoint,调用存储数据
public class SavePoint : MonoBehaviour, IInteractable { private SpriteRenderer spriteRenderer; public Sprite darkSprite; public Sprite lightSprite; public bool isDone; public VoidEventSO saveDataEvent; // 保存数据事件 private void Awake() { spriteRenderer = GetComponent<SpriteRenderer>(); } private void OnEnable() { spriteRenderer.sprite = isDone ? lightSprite : darkSprite; } public void TriggerAction() { if(!isDone){ Save(); spriteRenderer.sprite = lightSprite; GetComponent<Collider2D>().enabled = false; isDone = true; } } //存储数据 private void Save(){ Debug.Log("存储数据"); saveDataEvent.RaiseEvent(); } }
效果,按L测试读取数据,角色回到存储的位置
存储更多数据
修改Data,定义通用的float的类型,所有和float相关的类型都可用它保存
public class Data { //... public Dictionary<string, float> floatSaveData = new Dictionary<string, float>(); }
但是如何区分是人物的血条还是能量呢?我们可以加入不同的后缀,修改PlayerController
// 将对象的位置信息保存到数据中 public void GetSaveData(Data data) { // 检查数据中是否已经存在当前对象的位置信息 if (data.characterPosDict.ContainsKey(GetDataID().ID)) { // 如果已经存在,则更新位置信息 data.characterPosDict[GetDataID().ID] = transform.position; data.floatSaveData[GetDataID().ID + "Health"] = GetComponent<Character>().currentHealth; data.floatSaveData[GetDataID().ID + "Power"] = GetComponent<Character>().currentPower; } else { // 如果不存在,则添加新的位置信息 data.characterPosDict.Add(GetDataID().ID, transform.position); //存储玩家血量和能量 data.floatSaveData.Add(GetDataID().ID + "Health", GetComponent<Character>().currentHealth); data.floatSaveData.Add(GetDataID().ID + "Power", GetComponent<Character>().currentPower); } } // 从数据中加载对象的位置信息 public void LoadData(Data data) { // 检查数据中是否存在当前对象的位置信息 if (data.characterPosDict.ContainsKey(GetDataID().ID)) { // 如果存在,则将位置信息设置为对应的数值 transform.position = data.characterPosDict[GetDataID().ID]; GetComponent<Character>().currentHealth = data.floatSaveData[GetDataID().ID + "Health"]; GetComponent<Character>().currentPower = data.floatSaveData[GetDataID().ID + "Power"]; //更新血条能量UI GetComponent<Character>().OnHealthChanged?.Invoke(GetComponent<Character>()); } }
效果
同理你可以存储其他的比如宝箱,野猪等信息
存储场景信息
修改Data,将场景信息转为json数据进行存取
public string sceneToSave; public void SaveGameScene(SceneField savedScene){ sceneToSave = JsonUtility.ToJson(savedScene); } public SceneField GetSavedScene(){ SceneField loadedData = JsonUtility.FromJson<SceneField>(sceneToSave); return loadedData; }
修改SavePoint,存储场景信息
public SceneField currentLoadedScene; public class SavePoint : MonoBehaviour, IInteractable, ISaveable { //... public DataDefination GetDataID() { return null; } public void GetSaveData(Data data) { data.SaveGameScene(currentLoadedScene);//存储场景 } public void LoadData(Data data) { } }
配置当前场景
修改DataManager,我们希望加载存储场景完成后,再进行其他的LoadData操作,所以加载存储场景的操作我们就不放在LoadData里执行了。可以加入场景过渡渐变,让效果更好,这里我就不加了
/// <summary> /// 加载数据并应用到相应的 ISaveable 实例中。 /// </summary> public void Load() { //获取存储的场景 var scence = saveData.GetSavedScene(); if (scence != null) { // 获取当前活动的场景 Scene activeScene = SceneManager.GetActiveScene(); // 获取所有加载的场景 for (int i = 0; i < SceneManager.sceneCount; i++) { Scene loadedScene = SceneManager.GetSceneAt(i); Debug.Log("Loaded Scene " + i + ": " + loadedScene.name); if (activeScene.name != loadedScene.name) SceneManager.UnloadSceneAsync(loadedScene.name); // 异步卸载所有非主场景 } //加载scence场景 SceneManager.LoadSceneAsync(scence.SceneName, LoadSceneMode.Additive).completed += operation => { if (operation.isDone) { //获取相机边界方法 cameraControl.GetNewCameraBounds(); //加载其他数据 foreach (var saveable in saveableList) { saveable.LoadData(saveData); } } }; //控制按钮的显示隐藏 sceneLoadTrigger.StartMenu(); } }
效果
持久化存储数据
具体可以看我这篇文章:【unity小技巧】Unity存储存档保存——PlayerPrefs、JsonUtility和MySQL数据库的使用
修改DataManager
using Newtonsoft.Json; String savePath = "test.json"; /// <summary> /// 保存数据。 /// </summary> public void Save() { //。。。 //持久化存储数据 String jsonData = JsonConvert.SerializeObject(saveData); File.WriteAllText(savePath, jsonData); } /// <summary> /// 加载数据并应用到相应的 ISaveable 实例中。 /// </summary> public void Load() { //读取数据 string jsonData = File.ReadAllText(savePath); //将JSON数据反序列化为游戏数据对象 Data saveData = JsonConvert.DeserializeObject<Data>(jsonData); //。。。 }
查看存储的test.json数据
效果