using System; using System.Collections.Generic; using System.IO; using Unity.XR.CoreUtils; using Unity.XR.CoreUtils.Editor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.XR.Simulation; namespace UnityEditor.XR.Simulation { /// /// Manager that handles collection of simulation environment prefabs in the Editor. /// [ScriptableSettingsPath(SimulationConstants.userSettingsPath)] class SimulationEnvironmentAssetsManager : EditorScriptableSettings { static readonly Comparer k_PrefabPathsComparer = Comparer.Default; const string k_PrefabFilter = "t:prefab"; const string k_RefreshMenuItemCategory = "Assets/AR Foundation"; const string k_RefreshMenuItemName = "Refresh XR Environment List"; const string k_RefreshMenuItemPath = k_RefreshMenuItemCategory + "/" + k_RefreshMenuItemName; /// /// Default file name for a newly created environment asset. /// public const string newEnvironmentFileName = "Simulation Environment.prefab"; [SerializeField] [HideInInspector] List m_EnvironmentPrefabPaths = new(); [SerializeField] [HideInInspector] bool m_FallbackAtEndOfList; /// /// Number of environments available. /// public int environmentsCount => m_EnvironmentPrefabPaths.Count; /// /// Whether there is currently an active environment. /// public bool activeEnvironmentExists => XRSimulationPreferences.Instance.activeEnvironmentPrefab != null; public event Action activeEnvironmentChanged; public event Action availableEnvironmentsChanged; [MenuItem(k_RefreshMenuItemPath)] static void RefreshEnvironmentListMenuItem() { Instance.CollectEnvironments(); } /// /// Gathers all environment assets handled by this manager and saves them to a list of available environments. /// public void CollectEnvironments() { var simulationPreferences = XRSimulationPreferences.Instance; var fallbackEnvPrefab = simulationPreferences.fallbackEnvironmentPrefab; var fallbackEnvPath = ""; m_EnvironmentPrefabPaths.Clear(); var prefabGuids = AssetDatabase.FindAssets(k_PrefabFilter); foreach (var guid in prefabGuids) { var path = AssetDatabase.GUIDToAssetPath(guid); var simEnvironment = AssetDatabase.LoadAssetAtPath(path); if (simEnvironment == null) continue; if (simEnvironment.gameObject == fallbackEnvPrefab) fallbackEnvPath = path; else if (!simEnvironment.excludeFromSelectionUI) m_EnvironmentPrefabPaths.Add(path); } m_EnvironmentPrefabPaths.Sort(k_PrefabPathsComparer); // Show fallback environment at the bottom m_FallbackAtEndOfList = !string.IsNullOrEmpty(fallbackEnvPath); if (m_FallbackAtEndOfList) m_EnvironmentPrefabPaths.Add(fallbackEnvPath); EditorUtility.SetDirty(this); availableEnvironmentsChanged?.Invoke(); // If no environment is available, even the fallback, we treat this as the active environment being changed so UI can update if (m_EnvironmentPrefabPaths.Count == 0) { activeEnvironmentChanged?.Invoke(); return; } if (simulationPreferences.environmentPrefab == null) { // Ensure active environment is set if possible SelectEnvironmentAtIndex(0); } } /// /// Sets the active environment to the one at the given index in the list of available environments. /// public void SelectEnvironmentAtIndex(int index) { var envCount = environmentsCount; if (index < 0 || index >= envCount) throw new IndexOutOfRangeException($"Cannot select environment at index {index} outside the range of available environments."); var simulationPreferences = XRSimulationPreferences.Instance; if (index == envCount - 1 && m_FallbackAtEndOfList) { // Ensure fallback is used by clearing out the environment prefab field simulationPreferences.environmentPrefab = null; } else { var environmentPath = m_EnvironmentPrefabPaths[index]; var selectedEnvironmentPrefab = AssetDatabase.LoadAssetAtPath(environmentPath); if (selectedEnvironmentPrefab == null) { Debug.LogError($"Failed to load environment prefab '{environmentPath}'. " + $"Try refreshing environments by going to {k_RefreshMenuItemCategory} > {k_RefreshMenuItemName}."); return; } simulationPreferences.environmentPrefab = selectedEnvironmentPrefab; } EditorUtility.SetDirty(simulationPreferences); activeEnvironmentChanged?.Invoke(); } /// /// Gets the file path of the active environment asset. /// public string GetActiveEnvironmentPath() { var activeEnvironment = XRSimulationPreferences.Instance.activeEnvironmentPrefab; return activeEnvironment != null ? AssetDatabase.GetAssetPath(activeEnvironment) : null; } /// /// Gets the name of the active environment for displaying in UI. /// public string GetActiveEnvironmentDisplayName() { var activeEnvironment = XRSimulationPreferences.Instance.activeEnvironmentPrefab; return activeEnvironment != null ? activeEnvironment.name : null; } /// /// Gets the index of the active environment in the list of available environments. /// public int GetActiveEnvironmentIndex() { var activeEnvironment = XRSimulationPreferences.Instance.activeEnvironmentPrefab; if (activeEnvironment == null) return -1; var environmentPath = AssetDatabase.GetAssetPath(activeEnvironment); return m_EnvironmentPrefabPaths.IndexOf(environmentPath); } /// /// Fills out the given list with names for dropdown menu items corresponding to each environment in the list of available environments. /// public void GetAllEnvironmentMenuItemNames(List names) { var count = environmentsCount; if (count == 0) return; for (var i = 0; i < count - 1; i++) { names.Add(GetDirectoryAndFile(m_EnvironmentPrefabPaths[i])); } // Fallback environment is a special case - just show the file name var lastPath = m_EnvironmentPrefabPaths[count - 1]; names.Add(m_FallbackAtEndOfList ? Path.GetFileNameWithoutExtension(lastPath) : GetDirectoryAndFile(lastPath)); } static string GetDirectoryAndFile(string path) { if (string.IsNullOrEmpty(path)) return ""; var length = path.Length; var startIndex = length - 1; var lastIndex = length; var slashCount = 0; for (; startIndex >= 0 && slashCount <= 1; --startIndex) { var c = path[startIndex]; if (c == '/') slashCount++; else if (lastIndex == length && c == '.') lastIndex = startIndex; } if (startIndex < 0) { startIndex = 0; } else { // +1 to ignore last for loop iteration and +1 to ignore last '/' startIndex += 2; } return path.Substring(startIndex, lastIndex - startIndex); } /// /// Tries to create a new environment asset at the given path. /// /// File path where the new environment should be created. /// Resulting index of the new environment in the list of available environments. /// True if creation was successful, false otherwise. public bool TryCreateEnvironment(string assetPath, out int newEnvironmentIndex) { var newEnvironmentPath = AssetDatabase.GenerateUniqueAssetPath(assetPath); GameObject newEnvironmentGameObject = null; var defaultEnvironment = XRSimulationPreferences.Instance.fallbackEnvironmentPrefab; if (defaultEnvironment != null && defaultEnvironment.GetComponent() != null) { var defaultEnvironmentPath = AssetDatabase.GetAssetPath(defaultEnvironment); if (AssetDatabase.CopyAsset(defaultEnvironmentPath, newEnvironmentPath)) { var newEnvironment = AssetDatabase.LoadAssetAtPath(newEnvironmentPath); newEnvironment.excludeFromSelectionUI = false; EditorUtility.SetDirty(newEnvironment); newEnvironmentGameObject = newEnvironment.gameObject; } else Debug.LogWarning($"Failed to copy {defaultEnvironmentPath}. Creating blank environment."); } if (newEnvironmentGameObject == null) { var newEnvironmentInstance = new GameObject(); newEnvironmentInstance.AddComponent(); var newPrefab = PrefabUtility.SaveAsPrefabAsset(newEnvironmentInstance, newEnvironmentPath, out var creationSuccess); if (creationSuccess) newEnvironmentGameObject = newPrefab; DestroyImmediate(newEnvironmentInstance); } if (newEnvironmentGameObject != null) { ProjectWindowUtil.ShowCreatedAsset(newEnvironmentGameObject); newEnvironmentIndex = AddEnvironment(newEnvironmentPath); return true; } Debug.LogError($"Failed to create simulation environment at path {newEnvironmentPath}."); newEnvironmentIndex = -1; return false; } /// /// Tries to create a duplicate of the active environment asset at the given path. /// /// File path where the new environment should be created. /// Resulting index of the new environment in the list of available environments. /// True if duplication was successful, false otherwise. public bool TryDuplicateActiveEnvironment(string assetPath, out int newEnvironmentIndex) { var activeEnvironment = XRSimulationPreferences.Instance.activeEnvironmentPrefab; if (activeEnvironment == null) { Debug.LogError("No active environment available to duplicate."); newEnvironmentIndex = -1; return false; } var activeEnvironmentPath = AssetDatabase.GetAssetPath(activeEnvironment); var newEnvironmentPath = AssetDatabase.GenerateUniqueAssetPath(assetPath); if (AssetDatabase.CopyAsset(activeEnvironmentPath, newEnvironmentPath)) { var newEnvironmentGameObject = AssetDatabase.LoadAssetAtPath(newEnvironmentPath); ProjectWindowUtil.ShowCreatedAsset(newEnvironmentGameObject); newEnvironmentIndex = AddEnvironment(newEnvironmentPath); return true; } Debug.LogError($"Failed to duplicate simulation environment at path {activeEnvironmentPath}."); newEnvironmentIndex = -1; return false; } /// /// Adds an environment to the list of available environments. /// /// File path of the environment asset to add. /// The index of the new environment in the list of available environments. int AddEnvironment(string environmentAssetPath) { // The environment might already exist in the list, if it was caught by the asset postprocessor first var existingIndex = m_EnvironmentPrefabPaths.IndexOf(environmentAssetPath); if (existingIndex >= 0) return existingIndex; var envCount = environmentsCount; var countMinusFallback = m_FallbackAtEndOfList && envCount > 0 ? envCount - 1 : envCount; var environmentIndex = countMinusFallback; for (var i = 0; i < countMinusFallback; i++) { if (k_PrefabPathsComparer.Compare(environmentAssetPath, m_EnvironmentPrefabPaths[i]) > 0) continue; environmentIndex = i; break; } m_EnvironmentPrefabPaths.Insert(environmentIndex, environmentAssetPath); EditorUtility.SetDirty(this); availableEnvironmentsChanged?.Invoke(); return environmentIndex; } /// /// Is the active environment asset editable? /// public bool IsActiveEnvironmentEditable() { var activeEnvironment = XRSimulationPreferences.Instance.activeEnvironmentPrefab; return activeEnvironment != null && !PrefabUtility.IsPartOfImmutablePrefab(activeEnvironment); } /// /// Opens the active environment asset for editing. /// public void OpenActiveEnvironmentForEditing() { var activeEnvironment = XRSimulationPreferences.Instance.activeEnvironmentPrefab; if (activeEnvironment != null) PrefabStageUtility.OpenPrefab(AssetDatabase.GetAssetPath(activeEnvironment)); } public static GUID GetActiveEnvironmentAssetGuid() { var activeEnvironment = XRSimulationPreferences.Instance.activeEnvironmentPrefab; if (activeEnvironment == null) return default; var environmentPath = AssetDatabase.GetAssetPath(activeEnvironment); return AssetDatabase.GUIDFromAssetPath(environmentPath); } class EnvironmentAssetPostprocessor : AssetPostprocessor { static bool s_NeedsRefresh; void OnPostprocessPrefab(GameObject g) { if (g.GetComponent() != null) s_NeedsRefresh = true; // Settings instance may not exist on first import, and we should not try to create one if (BaseInstance == null) return; if (BaseInstance.m_EnvironmentPrefabPaths.Contains(AssetDatabase.GetAssetPath(g))) s_NeedsRefresh = true; } static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { if (!s_NeedsRefresh) { if (BaseInstance == null) return; var environmentPaths = BaseInstance.m_EnvironmentPrefabPaths; foreach (var deletedAssetPath in deletedAssets) { if (environmentPaths.Contains(deletedAssetPath)) { s_NeedsRefresh = true; break; } } } if (s_NeedsRefresh && BaseInstance != null) BaseInstance.CollectEnvironments(); s_NeedsRefresh = false; } } } }