【Unity】动作游戏开发实战详细分析-11-关卡的序列化
基本思想
对关卡内容进行序列化是一项十分有必要的操作。
关卡的检查点重置,存档等功能都依赖于序列化接口。
在游戏中可通过外部模块主动调用关卡模块序列化、反序列化接口,以完成关卡部分存档、读档功能的实现。
关卡序列化的主要问题是如何控制场景内不同组件的序列化顺序,因为有些游戏对象会在编辑器内被不断的删改,而另一些游戏对象则被动态创建。
代码实现
解决方案是,为每个组件分配一个GUID,在反序列化时可按照GUID来查找场景内存在的组件,所有的关卡组件需要实现IMissionArchiveItem
接口,它提供了基础的序列化与反序列化事件函数。
对于关卡来说,反序列化实际上就是角色进入关卡的状态初始化
public interface IMissionArchiveItem : IGuidObject
{
void OnSerialize(BinaryWriter writer);//序列化
void OnMissionArchiveInitialization(BinaryReader reader, bool hasSerializeData);//关卡初始化
}
其次就是最基本的接口,这个接口只有一个Guid属性
public interface IGuidObject
{
long Guid { get; }
}
为了方便理解,我们接下来介绍GuidObject
首先,它具有guid字段
我们可以通过C#自带的函数来初始化guid。并且通过函数OnValidate对组件的修改进行监听,当组件发生修改时,也自动修改他的guid,如果不想修改,也可以通过lockedGuid变量来锁定该值。
public class GuidObject : MonoBehaviour, IGuidObject
{
static long mRuntimeGuidCounter = long.MinValue;//动态对象的GUID计数变量
public long guid;
#if UNITY_EDITOR
public bool lockedGuid;//锁定GUID值
#endif
long IGuidObject.Guid { get { return guid; } }//接口实现
public void ArrangeRuntimeGuid()//动态对象初始化GUID
{
mRuntimeGuidCounter++;
guid = mRuntimeGuidCounter;
}
#if UNITY_EDITOR
protected virtual void OnValidate()
{
if (!lockedGuid)
guid = CreateLongGUID();
}
#endif
long CreateLongGUID()
{
var buffer = System.Guid.NewGuid().ToByteArray();//创建GUID字节数组
return System.BitConverter.ToInt64(buffer, 0);//转换为long类型
}
}
然后是组件管理脚本
注:
- ??为空合并运算符,如果左为空,则返回右;左不为空,则返回左
- using语句用于结束代码块后自动关闭文件流
该脚本系统提供了最基本的方法
首先它是一个单例,并且它具有一个集合字段,用于存储所有的组件
提供外部调用接口,
- 注册与反注册
- 关卡初始化
- 读档/回到检查点
- 存档(关卡序列化)
- 他会为每个注册的组件临时创建一个内存流,并将其写入字节数组整合到外部流中
这里为了方便演示,并没有将数据存储在文件中,而是存储在流中,因此,我们只需要定义外部流,他会将数据序列化后存储在流中,我们只需要将流中的字节自行存储即可。
public class MissionArchiveManager
{
static MissionArchiveManager mInstance;//这里使用非mono单例
public static MissionArchiveManager Instance { get { return mInstance ?? (mInstance = new MissionArchiveManager()); } }
List<IMissionArchiveItem> mMissionArchiveItemList;
public MissionArchiveManager()
{
mMissionArchiveItemList = new List<IMissionArchiveItem>();
}
public void RegistMissionArchiveItem(IMissionArchiveItem archiveItem)
{
mMissionArchiveItemList.Add(archiveItem);//注册组件
}
public void UnregistMissionArchiveItem(IMissionArchiveItem archiveItem)
{
mMissionArchiveItemList.Remove(archiveItem);//反注册组件
}
public void MissionInitialization()//常规进入关卡调用此初始化
{
for (int i = 0, iMax = mMissionArchiveItemList.Count; i < iMax; i++)
{
var item = mMissionArchiveItemList[i];
item.OnMissionArchiveInitialization(null, false);
}
}
public void MissionInitialization(Stream stream)//读档或检查点调用此初始化
{
using (var binaryReader = new BinaryReader(stream))
{
var serializeCount = binaryReader.ReadInt32();//获取之前组件数量
for (int i = 0; i < serializeCount; i++)
{
var guid = binaryReader.ReadInt64();//读到ID
var bytes_length = binaryReader.ReadInt32();
var bytes = binaryReader.ReadBytes(bytes_length);//读到字节
for (int archiveIndex = 0, archiveIndex_Max = mMissionArchiveItemList.Count; i < archiveIndex_Max; i++)
{
var item = mMissionArchiveItemList[archiveIndex];
if (item.Guid != guid) continue;//不匹配则跳出
using (var archiveItemStream = new MemoryStream(bytes))
using (var archiveItemStreamReader = new BinaryReader(archiveItemStream))
item.OnMissionArchiveInitialization(archiveItemStreamReader, true);//反序列化操作
}
}
}
}
public void MissionSerialize(Stream stream)//关卡序列化
{
using (var binaryWriter = new BinaryWriter(stream))
{
binaryWriter.Write(mMissionArchiveItemList.Count);//当前组件数
for (int i = 0, iMax = mMissionArchiveItemList.Count; i < iMax; i++)
{
var item = mMissionArchiveItemList[i];
using (var archiveItemStream = new MemoryStream())//组件的内存流
{
using (var archiveItemStreamWriter = new BinaryWriter(archiveItemStream))
{
item.OnSerialize(archiveItemStreamWriter);//序列化事件
var bytes = archiveItemStream.ToArray();
binaryWriter.Write(item.Guid);//写入ID
binaryWriter.Write(bytes.Length);
binaryWriter.Write(bytes);//写入字节
}
}
}
}
}
}
通常来说一个组件会在Awake中注册事件,并在Destroy中反注册事件。但是有一些组件在默认情况下可能是隐藏的,不会出发Awake函数,因此,我们还需要一个收集器解决该问题
这是一个自动初始化的类,用于绑定场景加载回调函数,并在场景加载时,自动完成所有组件的获取
[UnityEditor.InitializeOnLoad]
public class MissionArchiveCollector_Initialization
{
static MissionArchiveCollector_Initialization()
{
UnityEditor.SceneManagement.EditorSceneManager.sceneSaving += SceneSavingCallBack;
}
public static void SceneSavingCallBack(Scene scene,string scenePath)
{
var rootGameObjects = scene.GetRootGameObjects();
MissionArchiveCollector archiveCollector = null;
foreach (var m in rootGameObjects)
{
var component = m.GetComponentInChildren<MissionArchiveCollector>();
if (component != null)
{
archiveCollector = component;
}
}
if (archiveCollector == null) return;
List<IMissionArchiveItem> missionArchiveItems=new List<IMissionArchiveItem>();
foreach (var m in rootGameObjects)
{
var component = m.GetComponentsInChildren<IMissionArchiveItem>();
if (component != null)
{
missionArchiveItems.AddRange(component);
}
}
var archiveItemArray = missionArchiveItems.ToArray();
for(int i = 0; i < archiveItemArray.Length; i++)
{
var currentArchiveItem = archiveItemArray[i];
var currentArchiveItemMono = currentArchiveItem as MonoBehaviour;
if (!archiveCollector.missionArchiveItemsList.Contains(currentArchiveItemMono))
{
archiveCollector.missionArchiveItemsList.Add(currentArchiveItemMono);
}
}
}
}
这是收集器类,有了它,我们只需要在需要的场景中放置一个收集器就能够自动收集所有的组件
public class MissionArchiveCollector : MonoBehaviour
{
public List<MonoBehaviour> missionArchiveItemsList = new List<MonoBehaviour>();
private void Awake()
{
for (int i = 0, iMax = missionArchiveItemsList.Count; i < iMax; i++)
{
var item = missionArchiveItemsList[i] as IMissionArchiveItem;
MissionArchiveManager.Instance.RegistMissionArchiveItem(item);
}
}
private void OnDestroy()
{
for (int i = 0, iMax = missionArchiveItemsList.Count; i < iMax; i++)
{
var item = missionArchiveItemsList[i] as IMissionArchiveItem;
MissionArchiveManager.Instance.UnregistMissionArchiveItem(item);
}
}
}