using System.Collections.Generic; #if UNITY_EDITOR using UnityEditor; using UnityEditor.SceneManagement; #endif using UnityEngine; using UnityEngine.AI; #pragma warning disable IDE1006 // Unity-specific lower case public property names namespace Unity.AI.Navigation { /// Component used to create a navigable link between two NavMesh locations. [ExecuteAlways] [DefaultExecutionOrder(-101)] [AddComponentMenu("Navigation/NavMesh Link", 33)] [HelpURL(HelpUrls.Manual + "NavMeshLink.html")] public partial class NavMeshLink : MonoBehaviour { // Serialized version is used to upgrade older serialized data to the current format. // Version 0: Initial version. // Version 1: Added m_IsOverridingCost field and made m_CostModifier always positive. [SerializeField, HideInInspector] byte m_SerializedVersion = 0; [SerializeField] int m_AgentTypeID; [SerializeField] Vector3 m_StartPoint = new(0.0f, 0.0f, -2.5f); [SerializeField] Vector3 m_EndPoint = new(0.0f, 0.0f, 2.5f); [SerializeField] Transform m_StartTransform; [SerializeField] Transform m_EndTransform; [SerializeField] bool m_Activated = true; [SerializeField] float m_Width; // This field's value in combination with m_IsOverridingCost determines the value of the costModifier property, // where m_IsOverridingCost determines the sign of the value. The costModifier property is positive or zero when // m_IsOverridingCost is true, and negative when m_IsOverridingCost is false. // Note that when m_SerializedVersion >= 1, m_CostModifier will always become positive or zero. Newly created // components are always upgraded to at least version 1 at initialization time. [SerializeField] [Min(0f)] float m_CostModifier = -1f; [SerializeField] bool m_IsOverridingCost = false; [SerializeField] bool m_Bidirectional = true; [SerializeField] bool m_AutoUpdatePosition; [SerializeField] int m_Area; /// Gets or sets the type of agent that can use the link. public int agentTypeID { get => m_AgentTypeID; set { if (value == m_AgentTypeID) return; m_AgentTypeID = value; UpdateLink(); } } /// Gets or sets the local position at the middle of the link's start edge, relative to the GameObject origin. /// This property determines the position of the link's start edge only when is `null`. Otherwise, it is the `startTransform` that determines the edge's position.
/// The world scale of the GameObject is never used.
public Vector3 startPoint { get => m_StartPoint; set { if (value == m_StartPoint) return; m_StartPoint = value; UpdateLink(); } } /// Gets or sets the local position at the middle of the link's end edge, relative to the GameObject origin. /// This property determines the position of the link's end edge only when is `null`. Otherwise, it is the `endTransform` that determines the edge's position.
/// The world scale of the GameObject is never used.
public Vector3 endPoint { get => m_EndPoint; set { if (value == m_EndPoint) return; m_EndPoint = value; UpdateLink(); } } /// Gets or sets the tracked by the middle of the link's start edge. /// The link places the start edge at the world position of the object referenced by this property. In that case is not used. Otherwise, when this property is `null`, the component applies the GameObject's translation and rotation as a transform to in order to establish the world position of the link's start edge. public Transform startTransform { get => m_StartTransform; set { if (value == m_StartTransform) return; m_StartTransform = value; UpdateLink(); } } /// Gets or sets the tracked by the middle of the link's end edge. /// The link places the end edge at the world position of the object referenced by this property. In that case is not used. Otherwise, when this property is `null`, the component applies the GameObject's translation and rotation as a transform to in order to establish the world position of the link's end edge. public Transform endTransform { get => m_EndTransform; set { if (value == m_EndTransform) return; m_EndTransform = value; UpdateLink(); } } /// The width of the segments making up the ends of the link. /// The segments are created perpendicular to the line from start to end, in the XZ plane of the GameObject. public float width { get => m_Width; set { if (value.Equals(m_Width)) return; m_Width = value; UpdateLink(); } } /// Gets or sets a value that determines the cost of traversing the link. /// A negative value implies that the cost of traversing the link is obtained based on the area type.
/// A positive or zero value overrides the cost associated with the area type.
public float costModifier { get => m_IsOverridingCost ? m_CostModifier : -m_CostModifier; set { var shouldOverride = value >= 0f; if (value.Equals(costModifier) && shouldOverride == m_IsOverridingCost) return; m_IsOverridingCost = shouldOverride; m_CostModifier = Mathf.Abs(value); UpdateLink(); } } /// Gets or sets whether agents can traverse the link in both directions. /// When a link connects to NavMeshes at both ends, agents can always traverse that link from the start position to the end position. When this property is set to `true` it allows the agents to traverse the link from the end position to the start position as well. When the value is `false` the agents will not traverse the link from the end position to the start position. public bool bidirectional { get => m_Bidirectional; set { if (value == m_Bidirectional) return; m_Bidirectional = value; UpdateLink(); } } /// Gets or sets whether the world positions of the link's edges update whenever /// the GameObject transform, the or the change at runtime. public bool autoUpdate { get => m_AutoUpdatePosition; set { if (value == m_AutoUpdatePosition) return; m_AutoUpdatePosition = value; if (m_AutoUpdatePosition) AddTracking(this); else RemoveTracking(this); } } /// The area type of the link. public int area { get => m_Area; set { if (value == m_Area) return; m_Area = value; UpdateLink(); } } /// Gets or sets whether the link can be traversed by agents. /// When this property is set to `true` it allows the agents to traverse the link. When the value is `false` no paths pass through this link and no agent can traverse it as part of their autonomous movement. public bool activated { get => m_Activated; set { m_Activated = value; NavMesh.SetLinkActive(m_LinkInstance, m_Activated); } } /// Checks whether any agent occupies the link at this moment in time. /// This property evaluates the internal state of the link every time it is used. public bool occupied => NavMesh.IsLinkOccupied(m_LinkInstance); NavMeshLinkInstance m_LinkInstance; bool m_StartTransformWasEmpty = true; bool m_EndTransformWasEmpty = true; Vector3 m_LastStartWorldPosition = Vector3.positiveInfinity; Vector3 m_LastEndWorldPosition = Vector3.positiveInfinity; Vector3 m_LastPosition = Vector3.positiveInfinity; Quaternion m_LastRotation = Quaternion.identity; static readonly List s_Tracked = new(); #if UNITY_EDITOR bool m_DelayEndpointUpgrade; static string s_LastWarnedPrefab; static double s_NextPrefabWarningTime; #endif [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] static void ClearTrackedList() { s_Tracked.Clear(); } void UpgradeSerializedVersion() { if (m_SerializedVersion < 1) { #if UNITY_EDITOR if (!StartEndpointUpgrade()) return; #endif m_SerializedVersion = 1; m_IsOverridingCost = m_CostModifier >= 0f; m_CostModifier = Mathf.Abs(m_CostModifier); if (m_StartTransform == gameObject.transform) m_StartTransform = null; if (m_EndTransform == gameObject.transform) m_EndTransform = null; } } // ensures serialized version is up-to-date at run-time, in case it was not updated in the Editor void Awake() => UpgradeSerializedVersion(); void OnEnable() { AddLink(); if (m_AutoUpdatePosition && NavMesh.IsLinkValid(m_LinkInstance)) AddTracking(this); } void OnDisable() { RemoveTracking(this); NavMesh.RemoveLink(m_LinkInstance); } /// Replaces the link with a new one using the current settings. public void UpdateLink() { if (!isActiveAndEnabled) return; NavMesh.RemoveLink(m_LinkInstance); AddLink(); } static void AddTracking(NavMeshLink link) { #if UNITY_EDITOR if (s_Tracked.Contains(link)) { Debug.LogError("Link is already tracked: " + link); return; } #endif if (s_Tracked.Count == 0) NavMesh.onPreUpdate += UpdateTrackedInstances; s_Tracked.Add(link); link.RecordEndpointTransforms(); } static void RemoveTracking(NavMeshLink link) { s_Tracked.Remove(link); if (s_Tracked.Count == 0) NavMesh.onPreUpdate -= UpdateTrackedInstances; } /// Gets the world positions of the start and end points for the link. /// Returns the world position of if it is not null; otherwise, transformed into world space. /// Returns the world position of if it is not null; otherwise, transformed into world space. internal void GetWorldPositions( out Vector3 worldStartPosition, out Vector3 worldEndPosition) { var startIsLocal = m_StartTransform == null; var endIsLocal = m_EndTransform == null; var toWorld = startIsLocal || endIsLocal ? LocalToWorldUnscaled() : Matrix4x4.identity; worldStartPosition = startIsLocal ? toWorld.MultiplyPoint3x4(m_StartPoint) : m_StartTransform.position; worldEndPosition = endIsLocal ? toWorld.MultiplyPoint3x4(m_EndPoint) : m_EndTransform.position; } /// Gets the positions of the start and end points in the local space of the link. /// Returns the local position of if it is not null; otherwise, . /// Returns the local position of if it is not null; otherwise, . internal void GetLocalPositions( out Vector3 localStartPosition, out Vector3 localEndPosition) { var startIsLocal = m_StartTransform == null; var endIsLocal = m_EndTransform == null; var toLocal = startIsLocal && endIsLocal ? Matrix4x4.identity : LocalToWorldUnscaled().inverse; localStartPosition = startIsLocal ? m_StartPoint : toLocal.MultiplyPoint3x4(m_StartTransform.position); localEndPosition = endIsLocal ? m_EndPoint : toLocal.MultiplyPoint3x4(m_EndTransform.position); } void AddLink() { #if UNITY_EDITOR if (NavMesh.IsLinkValid(m_LinkInstance)) { Debug.LogError("Link is already added: " + this); return; } #endif GetLocalPositions(out var localStartPosition, out var localEndPosition); var link = new NavMeshLinkData { startPosition = localStartPosition, endPosition = localEndPosition, width = m_Width, costModifier = costModifier, bidirectional = m_Bidirectional, area = m_Area, agentTypeID = m_AgentTypeID, }; m_LinkInstance = NavMesh.AddLink(link, transform.position, transform.rotation); if (NavMesh.IsLinkValid(m_LinkInstance)) { NavMesh.SetLinkOwner(m_LinkInstance, this); NavMesh.SetLinkActive(m_LinkInstance, m_Activated); } m_LastPosition = transform.position; m_LastRotation = transform.rotation; RecordEndpointTransforms(); GetWorldPositions(out m_LastStartWorldPosition, out m_LastEndWorldPosition); } internal void RecordEndpointTransforms() { m_StartTransformWasEmpty = m_StartTransform == null; m_EndTransformWasEmpty = m_EndTransform == null; } internal bool HaveTransformsChanged() { var startIsLocal = m_StartTransform == null; var endIsLocal = m_EndTransform == null; if (startIsLocal && endIsLocal && m_StartTransformWasEmpty && m_EndTransformWasEmpty && transform.position == m_LastPosition && transform.rotation == m_LastRotation) return false; var toWorld = startIsLocal || endIsLocal ? LocalToWorldUnscaled() : Matrix4x4.identity; var startWorldPos = startIsLocal ? toWorld.MultiplyPoint3x4(m_StartPoint) : m_StartTransform.position; if (startWorldPos != m_LastStartWorldPosition) return true; var endWorldPos = endIsLocal ? toWorld.MultiplyPoint3x4(m_EndPoint) : m_EndTransform.position; return endWorldPos != m_LastEndWorldPosition; } internal Matrix4x4 LocalToWorldUnscaled() { return Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one); } void OnDidApplyAnimationProperties() { UpdateLink(); } static void UpdateTrackedInstances() { foreach (var instance in s_Tracked) { if (instance.HaveTransformsChanged()) instance.UpdateLink(); instance.RecordEndpointTransforms(); } } #if UNITY_EDITOR void OnValidate() { // Ensures serialized version is up-to-date in the Editor irrespective of GameObject active state UpgradeSerializedVersion(); m_Width = Mathf.Max(0.0f, m_Width); if (!NavMesh.IsLinkValid(m_LinkInstance)) return; UpdateLink(); if (!m_AutoUpdatePosition) { RemoveTracking(this); } else if (!s_Tracked.Contains(this)) { AddTracking(this); } } void Reset() { UpgradeSerializedVersion(); } bool StartEndpointUpgrade() { m_DelayEndpointUpgrade = (m_StartTransform != null && m_StartTransform != gameObject.transform && m_StartPoint.sqrMagnitude > 0.0001f) || (m_EndTransform != null && m_EndTransform != gameObject.transform && m_EndPoint.sqrMagnitude > 0.0001f); if (m_DelayEndpointUpgrade) { if (PrefabUtility.IsPartOfAnyPrefab(this)) { var isInstance = PrefabUtility.IsPartOfPrefabInstance(this); var prefabPath = isInstance ? PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(gameObject) : AssetDatabase.GetAssetPath(gameObject); if ((prefabPath != s_LastWarnedPrefab || EditorApplication.timeSinceStartup > s_NextPrefabWarningTime) && prefabPath != "") { var prefabToPing = AssetDatabase.LoadAssetAtPath(prefabPath); Debug.LogWarning(L10n.Tr( "A NavMesh Link component has an outdated format. " + "To upgrade it, open and save the prefab at: ") + prefabPath + (isInstance ? L10n.Tr(" . The prefab instance is ") + PrefabUtility.GetNearestPrefabInstanceRoot(gameObject).name : ""), prefabToPing); s_LastWarnedPrefab = prefabPath; s_NextPrefabWarningTime = EditorApplication.timeSinceStartup + 5f; } m_DelayEndpointUpgrade = false; return false; } if (IsInAuthoringScene()) { EditorApplication.delayCall += CompleteEndpointUpgrade; EditorApplication.delayCall -= WarnAboutUnsavedUpgrade; EditorApplication.delayCall += WarnAboutUnsavedUpgrade; EditorSceneManager.MarkSceneDirty(gameObject.scene); Debug.Log(L10n.Tr( "A NavMesh Link component has auto-upgraded and it references a newly created object. " + "Save your scene to keep the changes. " + "GameObject: ") + gameObject.name, gameObject); } else { Debug.LogWarning(L10n.Tr( "The NavMesh Link component does not reference the intended transforms. " + "To correct it, save this NavMesh Link again at edit time. GameObject: ") + gameObject.name, gameObject); } } return true; } static void WarnAboutUnsavedUpgrade() { Debug.LogWarning(L10n.Tr( "At least one NavMesh Link component has auto-upgraded to a new format. " + "Save your scene to keep the changes. ")); } void CompleteEndpointUpgrade() { var discardedByPrefabStageOnHiddenReload = this == null; if (discardedByPrefabStageOnHiddenReload || gameObject == null || !m_DelayEndpointUpgrade) return; var linkIndexString = ""; var allMyLinks = gameObject.GetComponents(); if (allMyLinks.Length > 1) { for (var i = 0; i < allMyLinks.Length; i++) { if (allMyLinks[i] == this) { linkIndexString = " " + i; break; } } } var localToWorldUnscaled = LocalToWorldUnscaled(); if (m_StartTransform != null && m_StartTransform != gameObject.transform && m_StartPoint.sqrMagnitude > 0.0001f) { var startGO = new GameObject($"Link Start {gameObject.name}{linkIndexString}"); startGO.transform.SetParent(m_StartTransform); startGO.transform.position = localToWorldUnscaled.MultiplyPoint3x4(transform.InverseTransformPoint(m_StartTransform.position + m_StartPoint)); m_StartTransform = startGO.transform; } if (m_EndTransform != null && m_EndTransform != gameObject.transform && m_EndPoint.sqrMagnitude > 0.0001f) { var endGO = new GameObject($"Link End {gameObject.name}{linkIndexString}"); endGO.transform.SetParent(m_EndTransform); endGO.transform.position = localToWorldUnscaled.MultiplyPoint3x4(transform.InverseTransformPoint(m_EndTransform.position + m_EndPoint)); m_EndTransform = endGO.transform; } if (IsInAuthoringScene()) EditorSceneManager.MarkSceneDirty(gameObject.scene); m_DelayEndpointUpgrade = false; } bool IsInAuthoringScene() { return !EditorApplication.isPlaying || PrefabStageUtility.GetPrefabStage(gameObject) != null; } #endif } }