using System; using System.Collections.Generic; using UnityEngine; #if UNITY_EDITOR #if UNITY_2021_2_OR_NEWER using UnityEditor.SceneManagement; #else using UnityEditor.Experimental.SceneManagement; #endif #endif namespace NeoSaveGames.Serialization { [DisallowMultipleComponent] [HelpURL("https://docs.neofps.com/manual/savegamesref-mb-neoserializedgameobject.html")] public class NeoSerializedGameObject : MonoBehaviour { [SerializeField, Tooltip("Save and reload the object's name. This is only required if the name would change at runtime")] private bool m_SaveName = false; [Header("Transform")] [SerializeField, Tooltip("If and how to serialize the object position")] private TransformSerialization m_Position = TransformSerialization.LocalSpace; [SerializeField, Tooltip("If and how to serialize the object rotation")] private TransformSerialization m_Rotation = TransformSerialization.LocalSpace; [SerializeField, Tooltip("Should the object local scale be serialized")] private bool m_LocalScale = false; [SerializeField, Tooltip("How to filter out child objects. If set to exclude, the objects in the list below will not be serialized. If set to include, only the objects below will be serialized")] private NeoSerializationFilter m_FilterChildObjects = NeoSerializationFilter.Exclude; [SerializeField] private NeoSerializedGameObject[] m_ChildObjects = new NeoSerializedGameObject[0]; [SerializeField, Tooltip("How to filter out serialized components. If set to components, the objects in the list below will not be serialized. If set to include, only the components below will be serialized")] private NeoSerializationFilter m_FilterNeoComponents = NeoSerializationFilter.Exclude; [SerializeField] private MonoBehaviour[] m_NeoComponents = new MonoBehaviour[0]; [SerializeField] private Component[] m_OtherComponents = new Component[0]; [SerializeField] private Override[] m_Overrides = new Override[0]; [SerializeField]//, HideInInspector] private NeoSerializedGameObjectChildContainer m_Children = null; [SerializeField, HideInInspector] private int m_PrefabStrongID = 0; // This is only required when registered for runtime instantiation. Could simplify private static readonly NeoSerializationKey k_NameKey = new NeoSerializationKey("name"); private static readonly NeoSerializationKey k_ActiveKey = new NeoSerializationKey("active"); private static readonly NeoSerializationKey k_EnabledKey = new NeoSerializationKey("enabled"); private static readonly NeoSerializationKey k_PositionKey = new NeoSerializationKey("position"); private static readonly NeoSerializationKey k_RotationKey = new NeoSerializationKey("rotation"); private static readonly NeoSerializationKey k_ScaleKey = new NeoSerializationKey("scale"); private static readonly NeoSerializationKey k_SettingsKey = new NeoSerializationKey("settings"); private static List s_GatheredNeoComponents = new List(16); private int m_SerializationKey = 0; private bool m_WasRuntimeInstantiated = false; private bool m_SaveSettings = false; public event Action onDestroyed; public enum TransformSerialization { LocalSpace, WorldSpace, Ignore } public int serializationKey { get { return m_SerializationKey; } set { if (m_SerializationKey == 0) m_SerializationKey = value; else { if (m_SerializationKey != value) Debug.LogError("Cannot change an objects serialization key once it has been set", gameObject); m_SerializationKey = value; } } } public int prefabStrongID { get { return m_PrefabStrongID; } } public bool wasRuntimeInstantiated { get { return m_WasRuntimeInstantiated; } set { m_WasRuntimeInstantiated = value; } } public bool saveName { get { return m_SaveName; } set { m_SaveName = value; m_SaveSettings = true; } } public NeoSerializationFilter filterChildObjects { get { return m_FilterChildObjects; } set { m_FilterChildObjects = value; m_SaveSettings = true; } } public NeoSerializationFilter filterNeoComponents { get { return m_FilterNeoComponents; } set { m_FilterNeoComponents = value; m_SaveSettings = true; } } public NeoSerializedScene serializedScene { get { return NeoSerializedScene.GetByPath(gameObject.scene.path); } } public NeoSerializedGameObjectChildContainer serializedChildren { get { return m_Children; } } public bool willBeSerialized { get { if (transform.parent == null) return true; if (m_WasRuntimeInstantiated) { var parentNsgo = transform.parent.GetComponent(); if (parentNsgo == null || !parentNsgo.willBeSerialized) return false; } var parent = GetParent(); if (parent == null) return false; if (!parent.willBeSerialized) return false; return parent.WillSerializeChildObject(this); } } bool WillSerializeChildObject(NeoSerializedGameObject child) { bool filtered = Array.IndexOf(m_ChildObjects, child) != -1; if (m_FilterChildObjects == NeoSerializationFilter.Include) return filtered; else return !filtered; } [Serializable] public class Override { [SerializeField, Tooltip("The save mode this override applies to")] private int m_SaveMode = 1; [Header("Transform")] [SerializeField, Tooltip("If and how to serialize the object position")] private OverrideTransformSerialization m_Position = OverrideTransformSerialization.UseDefault; [SerializeField, Tooltip("If and how to serialize the object rotation")] private OverrideTransformSerialization m_Rotation = OverrideTransformSerialization.UseDefault; [SerializeField, Tooltip("Should the object local scale be serialized")] private OverrideScaleSerialization m_LocalScale = OverrideScaleSerialization.UseDefault; [SerializeField, Tooltip("How to filter out child objects. If set to exclude, the objects in the list below will not be serialized. If set to include, only the objects below will be serialized")] private OverrideNeoSerializationFilter m_FilterChildObjects = OverrideNeoSerializationFilter.UseDefault; [SerializeField] private NeoSerializedGameObject[] m_ChildObjects = new NeoSerializedGameObject[0]; [SerializeField, Tooltip("How to filter out serialized components. If set to components, the objects in the list below will not be serialized. If set to include, only the components below will be serialized")] private OverrideNeoSerializationFilter m_FilterNeoComponents = OverrideNeoSerializationFilter.UseDefault; [SerializeField] private MonoBehaviour[] m_NeoComponents = new MonoBehaviour[0]; [SerializeField, Tooltip("Override the other components list instead of default")] private bool m_OverrideOtherComponents = false; [SerializeField] private Component[] m_OtherComponents = new Component[0]; #if UNITY_EDITOR // Inspector foldout expansion tracking [HideInInspector] public bool expandOverride = true; [HideInInspector] public bool expandChildObjects = false; [HideInInspector] public bool expandNeoComponents = false; [HideInInspector] public bool expandOtherComponents = false; #endif // Accessors public SaveMode saveMode { get { return m_SaveMode; } } public OverrideTransformSerialization serializePosition { get { return m_Position; } } public OverrideTransformSerialization serializeRotation { get { return m_Rotation; } } public OverrideScaleSerialization serializeLocalScale { get { return m_LocalScale; } } public OverrideNeoSerializationFilter filterChildObjects { get { return m_FilterChildObjects; } } public NeoSerializedGameObject[] childObjects { get { return m_ChildObjects; } } public OverrideNeoSerializationFilter filterNeoComponents { get { return m_FilterNeoComponents; } } public MonoBehaviour[] neoComponents { get { return m_NeoComponents; } } public bool overrideOtherComponents { get { return m_OverrideOtherComponents; } } public Component[] otherComponents { get { return m_OtherComponents; } } public enum OverrideTransformSerialization { UseDefault, LocalSpace, WorldSpace, Ignore } public enum OverrideScaleSerialization { UseDefault, True, False } public enum OverrideNeoSerializationFilter { UseDefault, Exclude, Include } public Override(SaveMode mode) { m_SaveMode = mode; } public void ApplyLimiter(INeoSerializedGameObjectLimiter limiter) { if (limiter.restrictChildObjects) { m_FilterChildObjects = OverrideNeoSerializationFilter.UseDefault; if (m_ChildObjects.Length > 0) m_ChildObjects = new NeoSerializedGameObject[0]; } if (limiter.restrictNeoComponents) { m_FilterNeoComponents = OverrideNeoSerializationFilter.UseDefault; if (m_NeoComponents.Length > 0) m_NeoComponents = new MonoBehaviour[0]; } if (limiter.restrictOtherComponents) { m_OverrideOtherComponents = false; } } } void Awake() { // Assign child container if (m_Children == null || !m_Children.isValid) m_Children = new NeoSerializedGameObjectChildContainer(this); m_Children.Awake(); } private void OnDestroy() { // Destroy child NeoSerializedGameObjectContainer // Prevents children from unregistering and bloating the destroyed objects list if (m_Children != null) m_Children.OnDestroy(); if (onDestroyed != null) onDestroyed(this); } public void OnValidate() { // Assign child container if (m_Children == null || !m_Children.isValid) m_Children = new NeoSerializedGameObjectChildContainer(this); // Check for restrictions var limiters = GetComponents(); for (int i = 0; i < limiters.Length; ++i) { if (limiters[i].restrictChildObjects) { m_FilterChildObjects = NeoSerializationFilter.Include; if (m_ChildObjects.Length > 0) m_ChildObjects = new NeoSerializedGameObject[0]; } if (limiters[i].restrictNeoComponents) { m_FilterNeoComponents = NeoSerializationFilter.Include; if (m_NeoComponents.Length > 0) m_NeoComponents = new MonoBehaviour[0]; } if (limiters[i].restrictOtherComponents) { if (m_OtherComponents.Length > 0) m_OtherComponents = new Component[0]; } for (int j = 0; j < m_Overrides.Length; ++j) m_Overrides[j].ApplyLimiter(limiters[i]); } #if UNITY_EDITOR if (!UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) { // Delay validation since PrefabUtility throws SendMessage warnings in OnValidate if (!m_ValidationPending) { m_ValidationPending = true; UnityEditor.EditorApplication.update += DeferredOnValidate; } } #endif } #if UNITY_EDITOR private NeoSerializedGameObject m_OriginalPrefab = null; private bool m_ValidationPending = false; void DeferredOnValidate() { // Remove delayed validation callback m_ValidationPending = false; UnityEditor.EditorApplication.update -= DeferredOnValidate; // Null check for when changing play mode, etc (MonoBehaviour pointers are safe pointers) if (this != null) { // Object is prefab but in a scene (could be staging scene for editing prefabs) var stage = PrefabStageUtility.GetCurrentPrefabStage(); if (stage != null && stage.IsPartOfPrefabContents(gameObject)) { // Object is prefab but in the prefab staging scene (editing the prefab) // Check if prefab is stored if (m_OriginalPrefab == null) { #if UNITY_2020_1_OR_NEWER m_OriginalPrefab = UnityEditor.AssetDatabase.LoadAssetAtPath(stage.assetPath).GetComponent(); #else m_OriginalPrefab = UnityEditor.AssetDatabase.LoadAssetAtPath(stage.prefabAssetPath).GetComponent(); #endif //m_OriginalPrefab.CheckPrefabID(); //m_OriginalPrefab.CheckSerializationKey(); } } else { if (!UnityEditor.PrefabUtility.IsPartOfPrefabAsset(gameObject)) { // Object is in a scene and not a prefab - check key only CheckSerializationKey(); } else { if (!UnityEditor.PrefabUtility.IsPartOfPrefabInstance(gameObject)) { // Object is prefab but not in a scene (project view only) CheckPrefabID(); CheckSerializationKey(); } } } } } private void OnBeforeTransformParentChanged() { // Double check this. It already caused an error in OnDestroy if (!Application.isPlaying) m_Children = null; } #endif public NeoSerializedGameObject GetParent() { var itr = transform.parent; while (itr != null) { var nsgo = itr.GetComponent(); if (nsgo != null) return nsgo; else itr = itr.parent; } return null; } public void SetParent(NeoSerializedGameObject target) { if (target != null) { // Unregister from current var current = GetParent(); if (current != null) { if (current == target) return; current.serializedChildren.UnregisterObject(this); } // Set transform parent transform.SetParent(target.transform); // Register with new parent target.serializedChildren.RegisterObject(this); } else Debug.LogError("Cannot set NeoSerializedGameObject parent to null"); } #region WRITING public void WriteGameObject(INeoSerializer writer, SaveMode saveMode) { // Write name if (m_SaveName) writer.WriteValue(k_NameKey, name); // Write active state writer.WriteValue(k_ActiveKey, gameObject.activeSelf); // Save settings (doesn't apply to overrides) if (m_SaveSettings) writer.WriteValue(k_SettingsKey, new Vector3Int(m_SaveName ? 1 : 0, (int)m_FilterChildObjects, (int)m_FilterNeoComponents)); // Check for overrides Override over = null; if (saveMode != SaveMode.Default) { for (int i = 0; i < m_Overrides.Length; ++i) { if (m_Overrides[i].saveMode == saveMode) { over = m_Overrides[i]; break; } } } // Get settings TransformSerialization serializePosition = m_Position; TransformSerialization serializeRotation = m_Rotation; bool serializeScale = m_LocalScale; var filterChildren = m_FilterChildObjects; var childObjects = m_ChildObjects; var filterNeoComponents = m_FilterNeoComponents; var neoComponents = m_NeoComponents; var otherComponents = m_OtherComponents; if (over != null) { switch (over.serializePosition) { case Override.OverrideTransformSerialization.LocalSpace: serializePosition = TransformSerialization.LocalSpace; break; case Override.OverrideTransformSerialization.WorldSpace: serializePosition = TransformSerialization.WorldSpace; break; case Override.OverrideTransformSerialization.Ignore: serializePosition = TransformSerialization.Ignore; break; } switch (over.serializeRotation) { case Override.OverrideTransformSerialization.LocalSpace: serializeRotation = TransformSerialization.LocalSpace; break; case Override.OverrideTransformSerialization.WorldSpace: serializeRotation = TransformSerialization.WorldSpace; break; case Override.OverrideTransformSerialization.Ignore: serializeRotation = TransformSerialization.Ignore; break; } switch (over.serializeLocalScale) { case Override.OverrideScaleSerialization.True: serializeScale = true; break; case Override.OverrideScaleSerialization.False: serializeScale = false; break; } switch (over.filterChildObjects) { case Override.OverrideNeoSerializationFilter.Exclude: filterChildren = NeoSerializationFilter.Exclude; childObjects = over.childObjects; break; case Override.OverrideNeoSerializationFilter.Include: filterChildren = NeoSerializationFilter.Include; childObjects = over.childObjects; break; } switch (over.filterNeoComponents) { case Override.OverrideNeoSerializationFilter.Exclude: filterNeoComponents = NeoSerializationFilter.Exclude; neoComponents = over.neoComponents; break; case Override.OverrideNeoSerializationFilter.Include: filterNeoComponents = NeoSerializationFilter.Include; neoComponents = over.neoComponents; break; } if (over.overrideOtherComponents) otherComponents = over.otherComponents; } // Write transform switch (serializePosition) { case TransformSerialization.LocalSpace: writer.WriteValue(k_PositionKey, transform.localPosition); break; case TransformSerialization.WorldSpace: writer.WriteValue(k_PositionKey, transform.position); break; } switch (serializeRotation) { case TransformSerialization.LocalSpace: writer.WriteValue(k_RotationKey, transform.localRotation); break; case TransformSerialization.WorldSpace: writer.WriteValue(k_RotationKey, transform.rotation); break; } if (serializeScale) writer.WriteValue(k_ScaleKey, transform.localScale); // Write neo-serialized components if (filterNeoComponents == NeoSerializationFilter.Include) { // Write components in m_NeoComponents array only for (int i = 0; i < neoComponents.Length; ++i) WriteNeoComponent(writer, neoComponents[i], saveMode); } else { GetComponents(s_GatheredNeoComponents); for (int i = 0; i < s_GatheredNeoComponents.Count; ++i) { // Skip components in m_NeoComponents if (Array.IndexOf(neoComponents, s_GatheredNeoComponents[i]) != -1) continue; WriteNeoComponent(writer, s_GatheredNeoComponents[i], saveMode); } s_GatheredNeoComponents.Clear(); } // Write other components (always manually included) for (int i = 0; i < otherComponents.Length; ++i) WriteOtherComponent(writer, otherComponents[i]); // Write child objects m_Children.WriteGameObjects(writer, filterChildren, childObjects, saveMode); } void WriteNeoComponent(INeoSerializer writer, MonoBehaviour component, SaveMode saveMode) { var c = component as INeoSerializableComponent; if (c != null) { writer.PushContext(SerializationContext.ComponentNeoSerialized, NeoSerializationUtilities.GetPersistentComponentID(component)); writer.WriteValue(k_EnabledKey, component.enabled); c.WriteProperties(writer, this, saveMode); writer.PopContext(SerializationContext.ComponentNeoSerialized); } } void WriteNeoComponent(INeoSerializer writer, INeoSerializableComponent component, SaveMode saveMode) { var c = component as MonoBehaviour; if (c != null) { writer.PushContext(SerializationContext.ComponentNeoSerialized, NeoSerializationUtilities.GetPersistentComponentID(c)); writer.WriteValue(k_EnabledKey, c.enabled); component.WriteProperties(writer, this, saveMode); writer.PopContext(SerializationContext.ComponentNeoSerialized); } } void WriteOtherComponent(INeoSerializer writer, Component component) { if (component != null) { writer.PushContext(SerializationContext.ComponentNeoFormatted, NeoSerializationUtilities.GetPersistentComponentID(component)); var formatter = NeoSerializationFormatters.GetFormatter(component); if (formatter != null) formatter.WriteProperties(writer, component, this); writer.PopContext(SerializationContext.ComponentNeoFormatted); } } #endregion #region READING public void ReadGameObjectHierarchy(INeoDeserializer reader) { // Rebuild the hierarchies separate to reading properties // Required to resolver references m_Children.ReadGameObjectHierarchy(reader); } public void ReadGameObjectProperties(INeoDeserializer reader) { string n; if (reader.TryReadValue(k_NameKey, out n, string.Empty)) { name = n; } Vector3Int settings; if (reader.TryReadValue(k_SettingsKey, out settings, Vector3Int.zero)) { m_SaveName = settings.x == 1; m_FilterChildObjects = (NeoSerializationFilter)settings.y; m_FilterNeoComponents = (NeoSerializationFilter)settings.z; m_SaveSettings = true; } // Read active state bool active; reader.TryReadValue(k_ActiveKey, out active, gameObject.activeSelf); gameObject.SetActive(active); // Read transform Vector3 position; if (reader.TryReadValue(k_PositionKey, out position, Vector3.zero)) { switch (m_Position) { case TransformSerialization.LocalSpace: transform.localPosition = position; break; case TransformSerialization.WorldSpace: transform.position = position; break; } } Quaternion rotation; if (reader.TryReadValue(k_RotationKey, out rotation, Quaternion.identity)) { switch (m_Rotation) { case TransformSerialization.LocalSpace: transform.localRotation = rotation; break; case TransformSerialization.WorldSpace: transform.rotation = rotation; break; } } if (m_LocalScale) { Vector3 scale; if (reader.TryReadValue(k_ScaleKey, out scale, Vector3.one)) transform.localScale = scale; } // Read neo-serialized components if (m_FilterNeoComponents == NeoSerializationFilter.Include) { // Write components in m_NeoComponents array only for (int i = 0; i < m_NeoComponents.Length; ++i) ReadNeoComponent(reader, m_NeoComponents[i]); } else { GetComponents(s_GatheredNeoComponents); for (int i = 0; i < s_GatheredNeoComponents.Count; ++i) { // Skip components in m_NeoComponents if (Array.IndexOf(m_NeoComponents, s_GatheredNeoComponents[i]) != -1 || s_GatheredNeoComponents[i] == null) continue; ReadNeoComponent(reader, s_GatheredNeoComponents[i]); } s_GatheredNeoComponents.Clear(); } // Read other components (always manually included) for (int i = 0; i < m_OtherComponents.Length; ++i) ReadOtherComponent(reader, m_OtherComponents[i]); // Read child objects m_Children.ReadGameObjectProperties(reader); } void ReadNeoComponent(INeoDeserializer reader, MonoBehaviour component) { var c = component as INeoSerializableComponent; if (c != null) { int id = NeoSerializationUtilities.GetPersistentComponentID(component); if (reader.PushContext(SerializationContext.ComponentNeoSerialized, id)) { try { // Get enabled state of the component // Do this first incase reading properties causes coroutines to be started, etc bool isEnabled = true; if (reader.TryReadValue(k_EnabledKey, out isEnabled, true)) component.enabled = isEnabled; c.ReadProperties(reader, this); } //catch (Exception e) //{ // Debug.LogError("Reading component failed due to error: " + e.Message, component.gameObject); //} finally { reader.PopContext(SerializationContext.ComponentNeoSerialized, id); } } } } void ReadNeoComponent(INeoDeserializer reader, INeoSerializableComponent component) { var c = component as MonoBehaviour; if (c != null) { int id = NeoSerializationUtilities.GetPersistentComponentID(c); if (reader.PushContext(SerializationContext.ComponentNeoSerialized, id)) { try { // Get enabled state of the component // Do this first incase reading properties causes coroutines to be started, etc bool isEnabled = true; if (reader.TryReadValue(k_EnabledKey, out isEnabled, true)) c.enabled = isEnabled; component.ReadProperties(reader, this); } //catch (Exception e) //{ // Debug.LogError(string.Format("Reading component ({0}) failed due to error: {1}", component.GetType(), e.Message), c.gameObject); //} finally { reader.PopContext(SerializationContext.ComponentNeoSerialized, id); } } } } void ReadOtherComponent(INeoDeserializer reader, Component component) { // Require formatters system before re-enabling this feature if (component != null) { int id = NeoSerializationUtilities.GetPersistentComponentID(component); if (reader.PushContext(SerializationContext.ComponentNeoFormatted, id)) { try { var formatter = NeoSerializationFormatters.GetFormatter(component); formatter.ReadProperties(reader, component, this); } //catch (Exception e) //{ // Debug.LogError("Reading component failed due to error: " + e.Message, component.gameObject); //} finally { reader.PopContext(SerializationContext.ComponentNeoFormatted, id); } } } } #endregion #region INSTANTIATION public T InstantiatePrefab(T prototype) where T : Component { return NeoSerializedObjectFactory.Instantiate(prototype, m_Children); } public T InstantiatePrefab(T prototype, Vector3 position, Quaternion rotation) where T : Component { return NeoSerializedObjectFactory.Instantiate(prototype, m_Children, position, rotation); } public T InstantiatePrefab(int prefabID, int serializationKey) where T : Component { var result = NeoSerializedObjectFactory.Instantiate(prefabID, serializationKey, m_Children); if (result != null) return result.GetComponent(); else return null; } #endregion #region EDITOR SPECIFIC (CONDITIONAL) #if UNITY_EDITOR // Inspector foldout expansion tracking [HideInInspector] public bool expandChildObjects = false; [HideInInspector] public bool expandNeoComponents = false; [HideInInspector] public bool expandOtherComponents = false; // Guid tracking (if it doesn't match the asset file's then update and use to generate a new prefabID) [HideInInspector] public string prefabGuid = string.Empty; private static List s_GatheredNsgos = new List(); void CheckSerializationKey() { var parent = GetParent(); if (parent == null) { if (gameObject.scene.IsValid()) { var nss = NeoSerializedScene.GetByBuildIndex(gameObject.scene.buildIndex); if (nss != null) nss.sceneObjects.Validate(this); } } else { parent.serializedChildren.Validate(this); } } public void CheckPrefabID() { string guid = UnityEditor.AssetDatabase.AssetPathToGUID(UnityEditor.AssetDatabase.GetAssetPath(gameObject)); if (!string.IsNullOrEmpty(guid) && prefabGuid != guid) { prefabGuid = guid; m_PrefabStrongID = NeoSerializationUtilities.StringToHash(guid); } } [UnityEditor.InitializeOnLoadMethod] static void AddPrefabHierarchyChecks() { PrefabStage.prefabStageOpened += ValidatePrefab; } private static void ValidatePrefab(PrefabStage stage) { var root = stage.prefabContentsRoot; var nsgo = root.GetComponent(); if (nsgo != null) nsgo.CheckPrefabHierarchy(); } public bool CheckPrefabHierarchy() { bool result = false; GetComponentsInChildren(true, s_GatheredNsgos); // Filter for invalid objects for (int i = s_GatheredNsgos.Count - 1; i >= 0; --i) { if (s_GatheredNsgos[i].GetParent() != this) s_GatheredNsgos.RemoveAt(i); } // Assign child container if required and validate if (m_Children == null || !m_Children.isValid) { m_Children = new NeoSerializedGameObjectChildContainer(this); result = true; } if (m_Children.Validate(s_GatheredNsgos)) { UnityEditor.EditorUtility.SetDirty(this); result = true; } s_GatheredNsgos.Clear(); return result; } #endif #endregion } }