using UnityEngine; using System.Collections.Generic; namespace Pathfinding { using Pathfinding.Util; /// /// Connects two nodes via two intermediate point nodes. /// In contrast to the NodeLink component, this link type will not connect the nodes directly /// instead it will create two point 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 point node at this object's position (call it B) and one point 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 point nodes in the middle. /// The link corresponding to one of those intermediate nodes can be retrieved using the method /// which can be of great use if you want to, for example, play a link specific animation when reaching the link. /// /// See: The example scene RecastExample2 contains a few links which you can take a look at to see how they are used. /// [AddComponentMenu("Pathfinding/Link2")] [HelpURL("http://arongranberg.com/astar/docs/class_pathfinding_1_1_node_link2.php")] public class NodeLink2 : GraphModifier { protected static Dictionary reference = new Dictionary(); public static NodeLink2 GetNodeLink (GraphNode node) { NodeLink2 v; reference.TryGetValue(node, out v); return v; } /// End position of the link public Transform end; /// /// The connection will be this times harder/slower to traverse. /// Note that values lower than 1 will not always make the pathfinder choose this path instead of another path even though this one should /// lead to a lower total cost unless you also adjust the Heuristic Scale in A* Inspector -> Settings -> Pathfinding or disable the heuristic altogether. /// public float costFactor = 1.0f; /// Make a one-way connection public bool oneWay = false; public Transform StartTransform { get { return transform; } } public Transform EndTransform { get { return end; } } public PointNode startNode { get; private set; } public PointNode endNode { get; private set; } GraphNode connectedNode1, connectedNode2; Vector3 clamped1, clamped2; bool postScanCalled = false; [System.Obsolete("Use startNode instead (lowercase s)")] public GraphNode StartNode { get { return startNode; } } [System.Obsolete("Use endNode instead (lowercase e)")] public GraphNode EndNode { get { return endNode; } } public override void OnPostScan () { InternalOnPostScan(); } public void InternalOnPostScan () { if (EndTransform == null || StartTransform == null) return; #if ASTAR_NO_POINT_GRAPH throw new System.Exception("Point graph is not included. Check your A* optimization settings."); #else if (AstarPath.active.data.pointGraph == null) { var graph = AstarPath.active.data.AddGraph(typeof(PointGraph)) as PointGraph; graph.name = "PointGraph (used for node links)"; } if (startNode != null && startNode.Destroyed) { reference.Remove(startNode); startNode = null; } if (endNode != null && endNode.Destroyed) { reference.Remove(endNode); endNode = null; } // Create new nodes on the point graph if (startNode == null) startNode = AstarPath.active.data.pointGraph.AddNode((Int3)StartTransform.position); if (endNode == null) endNode = AstarPath.active.data.pointGraph.AddNode((Int3)EndTransform.position); connectedNode1 = null; connectedNode2 = null; if (startNode == null || endNode == null) { startNode = null; endNode = null; return; } postScanCalled = true; reference[startNode] = this; reference[endNode] = this; Apply(true); #endif } public override void OnGraphsPostUpdate () { // Don't bother running it now since OnPostScan will be called later anyway if (AstarPath.active.isScanning) return; if (connectedNode1 != null && connectedNode1.Destroyed) { connectedNode1 = null; } if (connectedNode2 != null && connectedNode2.Destroyed) { connectedNode2 = null; } if (!postScanCalled) { OnPostScan(); } else { Apply(false); } } protected override void OnEnable () { base.OnEnable(); #if !ASTAR_NO_POINT_GRAPH if (Application.isPlaying && AstarPath.active != null && AstarPath.active.data != null && AstarPath.active.data.pointGraph != null && !AstarPath.active.isScanning) { // Call OnGraphsPostUpdate as soon as possible when it is safe to update the graphs AstarPath.active.AddWorkItem(OnGraphsPostUpdate); } #endif } protected override void OnDisable () { base.OnDisable(); postScanCalled = false; if (startNode != null) reference.Remove(startNode); if (endNode != null) reference.Remove(endNode); if (startNode != null && endNode != null) { startNode.RemoveConnection(endNode); endNode.RemoveConnection(startNode); if (connectedNode1 != null && connectedNode2 != null) { startNode.RemoveConnection(connectedNode1); connectedNode1.RemoveConnection(startNode); endNode.RemoveConnection(connectedNode2); connectedNode2.RemoveConnection(endNode); } } } void RemoveConnections (GraphNode node) { //TODO, might be better to replace connection node.ClearConnections(true); } [ContextMenu("Recalculate neighbours")] void ContextApplyForce () { if (Application.isPlaying) { Apply(true); } } public void Apply (bool forceNewCheck) { //TODO //This function assumes that connections from the n1,n2 nodes never need to be removed in the future (e.g because the nodes move or something) NNConstraint nn = NNConstraint.None; int graph = (int)startNode.GraphIndex; //Search all graphs but the one which start and end nodes are on nn.graphMask = ~(1 << graph); startNode.SetPosition((Int3)StartTransform.position); endNode.SetPosition((Int3)EndTransform.position); RemoveConnections(startNode); RemoveConnections(endNode); uint cost = (uint)Mathf.RoundToInt(((Int3)(StartTransform.position-EndTransform.position)).costMagnitude*costFactor); startNode.AddConnection(endNode, cost); endNode.AddConnection(startNode, cost); if (connectedNode1 == null || forceNewCheck) { var info = AstarPath.active.GetNearest(StartTransform.position, nn); connectedNode1 = info.node; clamped1 = info.position; } if (connectedNode2 == null || forceNewCheck) { var info = AstarPath.active.GetNearest(EndTransform.position, nn); connectedNode2 = info.node; clamped2 = info.position; } if (connectedNode2 == null || connectedNode1 == null) return; //Add connections between nodes, or replace old connections if existing connectedNode1.AddConnection(startNode, (uint)Mathf.RoundToInt(((Int3)(clamped1 - StartTransform.position)).costMagnitude*costFactor)); if (!oneWay) connectedNode2.AddConnection(endNode, (uint)Mathf.RoundToInt(((Int3)(clamped2 - EndTransform.position)).costMagnitude*costFactor)); if (!oneWay) startNode.AddConnection(connectedNode1, (uint)Mathf.RoundToInt(((Int3)(clamped1 - StartTransform.position)).costMagnitude*costFactor)); endNode.AddConnection(connectedNode2, (uint)Mathf.RoundToInt(((Int3)(clamped2 - EndTransform.position)).costMagnitude*costFactor)); } 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 virtual void OnDrawGizmosSelected () { OnDrawGizmos(true); } public void OnDrawGizmos () { OnDrawGizmos(false); } public void OnDrawGizmos (bool selected) { Color color = selected ? GizmosColorSelected : GizmosColor; if (StartTransform != null) { Draw.Gizmos.CircleXZ(StartTransform.position, 0.4f, color); } if (EndTransform != null) { Draw.Gizmos.CircleXZ(EndTransform.position, 0.4f, color); } if (StartTransform != null && EndTransform != null) { Draw.Gizmos.Bezier(StartTransform.position, EndTransform.position, color); if (selected) { Vector3 cross = Vector3.Cross(Vector3.up, (EndTransform.position-StartTransform.position)).normalized; Draw.Gizmos.Bezier(StartTransform.position+cross*0.1f, EndTransform.position+cross*0.1f, color); Draw.Gizmos.Bezier(StartTransform.position-cross*0.1f, EndTransform.position-cross*0.1f, color); } } } internal static void SerializeReferences (Pathfinding.Serialization.GraphSerializationContext ctx) { var links = GetModifiersOfType(); ctx.writer.Write(links.Count); foreach (var link in links) { ctx.writer.Write(link.uniqueID); ctx.SerializeNodeReference(link.startNode); ctx.SerializeNodeReference(link.endNode); ctx.SerializeNodeReference(link.connectedNode1); ctx.SerializeNodeReference(link.connectedNode2); ctx.SerializeVector3(link.clamped1); ctx.SerializeVector3(link.clamped2); ctx.writer.Write(link.postScanCalled); } } internal static void DeserializeReferences (Pathfinding.Serialization.GraphSerializationContext ctx) { int count = ctx.reader.ReadInt32(); for (int i = 0; i < count; i++) { var linkID = ctx.reader.ReadUInt64(); var startNode = ctx.DeserializeNodeReference(); var endNode = ctx.DeserializeNodeReference(); var connectedNode1 = ctx.DeserializeNodeReference(); var connectedNode2 = ctx.DeserializeNodeReference(); var clamped1 = ctx.DeserializeVector3(); var clamped2 = ctx.DeserializeVector3(); var postScanCalled = ctx.reader.ReadBoolean(); GraphModifier link; if (usedIDs.TryGetValue(linkID, out link)) { var link2 = link as NodeLink2; if (link2 != null) { if (startNode != null) reference[startNode] = link2; if (endNode != null) reference[endNode] = link2; // If any nodes happened to be registered right now if (link2.startNode != null) reference.Remove(link2.startNode); if (link2.endNode != null) reference.Remove(link2.endNode); link2.startNode = startNode as PointNode; link2.endNode = endNode as PointNode; link2.connectedNode1 = connectedNode1; link2.connectedNode2 = connectedNode2; link2.postScanCalled = postScanCalled; link2.clamped1 = clamped1; link2.clamped2 = clamped2; } else { throw new System.Exception("Tried to deserialize a NodeLink2 reference, but the link was not of the correct type or it has been destroyed.\nIf a NodeLink2 is included in serialized graph data, the same NodeLink2 component must be present in the scene when loading the graph data."); } } else { throw new System.Exception("Tried to deserialize a NodeLink2 reference, but the link could not be found in the scene.\nIf a NodeLink2 is included in serialized graph data, the same NodeLink2 component must be present in the scene when loading the graph data."); } } } } }