// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.MixedReality.Toolkit.Utilities; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using UnityEngine; using UnityEngine.SceneManagement; #if UNITY_EDITOR using UnityEditor; using UnityEditor.SceneManagement; #endif namespace Microsoft.MixedReality.Toolkit.SceneSystem { /// /// The default implementation of the /// This file handles the editor-oriented parts of the service. /// public partial class MixedRealitySceneSystem : BaseCoreSystem, IMixedRealitySceneSystem, IMixedRealitySceneSystemEditor { #if UNITY_EDITOR /// /// Detects asset modifications. /// Used to detect when lighting cache may be out of date. /// internal sealed class FileModificationWarning : UnityEditor.AssetModificationProcessor { public static HashSet ModifiedAssetPaths = new HashSet(); public static string[] OnWillSaveAssets(string[] paths) { foreach (string path in paths) { ModifiedAssetPaths.Add(path); } return paths; } } /// /// Returns the manager scene found in profile. /// public SceneInfo ManagerScene => Profile.ManagerScene; /// /// Returns all lighting scenes found in profile. /// public SceneInfo[] LightingScenes => contentTracker.SortedLightingScenes; /// /// Returns all content scenes found in profile. /// public SceneInfo[] ContentScenes => contentTracker.SortedContentScenes; /// /// Returns all content tags found in profile scenes. /// public IEnumerable ContentTags => Profile.ContentTags; // Cache these so we're not looking them up constantly private EditorBuildSettingsScene[] cachedBuildScenes = Array.Empty(); // These get set to dirty based on what events have been received from the editor private bool activeSceneDirty = false; private bool buildSettingsDirty = false; private bool heirarchyDirty = false; // These get set based on what our update methods are doing in response to events private bool updatingSettingsOnEditorChanged = false; private bool updatingCachedLightingSettings = false; // Checking for the manager scene via root game objects is very expensive // So only do it once in a while private const float lightingUpdateInterval = 5f; private const float managerSceneInstanceCheckInterval = 2f; private const int editorApplicationUpdateTickInterval = 10; private float managerSceneInstanceCheckTime; private int editorApplicationUpdateTicks; // Used to track which instance of the service we're using (for debugging purposes) private static int instanceIDCount; private int instanceID = -1; #region public editor methods /// /// Singly loads next content scene (if available) and unloads all other content scenes. /// Useful for inspectors. /// public void EditorLoadNextContent(bool wrap = false) { string contentSceneName; if (contentTracker.GetNextContent(wrap, out contentSceneName)) { foreach (SceneInfo contentScene in ContentScenes) { if (contentScene.Name == contentSceneName) { EditorSceneUtils.LoadScene(contentScene, false, out Scene scene); } else { EditorSceneUtils.UnloadScene(contentScene, false); } } } contentTracker.RefreshLoadedContent(); } /// /// Singly loads previous content scene (if available) and unloads all other content scenes. /// Useful for inspectors. /// public void EditorLoadPrevContent(bool wrap = false) { string contentSceneName; if (contentTracker.GetPrevContent(wrap, out contentSceneName)) { foreach (SceneInfo contentScene in ContentScenes) { if (contentScene.Name == contentSceneName) { EditorSceneUtils.LoadScene(contentScene, false, out Scene scene); } else { EditorSceneUtils.UnloadScene(contentScene, false); } } } contentTracker.RefreshLoadedContent(); } #endregion #region Initialization / Teardown private void EditorOnInitialize() { instanceID = instanceIDCount++; EditorSubscribeToEvents(); cachedBuildScenes = EditorBuildSettings.scenes; activeSceneDirty = true; buildSettingsDirty = true; heirarchyDirty = true; EditorCheckForChanges(); } private void EditorOnEnable() { EditorSubscribeToEvents(); cachedBuildScenes = EditorBuildSettings.scenes; activeSceneDirty = true; buildSettingsDirty = true; heirarchyDirty = true; EditorCheckForChanges(); } private void EditorOnDisable() { EditorUnsubscribeFromEvents(); } private void EditorOnDestroy() { EditorUnsubscribeFromEvents(); } private void EditorSubscribeToEvents() { EditorApplication.projectChanged += EditorApplicationProjectChanged; EditorApplication.hierarchyChanged += EditorApplicationHeirarcyChanged; EditorApplication.update += EditorApplicationUpdate; EditorSceneManager.newSceneCreated += EditorSceneManagerNewSceneCreated; EditorSceneManager.sceneOpened += EditorSceneManagerSceneOpened; EditorSceneManager.sceneClosed += EditorSceneManagerSceneClosed; EditorBuildSettings.sceneListChanged += EditorSceneListChanged; } private void EditorUnsubscribeFromEvents() { EditorApplication.projectChanged -= EditorApplicationProjectChanged; EditorApplication.hierarchyChanged -= EditorApplicationHeirarcyChanged; EditorApplication.update -= EditorApplicationUpdate; EditorSceneManager.newSceneCreated += EditorSceneManagerNewSceneCreated; EditorSceneManager.sceneOpened -= EditorSceneManagerSceneOpened; EditorSceneManager.sceneClosed -= EditorSceneManagerSceneClosed; EditorBuildSettings.sceneListChanged -= EditorSceneListChanged; } #endregion #region update triggers from editor events private void EditorApplicationUpdate() { editorApplicationUpdateTicks++; if (editorApplicationUpdateTicks > editorApplicationUpdateTickInterval) { activeSceneDirty = true; heirarchyDirty = true; editorApplicationUpdateTicks = 0; EditorCheckForChanges(); } } private void EditorApplicationHeirarcyChanged() { activeSceneDirty = true; heirarchyDirty = true; } private void EditorApplicationProjectChanged() { buildSettingsDirty = true; } private void EditorSceneListChanged() { buildSettingsDirty = true; EditorCheckForChanges(); } private void EditorSceneManagerSceneClosed(Scene scene) { activeSceneDirty = true; } private void EditorSceneManagerSceneOpened(Scene scene, OpenSceneMode mode) { activeSceneDirty = true; } private void EditorSceneManagerNewSceneCreated(Scene scene, NewSceneSetup setup, NewSceneMode mode) { activeSceneDirty = true; } #endregion /// /// Checks the state of service and profile based on changes made in editor and reacts accordingly. /// private async void EditorCheckForChanges() { if (!MixedRealityToolkit.IsInitialized || !MixedRealityToolkit.Instance.HasActiveProfile || !MixedRealityToolkit.Instance.ActiveProfile.IsSceneSystemEnabled) { return; } if (EditorSceneUtils.IsEditingPrefab()) { // Never change scene settings while editing a prefab - it will boot you out of the prefab scene stage. return; } if (updatingSettingsOnEditorChanged || EditorApplication.isPlayingOrWillChangePlaymode || EditorApplication.isCompiling) { // Make sure we don't double up on our updates via events we trigger during updates return; } if (updatingCachedLightingSettings) { // This is a long operation, don't interrupt it return; } // Update cached lighting settings, if the profile has requested it if (Profile.EditorLightingCacheUpdateRequested) { updatingCachedLightingSettings = true; updatingSettingsOnEditorChanged = true; await EditorUpdateCachedLighting(); updatingSettingsOnEditorChanged = false; updatingCachedLightingSettings = false; // This is an async operation which may take a while to execute // So exit when we're done - we'll pick up where we left off next time heirarchyDirty = true; return; } updatingSettingsOnEditorChanged = true; // Update editor settings if (FileModificationWarning.ModifiedAssetPaths.Count > 0) { EditorCheckIfCachedLightingOutOfDate(); FileModificationWarning.ModifiedAssetPaths.Clear(); } if (buildSettingsDirty) { buildSettingsDirty = false; EditorUpdateBuildSettings(); } if (activeSceneDirty || heirarchyDirty) { heirarchyDirty = false; activeSceneDirty = false; EditorUpdateManagerScene(); EditorUpdateLightingScene(heirarchyDirty); EditorUpdateContentScenes(activeSceneDirty); contentTracker.RefreshLoadedContent(); } EditorUtility.SetDirty(Profile); updatingSettingsOnEditorChanged = false; } /// /// Checks whether any of the save dates on our lighting scenes are later than the save date of our cached lighting data. /// private void EditorCheckIfCachedLightingOutOfDate() { DateTime cachedLightingTimestamp = Profile.GetEarliestLightingCacheTimestamp(); bool outOfDate = false; foreach (SceneInfo lightingScene in Profile.LightingScenes) { if (FileModificationWarning.ModifiedAssetPaths.Contains(lightingScene.Path)) { string lightingScenePath = System.IO.Path.Combine(Application.dataPath.Replace("/Assets", ""), lightingScene.Path); DateTime lightingSceneTimestamp = System.IO.File.GetLastWriteTime(lightingScenePath); if (lightingSceneTimestamp > cachedLightingTimestamp) { outOfDate = true; break; } } } if (outOfDate) { Profile.SetLightingCacheDirty(); } } /// /// Loads all lighting scenes, extracts their lighting data, then caches that data in the profile. /// private async Task EditorUpdateCachedLighting() { // Clear out our lighting cache Profile.ClearLightingCache(); Profile.EditorLightingCacheUpdateRequested = false; SceneInfo defaultLightingScene = Profile.DefaultLightingScene; foreach (SceneInfo lightingScene in Profile.LightingScenes) { // Load all our lighting scenes Scene scene; EditorSceneUtils.LoadScene(lightingScene, false, out scene); } // Wait for a moment so all loaded scenes have time to get set up await Task.Delay(100); foreach (SceneInfo lightingScene in Profile.LightingScenes) { Scene scene; EditorSceneUtils.GetSceneIfLoaded(lightingScene, out scene); EditorSceneUtils.SetActiveScene(scene); SerializedObject lightingSettingsObject; SerializedObject renderSettingsObject; EditorSceneUtils.GetLightingAndRenderSettings(out lightingSettingsObject, out renderSettingsObject); // Copy the serialized objects into new structs RuntimeLightingSettings lightingSettings = default(RuntimeLightingSettings); RuntimeRenderSettings renderSettings = default(RuntimeRenderSettings); RuntimeSunlightSettings sunlightSettings = default(RuntimeSunlightSettings); lightingSettings = SerializedObjectUtils.CopySerializedObjectToStruct(lightingSettingsObject, lightingSettings, "m_"); renderSettings = SerializedObjectUtils.CopySerializedObjectToStruct(renderSettingsObject, renderSettings, "m_"); // Extract sunlight settings based on sunlight object SerializedProperty sunProperty = renderSettingsObject.FindProperty("m_Sun"); if (sunProperty == null) { Debug.LogError("Sun settings may not be available in this version of Unity."); } else { Light sunLight = (Light)sunProperty.objectReferenceValue; if (sunLight != null) { sunlightSettings.UseSunlight = true; sunlightSettings.Color = sunLight.color; sunlightSettings.Intensity = sunLight.intensity; Vector3 eulerAngles = sunLight.transform.eulerAngles; sunlightSettings.XRotation = eulerAngles.x; sunlightSettings.YRotation = eulerAngles.y; sunlightSettings.ZRotation = eulerAngles.z; } } Profile.SetLightingCache(lightingScene, lightingSettings, renderSettings, sunlightSettings); } } /// /// Ensures that if a content scene is loaded, that scene is set active, rather than a lighting or manager scene. /// private void EditorUpdateContentScenes(bool activeSceneDirty) { if (!Profile.UseLightingScene || !Profile.EditorManageLoadedScenes) { // Nothing to do here return; } if (!activeSceneDirty) { // Nothing to do here either return; } bool contentSceneIsActive = false; SceneInfo firstLoadedContentScene = SceneInfo.Empty; foreach (SceneInfo contentScene in Profile.ContentScenes) { Scene scene; if (EditorSceneUtils.GetSceneIfLoaded(contentScene, out scene)) { if (firstLoadedContentScene.IsEmpty) { // If this is the first loaded content scene we've found, store it for later firstLoadedContentScene = contentScene; } Scene activeScene = EditorSceneManager.GetActiveScene(); if (activeScene.name == contentScene.Name) { contentSceneIsActive = true; } } } if (!firstLoadedContentScene.IsEmpty) { // If at least one content scene is loaded if (!contentSceneIsActive) { // And that content scene is NOT the active scene // Set that content to be the active scene Scene activeScene; EditorSceneUtils.GetSceneIfLoaded(firstLoadedContentScene, out activeScene); EditorSceneUtils.SetActiveScene(activeScene); } } } /// /// If a manager scene is being used, this loads the scene in editor and ensures that an instance of the MRTK has been added to it. /// private void EditorUpdateManagerScene() { if (!Profile.UseManagerScene || !Profile.EditorManageLoadedScenes) { // Nothing to do here. return; } if (EditorSceneUtils.LoadScene(Profile.ManagerScene, true, out Scene scene)) { // If we're managing scene hierarchy, move this to the front if (Profile.EditorEnforceSceneOrder) { Scene currentFirstScene = EditorSceneManager.GetSceneAt(0); if (currentFirstScene.name != scene.name) { EditorSceneManager.MoveSceneBefore(scene, currentFirstScene); } } if (Time.realtimeSinceStartup > managerSceneInstanceCheckTime) { managerSceneInstanceCheckTime = Time.realtimeSinceStartup + managerSceneInstanceCheckInterval; // Check for an MRTK instance bool foundToolkitInstance = false; try { foreach (GameObject rootGameObject in scene.GetRootGameObjects()) { MixedRealityToolkit instance = rootGameObject.GetComponent(); if (instance != null) { foundToolkitInstance = true; // If we found an instance, and it's not the active instance, we probably want to activate it if (instance != MixedRealityToolkit.Instance) { // The only exception would be if the new instance has a different profile than the current instance // If that's the case, we could end up ping-ponging between two sets of manager scenes if (!instance.HasActiveProfile) { // If it doesn't have a profile, set it to our current profile instance.ActiveProfile = MixedRealityToolkit.Instance.ActiveProfile; } else if (instance.ActiveProfile != MixedRealityToolkit.Instance.ActiveProfile) { Debug.LogWarning("The active profile of the instance in your manager scene is different from the profile that loaded your scene. This is not recommended."); } else { Debug.LogWarning("Setting the manager scene MixedRealityToolkit instance to the active instance."); MixedRealityToolkit.SetActiveInstance(instance); } } break; } } } catch (Exception) { // This can happen if the scene isn't valid // Not an issue - we'll take care of it on the next update. return; } if (!foundToolkitInstance) { GameObject mrtkGo = new GameObject("MixedRealityToolkit"); MixedRealityToolkit toolkitInstance = mrtkGo.AddComponent(); try { SceneManager.MoveGameObjectToScene(mrtkGo, scene); // Set the scene as dirty EditorSceneManager.MarkSceneDirty(scene); } catch (Exception) { // This can happen if the scene isn't valid // Not an issue - we'll take care of it on the next update. // Destroy the new manager GameObject.DestroyImmediate(mrtkGo); return; } MixedRealityToolkit.SetActiveInstance(toolkitInstance); Debug.LogWarning("Didn't find a MixedRealityToolkit instance in your manager scene. Creating one now."); } } } else { Debug.Log("Couldn't load manager scene!"); } } /// /// If a lighting scene is being used, this ensures that at least one lighting scene is loaded in editor. /// private void EditorUpdateLightingScene(bool heirarchyDirty) { if (!Profile.UseLightingScene || !Profile.EditorManageLoadedScenes) { return; } if (string.IsNullOrEmpty(ActiveLightingScene)) { ActiveLightingScene = Profile.DefaultLightingScene.Name; } else { foreach (SceneInfo lightingScene in Profile.LightingScenes) { if (lightingScene.Name == ActiveLightingScene) { Scene scene; if (EditorSceneUtils.LoadScene(lightingScene, false, out scene)) { EditorSceneUtils.CopyLightingSettingsToActiveScene(scene); if (Profile.EditorEnforceLightingSceneTypes && heirarchyDirty) { EditorEnforceLightingSceneTypes(scene); } } if (Profile.EditorEnforceSceneOrder) { // If we're enforcing scene order, make sure this scene comes after the current scene Scene currentFirstScene = EditorSceneManager.GetSceneAt(0); EditorSceneManager.MoveSceneAfter(scene, currentFirstScene); } } else { EditorSceneUtils.UnloadScene(lightingScene, true); } } } } /// /// Ensures that only approved component types are present in lighting scenes. /// private void EditorEnforceLightingSceneTypes(Scene scene) { if (EditorSceneManager.sceneCount == 1) { // There's nowhere to move invalid objects to. return; } List violations = new List(); if (EditorSceneUtils.EnforceSceneComponents(scene, Profile.PermittedLightingSceneComponentTypes, violations)) { Scene targetScene = default(Scene); for (int i = 0; i < EditorSceneManager.sceneCount; i++) { targetScene = EditorSceneManager.GetSceneAt(i); if (targetScene.path != scene.path) { // We'll move invalid items to this scene break; } } if (!targetScene.IsValid() || !targetScene.isLoaded) { // Something's gone wrong - don't proceed return; } HashSet rootObjectsToMove = new HashSet(); foreach (Component component in violations) { rootObjectsToMove.Add(component.transform.root); } List rootObjectNames = new List(); // Build a list of root objects so they know what's being moved foreach (Transform rootObject in rootObjectsToMove) { rootObjectNames.Add(rootObject.name); } EditorUtility.DisplayDialog( "Invalid components found in " + scene.name, "Only lighting-related components are permitted. The following GameObjects will be moved to another scene:\n\n" + String.Join("\n", rootObjectNames) + "\n\nTo disable this warning, un-check 'EditorEnforceLightingSceneTypes' in your SceneSystem profile.", "OK"); try { foreach (Transform rootObject in rootObjectsToMove) { EditorSceneManager.MoveGameObjectToScene(rootObject.gameObject, targetScene); } EditorGUIUtility.PingObject(rootObjectsToMove.FirstOrDefault()); } catch (Exception) { // This can happen if the move object operation fails. No big deal, we'll try again next time. return; } } } /// /// Adds all scenes from profile into build settings. /// private void EditorUpdateBuildSettings() { if (!Profile.EditorManageBuildSettings) { // Nothing to do here return; } if (Profile.UseManagerScene) { if (EditorSceneUtils.AddSceneToBuildSettings( Profile.ManagerScene, cachedBuildScenes, EditorSceneUtils.BuildIndexTarget.First)) { cachedBuildScenes = EditorBuildSettings.scenes; } } foreach (SceneInfo contentScene in Profile.ContentScenes) { if (EditorSceneUtils.AddSceneToBuildSettings( contentScene, cachedBuildScenes, EditorSceneUtils.BuildIndexTarget.None)) { cachedBuildScenes = EditorBuildSettings.scenes; } } if (Profile.UseLightingScene) { foreach (SceneInfo lightingScene in Profile.LightingScenes) { // Make sure ALL lighting scenes are added to build settings if (EditorSceneUtils.AddSceneToBuildSettings( lightingScene, cachedBuildScenes, EditorSceneUtils.BuildIndexTarget.Last)) { cachedBuildScenes = EditorBuildSettings.scenes; } } } EditorCheckForSceneNameDuplicates(); } /// /// Ensures that there are no scenes in build settings with duplicate names. /// If any are found, a resolve duplicates window is launched. /// private void EditorCheckForSceneNameDuplicates() { List allScenes = new List(); Dictionary> duplicates = new Dictionary>(); foreach (SceneInfo sceneInfo in Profile.LightingScenes) { if (!sceneInfo.IsEmpty) { // Don't bother with empty scenes, they'll be handled elsewhere. allScenes.Add(sceneInfo); } } foreach (SceneInfo sceneInfo in Profile.ContentScenes) { if (!sceneInfo.IsEmpty) { // Don't bother with empty scenes, they'll be handled elsewhere. allScenes.Add(sceneInfo); } } if (Profile.UseManagerScene && !Profile.ManagerScene.IsEmpty) { allScenes.Add(Profile.ManagerScene); } if (EditorSceneUtils.CheckBuildSettingsForDuplicates(allScenes, duplicates)) { // If it's already open, don't display if (!ResolveDuplicateScenesWindow.IsOpen) { ResolveDuplicateScenesWindow window = EditorWindow.GetWindow("Fix Duplicate Scene Names"); window.ResolveDuplicates(duplicates, allScenes); } } else if (ResolveDuplicateScenesWindow.IsOpen) { // If we fixed the issue without the window, close the window ResolveDuplicateScenesWindow.Instance.Close(); } } #endif } }