【unity实战】制作unity数据保存和加载系统——大型游戏存储的最优解

avatar
作者
筋斗云
阅读量:0

最终效果

在这里插入图片描述

文章目录

前言

前面写过小型游戏存储功能:
【unity实战】制作unity数据保存和加载系统——小型游戏存储的最优解(包含数据安全处理方案的加密解密)

这次做一个针对大型游戏,更加复杂和全面的存储系统,解决存储数据比较多的情况。其实这个功能在之前的实战项目中已经做过,在这里我只是单独提取出来,感兴趣可以回头去看看:
【制作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数据
在这里插入图片描述
在这里插入图片描述

效果
在这里插入图片描述

完结

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,以便我第一时间收到反馈,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!

好了,我是向宇https://xiangyu.blog.csdn.net

一位在小公司默默奋斗的开发者,出于兴趣爱好,最近开始自学unity,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!php是工作,unity是生活!如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~
在这里插入图片描述

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!