using System.Collections.Generic; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.AI; using UnityEngine.SceneManagement; namespace Unity.AI.Navigation.Updater { #pragma warning disable 618 internal static class OffMeshLinkUpdaterUtility { /// /// A structure holding the information of failed conversions. /// public struct FailedConversion { public int itemIndex; public string failureMessage; } /// /// Find all prefabs and scenes that contain OffMeshLink components. /// This method also finds Prefab Variants which has removed OffMeshLink components. /// /// Folders to search for prefabs and scenes. If null, the whole project is searched. /// List of asset GUIDs to convert. public static List FindObjectsToConvert(string[] searchInFolders = null) { var prefabGuids = AssetDatabase.FindAssets("t:Prefab", searchInFolders); var objectsToConvert = new HashSet(); foreach (var guid in prefabGuids) { var prefab = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid)); if (prefab.GetComponentsInChildren(true).Length > 0) objectsToConvert.Add(guid); var isPrefabVariant = PrefabUtility.IsPartOfVariantPrefab(prefab); if (isPrefabVariant) { var removedComponents = PrefabUtility.GetRemovedComponents(prefab); foreach (var removedComponent in removedComponents) { if (removedComponent.assetComponent is OffMeshLink) { objectsToConvert.Add(guid); break; } } } } var sceneGuids = AssetDatabase.FindAssets("t:Scene", searchInFolders); foreach (var guid in sceneGuids) { var scenePath = AssetDatabase.GUIDToAssetPath(guid); var scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive); var rootGameObjects = scene.GetRootGameObjects(); foreach (var rootGameObject in rootGameObjects) { var offMeshLinksInHierarchy = rootGameObject.GetComponentsInChildren(true); if (offMeshLinksInHierarchy.Length == 0) continue; var offMeshLinks = FindAllOffMeshLinksInHierarchy(rootGameObject); if (offMeshLinks.Count > 0) { objectsToConvert.Add(guid); break; } } CloseScene(scene); } var returnList = new List(objectsToConvert); return returnList; } static void CloseScene(Scene scene) { // EditorSceneManager does not support closing the last scene, so we open a new empty scene instead. if (EditorSceneManager.loadedSceneCount == 1) EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); else EditorSceneManager.CloseScene(scene, true); } /// /// Convert all objects in the list to use NavMeshLink instead of OffMeshLink. /// /// List of asset GUIDs to convert. /// List of failed conversions. public static void Convert(List objectsToConvert, out List failedConversions) { failedConversions = new List(); var offMeshLinkToNavMeshLink = new Dictionary(); var failedToConvert = new HashSet(); // Initial sorting of objects to convert. // We want to deal with source prefabs first, then their variants, so that the NavMeshLink components are created in the correct order. SortAssetsToConvert(objectsToConvert); // First convert, create NavMeshLink with OffMeshLink values foreach (var guid in objectsToConvert) { var pathToObject = AssetDatabase.GUIDToAssetPath(guid); if (!DoesAssetExistOnDisk(pathToObject)) { failedToConvert.Add(guid); failedConversions.Add(new FailedConversion { itemIndex = objectsToConvert.IndexOf(guid), failureMessage = $"Cannot find the asset at path {pathToObject}. Please make sure the file exists." }); continue; } if (!CanWriteToAsset(pathToObject)) { failedToConvert.Add(guid); failedConversions.Add(new FailedConversion { itemIndex = objectsToConvert.IndexOf(guid), failureMessage = $"Cannot write to the asset at path {pathToObject}. Please make sure the file is not read-only." }); continue; } if (TryGetPrefabFromPath(pathToObject, out var prefab)) ConvertPrefab(prefab, offMeshLinkToNavMeshLink); else if (TryGetSceneFromPath(pathToObject, out var scene)) { ConvertScene(scene, ref offMeshLinkToNavMeshLink); CloseScene(scene); } } // Remove failed conversions foreach (var guid in failedToConvert) objectsToConvert.Remove(guid); // Second convert, apply prefab overrides to NavMeshLink foreach (var guid in objectsToConvert) { var pathToObject = AssetDatabase.GUIDToAssetPath(guid); if (TryGetPrefabFromPath(pathToObject, out var prefab)) { ApplyOverrideDataToPrefab(prefab, offMeshLinkToNavMeshLink); SyncOverriddenRemovalsOfComponents(prefab, offMeshLinkToNavMeshLink); } else if (TryGetSceneFromPath(pathToObject, out var scene)) { ApplyOverrideDataToScene(scene, offMeshLinkToNavMeshLink); CloseScene(scene); } } // Remove OffMeshLinks from the objects foreach (var guid in objectsToConvert) { var pathToObject = AssetDatabase.GUIDToAssetPath(guid); if (TryGetPrefabFromPath(pathToObject, out var prefab)) { var offMeshLinks = prefab.GetComponentsInChildren(true); foreach (var offMeshLink in offMeshLinks) Object.DestroyImmediate(offMeshLink, true); // Since we are removing components in source prefabs, we have to "revert removed components" // on prefab instances, so that we don't store the removal data in the meta files. if (PrefabUtility.IsAnyPrefabInstanceRoot(prefab)) RevertRemovedComponent(prefab); PrefabUtility.SavePrefabAsset(prefab); } else if (TryGetSceneFromPath(pathToObject, out var scene)) { var rootGameObjects = scene.GetRootGameObjects(); foreach (var rootGameObject in rootGameObjects) { var offMeshLinks = rootGameObject.GetComponentsInChildren(true); foreach (var offMeshLink in offMeshLinks) Object.DestroyImmediate(offMeshLink, true); } EditorSceneManager.SaveScene(scene); CloseScene(scene); } } } /// /// Sorts the list of objects to convert so that source prefabs are first. /// /// List of asset GUIDs to convert. static void SortAssetsToConvert(List objectsToConvert) { var sortedAssetGuids = new List(objectsToConvert.Count); for (var i = 0; i < objectsToConvert.Count; i++) { var assetGuid = objectsToConvert[i]; if (sortedAssetGuids.Contains(assetGuid)) continue; var prefabGameObject = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(assetGuid)); if (prefabGameObject == null) sortedAssetGuids.Add(assetGuid); else SortPrefabsToConvert(sortedAssetGuids, prefabGameObject, assetGuid); } objectsToConvert.Clear(); objectsToConvert.AddRange(sortedAssetGuids); } static void SortPrefabsToConvert(List objectsToConvert, GameObject prefabRoot, string prefabRootGuid) { if (objectsToConvert.Contains(prefabRootGuid)) return; var source = PrefabUtility.GetCorrespondingObjectFromSource(prefabRoot); if (source == null) { objectsToConvert.Add(prefabRootGuid); return; } var sourceGuid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(source)); SortPrefabsToConvert(objectsToConvert, source, sourceGuid); objectsToConvert.Add(prefabRootGuid); } static void RevertRemovedComponent(GameObject prefab) { var removedComponents = PrefabUtility.GetRemovedComponents(prefab); if (removedComponents == null) return; foreach (var removedComponent in removedComponents) { if (removedComponent.assetComponent is OffMeshLink) PrefabUtility.RevertRemovedComponent(prefab, removedComponent.assetComponent, InteractionMode.AutomatedAction); } } static bool TryGetPrefabFromPath(string path, out GameObject prefab) { prefab = AssetDatabase.LoadAssetAtPath(path); return prefab != null && PrefabUtility.IsPartOfPrefabAsset(prefab); } static bool TryGetSceneFromPath(string path, out Scene scene) { var sceneAsset = AssetDatabase.LoadAssetAtPath(path); if (sceneAsset != null) { scene = EditorSceneManager.OpenScene(path, OpenSceneMode.Additive); return true; } scene = default; return false; } static List FindAllOffMeshLinksInHierarchy(GameObject rootGameObject) { var arr = rootGameObject.GetComponentsInChildren(true); var offMeshLinks = new List(arr); for (var i = offMeshLinks.Count - 1; i >= 0; i--) { var gameObject = offMeshLinks[i].gameObject; if (PrefabUtility.IsAnyPrefabInstanceRoot(gameObject) && gameObject != rootGameObject) offMeshLinks.RemoveAt(i); } return offMeshLinks; } static void ConvertPrefab(GameObject prefabRoot, Dictionary offMeshLinkToNavMeshLink) { var offMeshLinks = FindAllOffMeshLinksInHierarchy(prefabRoot); var updatedPrefab = false; foreach (var offMeshLink in offMeshLinks) { var gameObject = offMeshLink.gameObject; var isPrefabVariant = PrefabUtility.IsPartOfVariantPrefab(gameObject); // If the prefab is a variant and the OffMeshLink is not an override, // only store the variant's OffMeshLink and NavMeshLink pair, and skip the rest of the conversion. if (isPrefabVariant && !PrefabUtility.IsAddedComponentOverride(offMeshLink)) { var sourceOml = PrefabUtility.GetCorrespondingObjectFromSource(offMeshLink); var sourceNML = offMeshLinkToNavMeshLink[sourceOml]; var variantNML = GetCorrespondingVariantComponent(sourceNML, prefabRoot); offMeshLinkToNavMeshLink.Add(offMeshLink, variantNML); continue; } var navMeshLink = gameObject.AddComponent(); offMeshLinkToNavMeshLink.Add(offMeshLink, navMeshLink); // If the OffMeshLink is an override (created by a Prefab Variant), record the new NavMeshLink as an override. if (isPrefabVariant) PrefabUtility.RecordPrefabInstancePropertyModifications(gameObject); CopyValues(offMeshLink, navMeshLink); updatedPrefab = true; } if (updatedPrefab) PrefabUtility.SavePrefabAsset(prefabRoot); } static void ConvertScene(Scene scene, ref Dictionary offMeshLinkToNavMeshLink) { var rootGameObjects = scene.GetRootGameObjects(); foreach (var rootGameObject in rootGameObjects) { // Skip prefab instances, they are taken care of in the ConvertPrefab method. if (PrefabUtility.IsAnyPrefabInstanceRoot(rootGameObject)) continue; var offMeshLinks = FindAllOffMeshLinksInHierarchy(rootGameObject); foreach (var offMeshLink in offMeshLinks) { var navMeshLink = offMeshLink.gameObject.AddComponent(); offMeshLinkToNavMeshLink.Add(offMeshLink, navMeshLink); CopyValues(offMeshLink, navMeshLink); } } EditorSceneManager.SaveScene(scene); } static bool CanWriteToAsset(string filePath) { var fileInfo = new System.IO.FileInfo(filePath); return !fileInfo.Attributes.HasFlag(System.IO.FileAttributes.ReadOnly); } static bool DoesAssetExistOnDisk(string filePath) { return !string.IsNullOrEmpty(filePath) && System.IO.File.Exists(filePath); } static void CopyValues(OffMeshLink offMeshLink, NavMeshLink navMeshLink) { navMeshLink.activated = offMeshLink.activated; navMeshLink.autoUpdate = offMeshLink.autoUpdatePositions; navMeshLink.bidirectional = offMeshLink.biDirectional; navMeshLink.costModifier = offMeshLink.costOverride; navMeshLink.startTransform = offMeshLink.startTransform; navMeshLink.endTransform = offMeshLink.endTransform; } /// /// Transfers any existing override data from OffMeshLink components to NavMeshLink components. /// /// The out-most prefab root /// Dictionary storing the OffMeshLink and NavMeshLink pairs static void ApplyOverrideDataToPrefab(GameObject outerPrefabRoot, Dictionary offMeshLinkToNavMeshLink) { var didUpdate = false; var updatedRoots = new HashSet(); var offMeshLinks = outerPrefabRoot.GetComponentsInChildren(true); foreach (var offMeshLink in offMeshLinks) { // Find the prefab root and check if we have already updated it. // A null root means that the root is the outerPrefabRoot. var rootGameObject = PrefabUtility.GetNearestPrefabInstanceRoot(offMeshLink.gameObject); rootGameObject = rootGameObject == null ? offMeshLink.gameObject : rootGameObject; if (updatedRoots.Contains(rootGameObject)) continue; var propertyModifications = PrefabUtility.GetPropertyModifications(rootGameObject); if (propertyModifications == null) continue; UpdatePropertyModifications(ref propertyModifications, offMeshLinkToNavMeshLink); PrefabUtility.SetPropertyModifications(rootGameObject, propertyModifications); updatedRoots.Add(rootGameObject); didUpdate = true; } if (didUpdate) PrefabUtility.SavePrefabAsset(outerPrefabRoot); } /// /// Removes the NavMeshLink components that correspond to OffMeshLink components that were removed from the variant prefab. /// static void SyncOverriddenRemovalsOfComponents(GameObject prefabRoot, IReadOnlyDictionary offMeshLinkToNavMeshLink) { var isPrefabVariant = PrefabUtility.IsPartOfVariantPrefab(prefabRoot); if (!isPrefabVariant) return; var removedComponents = PrefabUtility.GetRemovedComponents(prefabRoot); var variantNavMeshLinks = prefabRoot.GetComponents(); foreach (var removedComponent in removedComponents) { if (removedComponent.assetComponent is not OffMeshLink removedOffMeshLink) continue; var sourceNavMeshLink = offMeshLinkToNavMeshLink[removedOffMeshLink]; foreach(var variantComponent in variantNavMeshLinks) { var correspondingComponent = PrefabUtility.GetCorrespondingObjectFromSource(variantComponent); if (correspondingComponent == sourceNavMeshLink) { Object.DestroyImmediate(variantComponent, true); break; } } } PrefabUtility.RecordPrefabInstancePropertyModifications(prefabRoot); PrefabUtility.SavePrefabAsset(prefabRoot); } static void ApplyOverrideDataToScene(Scene scene, Dictionary offMeshLinkToNavMeshLink) { var rootGameObjects = scene.GetRootGameObjects(); var updatedRoots = new HashSet(); foreach (var rootGameObject in rootGameObjects) { var offMeshLinks = rootGameObject.GetComponentsInChildren(); foreach (var offMeshLink in offMeshLinks) { // Non-prefab instances cannot have overrides, so continue. if (!PrefabUtility.IsPartOfAnyPrefab(offMeshLink)) continue; // Find the prefab root and check if it contains any overrides var prefabRoot = PrefabUtility.GetNearestPrefabInstanceRoot(offMeshLink.gameObject); if (!PrefabUtility.HasPrefabInstanceAnyOverrides(prefabRoot, false) && !updatedRoots.Contains(prefabRoot)) continue; var propertyModifications = PrefabUtility.GetPropertyModifications(prefabRoot); UpdatePropertyModifications(ref propertyModifications, offMeshLinkToNavMeshLink); PrefabUtility.SetPropertyModifications(prefabRoot, propertyModifications); updatedRoots.Add(prefabRoot); } } EditorSceneManager.SaveScene(scene); } static void UpdatePropertyModifications(ref PropertyModification[] propertyModifications, Dictionary offMeshLinkToNavMeshLink) { var updatedModifications = new List(propertyModifications); for (var i = 0; i < propertyModifications.Length; ++i) { var modification = propertyModifications[i]; // Skip if the target is not an OffMeshLink if (modification.target is not OffMeshLink oml) { updatedModifications.Add(modification); continue; } // Get the NavMeshLink that corresponds to the OffMeshLink. var navMeshLink = offMeshLinkToNavMeshLink[oml]; var newMod = new PropertyModification { target = navMeshLink, value = modification.value }; switch (modification.propertyPath) { case "m_AutoUpdatePositions": newMod.propertyPath = "m_AutoUpdatePosition"; break; case "m_CostOverride": newMod.propertyPath = "m_CostModifier"; if (float.TryParse(newMod.value, out var floatVal)) { if (floatVal > 0) { var overrideMod = new PropertyModification() { target = navMeshLink, propertyPath = "m_IsOverridingCost", value = "1" }; updatedModifications.Add(overrideMod); } } break; case "m_BiDirectional": newMod.propertyPath = "m_Bidirectional"; break; case "m_Start": newMod.propertyPath = "m_StartTransform"; break; case "m_End": newMod.propertyPath = "m_EndTransform"; break; case "m_Activated": newMod.propertyPath = modification.propertyPath; break; } updatedModifications.Add(newMod); } propertyModifications = updatedModifications.ToArray(); } /// /// Get the corresponding variant component of a source prefab component. /// /// The source prefab component /// The variant prefab /// The type of component to search for /// Returns the corresponding component on the variant of the source prefab component, if found, or null. static TComponent GetCorrespondingVariantComponent(TComponent component, GameObject variantPrefab) where TComponent : Component { var variantComponents = variantPrefab.GetComponents(component.GetType()); foreach (var variantComponent in variantComponents) { var correspondingComponent = PrefabUtility.GetCorrespondingObjectFromSource(variantComponent); if (correspondingComponent == component) return variantComponent as TComponent; } return null; } } #pragma warning restore 618 }