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