using UnityEngine;
using System.Collections.Generic;
using Pathfinding.Serialization;
using Pathfinding.Util;
using UnityEngine.Assertions;
namespace Pathfinding {
using System;
using Pathfinding.Drawing;
using Unity.Jobs;
///
/// Graph for off-mesh links.
///
/// This is an internal graph type which is used to store off-mesh links.
/// An off-mesh link between two nodes A and B is represented as: A <--> N1 <--> N2 <--> B
.
/// where N1 and N2 are two special nodes added to this graph at the exact start and endpoints of the link.
///
/// This graph is not persistent. So it will never be saved to disk and a new one will be created each time the game starts.
///
/// It is also not possible to query for the nearest node in this graph. The method will always return an empty result.
/// This is by design, as all pathfinding should start on the navmesh, not on an off-mesh link.
///
/// See:
/// See:
///
[JsonOptIn]
[Pathfinding.Util.Preserve]
public class LinkGraph : NavGraph {
LinkNode[] nodes = new LinkNode[0];
int nodeCount;
public override bool isScanned => true;
public override bool persistent => false;
public override bool showInInspector => false;
public override int CountNodes() => nodeCount;
protected override void DestroyAllNodes () {
base.DestroyAllNodes();
nodes = new LinkNode[0];
nodeCount = 0;
}
public override void GetNodes (Action action) {
if (nodes == null) return;
for (int i = 0; i < nodeCount; i++) action(nodes[i]);
}
internal LinkNode AddNode () {
AssertSafeToUpdateGraph();
if (nodeCount >= nodes.Length) {
Memory.Realloc(ref nodes, Mathf.Max(16, nodeCount * 2));
}
nodeCount++;
return nodes[nodeCount-1] = new LinkNode(active) {
nodeInGraphIndex = nodeCount - 1,
GraphIndex = graphIndex,
Walkable = true,
};
}
internal void RemoveNode (LinkNode node) {
if (nodes[node.nodeInGraphIndex] != node) throw new ArgumentException("Node is not in this graph");
// Remove and swap with the last node
nodeCount--;
nodes[node.nodeInGraphIndex] = nodes[nodeCount];
nodes[node.nodeInGraphIndex].nodeInGraphIndex = node.nodeInGraphIndex;
nodes[nodeCount] = null;
node.Destroy();
}
public override float NearestNodeDistanceSqrLowerBound(Vector3 position, NNConstraint constraint = null) => float.PositiveInfinity;
///
/// It's not possible to query for the nearest node in a link graph.
/// This method will always return an empty result.
///
public override NNInfo GetNearest(Vector3 position, NNConstraint constraint, float maxDistanceSqr) => default;
public override void OnDrawGizmos (DrawingData gizmos, bool drawNodes, RedrawScope redrawScope) {
// We rely on the link components themselves to draw the links
// TODO
base.OnDrawGizmos(gizmos, drawNodes, redrawScope);
}
class LinkGraphUpdatePromise : IGraphUpdatePromise {
public LinkGraph graph;
public void Apply (IGraphUpdateContext ctx) {
// Destroy all previous nodes (if any)
graph.DestroyAllNodes();
}
public IEnumerator Prepare() => null;
}
protected override IGraphUpdatePromise ScanInternal () => new LinkGraphUpdatePromise { graph = this };
}
public class LinkNode : PointNode {
public OffMeshLinks.OffMeshLinkSource linkSource;
public OffMeshLinks.OffMeshLinkConcrete linkConcrete;
public int nodeInGraphIndex;
public LinkNode() { }
public LinkNode(AstarPath active) : base(active) {}
public override void RemovePartialConnection (GraphNode node) {
linkConcrete.staleConnections = true;
// Mark the link as dirty so that it will be recalculated.
// Ensure that this does not immediately schedule an update.
// Nodes should only be updated during work items and while graphs are scanned,
// and in those cases node links will be refreshed anyway.
// However, this can also trigger when the AstarPath component is being destroyed,
// or when a graph is removed. In those cases, we don't want to schedule an update.
AstarPath.active.offMeshLinks.DirtyNoSchedule(linkSource);
base.RemovePartialConnection(node);
}
public override void Open (Path path, uint pathNodeIndex, uint gScore) {
// Note: Not calling path.OpenCandidateConnectionsToEndNode here, because link nodes should never be the end node of a path
if (connections == null) return;
var pathHandler = (path as IPathInternals).PathHandler;
var pn = pathHandler.pathNodes[pathNodeIndex];
// Check if our parent node was also a link node by checking if it is in the same graph as this node.
// If it is, we are allowed to connect to non-link nodes.
// Otherwise, we are at the start of the link and we must only connect to other link nodes.
// This is to avoid the path going to a link node, and then going directly back to a non-link node
// without actually traversing the link. It would technically be a valid path,
// but it causes confusion for other scripts that look for off-mesh links in the path.
// TODO: Store the other link node as a field to be able to do a more robust check here?
var isEndOfLink = !pathHandler.IsTemporaryNode(pn.parentIndex) && pathHandler.GetNode(pn.parentIndex).GraphIndex == GraphIndex;
var canTraverseNonLinkNodes = isEndOfLink;
for (int i = 0; i < connections.Length; i++) {
GraphNode other = connections[i].node;
if (canTraverseNonLinkNodes == (other.GraphIndex != GraphIndex) && path.CanTraverse(this, other)) {
if (other is PointNode) {
path.OpenCandidateConnection(pathNodeIndex, other.NodeIndex, gScore, connections[i].cost, 0, other.position);
} else {
// When connecting to a non-link node, use a special function to open the connection.
// The typical case for this is that we are at the end of an off-mesh link and we are connecting to a navmesh node.
// In that case, this node's position is in the interior of the navmesh node. We let the navmesh node decide how
// that should be handled.
other.OpenAtPoint(path, pathNodeIndex, position, gScore);
}
}
}
}
public override void OpenAtPoint (Path path, uint pathNodeIndex, Int3 pos, uint gScore) {
if (path.CanTraverse(this)) {
// Note: Not calling path.OpenCandidateConnectionsToEndNode here, because link nodes should never be the end node of a path
var cost = (uint)(pos - this.position).costMagnitude;
path.OpenCandidateConnection(pathNodeIndex, NodeIndex, gScore, cost, 0, position);
}
}
}
}