[Unity] detailed analysis of actual combat of action game development-11-serialization of levels
basic thought
Serializing the level content is a very necessary operation.
Checkpoint reset, archiving and other functions of the level depend on the serialization interface.
In the game, the serialization and deserialization interfaces of the level module can be actively called through the external module to complete the archiving and file reading functions of the level.
The main problem of level serialization is how to control the serialization order of different components in the scene, because some game objects will be constantly deleted in the editor, while others will be dynamically created.
code implementation
The solution is to assign a GUID to each component. When deserializing, you can find the components existing in the scene according to the GUID. All level components need to implement the IMissionArchiveItem interface, which provides basic serialization and deserialization event functions.
For a level, deserialization is actually the initialization of the status of the role entering the level
public interface IMissionArchiveItem : IGuidObject { void OnSerialize(BinaryWriter writer);//serialize void OnMissionArchiveInitialization(BinaryReader reader, bool hasSerializeData);//Level initialization }
The second is the most basic interface, which has only one Guid attribute
public interface IGuidObject { long Guid { get; } }
For convenience of understanding, we will next introduce GuidObject
First, it has a guid field
We can initialize the guid through the function provided by C# itself. And monitor the modification of the component through the function OnValidate. When the component is modified, its guid will be automatically modified. If you don't want to modify it, you can also lock the value through the lockedGuid variable.
public class GuidObject : MonoBehaviour, IGuidObject { static long mRuntimeGuidCounter = long.MinValue;//GUID count variable for dynamic object public long guid; #if UNITY_EDITOR public bool lockedGuid;//Lock GUID value #endif long IGuidObject.Guid { get { return guid; } }//Interface implementation public void ArrangeRuntimeGuid()//Dynamic object initialization GUID { mRuntimeGuidCounter++; guid = mRuntimeGuidCounter; } #if UNITY_EDITOR protected virtual void OnValidate() { if (!lockedGuid) guid = CreateLongGUID(); } #endif long CreateLongGUID() { var buffer = System.Guid.NewGuid().ToByteArray();//Create GUID byte array return System.BitConverter.ToInt64(buffer, 0);//Convert to long type } }
Then there is the component management script
Note:
- ?? Is an empty merge operator. If the left is empty, it returns the right; If left is not empty, return to left
- using statement is used to automatically close the file stream after ending the code block
The script system provides the most basic method
First, it is a singleton, and it has a set field for storing all components
Provide external call interface,
- Registration and de registration
- Level initialization
- Read file / return to checkpoint
- Archive (level serialization)
- He will temporarily create a memory stream for each registered component and write it into the byte array to integrate it into the external stream
For the convenience of demonstration, the data is not stored in a file, but in a stream. Therefore, we only need to define an external stream, which will serialize the data and store it in the stream. We only need to store the bytes in the stream by ourselves.
public class MissionArchiveManager { static MissionArchiveManager mInstance;//Non mono singleton is used here 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);//Register components } public void UnregistMissionArchiveItem(IMissionArchiveItem archiveItem) { mMissionArchiveItemList.Remove(archiveItem);//Unregister component } public void MissionInitialization()//This initialization is called by the normal entry level { for (int i = 0, iMax = mMissionArchiveItemList.Count; i < iMax; i++) { var item = mMissionArchiveItemList[i]; item.OnMissionArchiveInitialization(null, false); } } public void MissionInitialization(Stream stream)//File reading or checkpoint calls this initialization { using (var binaryReader = new BinaryReader(stream)) { var serializeCount = binaryReader.ReadInt32();//Get the number of components before for (int i = 0; i < serializeCount; i++) { var guid = binaryReader.ReadInt64();//Read ID var bytes_length = binaryReader.ReadInt32(); var bytes = binaryReader.ReadBytes(bytes_length);//Read bytes for (int archiveIndex = 0, archiveIndex_Max = mMissionArchiveItemList.Count; i < archiveIndex_Max; i++) { var item = mMissionArchiveItemList[archiveIndex]; if (item.Guid != guid) continue;//Jump out if it doesn't match using (var archiveItemStream = new MemoryStream(bytes)) using (var archiveItemStreamReader = new BinaryReader(archiveItemStream)) item.OnMissionArchiveInitialization(archiveItemStreamReader, true);//Deserialization operation } } } } public void MissionSerialize(Stream stream)//Level serialization { using (var binaryWriter = new BinaryWriter(stream)) { binaryWriter.Write(mMissionArchiveItemList.Count);//Current number of components for (int i = 0, iMax = mMissionArchiveItemList.Count; i < iMax; i++) { var item = mMissionArchiveItemList[i]; using (var archiveItemStream = new MemoryStream())//Memory flow of components { using (var archiveItemStreamWriter = new BinaryWriter(archiveItemStream)) { item.OnSerialize(archiveItemStreamWriter);//Serialize events var bytes = archiveItemStream.ToArray(); binaryWriter.Write(item.Guid);//Write ID binaryWriter.Write(bytes.Length); binaryWriter.Write(bytes);//Write Bytes } } } } } }
Generally speaking, a component will register events in wake and de register events in Destroy. However, some components may be hidden by default and will not start the wake function. Therefore, we also need a collector to solve this problem
This is an automatically initialized class, which is used to bind the scene loading callback function, and automatically complete the acquisition of all components when the scene is loaded
[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); } } } }
This is the collector class. With it, we only need to place a collector in the required scene to automatically collect all components
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); } } }