#define PREALLOCATE_NODES using System.Collections.Generic; using Pathfinding.Serialization; using Pathfinding.Util; using UnityEngine; namespace Pathfinding { /// Node used for the GridGraph public class GridNode : GridNodeBase { public GridNode() { } public GridNode (AstarPath astar) { astar.InitializeNode(this); } #if !ASTAR_NO_GRID_GRAPH private static GridGraph[] _gridGraphs = new GridGraph[0]; public static GridGraph GetGridGraph (uint graphIndex) { return _gridGraphs[(int)graphIndex]; } public static void SetGridGraph (int graphIndex, GridGraph graph) { if (_gridGraphs.Length <= graphIndex) { var gg = new GridGraph[graphIndex+1]; for (int i = 0; i < _gridGraphs.Length; i++) gg[i] = _gridGraphs[i]; _gridGraphs = gg; } _gridGraphs[graphIndex] = graph; } public static void ClearGridGraph (int graphIndex, GridGraph graph) { if (graphIndex < _gridGraphs.Length && _gridGraphs[graphIndex] == graph) { _gridGraphs[graphIndex] = null; } } /// Internal use only internal ushort InternalGridFlags { get { return gridFlags; } set { gridFlags = value; } } const int GridFlagsConnectionOffset = 0; const int GridFlagsConnectionBit0 = 1 << GridFlagsConnectionOffset; const int GridFlagsConnectionMask = 0xFF << GridFlagsConnectionOffset; const int GridFlagsAxisAlignedConnectionMask = 0xF << GridFlagsConnectionOffset; const int GridFlagsEdgeNodeOffset = 10; const int GridFlagsEdgeNodeMask = 1 << GridFlagsEdgeNodeOffset; public override bool HasConnectionsToAllEightNeighbours { get { return (InternalGridFlags & GridFlagsConnectionMask) == GridFlagsConnectionMask; } } public override bool HasConnectionsToAllAxisAlignedNeighbours { get { return (InternalGridFlags & GridFlagsAxisAlignedConnectionMask) == GridFlagsAxisAlignedConnectionMask; } } /// /// True if the node has a connection in the specified direction. /// The dir parameter corresponds to directions in the grid as: /// /// Z /// | /// | /// /// 6 2 5 /// \ | / /// -- 3 - X - 1 ----- X /// / | \ /// 7 0 4 /// /// | /// | /// /// /// See: /// See: /// See: /// See: /// See: /// public override bool HasConnectionInDirection (int dir) { return (gridFlags >> dir & GridFlagsConnectionBit0) != 0; } /// /// Enables or disables a connection in a specified direction on the graph. /// /// Note: This only changes the connection from this node to the other node. You may also want to call the same method on the other node with the opposite direction. /// /// See: /// See: /// public void SetConnection (int dir, bool value) { SetConnectionInternal(dir, value); var grid = GetGridGraph(GraphIndex); grid.nodeDataRef.connections[NodeInGridIndex] = (ulong)GetAllConnectionInternal(); } /// /// Enables or disables a connection in a specified direction on the graph. /// See: /// /// Warning: Using this method can make the graph data inconsistent. It's recommended to use other ways to update the graph, instead, for example . /// public void SetConnectionInternal (int dir, bool value) { // Set bit number #dir to 1 or 0 depending on #value unchecked { gridFlags = (ushort)(gridFlags & ~((ushort)1 << GridFlagsConnectionOffset << dir) | (value ? (ushort)1 : (ushort)0) << GridFlagsConnectionOffset << dir); } AstarPath.active.hierarchicalGraph.AddDirtyNode(this); } /// /// Sets the state of all grid connections. /// /// See: SetConnectionInternal /// /// Warning: Using this method can make the graph data inconsistent. It's recommended to use other ways to update the graph, instead. /// /// a bitmask of the connections (bit 0 is the first connection, bit 1 the second connection, etc.). public void SetAllConnectionInternal (int connections) { unchecked { gridFlags = (ushort)((gridFlags & ~GridFlagsConnectionMask) | (connections << GridFlagsConnectionOffset)); } AstarPath.active.hierarchicalGraph.AddDirtyNode(this); } /// Bitpacked int containing all 8 grid connections public int GetAllConnectionInternal () { return (gridFlags & GridFlagsConnectionMask) >> GridFlagsConnectionOffset; } public override bool HasAnyGridConnections => GetAllConnectionInternal() != 0; /// /// Disables all grid connections from this node. /// Note: Other nodes might still be able to get to this node. /// Therefore it is recommended to also disable the relevant connections on adjacent nodes. /// /// Warning: Using this method can make the graph data inconsistent. It's recommended to use other ways to update the graph, instead. /// public override void ResetConnectionsInternal () { unchecked { gridFlags = (ushort)(gridFlags & ~GridFlagsConnectionMask); } AstarPath.active.hierarchicalGraph.AddDirtyNode(this); } /// /// Work in progress for a feature that required info about which nodes were at the border of the graph. /// Note: This property is not functional at the moment. /// public bool EdgeNode { get { return (gridFlags & GridFlagsEdgeNodeMask) != 0; } set { unchecked { gridFlags = (ushort)(gridFlags & ~GridFlagsEdgeNodeMask | (value ? GridFlagsEdgeNodeMask : 0)); } } } public override GridNodeBase GetNeighbourAlongDirection (int direction) { if (HasConnectionInDirection(direction)) { GridGraph gg = GetGridGraph(GraphIndex); return gg.nodes[NodeInGridIndex+gg.neighbourOffsets[direction]]; } return null; } public override void ClearConnections (bool alsoReverse) { if (alsoReverse) { // Note: This assumes that all connections are bidirectional // which should hold for all grid graphs unless some custom code has been added for (int i = 0; i < 8; i++) { var other = GetNeighbourAlongDirection(i) as GridNode; if (other != null) { // Remove reverse connection. See doc for GridGraph.neighbourOffsets to see which indices are used for what. other.SetConnectionInternal(OppositeConnectionDirection(i), false); } } } ResetConnectionsInternal(); #if !ASTAR_GRID_NO_CUSTOM_CONNECTIONS base.ClearConnections(alsoReverse); #endif } public override void GetConnections(GetConnectionsWithData action, ref T data, int connectionFilter) { if ((connectionFilter & (Connection.IncomingConnection | Connection.OutgoingConnection)) == 0) return; GridGraph gg = GetGridGraph(GraphIndex); var neighbourOffsets = gg.neighbourOffsets; var nodes = gg.nodes; for (int i = 0; i < 8; i++) { if ((gridFlags >> i & GridFlagsConnectionBit0) != 0) { var other = nodes[NodeInGridIndex + neighbourOffsets[i]]; if (other != null) action(other, ref data); } } #if !ASTAR_GRID_NO_CUSTOM_CONNECTIONS base.GetConnections(action, ref data, connectionFilter); #endif } public override bool GetPortal (GraphNode other, out Vector3 left, out Vector3 right) { if (other.GraphIndex != GraphIndex) { left = right = Vector3.zero; return false; } GridGraph gg = GetGridGraph(GraphIndex); var cellOffset = (other as GridNode).CoordinatesInGrid - CoordinatesInGrid; var dir = OffsetToConnectionDirection(cellOffset.x, cellOffset.y); if (dir == -1 || !HasConnectionInDirection(dir)) { left = right = Vector3.zero; return false; } UnityEngine.Assertions.Assert.AreEqual(other, gg.nodes[NodeInGridIndex + gg.neighbourOffsets[dir]]); if (dir < 4) { Vector3 middle = ((Vector3)(position + other.position))*0.5f; Vector3 cross = Vector3.Cross(gg.collision.up, (Vector3)(other.position-position)); cross.Normalize(); cross *= gg.nodeSize*0.5f; left = middle - cross; right = middle + cross; } else { bool rClear = false; bool lClear = false; if (HasConnectionInDirection(dir-4)) { var n2 = gg.nodes[NodeInGridIndex + gg.neighbourOffsets[dir-4]]; if (n2.Walkable && n2.HasConnectionInDirection((dir-4+1)%4)) { rClear = true; } } if (HasConnectionInDirection((dir-4+1)%4)) { var n2 = gg.nodes[NodeInGridIndex + gg.neighbourOffsets[(dir-4+1)%4]]; if (n2.Walkable && n2.HasConnectionInDirection(dir-4)) { lClear = true; } } Vector3 middle = ((Vector3)(position + other.position))*0.5f; Vector3 cross = Vector3.Cross(gg.collision.up, (Vector3)(other.position-position)); cross.Normalize(); cross *= gg.nodeSize*1.4142f; left = middle - (lClear ? cross : Vector3.zero); right = middle + (rClear ? cross : Vector3.zero); } return true; } /// /// Filters diagonal connections based on the non-diagonal ones to prevent corner cutting and similar things. /// /// This involves some complicated bitshifting to calculate which diagonal connections /// should be active based on the non-diagonal ones. /// For example a path should not be able to pass from A to B if the \ represent nodes /// that we cannot traverse. /// /// /// # B /// A # /// /// /// Additionally if corner cutting is disabled we will also prevent a connection from A to B in this case: /// /// /// B /// A # /// /// /// If neighbours = 4 then only the 4 axis aligned connections will be enabled. /// /// If neighbours = 6 then only the connections which are valid for hexagonal graphs will be enabled. /// public static int FilterDiagonalConnections (int conns, NumNeighbours neighbours, bool cutCorners) { switch (neighbours) { case NumNeighbours.Four: // The first 4 bits are the axis aligned connections return conns & 0xF; // Default case exists only to make the compiler happy, it is never intended to be used. default: case NumNeighbours.Eight: if (cutCorners) { int axisConns = conns & 0xF; // If at least one axis aligned connection // is adjacent to this diagonal, then we can add a connection. // Bitshifting is a lot faster than calling node.HasConnectionInDirection. // We need to check if connection i and i+1 are enabled // but i+1 may overflow 4 and in that case need to be wrapped around // (so 3+1 = 4 goes to 0). We do that by checking both connection i+1 // and i+1-4 at the same time. Either i+1 or i+1-4 will be in the range // from 0 to 4 (exclusive) int diagConns = (axisConns | (axisConns >> 1 | axisConns << 3)) << 4; // Filter out diagonal connections that are invalid // This will also filter out some junk bits which may be set to true above bit 8 diagConns &= conns; return axisConns | diagConns; } else { int axisConns = conns & 0xF; // If exactly 2 axis aligned connections are adjacent to a diagonal connection // then the diagonal connection is ok to use. int diagConns = (axisConns & (axisConns >> 1 | axisConns << 3)) << 4; // Filter out diagonal connections that are invalid // This will also filter out some junk bits which may be set above bit 8 diagConns &= conns; return axisConns | diagConns; } case NumNeighbours.Six: // Hexagon layout return conns & GridGraph.HexagonConnectionMask; } } public override void Open (Path path, uint pathNodeIndex, uint gScore) { GridGraph gg = GetGridGraph(GraphIndex); int[] neighbourOffsets = gg.neighbourOffsets; uint[] neighbourCosts = gg.neighbourCosts; var nodes = gg.nodes; var index = NodeInGridIndex; // Bitmask of the 8 connections out of this node // Each bit represents one connection. var conns = gridFlags & GridFlagsConnectionMask; // Loop over all connections, first the 4 axis aligned ones and then the 4 diagonal ones. for (int dir = 0; dir < 8; dir++) { // dir=4 is the first diagonal connection. // At this point we know exactly which orthogonal (not diagonal) connections are actually traversable. // So we do some filtering to determine which diagonals should be traversable. // // We do this dynamically because each path may use different tags or different // ITraversalProviders that affect the result. // // When the grid graph is scanned this exact method is also run to pre-filter connections // based on their walkability values. // Doing pre-filtering is good because it allows users to use `HasConnectionInDirection` // and it will return accurate values even for diagonals (even though it will of course not // take into account any additional constraints such as tags or ITraversalProviders). if (dir == 4 && (path.traversalProvider == null || path.traversalProvider.filterDiagonalGridConnections)) { conns = FilterDiagonalConnections(conns, gg.neighbours, gg.cutCorners); } // Check if we have a connection in this direction if (((conns >> dir) & GridFlagsConnectionBit0) != 0) { var other = nodes[index + neighbourOffsets[dir]]; if (path.CanTraverse(this, other)) { path.OpenCandidateConnection(pathNodeIndex, other.NodeIndex, gScore, neighbourCosts[dir], 0, other.position); } else { // Mark that connection as not valid conns &= ~(GridFlagsConnectionBit0 << dir); } } } base.Open(path, pathNodeIndex, gScore); } public override void SerializeNode (GraphSerializationContext ctx) { base.SerializeNode(ctx); ctx.SerializeInt3(position); ctx.writer.Write(gridFlags); } public override void DeserializeNode (GraphSerializationContext ctx) { base.DeserializeNode(ctx); position = ctx.DeserializeInt3(); gridFlags = ctx.reader.ReadUInt16(); } public override void AddPartialConnection (GraphNode node, uint cost, bool isOutgoing, bool isIncoming) { // In case the node was already added as an internal grid connection, // we need to remove that connection before we insert it as a custom connection. // Using a custom connection is necessary because it has a custom cost. if (node is GridNode gn && gn.GraphIndex == GraphIndex) { RemoveGridConnection(gn); } base.AddPartialConnection(node, cost, isOutgoing, isIncoming); } public override void RemovePartialConnection (GraphNode node) { base.RemovePartialConnection(node); // If the node is a grid node on the same graph, it might be added as an internal connection and not a custom one. if (node is GridNode gn && gn.GraphIndex == GraphIndex) { RemoveGridConnection(gn); } } /// /// Removes a connection from the internal grid connections. /// See: SetConnectionInternal /// protected void RemoveGridConnection (GridNode node) { var nodeIndex = NodeInGridIndex; var gg = GetGridGraph(GraphIndex); for (int i = 0; i < 8; i++) { if (nodeIndex + gg.neighbourOffsets[i] == node.NodeInGridIndex && GetNeighbourAlongDirection(i) == node) { SetConnectionInternal(i, false); break; } } } #else public override void AddPartialConnection (GraphNode node, uint cost, bool isOutgoing, bool isIncoming) { throw new System.NotImplementedException(); } public override void ClearConnections (bool alsoReverse) { throw new System.NotImplementedException(); } public override void GetConnections (GraphNodeDelegate del) { throw new System.NotImplementedException(); } public override void Open (Path path, PathNode pathNode, PathHandler handler) { throw new System.NotImplementedException(); } public override void AddPartialConnection (GraphNode node) { throw new System.NotImplementedException(); } #endif } }