// Author: JohannesMP (2018-08-12) // // A wrapper that provides the means to safely serialize Scene Asset References. // // Internally we serialize an Object to the SceneAsset which only exists at editor time. // Any time the object is serialized, we store the path provided by this Asset (assuming it was valid). // // This means that, come build time, the string path of the scene asset is always already stored, which if // the scene was added to the build settings means it can be loaded. // // It is up to the user to ensure the scene exists in the build settings so it is loadable at runtime. // To help with this, a custom PropertyDrawer displays the scene build settings state. // // Known issues: // - When reverting back to a prefab which has the asset stored as null, Unity will show the property // as modified despite having just reverted. This only happens the fist time, and reverting again // using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Linq; using System.IO; #if UNITY_EDITOR using UnityEditor; #endif [System.Serializable] public class SceneReferenceLite : ISerializationCallbackReceiver { #if UNITY_EDITOR // What we use in editor to select the scene [SerializeField] private Object sceneAsset = null; bool IsValidSceneAsset { get { if (sceneAsset == null) return false; return sceneAsset.GetType().Equals(typeof(SceneAsset)); } } #endif // This should only ever be set during serialization/deserialization! [SerializeField] private string scenePath = string.Empty; // Use this when you want to actually have the scene path public string ScenePath { get { #if UNITY_EDITOR // In editor we always use the asset's path return GetScenePathFromAsset(); #else // At runtime we rely on the stored path value which we assume was serialized correctly at build time. // See OnBeforeSerialize and OnAfterDeserialize return scenePath; #endif } set { scenePath = value; #if UNITY_EDITOR sceneAsset = GetSceneAssetFromPath(); #endif } } // Use this when you want to have the scene name public string SceneName { get { return Path.GetFileNameWithoutExtension(ScenePath); // At runtime we rely on the stored path value which we assume was serialized correctly at build time. // See OnBeforeSerialize and OnAfterDeserialize //return scenePath; } } public static implicit operator string(SceneReferenceLite sceneReference) { return sceneReference.ScenePath; } // Called to prepare this data for serialization. Stubbed out when not in editor. public void OnBeforeSerialize() { #if UNITY_EDITOR HandleBeforeSerialize(); #endif } // Called to set up data for deserialization. Stubbed out when not in editor. public void OnAfterDeserialize() { #if UNITY_EDITOR // We sadly cannot touch assetdatabase during serialization, so defer by a bit. EditorApplication.update += HandleAfterDeserialize; #endif } #if UNITY_EDITOR private SceneAsset GetSceneAssetFromPath() { if (string.IsNullOrEmpty(scenePath)) return null; return AssetDatabase.LoadAssetAtPath(scenePath); } private string GetScenePathFromAsset() { if (sceneAsset == null) return string.Empty; return AssetDatabase.GetAssetPath(sceneAsset); } private void HandleBeforeSerialize() { // Asset is invalid but have Path to try and recover from if (IsValidSceneAsset == false && string.IsNullOrEmpty(scenePath) == false) { sceneAsset = GetSceneAssetFromPath(); if (sceneAsset == null) scenePath = string.Empty; UnityEditor.SceneManagement.EditorSceneManager.MarkAllScenesDirty(); } // Asset takes precendence and overwrites Path else { scenePath = GetScenePathFromAsset(); } } private void HandleAfterDeserialize() { EditorApplication.update -= HandleAfterDeserialize; // Asset is valid, don't do anything - Path will always be set based on it when it matters if (IsValidSceneAsset) return; // Asset is invalid but have path to try and recover from if (string.IsNullOrEmpty(scenePath) == false) { sceneAsset = GetSceneAssetFromPath(); // No asset found, path was invalid. Make sure we don't carry over the old invalid path if (sceneAsset == null) scenePath = string.Empty; if (Application.isPlaying == false) UnityEditor.SceneManagement.EditorSceneManager.MarkAllScenesDirty(); } } #endif } #if UNITY_EDITOR /// /// Display a Scene Reference object in the editor. /// If scene is valid, provides basic buttons to interact with the scene's role in Build Settings. /// [CustomPropertyDrawer(typeof(SceneReferenceLite))] public class SceneReferenceLitePropertyDrawer : PropertyDrawer { // The exact name of the asset Object variable in the SceneReference object const string sceneAssetPropertyString = "sceneAsset"; // The exact name of the scene Path variable in the SceneReference object const string scenePathPropertyString = "scenePath"; static readonly RectOffset boxPadding = EditorStyles.helpBox.padding; static readonly float padSize = 2f; static readonly float lineHeight = EditorGUIUtility.singleLineHeight; static readonly float paddedLine = lineHeight + padSize; static readonly float footerHeight = 10f; /// /// Drawing the 'SceneReference' property /// public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { var sceneAssetProperty = GetSceneAssetProperty(property); // Draw the Box Background position.height -= footerHeight; //GUI.Box(EditorGUI.IndentedRect(position), GUIContent.none, EditorStyles.helpBox); position = boxPadding.Remove(position); position.height = lineHeight; // Draw the main Object field //label.tooltip = "The actual Scene Asset reference.\nOn serialize this is also stored as the asset's path."; EditorGUI.BeginProperty(position, GUIContent.none, property); EditorGUI.BeginChangeCheck(); int sceneControlID = GUIUtility.GetControlID(FocusType.Passive); var selectedObject = EditorGUI.ObjectField(position, label, sceneAssetProperty.objectReferenceValue, typeof(SceneAsset), false); if (EditorGUI.EndChangeCheck()) { sceneAssetProperty.objectReferenceValue = selectedObject; } position.y += paddedLine; EditorGUI.EndProperty(); } /// /// Ensure that what we draw in OnGUI always has the room it needs /// public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { int lines = 2; SerializedProperty sceneAssetProperty = GetSceneAssetProperty(property); if (sceneAssetProperty.objectReferenceValue == null) lines = 1; return boxPadding.vertical + lineHeight * lines + padSize * (lines - 1) + footerHeight; } static SerializedProperty GetSceneAssetProperty(SerializedProperty property) { return property.FindPropertyRelative(sceneAssetPropertyString); } static SerializedProperty GetScenePathProperty(SerializedProperty property) { return property.FindPropertyRelative(scenePathPropertyString); } } #endif