[Unity] detailed analysis of actual combat of action game development-11-serialization of levels

[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);
    }
  }
}

Tags: C# Unity

Posted by MicahCarrick on Sun, 17 Jul 2022 10:30:47 +0930