Grid/Grid.Nav.cs

Part of a Grid A* pathfinding implementation. Computes an A* path from a start cell to a target cell with filtering by tags, occupancy, drop height, optional reversed output, and an option to accept partial paths. RetracePath builds the node path and reconstructs nodes with parent links and movement tags.

File Access
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace GridAStar;

public partial class Grid
{
	/// <summary>
	/// Computes a path from the starting point to a target point. Reversing the path if needed.
	/// </summary>
	internal List<AStarNode> ComputePathInternal( AStarPathBuilder pathBuilder, Cell startingCell, Cell targetCell, CancellationToken token, bool reversed = false, bool withCellConnections = true )
	{
		// Setup.
		var path = new List<AStarNode>();

		var startingNode = new AStarNode( startingCell );
		var targetNode = new AStarNode( targetCell );

		var maxCells = AllCells.Count();
		var openSet = new Heap<AStarNode>( maxCells );
		var closedSet = new HashSet<AStarNode>();
		var openSetReference = new Dictionary<int, AStarNode>();
		var initialDistance = startingNode.Distance( targetNode );
		var maxDistance = Math.Max( initialDistance, initialDistance + pathBuilder.MaxCheckDistance ) + CellSize;

		openSet.Add( startingNode );
		openSetReference.Add( startingNode.GetHashCode(), startingNode );

		while ( openSet.Count > 0 && !token.IsCancellationRequested )
		{
			var currentNode = openSet.RemoveFirst();
			closedSet.Add( currentNode );

			if ( currentNode.Current == targetNode.Current )
			{
				RetracePath( ref path, startingNode, currentNode );
				break;
			}

			foreach ( var neighbour in withCellConnections ? currentNode.Current.GetNeighbourAndConnections() : currentNode.Current.GetNeighbourConnections() )
			{
				if ( pathBuilder.HasOccupiedTagToExclude && !pathBuilder.HasPathCreator && neighbour.Occupied ) continue;
				if ( pathBuilder.HasOccupiedTagToExclude && pathBuilder.HasPathCreator && neighbour.Occupied && neighbour.OccupyingEntity != pathBuilder.PathCreator ) continue;
				if ( pathBuilder.HasTagsToExlude && neighbour.Tags.Has( pathBuilder.TagsToExclude ) ) continue;
				if ( pathBuilder.HasTagsToInclude && !neighbour.Tags.Has( pathBuilder.TagsToInclude ) ) continue;
				if ( neighbour.MovementTag == "drop" && currentNode.Current.Bottom.z - neighbour.Current.Position.z > pathBuilder.MaxDropHeight ) continue;
				if ( closedSet.Contains( neighbour ) ) continue;

				var isInOpenSet = openSetReference.ContainsKey( neighbour.GetHashCode() );
				var currentNeighbour = isInOpenSet ? openSetReference[neighbour.GetHashCode()] : neighbour;

				var malus = 0f;

				if ( pathBuilder.HasTagsToAvoid && currentNeighbour.Tags.Has( pathBuilder.TagsToAvoid.Keys ) )
					foreach ( var tag in currentNeighbour.Tags.All )
						if ( pathBuilder.TagsToAvoid.TryGetValue( tag, out float tagMalus ) )
							malus += tagMalus;

				var newMovementCostToNeighbour = currentNode.gCost + currentNode.Distance( currentNeighbour ) + malus / 2f;
				var distanceToTarget = currentNeighbour.Distance( targetNode ) + malus / 2f;

				if ( distanceToTarget > maxDistance ) continue;

				if ( newMovementCostToNeighbour < currentNeighbour.gCost || !isInOpenSet )
				{
					currentNeighbour.gCost = newMovementCostToNeighbour;
					currentNeighbour.hCost = distanceToTarget;
					currentNeighbour.Parent = currentNode;

					if ( !isInOpenSet )
					{
						openSet.Add( currentNeighbour );
						openSetReference.Add( currentNeighbour.GetHashCode(), currentNeighbour );
					}
				}
			}
		}

		if ( token.IsCancellationRequested )
			return path;

		if ( path.Count == 0 && pathBuilder.AcceptsPartial )
		{
			var closestNode = closedSet.OrderBy( x => x.hCost )
				.Where( x => x.gCost != 0f )
				.FirstOrDefault();

			if ( closestNode is not null )
				RetracePath( ref path, startingNode, closestNode );
		}

		if ( reversed )
			path.Reverse();

		return path;
	}

	private static void RetracePath( ref List<AStarNode> pathList, AStarNode startNode, AStarNode targetNode )
	{
		var currentNode = targetNode;

		while ( currentNode != startNode )
		{
			pathList.Add( currentNode );
			currentNode = currentNode.Parent;
		}
		pathList.Reverse();

		var fixedList = new List<AStarNode>();

		foreach ( var node in pathList )
		{
			if ( node.Parent?.Current == null )
				continue;

			var newNode = new AStarNode( node.Parent.Current, node, node.MovementTag );
			fixedList.Add( newNode );
		}

		pathList = fixedList;
	}
}