// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #if UNITY_EDITOR using Microsoft.MixedReality.Toolkit.SceneSystem; using System; using System.Collections.Generic; using System.Reflection; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; namespace Microsoft.MixedReality.Toolkit.Utilities { /// /// Utilities for loading / saving scenes in editor via SceneInfo. /// Because SceneInfo is defined in MixedRealityToolkit, this can't be kept in Editor utilities. /// public static class EditorSceneUtils { /// /// Enum used by this class to specify build settings order /// public enum BuildIndexTarget { First, None, Last, } /// /// Creates a new scene with sceneName and saves to path. /// public static SceneInfo CreateAndSaveScene(string sceneName, string path = null) { SceneInfo sceneInfo = default(SceneInfo); if (!EditorSceneManager.EnsureUntitledSceneHasBeenSaved("Save untitled scene before proceeding?")) { return sceneInfo; } Scene newScene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive); if (string.IsNullOrEmpty(path)) { path = "Assets/" + sceneName + ".unity"; } if (!EditorSceneManager.SaveScene(newScene, path)) { Debug.LogError("Couldn't create and save scene " + sceneName + " at path " + path); return sceneInfo; } SceneAsset sceneAsset = AssetDatabase.LoadAssetAtPath(path); sceneInfo.Asset = sceneAsset; sceneInfo.Name = sceneAsset.name; sceneInfo.Path = path; return sceneInfo; } /// /// Adds scene to build settings. /// /// Scene object reference. /// Sets as first scene to be loaded. public static bool AddSceneToBuildSettings( SceneInfo scene, EditorBuildSettingsScene[] scenes, BuildIndexTarget buildIndexTarget = BuildIndexTarget.None) { if (scene.IsEmpty) { // Can't add a null scene to build settings return false; } long localID; string managerGuidString; AssetDatabase.TryGetGUIDAndLocalFileIdentifier(scene.Asset, out managerGuidString, out localID); GUID sceneGuid = new GUID(managerGuidString); List newScenes = new List(scenes); // See if / where the scene exists in build settings int buildIndex = EditorSceneUtils.GetSceneBuildIndex(sceneGuid, newScenes); if (buildIndex < 0) { // It doesn't exist in the build settings, add it now switch (buildIndexTarget) { case BuildIndexTarget.First: // Add it to index 0 newScenes.Insert(0, new EditorBuildSettingsScene(sceneGuid, true)); break; case BuildIndexTarget.None: default: // Just add it to the end newScenes.Add(new EditorBuildSettingsScene(sceneGuid, true)); break; } EditorBuildSettings.scenes = newScenes.ToArray(); return true; } else { switch (buildIndexTarget) { // If it does exist, but isn't in the right spot, move it now case BuildIndexTarget.First: if (buildIndex != 0) { Debug.LogWarning("Scene '" + scene.Name + "' was not first in build order. Changing build settings now."); newScenes.RemoveAt(buildIndex); newScenes.Insert(0, new EditorBuildSettingsScene(sceneGuid, true)); EditorBuildSettings.scenes = newScenes.ToArray(); } return true; case BuildIndexTarget.Last: if (buildIndex != EditorSceneManager.sceneCountInBuildSettings - 1) { newScenes.RemoveAt(buildIndex); newScenes.Insert(newScenes.Count - 1, new EditorBuildSettingsScene(sceneGuid, true)); EditorBuildSettings.scenes = newScenes.ToArray(); } return true; case BuildIndexTarget.None: default: // Do nothing return false; } } } /// /// Gets the build index for a scene GUID. /// There are many ways to do this in Unity but this is the only 100% reliable method I know of. /// public static int GetSceneBuildIndex(GUID sceneGUID, List scenes) { int buildIndex = -1; int sceneCount = 0; for (int i = 0; i < scenes.Count; i++) { if (scenes[i].guid == sceneGUID) { buildIndex = sceneCount; break; } if (scenes[i].enabled) { sceneCount++; } } return buildIndex; } /// /// Attempts to load scene in editor using a scene object reference. /// /// Scene object reference. /// Whether to set first in the hierarchy window. /// The loaded scene. /// True if successful. public static bool LoadScene(SceneInfo sceneInfo, bool setAsFirst, out Scene editorScene) { editorScene = default(Scene); try { editorScene = SceneManager.GetSceneByName(sceneInfo.Name); if (editorScene.isLoaded) { // Already open - no need to do anything! return true; } string scenePath = AssetDatabase.GetAssetOrScenePath(sceneInfo.Asset); EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive); if (setAsFirst #if UNITY_2022_2_OR_NEWER && SceneManager.loadedSceneCount >= 1) #else && EditorSceneManager.loadedSceneCount >= 1) #endif { // Move the scene to first in order in the hierarchy Scene nextScene = SceneManager.GetSceneAt(0); EditorSceneManager.MoveSceneBefore(editorScene, nextScene); } } catch (InvalidOperationException) { // This can happen if we're trying to load immediately upon recompilation. return false; } catch (ArgumentException) { // This can happen if the scene is an invalid scene and we try to SetActive. return false; } catch (NullReferenceException) { // This can happen if the scene object is null. return false; } catch (MissingReferenceException) { // This can happen if the scene object is null. return false; } return true; } /// /// Finds the scene if loaded. /// /// True if scene is loaded public static bool GetSceneIfLoaded(SceneInfo sceneInfo, out Scene editorScene) { editorScene = default(Scene); editorScene = EditorSceneManager.GetSceneByName(sceneInfo.Name); return editorScene.IsValid() && editorScene.isLoaded; } /// /// Returns all root GameObjects in all open scenes. /// public static IEnumerable GetRootGameObjectsInLoadedScenes() { for (int i = 0; i < EditorSceneManager.sceneCount; i++) { Scene openScene = EditorSceneManager.GetSceneAt(i); if (!openScene.isLoaded) { // Oh, Unity. continue; } foreach (GameObject rootGameObject in openScene.GetRootGameObjects()) yield return rootGameObject; } yield break; } /// /// Returns true if user is currently editing a prefab. /// public static bool IsEditingPrefab() { #if UNITY_2021_2_OR_NEWER var prefabStage = UnityEditor.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage(); #else var prefabStage = UnityEditor.Experimental.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage(); #endif return prefabStage != null; } /// /// Unloads a scene in the editor and catches any errors that can happen along the way. /// public static bool UnloadScene(SceneInfo sceneInfo, bool removeFromHeirarchy) { Scene editorScene = default(Scene); try { editorScene = EditorSceneManager.GetSceneByName(sceneInfo.Name); if (editorScene.isLoaded) { EditorSceneManager.CloseScene(editorScene, removeFromHeirarchy); } } catch (InvalidOperationException) { // This can happen if we're trying to load immediately upon recompilation. return false; } catch (ArgumentException) { // This can happen if the scene is an invalid scene and we try to SetActive. return false; } catch (NullReferenceException) { // This can happen if the scene object is null. return false; } catch (MissingReferenceException) { // This can happen if the scene object is null. return false; } return true; } /// /// Attempts to set the active scene and catches all the various ways it can go wrong. /// Returns true if successful. /// public static bool SetActiveScene(Scene scene) { try { EditorSceneManager.SetActiveScene(scene); } catch (InvalidOperationException) { // This can happen if we're trying to load immediately upon recompilation. return false; } catch (ArgumentException) { // This can happen if the scene is an invalid scene and we try to SetActive. return false; } catch (NullReferenceException) { // This can happen if the scene object is null. return false; } catch (MissingReferenceException) { // This can happen if the scene object is null. return false; } return true; } /// /// Copies the lighting settings from the lighting scene to the active scene /// public static void CopyLightingSettingsToActiveScene(Scene lightingScene) { // Store the active scene on entry Scene activeSceneOnEnter = EditorSceneManager.GetActiveScene(); // No need to do anything if (activeSceneOnEnter == lightingScene) return; SerializedObject sourceLightmapSettings; SerializedObject sourceRenderSettings; // Set the active scene to the lighting scene SetActiveScene(lightingScene); // If we can't get the source settings for some reason, abort if (!GetLightingAndRenderSettings(out sourceLightmapSettings, out sourceRenderSettings)) { return; } bool madeChanges = false; // Set active scene back to the active scene on enter if (SetActiveScene(activeSceneOnEnter)) { SerializedObject targetLightmapSettings; SerializedObject targetRenderSettings; if (GetLightingAndRenderSettings(out targetLightmapSettings, out targetRenderSettings)) { string[] propsToIgnore = new string[] { "m_IndirectSpecularColor" }; madeChanges |= SerializedObjectUtils.CopySerializedObject(sourceLightmapSettings, targetLightmapSettings, propsToIgnore); madeChanges |= SerializedObjectUtils.CopySerializedObject(sourceRenderSettings, targetRenderSettings, propsToIgnore); } } if (madeChanges) { Debug.LogWarning("Changed lighting settings in scene " + activeSceneOnEnter.name + " to match lighting scene " + lightingScene.name); EditorSceneManager.MarkSceneDirty(activeSceneOnEnter); } } /// /// Goes through a scene's objects and checks for components that aren't found in permittedComponentTypes /// If any are found, they're added to the violations list. /// public static bool EnforceSceneComponents(Scene scene, IEnumerable permittedComponentTypes, List violations) { if (!scene.IsValid() || !scene.isLoaded) { return false; } int typesEvaluated = 0; bool foundAtLeastOneViolation = false; try { foreach (GameObject rootGameObject in scene.GetRootGameObjects()) { foreach (Component component in rootGameObject.GetComponentsInChildren()) { bool componentIsPermitted = false; foreach (Type type in permittedComponentTypes) { if (type.IsAssignableFrom(component.GetType())) { componentIsPermitted = true; break; } typesEvaluated++; } if (!componentIsPermitted) { foundAtLeastOneViolation = true; violations.Add(component); } } } } catch (Exception) { // This can go wrong if GetRootSceneObjects fails. } if (typesEvaluated == 0) { Debug.LogError("Permitted component types must include at least one type."); } return foundAtLeastOneViolation; } /// /// Gets serialized objects for lightmap and render settings from active scene. /// public static bool GetLightingAndRenderSettings(out SerializedObject lightmapSettings, out SerializedObject renderSettings) { lightmapSettings = null; renderSettings = null; UnityEngine.Object lightmapSettingsObject = null; UnityEngine.Object renderSettingsObject = null; try { // Use reflection to get the serialized objects of lightmap settings and render settings Type lightmapSettingsType = typeof(LightmapEditorSettings); Type renderSettingsType = typeof(RenderSettings); lightmapSettingsObject = (UnityEngine.Object)lightmapSettingsType.GetMethod("GetLightmapSettings", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, null); renderSettingsObject = (UnityEngine.Object)renderSettingsType.GetMethod("GetRenderSettings", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, null); } catch (Exception) { Debug.LogWarning("Couldn't get lightmap or render settings. This version of Unity may not support this operation."); return false; } if (lightmapSettingsObject == null) { Debug.LogWarning("Couldn't get lightmap settings object"); return false; } if (renderSettingsObject == null) { Debug.LogWarning("Couldn't get render settings object"); return false; } // Store the settings in serialized objects lightmapSettings = new SerializedObject(lightmapSettingsObject); renderSettings = new SerializedObject(renderSettingsObject); return true; } /// /// Checks build settings for possible errors and displays warnings. /// public static bool CheckBuildSettingsForDuplicates(List allScenes, Dictionary> duplicates) { duplicates.Clear(); List indexes = null; bool foundDuplicate = false; foreach (SceneInfo sceneInfo in allScenes) { if (duplicates.TryGetValue(sceneInfo.Name, out indexes)) { indexes.Add(sceneInfo.BuildIndex); foundDuplicate = true; } else { duplicates.Add(sceneInfo.Name, new List { sceneInfo.BuildIndex }); } } return foundDuplicate; } } } #endif