此文章已废弃,存档系统已经做了很多修改。
源码
首先,先上源码,解释项目结构,后面再讲每个类、结构体和函数的作用
源代码分两个类:ArchiveManager.cs和ArchiveSO.cs
ArchiveManager.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
public class ArchiveManager : MonoBehaviour
{
#region 成员、属性
#region 静态
private static ArchiveManager instance;
public static ArchiveManager Instance { get => instance; }
public static ArchiveData CurrentArchive = ArchiveData.None;
public static Scene CurrentScene;
public ArchiveData SceneBuffer = new ArchiveData("Buffer","Buffer");
#endregion
#region 非静态
[SerializeField]
private ArchiveSO ArchiveSoData;
#endregion
#endregion
#region 生命周期
private void Awake()
{
InstanceInit();
}
private void OnEnable()
{
SceneManager.sceneUnloaded += SceneUnloaded;
SceneManager.sceneLoaded += SceneLoaded;
}
private void OnDisable()
{
SceneManager.sceneUnloaded -= SceneUnloaded;
SceneManager.sceneLoaded -= SceneLoaded;
}
#endregion
#region 单例
/// <summary>
/// 实现单例
/// </summary>
private void InstanceInit()
{
if (instance != null)
{
Destroy(this);
}
instance = this;
DontDestroyOnLoad(this);
}
#endregion
/// <summary>
/// 卸载场景前,对场景所有物体进行遍历,将其值存到缓存结构体中
/// </summary>
/// <param name="scene"></param>
private void SceneUnloaded(Scene scene)
{
//TODO:装载场景里所有GameObject到缓存池中
var objs = GameObject.FindObjectsOfType<MonoBehaviour>().OfType<IArchive>();
SceneData sceneData = new SceneData(scene.name);
foreach (var archive in objs)
{
long id = archive.GetComponent().GetOnlyID();
string value = archive.Save();
sceneData.SaveValue(id,value);
}
SceneBuffer.SaveValue(scene,sceneData);
}
/// <summary>
/// 场景加载后,对场景所有物体进行遍历,调用接口赋值
/// </summary>
/// <param name="scene"></param>
/// <param name="mode"></param>
private void SceneLoaded(Scene scene, LoadSceneMode mode)
{
//TODO:加载数据到场景中
if(CurrentArchive == ArchiveData.None)
return;
var sceneData = CurrentArchive.LoadValue(scene);
if(sceneData == SceneData.None)
return;
var objs = GameObject.FindObjectsOfType<MonoBehaviour>().OfType<IArchive>();
foreach (var archive in objs)
{
long key = archive.GetComponent().GetOnlyID();
var value = sceneData.LoadValue(key);
archive.Load(value);
}
CurrentScene = scene;
}
#region 函数
/// <summary>
/// 返回给UI去查看有哪些存档
/// </summary>
/// <returns></returns>
public List<ArchiveData> GetArchiveList()
{
return ArchiveSoData.Archives;
}
public void LoadInIndex(int index)
{
if (index < 0 || index >= ArchiveSoData.Archives.Count)
{
Debug.LogError("下标不在范围内");
return;
}
CurrentArchive = ArchiveSoData.Archives[index];
CurrentArchive.ArchiveValue = CurrentArchive.FromJSON();
}
/// <summary>
/// 保存为新存档
/// </summary>
public void SaveAsNew()
{
ArchiveSoData.Archives.Add(Save());
}
/// <summary>
/// 替换存档
/// </summary>
/// <param name="index"></param>
public void SaveInIndex(int index)
{
if(index < 0 || index >= ArchiveSoData.Archives.Count)
return;
ArchiveSoData.Archives[index] = Save();
}
/// <summary>
/// 创建新档
/// </summary>
public void CreatAsNew()
{
CurrentArchive = ArchiveData.None;
}
private ArchiveData Save()
{
var data = new ArchiveData(String.Empty,SceneManager.GetActiveScene().name);
//更新当前场景的数据到缓存中
SceneUnloaded(SceneManager.GetActiveScene());
data.ArchiveValue = SceneBuffer.ArchiveValue;
data.ArchiveValueJSON = data.ToJSON();
return data;
}
#endregion
}
/// <summary>
/// 单个场景存储的信息
/// </summary>
[Serializable]
public struct SceneData
{
public static SceneData None = new SceneData() { SceneName = null };
/// <summary>
/// 场景名称
/// </summary>
public string SceneName;
/// <summary>
/// 储存的数据
/// </summary>
public Dictionary<long, string> sceneValue;
public SceneData(string sceneName)
{
this.SceneName = sceneName;
sceneValue = new Dictionary<long, string>();
}
public static bool operator ==(SceneData data1, SceneData data2)
{
if (data1.SceneName == data2.SceneName)
return true;
return false;
}
public static bool operator !=(SceneData data1, SceneData data2)
{
if (data1.SceneName == data2.SceneName)
return false;
return true;
}
}
/// <summary>
/// 单个存档的信息
/// </summary>
[Serializable]
public struct ArchiveData
{
public static ArchiveData None = new ArchiveData();
/// <summary>
/// 当前存档名
/// </summary>
public string ArchiveName;
/// <summary>
/// 存档最后一次存储时间
/// </summary>
public string CurrentTime;
/// <summary>
/// 存档所处的场景
/// </summary>
public string CurrentSceneName;
[TextArea]
public string ArchiveValueJSON;
public Dictionary<string, SceneData> ArchiveValue;
public ArchiveData(string archiveName,string sceneName)
{
ArchiveName = archiveName;
CurrentTime = System.DateTime.Now.ToString();
CurrentSceneName = sceneName;
ArchiveValue = new Dictionary<string, SceneData>();
ArchiveValueJSON = String.Empty;
}
public static bool operator ==(ArchiveData data1, ArchiveData data2)
{
if (data1.ArchiveName == data2.ArchiveName && data1.ArchiveValue == data2.ArchiveValue)
return true;
return false;
}
public static bool operator !=(ArchiveData data1, ArchiveData data2)
{
if (data1.ArchiveName == data2.ArchiveName && data1.ArchiveValue == data2.ArchiveValue)
return false;
return true;
}
}
public static class SceneDataExtension
{
public static void SaveValue(this SceneData data, Component component, string jsonValue)
{
long onlyID = component.GetOnlyID();
data.SaveValue(onlyID,jsonValue);
}
public static void SaveValue(this SceneData data, long id, string jsonValue)
{
if (data.sceneValue.ContainsKey(id))
data.sceneValue[id] = jsonValue;
else
data.sceneValue.Add(id,jsonValue);
}
public static string LoadValue(this SceneData data, Component component)
{
long id = component.GetOnlyID();
return data.LoadValue(id);
}
public static string LoadValue(this SceneData data, long id)
{
if(!data.sceneValue.ContainsKey(id))
return String.Empty;
return data.sceneValue[id];
}
}
public static class ArchiveDataExtension
{
public static void SaveValue(this ArchiveData data,string sceneName,SceneData sceneData)
{
if (data.ArchiveValue.ContainsKey(sceneName))
data.ArchiveValue[sceneName] = sceneData;
else
data.ArchiveValue.Add(sceneName,sceneData);
}
public static void SaveValue(this ArchiveData data, Scene scene, SceneData sceneData)
{
data.SaveValue(scene.name,sceneData);
}
public static SceneData LoadValue(this ArchiveData data,string sceneName)
{
if(!data.ArchiveValue.ContainsKey(sceneName))
return SceneData.None;
return data.ArchiveValue[sceneName];
}
public static SceneData LoadValue(this ArchiveData data, Scene scene)
{
return data.LoadValue(scene.name);
}
public static string ToJSON(this ArchiveData data)
{
string value = JsonConvert.SerializeObject(data.ArchiveValue);
return value;
}
public static Dictionary<string, SceneData> FromJSON(this ArchiveData data)
{
if(data.ArchiveValueJSON == String.Empty)
return null;
Dictionary<string, SceneData> datas =
JsonConvert.DeserializeObject<Dictionary<string, SceneData>>(data.ArchiveValueJSON);
return datas;
}
}
public static class ComponentExtension
{
public static long GetOnlyID(this Component component)
{
long onlyID = component.GetInstanceID() - component.gameObject.GetInstanceID();
string s = onlyID.ToString() + component.GetLocalID();
onlyID = Convert.ToInt64(s);
return onlyID;
}
public static int GetLocalID(this Component component)
{
PropertyInfo info = typeof(SerializedObject).GetProperty("inspectorMode", BindingFlags.NonPublic | BindingFlags.Instance);
SerializedObject sObj = new SerializedObject(component);
info.SetValue(sObj, InspectorMode.Debug, null);
SerializedProperty localIdProp = sObj.FindProperty("m_LocalIdentfierInFile");
return localIdProp.intValue;
}
}
public interface IArchive
{
public void Load(string jsonValue);
public string Save();
public Component GetComponent();
}
此文件包含
一个单例:ArchiveManager,
两个结构体定义:ArchiveData、SceneData
一个接口:IArchive
三个拓展类:SceneDataExtension、ArchiveDataExtension、ComponentExtension
ArchiveSO.cs
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "ArchiveSO", menuName = "Data/ArchiveSO")]
public class ArchiveSO : ScriptableObject
{
public List<ArchiveData> Archives = new List<ArchiveData>();
}
此文件包含
一个继承SciptableObject的类:ArchiveSO
存储原理
该系统本着可拓展,简易,低耦合的理念(实际上是我不会)
其核心存储介质其实就是ScriptableObject,数据类型是Json。
本来是打算用PlayerPrefs做存储介质,但项目开工前做技术调查发现,好像PlayerPrefs的SetString有点问题,且加上这个类是静态类不方便我管理和查看(可视化),所以最终使用ScriptableObject作为存储介质。
数据架构
从上往下
代码解释
SceneData(struct)
[Serializable]
public struct SceneData
{
public static SceneData None = new SceneData() { SceneName = null };
/// <summary>
/// 场景名称
/// </summary>
public string SceneName;
/// <summary>
/// 储存的数据
/// </summary>
public Dictionary<long, string> sceneValue;
public SceneData(string sceneName)
{
this.SceneName = sceneName;
sceneValue = new Dictionary<long, string>();
}
public static bool operator ==(SceneData data1, SceneData data2)
{
if (data1.SceneName == data2.SceneName)
return true;
return false;
}
public static bool operator !=(SceneData data1, SceneData data2)
{
if (data1.SceneName == data2.SceneName)
return false;
return true;
}
}
SceneData结构体主要用于存储单个场景里需要保存的信息,其主要由一个SceneName(string)记录场景名,和一个sceneValue(Dictionary<long, string>)字典,主要讲解一下这个字典存的什么东西。
public Dictionary<long, string> sceneValue
在讲解这个字典前,我们需要先理清一下思路:
首先,我们使用
GameObject.FindObjectsOfType<MonoBehaviour>().OfType<IArchive>();
查找场景内,所有实例中,带有继承了IArchive接口的组件的实例。
然后调用其接口,把每一个实例需要保存的JSON数据存在当前场景的SceneData的字典中。
那么问题来了,我们应该如何设置字典的key值,以达到每一个实例的每一个脚本(Component)都独一无二,能精准在字典里获取其独有的数据。
可能稍微了解过一点GameObject类的读者会想到,使用gameObject.Getinstance()作为字典的key值。
确实,在Unity编辑器的Inspector的Debug模式下,我们可以看到,一个GameObject的每个组件的InstanceID都是互不相同的。但我们需要注意一个点,我们这里的key,需要保证在整个项目里独一无二。但我们可以看官方API文档解释
这一句:The instance ID of an object is always unique.
翻译中文:对象的实例 ID 始终是唯一的。
只是看这句话,很多人可能都会和我一样被这句话误导,会以为每个对象的InstanceID在这个对象被创立的时候就固定不变了。
实际上不是的,如果我们单只开一个场景查看,不管怎么重启项目,移动实例,确实它的InstanceID始终是不变的,但如果我们尝试使用SceneManagment卸载加载场景,我们会发现它的InstanceID发生了改变。
如果读者愿意再去查查,会发现Unity的实例的InstanceID并不是固定存在,而是通过两个值计算而来:GUID、LocalID。
GUID读者可以自行百度,该系统没有使用到该属性就不做讲解。
如果我们再仔细查看,我们可以发现
组件属性里有一个Local Identfier In File的属性,这个属性值无论怎么切换场景,重启项目,值都是不变的,根据官方说法,这个值是存在Asset下,至于具体怎么存的我们先不管。 然后就是我们可以看到这个值在整个GameObject中所有组件包括GameObject都是一样的,所以可以知道这个是指的GameObject实例的localID。
这个ID的获取,Unity并没有提供方法,我们需要使用C#的反射来获取
public static int GetLocalID(this Component component)
{
PropertyInfo info = typeof(SerializedObject).GetProperty("inspectorMode", BindingFlags.NonPublic | BindingFlags.Instance);
SerializedObject sObj = new SerializedObject(component);
info.SetValue(sObj, InspectorMode.Debug, null);
SerializedProperty localIdProp = sObj.FindProperty("m_LocalIdentfierInFile");
return localIdProp.intValue;
}
如此,我们就获取了GameObject实例的唯一ID。
接下来,我们需要解决的是,单个实例下,多个相同组件的唯一问题。
其实解决方法很简单,组件Component有自己的InstanceID,GameObject也有自己的InstanceID,这两者并不相同,而且通过实验,组件的id和组件附属的GameObject的id之间的差值,其实是固定的,也就是:
long onlyID = component.GetInstanceID() - component.gameObject.GetInstanceID();
这里使用long数据类型来储存,为后面的处理做准备。
然后将onlyID和LocalID组合一下,就得到该组件的唯一ID;
拓展方法如下
public static class ComponentExtension
{
public static long GetOnlyID(this Component component)
{
long onlyID = component.GetInstanceID() - component.gameObject.GetInstanceID();
string s = onlyID.ToString() + component.GetLocalID();
onlyID = Convert.ToInt64(s);
return onlyID;
}
public static int GetLocalID(this Component component)
{
PropertyInfo info = typeof(SerializedObject).GetProperty("inspectorMode", BindingFlags.NonPublic | BindingFlags.Instance);
SerializedObject sObj = new SerializedObject(component);
info.SetValue(sObj, InspectorMode.Debug, null);
SerializedProperty localIdProp = sObj.FindProperty("m_LocalIdentfierInFile");
return localIdProp.intValue;
}
}
综上,sceneValue的key就可以确定好了。
然后就是value,文章开头已经解释了,该存档系统的存储数据格式就是JSON字符串,所以这里的value也就是字符串,不过是序列化成json格式的字符串。
SceneData函数拓展
public static class SceneDataExtension
{
public static void SaveValue(this SceneData data, Component component, string jsonValue)
{
long onlyID = component.GetOnlyID();
data.SaveValue(onlyID,jsonValue);
}
public static void SaveValue(this SceneData data, long id, string jsonValue)
{
if (data.sceneValue.ContainsKey(id))
data.sceneValue[id] = jsonValue;
else
data.sceneValue.Add(id,jsonValue);
}
public static string LoadValue(this SceneData data, Component component)
{
long id = component.GetOnlyID();
return data.LoadValue(id);
}
public static string LoadValue(this SceneData data, long id)
{
if(!data.sceneValue.ContainsKey(id))
return String.Empty;
return data.sceneValue[id];
}
}
ArchiveData(Struct)
[Serializable]
public struct ArchiveData
{
public static ArchiveData None = new ArchiveData();
/// <summary>
/// 当前存档名
/// </summary>
public string ArchiveName;
/// <summary>
/// 存档最后一次存储时间
/// </summary>
public string CurrentTime;
/// <summary>
/// 存档所处的场景
/// </summary>
public string CurrentSceneName;
[TextArea]
public string ArchiveValueJSON;
public Dictionary<string, SceneData> ArchiveValue;
public ArchiveData(string archiveName,string sceneName)
{
ArchiveName = archiveName;
CurrentTime = System.DateTime.Now.ToString();
CurrentSceneName = sceneName;
ArchiveValue = new Dictionary<string, SceneData>();
ArchiveValueJSON = String.Empty;
}
public static bool operator ==(ArchiveData data1, ArchiveData data2)
{
if (data1.ArchiveName == data2.ArchiveName && data1.ArchiveValue == data2.ArchiveValue)
return true;
return false;
}
public static bool operator !=(ArchiveData data1, ArchiveData data2)
{
if (data1.ArchiveName == data2.ArchiveName && data1.ArchiveValue == data2.ArchiveValue)
return false;
return true;
}
}
ArchiveData结构体是单个存档的数据结构,内容和SceneData相似。
存档名称、存档最后一次存储时间、存档最后一次存储时的场景名称三个属性就不一一描述了,都是string类型,最后一个场景名称就是Scene.name,主要方便告诉场景管理器这个存档最后一次存储时所在的场景,如果游戏有需要,可以选择加载存档时加载到该场景。
主要讲解内容是
ArchiveValueJSON(string)
ArchiveValue(Dictionary<string, SceneData>)
可能会有读者会有疑惑,ArchiveValueJSON的作用是什么,不是已经有一个字典存数据了嘛。
这里就涉及到一个Unity理论知识,根据官方定义,Unity的ScriptableObject只能存储可序列化的数据,也就是那个标签[Serializable]。
熟悉Unity序列化的读者都知道,Unity的字典是没有被序列化的,可能会有人想到Odin的对字典序列化。
但我这里为了降低耦合度,非必要我是不会使用非官方插件,而且Odin的序列化只在Odin插件中有效,如果要存储到ScriptableObject中,可能需要重写一些东西(没仔细研究过Odin)。
所以,字典的数据最终是不会被存储到ScriptableObject中,所以这里我们使用Newtonsoft.Json库的Json序列化字典为json格式字符串并储存,读取时再取出来反序列化为字典。
ArchiveData函数拓展
public static class ArchiveDataExtension
{
public static void SaveValue(this ArchiveData data,string sceneName,SceneData sceneData)
{
if (data.ArchiveValue.ContainsKey(sceneName))
data.ArchiveValue[sceneName] = sceneData;
else
data.ArchiveValue.Add(sceneName,sceneData);
}
public static void SaveValue(this ArchiveData data, Scene scene, SceneData sceneData)
{
data.SaveValue(scene.name,sceneData);
}
public static SceneData LoadValue(this ArchiveData data,string sceneName)
{
if(!data.ArchiveValue.ContainsKey(sceneName))
return SceneData.None;
return data.ArchiveValue[sceneName];
}
public static SceneData LoadValue(this ArchiveData data, Scene scene)
{
return data.LoadValue(scene.name);
}
public static string ToJSON(this ArchiveData data)
{
string value = JsonConvert.SerializeObject(data.ArchiveValue);
return value;
}
public static Dictionary<string, SceneData> FromJSON(this ArchiveData data)
{
if(data.ArchiveValueJSON == String.Empty)
return null;
Dictionary<string, SceneData> datas =
JsonConvert.DeserializeObject<Dictionary<string, SceneData>>(data.ArchiveValueJSON);
return datas;
}
}
ArchiveSO(ScriptableObject)
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "ArchiveSO", menuName = "Data/ArchiveSO")]
public class ArchiveSO : ScriptableObject
{
public List<ArchiveData> Archives = new List<ArchiveData>();
}
这个就没必要多解释,一个list列表。
ArchiveManager(MonoBehaviour)
public class ArchiveManager : MonoBehaviour
{
// public ButtonFunction 保存存档;
#region 成员、属性
#region 静态
private static ArchiveManager instance;
public static ArchiveManager Instance { get => instance; }
public static ArchiveData CurrentArchive = ArchiveData.None;
public static Scene CurrentScene;
public ArchiveData SceneBuffer = new ArchiveData("Buffer","Buffer");
#endregion
#region 非静态
[SerializeField]
private ArchiveSO ArchiveSoData;
#endregion
#endregion
#region 生命周期
private void Awake()
{
InstanceInit();
// 保存存档 = new ButtonFunction(this,"保存新档", SaveAsNew);
}
private void OnEnable()
{
SceneManager.sceneUnloaded += SceneUnloaded;
SceneManager.sceneLoaded += SceneLoaded;
}
private void OnDisable()
{
SceneManager.sceneUnloaded -= SceneUnloaded;
SceneManager.sceneLoaded -= SceneLoaded;
}
#endregion
#region 单例
/// <summary>
/// 实现单例
/// </summary>
private void InstanceInit()
{
if (instance != null)
{
Destroy(this);
}
instance = this;
DontDestroyOnLoad(this);
}
#endregion
/// <summary>
/// 卸载场景前,对场景所有物体进行遍历,将其值存到缓存结构体中
/// </summary>
/// <param name="scene"></param>
private void SceneUnloaded(Scene scene)
{
//TODO:装载场景里所有GameObject到缓存池中
var objs = GameObject.FindObjectsOfType<MonoBehaviour>().OfType<IArchive>();
SceneData sceneData = new SceneData(scene.name);
foreach (var archive in objs)
{
long id = archive.GetComponent().GetOnlyID();
string value = archive.Save();
sceneData.SaveValue(id,value);
}
SceneBuffer.SaveValue(scene,sceneData);
}
/// <summary>
/// 场景加载后,对场景所有物体进行遍历,调用接口赋值
/// </summary>
/// <param name="scene"></param>
/// <param name="mode"></param>
private void SceneLoaded(Scene scene, LoadSceneMode mode)
{
//TODO:加载数据到场景中
if(CurrentArchive == ArchiveData.None)
return;
var sceneData = CurrentArchive.LoadValue(scene);
if(sceneData == SceneData.None)
return;
var objs = GameObject.FindObjectsOfType<MonoBehaviour>().OfType<IArchive>();
foreach (var archive in objs)
{
long key = archive.GetComponent().GetOnlyID();
var value = sceneData.LoadValue(key);
archive.Load(value);
}
CurrentScene = scene;
}
#region 函数
/// <summary>
/// 返回给UI去查看有哪些存档
/// </summary>
/// <returns></returns>
public List<ArchiveData> GetArchiveList()
{
return ArchiveSoData.Archives;
}
/// <summary>
/// 保存为新存档
/// </summary>
public void SaveAsNew()
{
ArchiveSoData.Archives.Add(Save());
}
/// <summary>
/// 替换存档
/// </summary>
/// <param name="index"></param>
public void SaveInIndex(int index)
{
if(index < 0 || index >= ArchiveSoData.Archives.Count)
return;
ArchiveSoData.Archives[index] = Save();
}
/// <summary>
/// 创建新档
/// </summary>
public void LoadAsNew()
{
CurrentArchive = ArchiveData.None;
}
public void LoadInIndex(int index)
{
if (index < 0 || index >= ArchiveSoData.Archives.Count)
{
Debug.LogError("下标不在范围内");
return;
}
CurrentArchive = ArchiveSoData.Archives[index];
CurrentArchive.ArchiveValue = CurrentArchive.FromJSON();
}
private ArchiveData Save()
{
var data = new ArchiveData(String.Empty,SceneManager.GetActiveScene().name);
//更新当前场景的数据到缓存中
SceneUnloaded(SceneManager.GetActiveScene());
data.ArchiveValue = SceneBuffer.ArchiveValue;
data.ArchiveValueJSON = data.ToJSON();
return data;
}
#endregion
}
ArchiveManager类作为管理类,其使用单例模式保证项目里只会有一个实例。
我们先讲该类的运行逻辑再根据逻辑衍生去讲属性和函数的意义。
程序逻辑
当我们新建存档后,就加载进游戏场景,然后根据游戏需求,可能会存在卸载场景,加载场景的步骤。根据官方api文档介绍,SceneManager.sceneUnloaded
SceneManager.sceneLoaded
两个事件分别对应场景卸载前和场景加载后(?加载后还有待商议,为进行大场景测试)。
根据我们的框架逻辑:场景卸载前遍历场景实例储存数据到临时的ArchiveData容器,场景加载后遍历场景实例加载当前存档数据到实例中。
于是我们就可以订阅这两个事件,让框架自动在切换场景时存储和加载数据。
PS,笔者能力、时间有限,为进行大场景测试,所有这里笔者推荐大家不要使用框架的自动读取加载,而是自己手动控制流程
如
然后介绍一下几个公共函数接口
GetArchiveList :返回存档的列表
SaveAsNew :保存为新建存档
SaveInIndex(int index):替换存档列表里下标为index的存档为当前存档。
LoadAsNew:新建存档时调用此函数,重置存档管理器的临时存档。
LoadInIndex(int index):加载下标index的存档