code/BaseVoxelVolume/Pathfinding/BaseVoxelVolume.Pathfinding.cs
namespace Boxfish;
partial class BaseVoxelVolume<T, U>
{
/// <summary>
/// Check if there's only air above
/// </summary>
/// <param name="position"></param>
/// <param name="amount"></param>
/// <returns></returns>
public bool IsAboveFree( Vector3Int position, int amount = 3 )
{
for ( int z = 1; z <= amount; z++ )
if ( Query( position + new Vector3Int( 0, 0, z ) ).HasVoxel )
return false;
return true;
}
/// <summary>
/// Find the nearest walkable voxel, EXPENSIVE! Only checks below, no reason to check up
/// </summary>
/// <param name="point"></param>
/// <param name="maxDistance"></param>
/// <param name="possibleTokenSource"></param>
/// <returns></returns>
public VoxelQueryData? GetClosestWalkable( Vector3Int point, int maxDistance = 400, CancellationTokenSource possibleTokenSource = null )
{
if ( !this.IsValid() )
return null;
var initial = Query( point );
if ( initial.HasVoxel && IsAboveFree( initial.GlobalPosition ) ) // Worth a try!
return initial;
for ( int z = -1; z > -3; z-- )
{
try
{
if ( possibleTokenSource is { Token.IsCancellationRequested: true } ) return null;
}
catch ( ObjectDisposedException ) { return null; }
var belowVoxel = Query( point + new Vector3Int( 0, 0, z ) );
if ( belowVoxel.HasVoxel && IsAboveFree( belowVoxel.GlobalPosition ) ) // Worth a shot!
return belowVoxel;
}
var directions = new Vector3Int[]
{
new Vector3Int(1, 0, 0), new Vector3Int(-1, 0, 0),
new Vector3Int(0, 1, 0), new Vector3Int(0, -1, 0),
new Vector3Int(0, 0, -1)
};
var queue = new Queue<Vector3Int>();
var visited = new HashSet<Vector3Int>();
queue.Enqueue( initial.GlobalPosition );
visited.Add( initial.GlobalPosition );
while ( queue.Count > 0 )
{
try
{
if ( possibleTokenSource is { Token.IsCancellationRequested: true } ) return null;
}
catch ( ObjectDisposedException ) { return null; }
var currentPos = queue.Dequeue();
if ( !this.IsValid() || currentPos.Distance( initial.GlobalPosition ) > maxDistance )
continue;
var current = Query( currentPos );
if ( current.HasVoxel )
{
if ( IsAboveFree( current.GlobalPosition, 3 ) )
return current;
// Since this voxel isn't air we know the ones below aren't available
visited.Add( current.GlobalPosition - new Vector3Int( 0, 0, 1 ) );
visited.Add( current.GlobalPosition - new Vector3Int( 0, 0, 2 ) );
visited.Add( current.GlobalPosition - new Vector3Int( 0, 0, 3 ) );
}
foreach ( var direction in directions )
{
var neighbor = current.GlobalPosition + direction;
if ( !visited.Contains( neighbor ) )
{
queue.Enqueue( neighbor );
visited.Add( neighbor );
}
}
}
return null;
}
/// <summary>
/// Computes a path from the starting point to a target point. Reversing the path if needed.
/// </summary>
/// <param name="startNode">The starting point of the path.</param>
/// <param name="endNode">The desired destination point of the path.</param>
/// <param name="token">A cancellation token used to cancel computing the path.</param>
/// <param name="maxDistance">Max distance it will search for, then will just approach the closest point</param>
/// <param name="acceptPartial">If it doesn't find the target, return the closest node found</param>
/// <param name="reversed">Whether or not to reverse the resulting path.</param>
/// <returns>A path</returns>
public async Task<AStarPath> ComputePath( AStarNode startNode, AStarNode endNode, CancellationToken token, float maxDistance = 2048f, bool acceptPartial = true, bool reversed = false )
{
var path = new List<AStarNode>();
var maxCells = 8192;
var openSet = new Heap<AStarNode>( maxCells );
var openSetReference = new Dictionary<int, AStarNode>();
var closedSet = new HashSet<AStarNode>();
openSet.Add( startNode );
openSetReference.Add( startNode.GetHashCode(), startNode );
try
{
var cellsChecked = 0;
while ( openSet.Count > 0 && cellsChecked < maxCells )
{
if ( token.IsCancellationRequested )
{
return new AStarPath( path );
}
var currentNode = openSet.RemoveFirst();
closedSet.Add( currentNode );
if ( currentNode.Data.GlobalPosition == endNode.Data.GlobalPosition )
{
RetracePath( ref path, startNode, currentNode );
break;
}
foreach ( var neighbour in currentNode.GetNeighbours() )
{
if ( closedSet.Contains( neighbour ) )
continue;
var isInOpenSet = openSetReference.ContainsKey( neighbour.GetHashCode() );
var currentNeighbour = isInOpenSet ? openSetReference[neighbour.GetHashCode()] : neighbour;
var newMovementCostToNeighbour = currentNode.gCost + currentNode.Distance( currentNeighbour );
var distanceToTarget = currentNeighbour.Distance( endNode );
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 );
}
}
}
cellsChecked++;
}
}
catch ( OperationCanceledException )
{
await Task.CompletedTask;
return new AStarPath( path );
}
if ( path.Count == 0 && acceptPartial )
{
var closestNode = closedSet
.OrderBy( x => x.hCost )
.FirstOrDefault( x => x.gCost != 0f );
if ( closestNode != null )
{
RetracePath( ref path, startNode, closestNode );
}
}
if ( reversed )
{
path.Reverse();
}
await Task.CompletedTask;
return new AStarPath( path );
}
private static void RetracePath( ref List<AStarNode> pathList, AStarNode startNode, AStarNode targetNode )
{
if ( pathList == null )
return;
if ( targetNode == null || startNode == null ) return;
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 == null )
continue;
var newNode = new AStarNode( node.Parent.Data, node ) { Volume = startNode.Volume };
fixedList.Add( newNode );
}
pathList = fixedList;
//pathList = pathList.Select( node => new AStarNode( node.Parent.Current, node, node.MovementTag ) ).ToList(); // Cell connections are flipped when we reversed earlier
}
public bool LineOfSight( AStarNode start, AStarNode end, bool debugShow = false )
{
var startingPosition = start.Data.GlobalPosition;
var endingPosition = end.Data.GlobalPosition;
var distanceInSteps = (int)Math.Ceiling( VoxelToWorld( startingPosition ).Distance( VoxelToWorld( endingPosition ) ) / Scale );
var lastNode = start;
for ( int i = 0; i <= distanceInSteps; i++ )
{
var direction = (endingPosition - lastNode.Data.GlobalPosition).Normal;
var nodeToCheck = lastNode.GetNeighbourInDirection( direction );
if ( nodeToCheck == null ) return false;
//if ( debugShow )
// DebugOverlay.BBox( BBox.FromPositionAndSize( VoxelToWorld( nodeToCheck.Data.GlobalPosition ) + VoxelUtility.Scale / 2f, VoxelUtility.Scale * 1.1f ), DebugStyle.Solid, Color.Gray, 1f );
if ( nodeToCheck == end ) return true;
if ( nodeToCheck == lastNode ) continue;
if ( !nodeToCheck.IsNeighbour( lastNode ) ) return false;
lastNode = nodeToCheck;
}
return true;
}
}