方式一:使用 PlayerPrefs + JsonUtility
优点:使用方便 缺点:PlayerPrefs只能保存 int float string 类型
以下SaveManger类简单实现了Save和Load方法
public class SaveManager : Singleton<SaveManager>
{
public void Save(Object data,string key)
{
var jsonData = JsonUtility.ToJson(data,true);
PlayerPrefs.SetString(key,jsonData);
PlayerPrefs.Save();
}
public void Load(Object data, string key)
{
if(PlayerPrefs.HasKey(key))
{
JsonUtility.FromJsonOverwrite(PlayerPrefs.GetString(key), data);
}
}
}
方法二: 使用 接口 + Newtonsoft.Json
后续补充:Json在序列化的时候默认不会将对象的类型信息也序列化,因此在使用继承(使用父类来表示子类)的时候要注意。
解决办法一:调整data数据,不使用上述的继承方式,转虚函数调用,将save 和 load 的实现均放在子类中。
解决办法二:序列化的过程可以设置参数,将TypeNameHandling 补充上即可。
private JsonSerializerSettings settings = new JsonSerializerSettings();
settings.TypeNameHandling = TypeNameHandling.Auto;
string jsonData = JsonConvert.SerializeObject(saveData, settings);
saveData = JsonConvert.DeserializeObject<Data>(stringData,settings);
DataDefination:对于可读写(需要存储)的对象,在OnValidata中赋予其一个唯一的Guid作为标识使用。
public class DataDefination : MonoBehaviour
{
[SerializeField]private PersistentType persistentType;
public string id;
private void OnValidate()
{
if (persistentType == PersistentType.ReadWrite)
{
if (id == string.Empty)
id = Guid.NewGuid().ToString();
}
else
id = string.Empty;
}
}
保存接口:
1:提供注册,注销的方法 2:提供保存数据,读取数据的方法
public interface ISavable
{
string GetDataID();
public void RegisterSaveData()=>DataManager.Instance.RegisterSaveData(this);
public void UnRegisterSaveData()=>DataManager.Instance.UnRegisterSaveData((this));
/// <summary>
/// 将数据保存到data当中
/// </summary>
void SaveData(Data data);
/// <summary>
/// 从Data中读取数据
/// </summary>
void LoadData(Data data);
}
Data类:定义所有需要存储的数据 (包括场景的保存(使用了JsonUtility))
using System.Collections.Generic;
using Newtonsoft.Json;
using UnityEngine;
//记录所有需要存储的数据
public class Data
{
public string sceneToSave;
public SerializeVector3 playerPos;
//所有角色的状态信息
public Dictionary<string, CharacterData_SO> characterStatsData = new();
public List<TaskData_SO> tasks = new ();
public Dictionary<string, TaskData_SO> taskDict = new();
#region 背包信息
public InventoryData_SO consumableInventory = ScriptableObject.CreateInstance<InventoryData_SO>();
public InventoryData_SO equipmentsInventory = ScriptableObject.CreateInstance<InventoryData_SO>();
public InventoryData_SO playerEquipmentInventory=ScriptableObject.CreateInstance<InventoryData_SO>();
public InventoryData_SO actionInventory = ScriptableObject.CreateInstance<InventoryData_SO>();
#endregion
public void SaveScene(GameSceneSO gameScene)
{
sceneToSave = JsonUtility.ToJson(gameScene);
}
public GameSceneSO LoadScene()
{
var gameScene = ScriptableObject.CreateInstance<GameSceneSO>();
JsonUtility.FromJsonOverwrite(sceneToSave, gameScene);
return gameScene;
}
}
DataManager类: [DefaultExecutionOrder(-100)] 确保DataManager能更早地执行
1:使用观察者模式保存所用的接口,接口默认实现注册注销(在Enable和Disable时调用)
2:定义存储save文件位置为 jsonFolder = Application.persistentDataPath + "/Save Data/";
3:在Awake中新建Data类型saveData,并且尝试从文件中读取数据
4:Save函数:调用所有接口的SaveData方法,然后将data写入文件
5:Load函数:调用所有接口的LoadData方法
[DefaultExecutionOrder(-100)]
public class DataManager : Singleton<DataManager>
{
private HashSet<ISavable> savableSet = new HashSet<ISavable>();
private Data saveData;
private string jsonFolder;
protected override void Awake()
{
base.Awake();
saveData = new Data();
jsonFolder = Application.persistentDataPath + "/Save Data";
Debug.Log(jsonFolder);
ReadSaveData();
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.O))
{
Save();
}
if (Input.GetKeyDown(KeyCode.L))
{
Load();
}
}
public void RegisterSaveData(ISavable savable)
{
if (!savableSet.Contains(savable))
savableSet.Add(savable);
}
public void UnRegisterSaveData(ISavable savable)
{
if (savableSet.Contains(savable))
savableSet.Remove(savable);
}
//将save列表中的数据全部保存到文件中
public void Save()
{
foreach (var savable in savableSet)
{
savable.SaveData(saveData);
}
string resultPath = jsonFolder + "saveData.sav";
string jsonData = JsonConvert.SerializeObject(saveData);
if (!File.Exists(resultPath))
{
Directory.CreateDirectory(jsonFolder);
}
File.WriteAllText(resultPath,jsonData);
}
//根据save data中的数据调整游戏数据
public void Load()
{
foreach (var savable in savableSet)
{
savable.LoadData(saveData);
}
}
//将文件中的数据放置到sava data中
private void ReadSaveData()
{
string resultPath = jsonFolder + "saveData.sav";
if (File.Exists(resultPath))
{
string stringData = File.ReadAllText(resultPath);
saveData = JsonConvert.DeserializeObject<Data>(stringData);
}
}
}
应用示例:
1:存储角色的数据类型 CharacterData_SO
(1)在Data中新增 字典<角色的GUID,角色的数据> (为所有角色添加DataDefination)
public Dictionary<string, CharacterData_SO> characterStatsData = new();
(2)在CharacterStats中添加 Isavable接口 并实现
public string GetDataID()
{
return GetComponent<DataDefination>().id;
}
public void SaveData(Data data)
{
var saveCharacterData = Instantiate(originalCharacterData);
saveCharacterData.InitCharacterData(characterData);
if (data.characterStatsData.ContainsKey(GetDataID()))
data.characterStatsData[GetDataID()] = saveCharacterData;
else
data.characterStatsData.Add(GetDataID(), saveCharacterData);
}
public void LoadData(Data data)
{
if (data.characterStatsData.ContainsKey(GetDataID()))
{
CharacterData_SO loadCharacterData = data.characterStatsData[GetDataID()];
float maxHealthChange = loadCharacterData.maxHealth - characterData.maxHealth;
float maxEnergyChange = loadCharacterData.maxEnergy - characterData.maxEnergy;
float maxMagicChange = loadCharacterData.maxMagic - characterData.maxMagic;
characterData.InitCharacterData(loadCharacterData);
UpdateUIInfo(maxHealthChange, maxEnergyChange, maxMagicChange);
}
}
2:存储背包类型
(1)在Data中新增 所有的需要存储的背包内容
#region 背包信息
public InventoryData_SO consumableInventory = ScriptableObject.CreateInstance<InventoryData_SO>();
public InventoryData_SO equipmentsInventory = ScriptableObject.CreateInstance<InventoryData_SO>();
public InventoryData_SO playerEquipmentInventory=ScriptableObject.CreateInstance<InventoryData_SO>();
public InventoryData_SO actionInventory = ScriptableObject.CreateInstance<InventoryData_SO>();
#endregion
(2)在InventoryData_SO中提供Save 和 Load 方法(参数为InventoryData_SO)
这里要注意由于物品ItemData_SO模板是唯一且是拖拽赋值到物品上的,在保存并且重新开始游戏加载后可能会出现Type Mismatch错误(尽管物品还能正确使用(如果正确复制的话))。并且由于在比较物品的时候如果直接根据ItemData_SO(因为它唯一嘛)直接进行比较操作,就会导致我们重新开始游戏(并不是在本局内,拖拽赋值的物品模板的地址发生了改变)并且加载后ItemData_SO对不上。
虽然问题出在了比较两个引用的值(物品模板会在重新开始游戏时被改变地址),Debug.Log(物品)类型没问题,但是总之就是会出现Type Mismatch的错误,解决方法有两种:
一:使用ItemName进行比较 操作。
二:设置物品字典(名字->物品模板),然后再加载数据时正确读取当前游戏中的物品模板。
public void SaveInventory(InventoryData_SO inventoryDataSo)
{
var targetItems = inventoryDataSo.items;
items = new List<InventoryItem>(targetItems.Count);
for (int i = 0; i < targetItems.Count; i++)
{
items.Add(new InventoryItem(targetItems[i].itemData, targetItems[i].itemAmount));
}
itemCapacity = inventoryDataSo.itemCapacity;
itemCount = inventoryDataSo.itemCount;
}
public void LoadInventory(InventoryData_SO inventoryDataSo)
{
var targetItems = inventoryDataSo.items;
for (int i = 0; i < targetItems.Count; i++)
{
if (targetItems[i].itemData != null)
items[i].itemData = InventoryManager.Instance.GetItemByName(targetItems[i].itemData.itemName);
else
items[i].itemData = null;
items[i].itemAmount = targetItems[i].itemAmount;
}
itemCapacity = inventoryDataSo.itemCapacity;
itemCount = inventoryDataSo.itemCount;
}
(3)在InventoryManager中 添加 Isavable接口 并且实现
public void SaveData(Data data)
{
//Save所有的背包
data.consumableInventory.SaveInventory(consumableInventory);
data.equipmentsInventory.SaveInventory(equipmentsInventory);
data.playerEquipmentInventory.SaveInventory(playerEquipmentInventory);
data.actionInventory.SaveInventory(actionInventory);
}
public void LoadData(Data data)
{
//Load所有的背包
consumableInventory.LoadInventory(data.consumableInventory);
equipmentsInventory.LoadInventory(data.equipmentsInventory);
playerEquipmentInventory.LoadInventory(data.playerEquipmentInventory);
actionInventory.LoadInventory(data.actionInventory);
//刷新所有的背包
RefreshAllContainer();
}
对于一些无法被 Newtonsort.Json 序列化的类型解决方法举例:
补充:其实都可以使用JsonUtility作为中间层工具。
1:Vector3:创建一个 SerializeVector3 实现这两个类之间的相互转换
public class SerializeVector3
{
public float x, y, z;
public SerializeVector3(Vector3 pos)
{
this.x = pos.x;
this.y = pos.y;
this.z = pos.z;
}
public Vector3 ToVector3()
{
return new Vector3(x, y, z);
}
}
2:Sprite:记录资源的地址 使用动态资源加载方式(如Resource 或 AssetReference)
public class SerializeSprite
{
public string spriteAddress;
}