此文章已废弃,存档系统已经做了很多修改。
源码
首先,先上源码,解释项目结构,后面再讲每个类、结构体和函数的作用
源代码分两个类: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
在讲解这个字典前,我们需要先理清一下思路:
首先,我们使用
查找场景内,所有实例中,带有继承了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和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的存档
发表评论