code/BaseVoxelVolume/Pathfinding/BaseVoxelVolume.AStar.cs
namespace Boxfish;
partial class BaseVoxelVolume<T, U>
{
/// <summary>
/// We store information about A* nodes and some utility methods inside of this class.
/// </summary>
public class AStarNode : IHeapItem<AStarNode>, IEquatable<AStarNode>, IValid
{
public VoxelQueryData Data { get; internal set; }
public BaseVoxelVolume<T, U> Volume { get; set; }
public float gCost { get; set; } = 0f;
public float hCost { get; set; } = 0f;
public float fCost => gCost + hCost;
public int HeapIndex { get; set; }
public bool IsValid = false;
bool IValid.IsValid => IsValid;
public AStarNode Parent;
public AStarNode() { }
public AStarNode( VoxelQueryData data, AStarNode parent = null )
{
Data = data;
Parent = parent;
IsValid = true;
}
/// <summary>
/// Get all neighbouring voxels, even ones we can jump on or land down
/// </summary>
/// <returns></returns>
public IEnumerable<AStarNode> GetNeighbours()
{
for ( int x = -1; x <= 1; x++ )
for ( int y = -1; y <= 1; y++ )
for ( int z = -2; z <= 2; z++ )
{
if ( x == 0 && y == 0 ) continue; // Erm don't check the blocks we're on?
var offset = new Vector3Int( x, y, z );
var checkPosition = Data.GlobalPosition + offset;
var queryData = Volume.Query( checkPosition );
if ( !queryData.HasVoxel ) continue; // Not solid ground to stand
if ( !Volume.IsAboveFree( checkPosition, Math.Min( 3, 3 - z ) ) ) continue; // Space is not free for us to occupy (DEFAULT IS 3 BLOCKS TALL, WILL MODIFY
z = 3; // If you think about it, we checked the above voxels already
yield return new AStarNode( queryData, this ) { Volume = Volume };
}
}
/// <summary>
/// Get the distance between the node's voxel's position
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public float Distance( AStarNode other ) => Data.GlobalPosition.Distance( other.Data.GlobalPosition );
/// <summary>
/// Get the horizontal distance between the node's voxel's position
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public float HorizontalDistance( AStarNode other ) => Data.GlobalPosition.WithZ( 0 ).Distance( other.Data.GlobalPosition.WithZ( 0 ) );
public bool IsNeighbour( AStarNode node )
{
var xDistance = node.Data.GlobalPosition.x - Data.GlobalPosition.x;
var yDistance = node.Data.GlobalPosition.y - Data.GlobalPosition.y;
if ( xDistance < -1 || xDistance > 1 || yDistance < -1 || yDistance > 1 ) return false;
if ( node == this ) return true;
if ( xDistance == 0 && yDistance == 0 ) return false;
foreach ( var neighbour in GetNeighbours() )
if ( node == neighbour ) return true;
return false;
}
/// <summary>
/// Returns the neighbour in that direction
/// </summary>
/// <param name="direction"></param>
/// <returns></returns>
public AStarNode GetNeighbourInDirection( Vector3 direction )
{
var horizontalDirection = direction.WithZ( 0 ).Normal;
var localCoordinates = new Vector2Int( MathX.FloorToInt( horizontalDirection.x + 0.5f ), MathX.FloorToInt( horizontalDirection.y + 0.5f ) );
var coordinatesToCheck = Data.GlobalPosition + localCoordinates;
foreach ( var neighbour in GetNeighbours() )
if ( neighbour.Data.GlobalPosition.x == coordinatesToCheck.x && neighbour.Data.GlobalPosition.y == coordinatesToCheck.y )
return neighbour;
return null;
}
public int CompareTo( AStarNode other )
{
var compare = fCost.CompareTo( other.fCost );
if ( compare == 0 )
compare = hCost.CompareTo( other.hCost );
return -compare;
}
public static bool operator ==( AStarNode a, AStarNode b )
{
if ( ReferenceEquals( a, b ) ) return true; // Both are the same instance or both are null
if ( a is null || b is null ) return false; // One is null and the other is not
return a.Equals( b );
}
public static bool operator !=( AStarNode a, AStarNode b ) => !(a == b);
public override bool Equals( object obj )
{
if ( obj == null ) return false;
if ( obj is not AStarNode node ) return false;
return Data.GlobalPosition.Equals( node.Data.GlobalPosition );
}
public bool Equals( AStarNode other )
{
if ( other == null ) return false;
return Data.GlobalPosition.Equals( other.Data.GlobalPosition );
}
public override int GetHashCode()
{
return Data.GlobalPosition.GetHashCode();
}
}
/// <summary>
/// This struct defines the path full A* path.
/// </summary>
public struct AStarPath
{
public List<AStarNode> Nodes { get; private set; } = new();
public int Count => Nodes?.Count() ?? 0;
public bool IsEmpty => Nodes == null || Count == 0;
public AStarPath() { }
public AStarPath( List<AStarNode> nodes ) : this()
{
Nodes = nodes;
}
/// <summary>
/// Return the total length of the path
/// </summary>
/// <returns></returns>
public float GetLength()
{
var length = 0f;
for ( int i = 0; i < Nodes.Count - 1; i++ )
length += Nodes[i].Data.GlobalPosition.Distance( Nodes[i + 1].Data.GlobalPosition );
return length;
}
/// <summary>
/// Simplify the path by iterating over line of sights between the given segment size, joining them if valid
/// </summary>
/// <param name="segmentAmounts"></param>
/// <param name="iterations"></param>
/// <returns></returns>
public void Simplify( int segmentAmounts = 2, int iterations = 1 ) // TODO DOESN'T WORK THAT WELL
{
for ( int iteration = 0; iteration < iterations; iteration++ )
{
var segmentStart = 0;
var segmentEnd = Math.Min( segmentAmounts, Count - 1 );
while ( Count > 2 && segmentEnd < Count - 1 )
{
var currentNode = Nodes[segmentStart];
var nextNode = Nodes[segmentStart + 1];
var furtherNode = Nodes[segmentEnd];
if ( currentNode.Volume.LineOfSight( currentNode, furtherNode, false ) )
for ( int toDelete = segmentStart + 1; toDelete < segmentEnd; toDelete++ )
Nodes.RemoveAt( toDelete );
if ( segmentEnd == Count - 1 )
break;
segmentStart++;
segmentEnd = Math.Min( segmentStart + segmentAmounts, Count - 1 );
}
}
}
}
}