最终效果
文章目录
- 最终效果
- 前言
- 存储位置信息
- 存储更多数据
- 存储场景信息
- 持久化存储数据
前言
前面写过小型游戏存储功能:
【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数据
效果