UnityGame/Library/PackageCache/com.unity.ai.navigation/Runtime/NavMeshLink.cs
2024-10-27 10:53:47 +03:00

607 lines
23 KiB
C#

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
{
/// <summary> Component used to create a navigable link between two NavMesh locations. </summary>
[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;
/// <summary> Gets or sets the type of agent that can use the link. </summary>
public int agentTypeID
{
get => m_AgentTypeID;
set
{
if (value == m_AgentTypeID)
return;
m_AgentTypeID = value;
UpdateLink();
}
}
/// <summary> Gets or sets the local position at the middle of the link's start edge, relative to the GameObject origin. </summary>
/// <remarks> This property determines the position of the link's start edge only when <see cref="startTransform"/> is `null`. Otherwise, it is the `startTransform` that determines the edge's position. <br/>
/// The world scale of the GameObject is never used.</remarks>
public Vector3 startPoint
{
get => m_StartPoint;
set
{
if (value == m_StartPoint)
return;
m_StartPoint = value;
UpdateLink();
}
}
/// <summary> Gets or sets the local position at the middle of the link's end edge, relative to the GameObject origin. </summary>
/// <remarks> This property determines the position of the link's end edge only when <see cref="endTransform"/> is `null`. Otherwise, it is the `endTransform` that determines the edge's position. <br/>
/// The world scale of the GameObject is never used.</remarks>
public Vector3 endPoint
{
get => m_EndPoint;
set
{
if (value == m_EndPoint)
return;
m_EndPoint = value;
UpdateLink();
}
}
/// <summary> Gets or sets the <see cref="Transform"/> tracked by the middle of the link's start edge. </summary>
/// <remarks> The link places the start edge at the world position of the object referenced by this property. In that case <see cref="startPoint"/> is not used. Otherwise, when this property is `null`, the component applies the GameObject's translation and rotation as a transform to <see cref="startPoint"/> in order to establish the world position of the link's start edge. </remarks>
public Transform startTransform
{
get => m_StartTransform;
set
{
if (value == m_StartTransform)
return;
m_StartTransform = value;
UpdateLink();
}
}
/// <summary> Gets or sets the <see cref="Transform"/> tracked by the middle of the link's end edge. </summary>
/// <remarks> The link places the end edge at the world position of the object referenced by this property. In that case <see cref="endPoint"/> is not used. Otherwise, when this property is `null`, the component applies the GameObject's translation and rotation as a transform to <see cref="endPoint"/> in order to establish the world position of the link's end edge. </remarks>
public Transform endTransform
{
get => m_EndTransform;
set
{
if (value == m_EndTransform)
return;
m_EndTransform = value;
UpdateLink();
}
}
/// <summary> The width of the segments making up the ends of the link. </summary>
/// <remarks> The segments are created perpendicular to the line from start to end, in the XZ plane of the GameObject. </remarks>
public float width
{
get => m_Width;
set
{
if (value.Equals(m_Width))
return;
m_Width = value;
UpdateLink();
}
}
/// <summary> Gets or sets a value that determines the cost of traversing the link.</summary>
/// <remarks> A negative value implies that the cost of traversing the link is obtained based on the area type.<br/>
/// A positive or zero value overrides the cost associated with the area type.</remarks>
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();
}
}
/// <summary> Gets or sets whether agents can traverse the link in both directions. </summary>
/// <remarks> 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. </remarks>
public bool bidirectional
{
get => m_Bidirectional;
set
{
if (value == m_Bidirectional)
return;
m_Bidirectional = value;
UpdateLink();
}
}
/// <summary> Gets or sets whether the world positions of the link's edges update whenever
/// the GameObject transform, the <see cref="startTransform"/> or the <see cref="endTransform"/> change at runtime. </summary>
public bool autoUpdate
{
get => m_AutoUpdatePosition;
set
{
if (value == m_AutoUpdatePosition)
return;
m_AutoUpdatePosition = value;
if (m_AutoUpdatePosition)
AddTracking(this);
else
RemoveTracking(this);
}
}
/// <summary> The area type of the link. </summary>
public int area
{
get => m_Area;
set
{
if (value == m_Area)
return;
m_Area = value;
UpdateLink();
}
}
/// <summary> Gets or sets whether the link can be traversed by agents. </summary>
/// <remarks> 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. </remarks>
public bool activated
{
get => m_Activated;
set
{
m_Activated = value;
NavMesh.SetLinkActive(m_LinkInstance, m_Activated);
}
}
/// <summary> Checks whether any agent occupies the link at this moment in time. </summary>
/// <remarks> This property evaluates the internal state of the link every time it is used. </remarks>
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<NavMeshLink> 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);
}
/// <summary> Replaces the link with a new one using the current settings. </summary>
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;
}
/// <summary>Gets the world positions of the start and end points for the link.</summary>
/// <param name="worldStartPosition">Returns the world position of <see cref="startTransform"/> if it is not <c>null</c>; otherwise, <see cref="startPoint"/> transformed into world space.</param>
/// <param name="worldEndPosition">Returns the world position of <see cref="endTransform"/> if it is not <c>null</c>; otherwise, <see cref="endPoint"/> transformed into world space.</param>
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;
}
/// <summary>Gets the positions of the start and end points in the local space of the link.</summary>
/// <param name="localStartPosition">Returns the local position of <see cref="startTransform"/> if it is not <c>null</c>; otherwise, <see cref="startPoint"/>.</param>
/// <param name="localEndPosition">Returns the local position of <see cref="endTransform"/> if it is not <c>null</c>; otherwise, <see cref="endPoint"/>.</param>
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<Object>(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<NavMeshLink>();
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
}
}