diff options
author | chai <215380520@qq.com> | 2024-05-23 10:08:29 +0800 |
---|---|---|
committer | chai <215380520@qq.com> | 2024-05-23 10:08:29 +0800 |
commit | 8722a9920c1f6119bf6e769cba270e63097f8e25 (patch) | |
tree | 2eaf9865de7fb1404546de4a4296553d8f68cc3b /Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink2.cs | |
parent | 3ba4020b69e5971bb0df7ee08b31d10ea4d01937 (diff) |
+ astar project
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink2.cs')
-rw-r--r-- | Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink2.cs | 405 |
1 files changed, 405 insertions, 0 deletions
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink2.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink2.cs new file mode 100644 index 0000000..9f99d46 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Misc/NodeLink2.cs @@ -0,0 +1,405 @@ +using UnityEngine; + +namespace Pathfinding { + using Pathfinding.Util; + using Pathfinding.Drawing; + + /// <summary> + /// Interface for handling off-mesh links. + /// + /// If a component implements this interface, and is attached to the same GameObject as a NodeLink2 component, + /// then the OnTraverseOffMeshLink method will be called when an agent traverses the off-mesh link. + /// + /// This only works with the <see cref="FollowerEntity"/> component. + /// + /// Note: <see cref="FollowerEntity.onTraverseOffMeshLink"/> overrides this callback, if set. + /// + /// The <see cref="Interactable"/> component implements this interface, and allows a small state-machine to run when the agent traverses the link. + /// + /// See: <see cref="FollowerEntity.onTraverseOffMeshLink"/> + /// See: offmeshlinks (view in online documentation for working links) + /// </summary> + public interface IOffMeshLinkHandler { + /// <summary> + /// Name of the handler. + /// This is used to identify the handler in the inspector. + /// </summary> + public string name => null; + +#if MODULE_ENTITIES + /// <summary> + /// Called when an agent starts traversing an off-mesh link. + /// + /// This can be used to perform any setup that is necessary for the traversal. + /// + /// Returns: An object which will be used to control the agent for the full duration of the link traversal. + /// + /// For simple cases, you can often implement <see cref="IOffMeshLinkHandler"/> and <see cref="IOffMeshLinkStateMachine"/> in the same class, and then + /// just make this method return this. + /// For more complex cases, you may want to keep track of the agent's identity, and thus want to return a new object for each agent that traverses the link. + /// </summary> + /// <param name="context">Context for the traversal. Provides information about the link and the agent, as well as some helper methods for movement. + /// This context is only valid for this method call. Do not store it and use it later.</param> + public IOffMeshLinkStateMachine GetOffMeshLinkStateMachine(ECS.AgentOffMeshLinkTraversalContext context); +#endif + } + + public interface IOffMeshLinkStateMachine { +#if MODULE_ENTITIES + /// <summary> + /// Called when an agent traverses an off-mesh link. + /// This method should be a coroutine (i.e return an IEnumerable) which will be iterated over until it finishes, or the agent is destroyed. + /// The coroutine should yield null every frame until the agent has finished traversing the link. + /// + /// When the coroutine completes, the agent will be assumed to have reached the end of the link and control + /// will be returned to the normal movement code. + /// + /// The coroutine typically moves the agent to the end of the link over some time, and perform any other actions that are necessary. + /// For example, it could play an animation, or move the agent in a specific way. + /// </summary> + /// <param name="context">Context for the traversal. Provides information about the link and the agent, as well as some helper methods for movement. + /// This context is only valid when this coroutine steps forward. Do not store it and use it elsewhere.</param> + System.Collections.IEnumerable OnTraverseOffMeshLink(ECS.AgentOffMeshLinkTraversalContext context) => ECS.JobStartOffMeshLinkTransition.DefaultOnTraverseOffMeshLink(context); + + /// <summary> + /// Called when an agent finishes traversing an off-mesh link. + /// + /// This can be used to perform any cleanup that is necessary after the traversal. + /// + /// Either <see cref="OnFinishTraversingOffMeshLink"/> or <see cref="OnAbortTraversingOffMeshLink"/> will be called, but not both. + /// </summary> + /// <param name="context">Context for the traversal. Provides information about the link and the agent, as well as some helper methods for movement. + /// This context is only valid for this method call. Do not store it and use it later.</param> + void OnFinishTraversingOffMeshLink (ECS.AgentOffMeshLinkTraversalContext context) {} +#endif + + /// <summary> + /// Called when an agent fails to finish traversing an off-mesh link. + /// + /// This can be used to perform any cleanup that is necessary after the traversal. + /// + /// An abort can happen if the agent was destroyed while it was traversing the link. It can also happen if the agent was teleported somewhere else while traversing the link. + /// + /// Either <see cref="OnFinishTraversingOffMeshLink"/> or <see cref="OnAbortTraversingOffMeshLink"/> will be called, but not both. + /// + /// Warning: When this is called, the agent may already be destroyed. The handler component itself could also be destroyed at this point. + /// </summary> + void OnAbortTraversingOffMeshLink () {} + } + + /// <summary> + /// Connects two nodes using an off-mesh link. + /// In contrast to the <see cref="NodeLink"/> component, this link type will not connect the nodes directly + /// instead it will create two link nodes at the start and end position of this link and connect + /// through those nodes. + /// + /// If the closest node to this object is called A and the closest node to the end transform is called + /// D, then it will create one link node at this object's position (call it B) and one link node at + /// the position of the end transform (call it C), it will then connect A to B, B to C and C to D. + /// + /// This link type is possible to detect while following since it has these special link nodes in the middle. + /// The link corresponding to one of those intermediate nodes can be retrieved using the <see cref="GetNodeLink"/> method + /// which can be of great use if you want to, for example, play a link specific animation when reaching the link. + /// + /// \inspectorField{End, NodeLink2.end} + /// \inspectorField{Cost Factor, NodeLink2.costFactor} + /// \inspectorField{One Way, NodeLink2.oneWay} + /// \inspectorField{Pathfinding Tag, NodeLink2.pathfindingTag} + /// \inspectorField{Graph Mask, NodeLink2.graphMask} + /// + /// See: offmeshlinks (view in online documentation for working links) + /// See: The example scene RecastExample2 contains a few links which you can take a look at to see how they are used. + /// + /// Note: If you make any modifications to the node link's settings after it has been created, you need to call the <see cref="Apply"/> method in order to apply the changes to the graph. + /// </summary> + [AddComponentMenu("Pathfinding/Link2")] + [HelpURL("https://arongranberg.com/astar/documentation/stable/nodelink2.html")] + public class NodeLink2 : GraphModifier { + /// <summary>End position of the link</summary> + public Transform end; + + /// <summary> + /// The connection will be this times harder/slower to traverse. + /// + /// A cost factor of 1 means that the link is equally expensive as moving the same distance on the normal navmesh. But a cost factor greater than 1 means that it is proportionally more expensive. + /// + /// You should not use a cost factor less than 1 unless you also change the <see cref="AstarPath.heuristicScale"/> field (A* Inspector -> Settings -> Pathfinding) to at most the minimum cost factor that you use anywhere in the scene (or disable the heuristic altogether). + /// This is because the pathfinding algorithm assumes that paths are at least as costly as walking just the straight line distance to the target, and if you use a cost factor less than 1, that assumption is no longer true. + /// What then happens is that the pathfinding search may ignore some links because it doesn't even think to search in that direction, even if they would have lead to a lower path cost. + /// + /// Warning: Reducing the heuristic scale or disabling the heuristic can significantly increase the cpu cost for pathfinding, especially for large graphs. + /// + /// Read more about this at https://en.wikipedia.org/wiki/Admissible_heuristic. + /// </summary> + public float costFactor = 1.0f; + + /// <summary>Make a one-way connection</summary> + public bool oneWay = false; + + /// <summary> + /// The tag to apply to the link. + /// + /// This can be used to exclude certain agents from using the link, or make it more expensive to use. + /// + /// See: tags (view in online documentation for working links) + /// </summary> + public PathfindingTag pathfindingTag = 0; + + /// <summary> + /// Which graphs this link is allowed to connect. + /// + /// The link will always connect the nodes closest to the start and end points on the graphs that it is allowed to connect. + /// </summary> + public GraphMask graphMask = -1; + + public Transform StartTransform => transform; + + public Transform EndTransform => end; + + protected OffMeshLinks.OffMeshLinkSource linkSource; + + /// <summary> + /// Returns the link component associated with the specified node. + /// Returns: The link associated with the node or null if the node is not a link node, or it is not associated with a <see cref="NodeLink2"/> component. + /// </summary> + /// <param name="node">The node to get the link for.</param> + public static NodeLink2 GetNodeLink(GraphNode node) => node is LinkNode linkNode ? linkNode.linkSource.component as NodeLink2 : null; + + /// <summary> + /// True if the link is connected to the graph. + /// + /// This will be true if the link has been successfully connected to the graph, and false if it either has failed, or if the component/gameobject is disabled. + /// + /// When the component is enabled, the link will be scheduled to be added to the graph, it will not take effect immediately. + /// This means that this property will return false until the next time graph updates are processed (usually later this frame, or next frame). + /// To ensure the link is refreshed immediately, you can call <see cref="AstarPath.active.FlushWorkItems"/>. + /// </summary> + internal bool isActive => linkSource != null && (linkSource.status & OffMeshLinks.OffMeshLinkStatus.Active) != 0; + + IOffMeshLinkHandler onTraverseOffMeshLinkHandler; + + /// <summary> + /// Callback to be called when an agent starts traversing an off-mesh link. + /// + /// The handler will be called when the agent starts traversing an off-mesh link. + /// It allows you to to control the agent for the full duration of the link traversal. + /// + /// Use the passed context struct to get information about the link and to control the agent. + /// + /// <code> + /// using UnityEngine; + /// using Pathfinding; + /// using System.Collections; + /// using Pathfinding.ECS; + /// + /// namespace Pathfinding.Examples { + /// public class FollowerJumpLink : MonoBehaviour, IOffMeshLinkHandler, IOffMeshLinkStateMachine { + /// // Register this class as the handler for off-mesh links when the component is enabled + /// void OnEnable() => GetComponent<NodeLink2>().onTraverseOffMeshLink = this; + /// void OnDisable() => GetComponent<NodeLink2>().onTraverseOffMeshLink = null; + /// + /// IOffMeshLinkStateMachine IOffMeshLinkHandler.GetOffMeshLinkStateMachine(AgentOffMeshLinkTraversalContext context) => this; + /// + /// void IOffMeshLinkStateMachine.OnFinishTraversingOffMeshLink (AgentOffMeshLinkTraversalContext context) { + /// Debug.Log("An agent finished traversing an off-mesh link"); + /// } + /// + /// void IOffMeshLinkStateMachine.OnAbortTraversingOffMeshLink () { + /// Debug.Log("An agent aborted traversing an off-mesh link"); + /// } + /// + /// IEnumerable IOffMeshLinkStateMachine.OnTraverseOffMeshLink (AgentOffMeshLinkTraversalContext ctx) { + /// var start = (Vector3)ctx.link.relativeStart; + /// var end = (Vector3)ctx.link.relativeEnd; + /// var dir = end - start; + /// + /// // Disable local avoidance while traversing the off-mesh link. + /// // If it was enabled, it will be automatically re-enabled when the agent finishes traversing the link. + /// ctx.DisableLocalAvoidance(); + /// + /// // Move and rotate the agent to face the other side of the link. + /// // When reaching the off-mesh link, the agent may be facing the wrong direction. + /// while (!ctx.MoveTowards( + /// position: start, + /// rotation: Quaternion.LookRotation(dir, ctx.movementPlane.up), + /// gravity: true, + /// slowdown: true).reached) { + /// yield return null; + /// } + /// + /// var bezierP0 = start; + /// var bezierP1 = start + Vector3.up*5; + /// var bezierP2 = end + Vector3.up*5; + /// var bezierP3 = end; + /// var jumpDuration = 1.0f; + /// + /// // Animate the AI to jump from the start to the end of the link + /// for (float t = 0; t < jumpDuration; t += ctx.deltaTime) { + /// ctx.transform.Position = AstarSplines.CubicBezier(bezierP0, bezierP1, bezierP2, bezierP3, Mathf.SmoothStep(0, 1, t / jumpDuration)); + /// yield return null; + /// } + /// } + /// } + /// } + /// </code> + /// + /// Warning: Off-mesh links can be destroyed or disabled at any moment. The built-in code will attempt to make the agent continue following the link even if it is destroyed, + /// but if you write your own traversal code, you should be aware of this. + /// + /// You can alternatively set the corresponding property property on the agent (<see cref="FollowerEntity.onTraverseOffMeshLink"/>) to specify a callback for a all off-mesh links. + /// + /// Note: The agent's off-mesh link handler takes precedence over the link's off-mesh link handler, if both are set. + /// + /// Warning: This property only works with the <see cref="FollowerEntity"/> component. Use <see cref="RichAI.onTraverseOffMeshLink"/> if you are using the <see cref="RichAI"/> movement script. + /// + /// See: offmeshlinks (view in online documentation for working links) for more details and example code + /// </summary> + public IOffMeshLinkHandler onTraverseOffMeshLink { + get => onTraverseOffMeshLinkHandler; + set { + onTraverseOffMeshLinkHandler = value; + if (linkSource != null) linkSource.handler = value; + } + } + + public override void OnPostScan () { + TryAddLink(); + } + + protected override void OnEnable () { + base.OnEnable(); + if (Application.isPlaying && !BatchedEvents.Has(this)) BatchedEvents.Add(this, BatchedEvents.Event.Update, OnUpdate); + TryAddLink(); + } + + static void OnUpdate (NodeLink2[] components, int count) { + // Only check for moved links every N frames, for performance + if ((Time.frameCount % 16) != 0) return; + + for (int i = 0; i < count; i++) { + var comp = components[i]; + var start = comp.StartTransform; + var end = comp.EndTransform; + var added = comp.linkSource != null; + if ((start != null && end != null) != added || (added && (start.hasChanged || end.hasChanged))) { + if (start != null) start.hasChanged = false; + if (end != null) end.hasChanged = false; + comp.RemoveLink(); + comp.TryAddLink(); + } + } + } + + void TryAddLink () { + // In case the AstarPath component has been destroyed (destroying the link). + // But do not clear it if the link is inactive because it failed to become enabled + if (linkSource != null && (linkSource.status == OffMeshLinks.OffMeshLinkStatus.Inactive || (linkSource.status & OffMeshLinks.OffMeshLinkStatus.PendingRemoval) != 0)) linkSource = null; + + if (linkSource == null && AstarPath.active != null && EndTransform != null) { + StartTransform.hasChanged = false; + EndTransform.hasChanged = false; + + linkSource = new OffMeshLinks.OffMeshLinkSource { + start = new OffMeshLinks.Anchor { + center = StartTransform.position, + rotation = StartTransform.rotation, + width = 0f, + }, + end = new OffMeshLinks.Anchor { + center = EndTransform.position, + rotation = EndTransform.rotation, + width = 0f, + }, + directionality = oneWay ? OffMeshLinks.Directionality.OneWay : OffMeshLinks.Directionality.TwoWay, + tag = pathfindingTag, + costFactor = costFactor, + graphMask = graphMask, + maxSnappingDistance = 1, // TODO + component = this, + handler = onTraverseOffMeshLink, + }; + AstarPath.active.offMeshLinks.Add(linkSource); + } + } + + void RemoveLink () { + if (AstarPath.active != null && linkSource != null) AstarPath.active.offMeshLinks.Remove(linkSource); + linkSource = null; + } + + protected override void OnDisable () { + base.OnDisable(); + BatchedEvents.Remove(this); + RemoveLink(); + } + + [ContextMenu("Recalculate neighbours")] + void ContextApplyForce () { + Apply(); + } + + /// <summary> + /// Disconnects and then reconnects the link to the graph. + /// + /// If you have moved the link or otherwise modified it you need to call this method to apply those changes. + /// </summary> + public virtual void Apply () { + RemoveLink(); + TryAddLink(); + } + + private readonly static Color GizmosColor = new Color(206.0f/255.0f, 136.0f/255.0f, 48.0f/255.0f, 0.5f); + private readonly static Color GizmosColorSelected = new Color(235.0f/255.0f, 123.0f/255.0f, 32.0f/255.0f, 1.0f); + + public override void DrawGizmos () { + if (StartTransform == null || EndTransform == null) return; + + var startPos = StartTransform.position; + var endPos = EndTransform.position; + if (linkSource != null && (Time.renderedFrameCount % 16) == 0 && Application.isEditor) { + // Check if the link has moved + // During runtime, this will be done by the OnUpdate method instead + if (linkSource.start.center != startPos || linkSource.end.center != endPos || linkSource.directionality != (oneWay ? OffMeshLinks.Directionality.OneWay : OffMeshLinks.Directionality.TwoWay) || linkSource.costFactor != costFactor || linkSource.graphMask != graphMask || linkSource.tag != pathfindingTag) { + Apply(); + } + } + + bool selected = GizmoContext.InActiveSelection(this); + var graphs = linkSource != null && AstarPath.active != null? AstarPath.active.offMeshLinks.ConnectedGraphs(linkSource) : null; + var up = Vector3.up; + + // Find the natural up direction of the connected graphs, so that we can orient the gizmos appropriately + if (graphs != null) { + for (int i = 0; i < graphs.Count; i++) { + var graph = graphs[i]; + if (graph != null) { + if (graph is NavmeshBase navmesh) { + up = navmesh.transform.WorldUpAtGraphPosition(Vector3.zero); + break; + } else if (graph is GridGraph grid) { + up = grid.transform.WorldUpAtGraphPosition(Vector3.zero); + break; + } + } + } + ListPool<NavGraph>.Release(ref graphs); + } + + var active = linkSource != null && linkSource.status == OffMeshLinks.OffMeshLinkStatus.Active; + Color color = selected ? GizmosColorSelected : GizmosColor; + if (active) color = Color.green; + + Draw.Circle(startPos, up, 0.4f, linkSource != null && linkSource.status.HasFlag(OffMeshLinks.OffMeshLinkStatus.FailedToConnectStart) ? Color.red : color); + Draw.Circle(endPos, up, 0.4f, linkSource != null && linkSource.status.HasFlag(OffMeshLinks.OffMeshLinkStatus.FailedToConnectEnd) ? Color.red : color); + + NodeLink.DrawArch(startPos, endPos, up, color); + if (selected) { + Vector3 cross = Vector3.Cross(up, endPos-startPos).normalized; + using (Draw.WithLineWidth(2)) { + NodeLink.DrawArch(startPos+cross*0.0f, endPos+cross*0.0f, up, color); + } + // NodeLink.DrawArch(startPos-cross*0.1f, endPos-cross*0.1f, color); + } + } + } +} |