2023-08-01 06:49:57 +00:00
using UnityEngine ;
using System.Collections.Generic ;
using UnityEngine.Profiling ;
namespace Pathfinding {
using System.IO ;
using Pathfinding.Util ;
using Pathfinding.Serialization ;
using Math = System . Math ;
using System.Linq ;
/// <summary>Base class for <see cref="RecastGraph"/> and <see cref="NavMeshGraph"/></summary>
public abstract class NavmeshBase : NavGraph , INavmesh , INavmeshHolder , ITransformedGraph
, IRaycastableGraph {
#if ASTAR_RECAST_LARGER_TILES
// Larger tiles
public const int VertexIndexMask = 0xFFFFF ;
public const int TileIndexMask = 0x7FF ;
public const int TileIndexOffset = 20 ;
#else
// Larger worlds
public const int VertexIndexMask = 0xFFF ;
public const int TileIndexMask = 0x7FFFF ;
public const int TileIndexOffset = 12 ;
#endif
/// <summary>Size of the bounding box.</summary>
[JsonMember]
public Vector3 forcedBoundsSize = new Vector3 ( 100 , 40 , 100 ) ;
/// <summary>Size of a tile in world units along the X axis</summary>
public abstract float TileWorldSizeX { get ; }
/// <summary>Size of a tile in world units along the Z axis</summary>
public abstract float TileWorldSizeZ { get ; }
/// <summary>
/// Maximum (vertical) distance between the sides of two nodes for them to be connected across a tile edge.
/// When tiles are connected to each other, the nodes sometimes do not line up perfectly
/// so some allowance must be made to allow tiles that do not match exactly to be connected with each other.
/// </summary>
protected abstract float MaxTileConnectionEdgeDistance { get ; }
/// <summary>Show an outline of the polygons in the Unity Editor</summary>
[JsonMember]
public bool showMeshOutline = true ;
/// <summary>Show the connections between the polygons in the Unity Editor</summary>
[JsonMember]
public bool showNodeConnections ;
/// <summary>Show the surface of the navmesh</summary>
[JsonMember]
public bool showMeshSurface = true ;
/// <summary>Number of tiles along the X-axis</summary>
public int tileXCount ;
/// <summary>Number of tiles along the Z-axis</summary>
public int tileZCount ;
/// <summary>
/// All tiles.
///
/// See: <see cref="GetTile"/>
/// </summary>
protected NavmeshTile [ ] tiles ;
/// <summary>
/// Perform nearest node searches in XZ space only.
/// Recomended for single-layered environments. Faster but can be inaccurate esp. in multilayered contexts.
/// You should not use this if the graph is rotated since then the XZ plane no longer corresponds to the ground plane.
///
/// This can be important on sloped surfaces. See the image below in which the closest point for each blue point is queried for:
/// [Open online documentation to see images]
///
/// You can also control this using a <see cref="Pathfinding.NNConstraint.distanceXZ field on an NNConstraint"/>.
/// </summary>
[JsonMember]
public bool nearestSearchOnlyXZ ;
/// <summary>
/// Should navmesh cuts affect this graph.
/// See: <see cref="navmeshUpdateData"/>
/// </summary>
[JsonMember]
public bool enableNavmeshCutting = true ;
/// <summary>
/// Handles navmesh cutting.
/// See: <see cref="enableNavmeshCutting"/>
/// See: <see cref="Pathfinding.NavmeshUpdates"/>
/// </summary>
internal readonly NavmeshUpdates . NavmeshUpdateSettings navmeshUpdateData ;
/// <summary>Currently updating tiles in a batch</summary>
bool batchTileUpdate ;
/// <summary>List of tiles updating during batch</summary>
List < int > batchUpdatedTiles = new List < int > ( ) ;
/// <summary>List of nodes that are going to be destroyed as part of a batch update</summary>
List < MeshNode > batchNodesToDestroy = new List < MeshNode > ( ) ;
/// <summary>
/// Determines how the graph transforms graph space to world space.
/// See: <see cref="CalculateTransform"/>
/// </summary>
public GraphTransform transform = new GraphTransform ( Matrix4x4 . identity ) ;
GraphTransform ITransformedGraph . transform { get { return transform ; } }
/// <summary>\copydoc Pathfinding::NavMeshGraph::recalculateNormals</summary>
2024-02-20 18:34:40 +00:00
public abstract bool RecalculateNormals { get ; }
public override bool isScanned = > tiles ! = null ;
2023-08-01 06:49:57 +00:00
/// <summary>
/// Returns a new transform which transforms graph space to world space.
/// Does not update the <see cref="transform"/> field.
/// See: <see cref="RelocateNodes(GraphTransform)"/>
/// </summary>
public abstract GraphTransform CalculateTransform ( ) ;
/// <summary>
/// Called when tiles have been completely recalculated.
/// This is called after scanning the graph and after
/// performing graph updates that completely recalculate tiles
/// (not ones that simply modify e.g penalties).
/// It is not called after NavmeshCut updates.
/// </summary>
public System . Action < NavmeshTile [ ] > OnRecalculatedTiles ;
/// <summary>
/// Tile at the specified x, z coordinate pair.
/// The first tile is at (0,0), the last tile at (tileXCount-1, tileZCount-1).
///
/// <code>
/// var graph = AstarPath.active.data.recastGraph;
/// int tileX = 5;
/// int tileZ = 8;
/// NavmeshTile tile = graph.GetTile(tileX, tileZ);
///
/// for (int i = 0; i < tile.nodes.Length; i++) {
/// // ...
/// }
/// // or you can access the nodes like this:
/// tile.GetNodes(node => {
/// // ...
/// });
/// </code>
/// </summary>
public NavmeshTile GetTile ( int x , int z ) {
return tiles [ x + z * tileXCount ] ;
}
/// <summary>
/// Vertex coordinate for the specified vertex index.
///
/// Throws: IndexOutOfRangeException if the vertex index is invalid.
/// Throws: NullReferenceException if the tile the vertex is in is not calculated.
///
/// See: NavmeshTile.GetVertex
/// </summary>
public Int3 GetVertex ( int index ) {
int tileIndex = ( index > > TileIndexOffset ) & TileIndexMask ;
return tiles [ tileIndex ] . GetVertex ( index ) ;
}
/// <summary>Vertex coordinate in graph space for the specified vertex index</summary>
public Int3 GetVertexInGraphSpace ( int index ) {
int tileIndex = ( index > > TileIndexOffset ) & TileIndexMask ;
return tiles [ tileIndex ] . GetVertexInGraphSpace ( index ) ;
}
/// <summary>Tile index from a vertex index</summary>
public static int GetTileIndex ( int index ) {
return ( index > > TileIndexOffset ) & TileIndexMask ;
}
public int GetVertexArrayIndex ( int index ) {
return index & VertexIndexMask ;
}
/// <summary>Tile coordinates from a tile index</summary>
public void GetTileCoordinates ( int tileIndex , out int x , out int z ) {
//z = System.Math.DivRem (tileIndex, tileXCount, out x);
z = tileIndex / tileXCount ;
x = tileIndex - z * tileXCount ;
}
/// <summary>
/// All tiles.
/// Warning: Do not modify this array
/// </summary>
public NavmeshTile [ ] GetTiles ( ) {
return tiles ;
}
/// <summary>
/// Returns a bounds object with the bounding box of a group of tiles.
/// The bounding box is defined in world space.
/// </summary>
public Bounds GetTileBounds ( IntRect rect ) {
return GetTileBounds ( rect . xmin , rect . ymin , rect . Width , rect . Height ) ;
}
/// <summary>
/// Returns a bounds object with the bounding box of a group of tiles.
/// The bounding box is defined in world space.
/// </summary>
public Bounds GetTileBounds ( int x , int z , int width = 1 , int depth = 1 ) {
return transform . Transform ( GetTileBoundsInGraphSpace ( x , z , width , depth ) ) ;
}
public Bounds GetTileBoundsInGraphSpace ( IntRect rect ) {
return GetTileBoundsInGraphSpace ( rect . xmin , rect . ymin , rect . Width , rect . Height ) ;
}
/// <summary>Returns an XZ bounds object with the bounds of a group of tiles in graph space</summary>
public Bounds GetTileBoundsInGraphSpace ( int x , int z , int width = 1 , int depth = 1 ) {
var b = new Bounds ( ) ;
b . SetMinMax (
new Vector3 ( x * TileWorldSizeX , 0 , z * TileWorldSizeZ ) ,
new Vector3 ( ( x + width ) * TileWorldSizeX , forcedBoundsSize . y , ( z + depth ) * TileWorldSizeZ )
) ;
return b ;
}
/// <summary>
/// Returns the tile coordinate which contains the specified position.
/// It is not necessarily a valid tile (i.e it could be out of bounds).
/// </summary>
public Int2 GetTileCoordinates ( Vector3 position ) {
position = transform . InverseTransform ( position ) ;
position . x / = TileWorldSizeX ;
position . z / = TileWorldSizeZ ;
return new Int2 ( ( int ) position . x , ( int ) position . z ) ;
}
2024-02-20 18:34:40 +00:00
protected override void DestroyAllNodes ( ) {
// Remove cross-graph connections
GetNodes ( node = > {
node . GetConnections ( other = > {
if ( node . GraphIndex ! = other . GraphIndex ) other . RemoveConnection ( node ) ;
} ) ;
} ) ;
// Destroy all nodes
GetNodes ( node = > {
node . Destroy ( ) ;
} ) ;
2023-08-01 06:49:57 +00:00
// Cleanup
TriangleMeshNode . SetNavmeshHolder ( active . data . GetGraphIndex ( this ) , null ) ;
if ( tiles ! = null ) {
for ( int i = 0 ; i < tiles . Length ; i + + ) {
Pathfinding . Util . ObjectPool < BBTree > . Release ( ref tiles [ i ] . bbTree ) ;
}
2024-02-20 18:34:40 +00:00
tiles = null ;
2023-08-01 06:49:57 +00:00
}
}
public override void RelocateNodes ( Matrix4x4 deltaMatrix ) {
RelocateNodes ( deltaMatrix * transform ) ;
}
/// <summary>
/// Moves the nodes in this graph.
/// Moves all the nodes in such a way that the specified transform is the new graph space to world space transformation for the graph.
/// You usually use this together with the <see cref="CalculateTransform"/> method.
///
/// So for example if you want to move and rotate all your nodes in e.g a recast graph you can do
/// <code>
/// var graph = AstarPath.data.recastGraph;
/// graph.rotation = new Vector3(45, 0, 0);
/// graph.forcedBoundsCenter = new Vector3(20, 10, 10);
/// var transform = graph.CalculateTransform();
/// graph.RelocateNodes(transform);
/// </code>
/// This will move all the nodes to new positions as if the new graph settings had been there from the start.
///
/// Note: RelocateNodes(deltaMatrix) is not equivalent to RelocateNodes(new GraphTransform(deltaMatrix)).
/// The overload which takes a matrix multiplies all existing node positions with the matrix while this
/// overload does not take into account the current positions of the nodes.
///
/// See: <see cref="CalculateTransform"/>
/// </summary>
public void RelocateNodes ( GraphTransform newTransform ) {
transform = newTransform ;
if ( tiles ! = null ) {
// Move all the vertices in each tile
for ( int tileIndex = 0 ; tileIndex < tiles . Length ; tileIndex + + ) {
var tile = tiles [ tileIndex ] ;
if ( tile ! = null ) {
tile . vertsInGraphSpace . CopyTo ( tile . verts , 0 ) ;
// Transform the graph space vertices to world space
transform . Transform ( tile . verts ) ;
for ( int nodeIndex = 0 ; nodeIndex < tile . nodes . Length ; nodeIndex + + ) {
tile . nodes [ nodeIndex ] . UpdatePositionFromVertices ( ) ;
}
tile . bbTree . RebuildFrom ( tile . nodes ) ;
}
}
}
}
/// <summary>Creates a single new empty tile</summary>
protected NavmeshTile NewEmptyTile ( int x , int z ) {
return new NavmeshTile {
x = x ,
z = z ,
w = 1 ,
d = 1 ,
verts = new Int3 [ 0 ] ,
vertsInGraphSpace = new Int3 [ 0 ] ,
tris = new int [ 0 ] ,
nodes = new TriangleMeshNode [ 0 ] ,
bbTree = ObjectPool < BBTree > . Claim ( ) ,
graph = this ,
} ;
}
public override void GetNodes ( System . Action < GraphNode > action ) {
if ( tiles = = null ) return ;
for ( int i = 0 ; i < tiles . Length ; i + + ) {
if ( tiles [ i ] = = null | | tiles [ i ] . x + tiles [ i ] . z * tileXCount ! = i ) continue ;
TriangleMeshNode [ ] nodes = tiles [ i ] . nodes ;
if ( nodes = = null ) continue ;
for ( int j = 0 ; j < nodes . Length ; j + + ) action ( nodes [ j ] ) ;
}
}
/// <summary>
/// Returns a rect containing the indices of all tiles touching the specified bounds.
/// If a margin is passed, the bounding box in graph space is expanded by that amount in every direction.
/// </summary>
public IntRect GetTouchingTiles ( Bounds bounds , float margin = 0 ) {
bounds = transform . InverseTransform ( bounds ) ;
// Calculate world bounds of all affected tiles
var r = new IntRect ( Mathf . FloorToInt ( ( bounds . min . x - margin ) / TileWorldSizeX ) , Mathf . FloorToInt ( ( bounds . min . z - margin ) / TileWorldSizeZ ) , Mathf . FloorToInt ( ( bounds . max . x + margin ) / TileWorldSizeX ) , Mathf . FloorToInt ( ( bounds . max . z + margin ) / TileWorldSizeZ ) ) ;
// Clamp to bounds
r = IntRect . Intersection ( r , new IntRect ( 0 , 0 , tileXCount - 1 , tileZCount - 1 ) ) ;
return r ;
}
/// <summary>Returns a rect containing the indices of all tiles touching the specified bounds.</summary>
/// <param name="rect">Graph space rectangle (in graph space all tiles are on the XZ plane regardless of graph rotation and other transformations, the first tile has a corner at the origin)</param>
public IntRect GetTouchingTilesInGraphSpace ( Rect rect ) {
// Calculate world bounds of all affected tiles
var r = new IntRect ( Mathf . FloorToInt ( rect . xMin / TileWorldSizeX ) , Mathf . FloorToInt ( rect . yMin / TileWorldSizeZ ) , Mathf . FloorToInt ( rect . xMax / TileWorldSizeX ) , Mathf . FloorToInt ( rect . yMax / TileWorldSizeZ ) ) ;
// Clamp to bounds
r = IntRect . Intersection ( r , new IntRect ( 0 , 0 , tileXCount - 1 , tileZCount - 1 ) ) ;
return r ;
}
/// <summary>
/// Returns a rect containing the indices of all tiles by rounding the specified bounds to tile borders.
/// This is different from GetTouchingTiles in that the tiles inside the rectangle returned from this method
/// may not contain the whole bounds, while that is guaranteed for GetTouchingTiles.
/// </summary>
public IntRect GetTouchingTilesRound ( Bounds bounds ) {
bounds = transform . InverseTransform ( bounds ) ;
//Calculate world bounds of all affected tiles
var r = new IntRect ( Mathf . RoundToInt ( bounds . min . x / TileWorldSizeX ) , Mathf . RoundToInt ( bounds . min . z / TileWorldSizeZ ) , Mathf . RoundToInt ( bounds . max . x / TileWorldSizeX ) - 1 , Mathf . RoundToInt ( bounds . max . z / TileWorldSizeZ ) - 1 ) ;
//Clamp to bounds
r = IntRect . Intersection ( r , new IntRect ( 0 , 0 , tileXCount - 1 , tileZCount - 1 ) ) ;
return r ;
}
protected void ConnectTileWithNeighbours ( NavmeshTile tile , bool onlyUnflagged = false ) {
if ( tile . w ! = 1 | | tile . d ! = 1 ) {
throw new System . ArgumentException ( "Tile widths or depths other than 1 are not supported. The fields exist mainly for possible future expansions." ) ;
}
// Loop through z and x offsets to adjacent tiles
// _ x _
// x _ x
// _ x _
for ( int zo = - 1 ; zo < = 1 ; zo + + ) {
var z = tile . z + zo ;
if ( z < 0 | | z > = tileZCount ) continue ;
for ( int xo = - 1 ; xo < = 1 ; xo + + ) {
var x = tile . x + xo ;
if ( x < 0 | | x > = tileXCount ) continue ;
// Ignore diagonals and the tile itself
if ( ( xo = = 0 ) = = ( zo = = 0 ) ) continue ;
var otherTile = tiles [ x + z * tileXCount ] ;
if ( ! onlyUnflagged | | ! otherTile . flag ) {
ConnectTiles ( otherTile , tile ) ;
}
}
}
}
protected void RemoveConnectionsFromTile ( NavmeshTile tile ) {
if ( tile . x > 0 ) {
int x = tile . x - 1 ;
for ( int z = tile . z ; z < tile . z + tile . d ; z + + ) RemoveConnectionsFromTo ( tiles [ x + z * tileXCount ] , tile ) ;
}
if ( tile . x + tile . w < tileXCount ) {
int x = tile . x + tile . w ;
for ( int z = tile . z ; z < tile . z + tile . d ; z + + ) RemoveConnectionsFromTo ( tiles [ x + z * tileXCount ] , tile ) ;
}
if ( tile . z > 0 ) {
int z = tile . z - 1 ;
for ( int x = tile . x ; x < tile . x + tile . w ; x + + ) RemoveConnectionsFromTo ( tiles [ x + z * tileXCount ] , tile ) ;
}
if ( tile . z + tile . d < tileZCount ) {
int z = tile . z + tile . d ;
for ( int x = tile . x ; x < tile . x + tile . w ; x + + ) RemoveConnectionsFromTo ( tiles [ x + z * tileXCount ] , tile ) ;
}
}
protected void RemoveConnectionsFromTo ( NavmeshTile a , NavmeshTile b ) {
if ( a = = null | | b = = null ) return ;
//Same tile, possibly from a large tile (one spanning several x,z tile coordinates)
if ( a = = b ) return ;
int tileIdx = b . x + b . z * tileXCount ;
for ( int i = 0 ; i < a . nodes . Length ; i + + ) {
TriangleMeshNode node = a . nodes [ i ] ;
if ( node . connections = = null ) continue ;
for ( int j = 0 ; ; j + + ) {
//Length will not be constant if connections are removed
if ( j > = node . connections . Length ) break ;
var other = node . connections [ j ] . node as TriangleMeshNode ;
//Only evaluate TriangleMeshNodes
if ( other = = null ) continue ;
int tileIdx2 = other . GetVertexIndex ( 0 ) ;
tileIdx2 = ( tileIdx2 > > TileIndexOffset ) & TileIndexMask ;
if ( tileIdx2 = = tileIdx ) {
node . RemoveConnection ( node . connections [ j ] . node ) ;
j - - ;
}
}
}
}
static readonly NNConstraint NNConstraintDistanceXZ = new NNConstraint { distanceXZ = true } ;
public override NNInfoInternal GetNearest ( Vector3 position , NNConstraint constraint , GraphNode hint ) {
return GetNearestForce ( position , constraint ! = null & & constraint . distanceXZ ? NNConstraintDistanceXZ : null ) ;
}
public override NNInfoInternal GetNearestForce ( Vector3 position , NNConstraint constraint ) {
if ( tiles = = null ) return new NNInfoInternal ( ) ;
var tileCoords = GetTileCoordinates ( position ) ;
// Clamp to graph borders
tileCoords . x = Mathf . Clamp ( tileCoords . x , 0 , tileXCount - 1 ) ;
tileCoords . y = Mathf . Clamp ( tileCoords . y , 0 , tileZCount - 1 ) ;
int wmax = Math . Max ( tileXCount , tileZCount ) ;
var best = new NNInfoInternal ( ) ;
float bestDistance = float . PositiveInfinity ;
bool xzSearch = nearestSearchOnlyXZ | | ( constraint ! = null & & constraint . distanceXZ ) ;
// Search outwards in a diamond pattern from the closest tile
// 2
// 2 1 2
// 2 1 0 1 2 etc.
// 2 1 2
// 2
for ( int w = 0 ; w < wmax ; w + + ) {
// Stop the loop when we can guarantee that no nodes will be closer than the ones we have already searched
if ( bestDistance < ( w - 2 ) * Math . Max ( TileWorldSizeX , TileWorldSizeX ) ) break ;
int zmax = Math . Min ( w + tileCoords . y + 1 , tileZCount ) ;
for ( int z = Math . Max ( - w + tileCoords . y , 0 ) ; z < zmax ; z + + ) {
// Solve for z such that abs(x-tx) + abs(z-tx) == w
// Delta X coordinate
int originalDx = Math . Abs ( w - Math . Abs ( z - tileCoords . y ) ) ;
var dx = originalDx ;
// Solution is dx + tx and -dx + tx
// This loop will first check +dx and then -dx
// If dx happens to be zero, then it will not run twice
do {
// Absolute x coordinate
int x = - dx + tileCoords . x ;
if ( x > = 0 & & x < tileXCount ) {
NavmeshTile tile = tiles [ x + z * tileXCount ] ;
if ( tile ! = null ) {
if ( xzSearch ) {
best = tile . bbTree . QueryClosestXZ ( position , constraint , ref bestDistance , best ) ;
} else {
best = tile . bbTree . QueryClosest ( position , constraint , ref bestDistance , best ) ;
}
}
}
dx = - dx ;
} while ( dx ! = originalDx ) ;
}
}
best . node = best . constrainedNode ;
best . constrainedNode = null ;
best . clampedPosition = best . constClampedPosition ;
return best ;
}
/// <summary>
/// Finds the first node which contains position.
/// "Contains" is defined as position is inside the triangle node when seen from above. So only XZ space matters.
/// In case of a multilayered environment, which node of the possibly several nodes
/// containing the point is undefined.
///
/// Returns null if there was no node containing the point. This serves as a quick
/// check for "is this point on the navmesh or not".
///
/// Note that the behaviour of this method is distinct from the GetNearest method.
/// The GetNearest method will return the closest node to a point,
/// which is not necessarily the one which contains it in XZ space.
///
/// See: GetNearest
/// </summary>
public GraphNode PointOnNavmesh ( Vector3 position , NNConstraint constraint ) {
if ( tiles = = null ) return null ;
var tileCoords = GetTileCoordinates ( position ) ;
// Graph borders
if ( tileCoords . x < 0 | | tileCoords . y < 0 | | tileCoords . x > = tileXCount | | tileCoords . y > = tileZCount ) return null ;
NavmeshTile tile = GetTile ( tileCoords . x , tileCoords . y ) ;
if ( tile ! = null ) {
GraphNode node = tile . bbTree . QueryInside ( position , constraint ) ;
return node ;
}
return null ;
}
/// <summary>Fills graph with tiles created by NewEmptyTile</summary>
protected void FillWithEmptyTiles ( ) {
for ( int z = 0 ; z < tileZCount ; z + + ) {
for ( int x = 0 ; x < tileXCount ; x + + ) {
tiles [ z * tileXCount + x ] = NewEmptyTile ( x , z ) ;
}
}
}
/// <summary>
/// Create connections between all nodes.
/// Version: Since 3.7.6 the implementation is thread safe
/// </summary>
protected static void CreateNodeConnections ( TriangleMeshNode [ ] nodes ) {
List < Connection > connections = ListPool < Connection > . Claim ( ) ;
var nodeRefs = ObjectPoolSimple < Dictionary < Int2 , int > > . Claim ( ) ;
nodeRefs . Clear ( ) ;
// Build node neighbours
for ( int i = 0 ; i < nodes . Length ; i + + ) {
TriangleMeshNode node = nodes [ i ] ;
int av = node . GetVertexCount ( ) ;
for ( int a = 0 ; a < av ; a + + ) {
// Recast can in some very special cases generate degenerate triangles which are simply lines
// In that case, duplicate keys might be added and thus an exception will be thrown
// It is safe to ignore the second edge though... I think (only found one case where this happens)
var key = new Int2 ( node . GetVertexIndex ( a ) , node . GetVertexIndex ( ( a + 1 ) % av ) ) ;
if ( ! nodeRefs . ContainsKey ( key ) ) {
nodeRefs . Add ( key , i ) ;
}
}
}
for ( int i = 0 ; i < nodes . Length ; i + + ) {
TriangleMeshNode node = nodes [ i ] ;
connections . Clear ( ) ;
int av = node . GetVertexCount ( ) ;
for ( int a = 0 ; a < av ; a + + ) {
int first = node . GetVertexIndex ( a ) ;
int second = node . GetVertexIndex ( ( a + 1 ) % av ) ;
int connNode ;
if ( nodeRefs . TryGetValue ( new Int2 ( second , first ) , out connNode ) ) {
TriangleMeshNode other = nodes [ connNode ] ;
int bv = other . GetVertexCount ( ) ;
for ( int b = 0 ; b < bv ; b + + ) {
/// <summary>TODO: This will fail on edges which are only partially shared</summary>
if ( other . GetVertexIndex ( b ) = = second & & other . GetVertexIndex ( ( b + 1 ) % bv ) = = first ) {
connections . Add ( new Connection (
other ,
( uint ) ( node . position - other . position ) . costMagnitude ,
( byte ) a
) ) ;
break ;
}
}
}
}
node . connections = connections . ToArrayFromPool ( ) ;
node . SetConnectivityDirty ( ) ;
}
nodeRefs . Clear ( ) ;
ObjectPoolSimple < Dictionary < Int2 , int > > . Release ( ref nodeRefs ) ;
ListPool < Connection > . Release ( ref connections ) ;
}
/// <summary>
/// Generate connections between the two tiles.
/// The tiles must be adjacent.
/// </summary>
protected void ConnectTiles ( NavmeshTile tile1 , NavmeshTile tile2 ) {
if ( tile1 = = null | | tile2 = = null ) return ;
if ( tile1 . nodes = = null ) throw new System . ArgumentException ( "tile1 does not contain any nodes" ) ;
if ( tile2 . nodes = = null ) throw new System . ArgumentException ( "tile2 does not contain any nodes" ) ;
int t1x = Mathf . Clamp ( tile2 . x , tile1 . x , tile1 . x + tile1 . w - 1 ) ;
int t2x = Mathf . Clamp ( tile1 . x , tile2 . x , tile2 . x + tile2 . w - 1 ) ;
int t1z = Mathf . Clamp ( tile2 . z , tile1 . z , tile1 . z + tile1 . d - 1 ) ;
int t2z = Mathf . Clamp ( tile1 . z , tile2 . z , tile2 . z + tile2 . d - 1 ) ;
int coord , altcoord ;
int t1coord , t2coord ;
float tileWorldSize ;
// Figure out which side that is shared between the two tiles
// and what coordinate index is fixed along that edge (x or z)
if ( t1x = = t2x ) {
coord = 2 ;
altcoord = 0 ;
t1coord = t1z ;
t2coord = t2z ;
tileWorldSize = TileWorldSizeZ ;
} else if ( t1z = = t2z ) {
coord = 0 ;
altcoord = 2 ;
t1coord = t1x ;
t2coord = t2x ;
tileWorldSize = TileWorldSizeX ;
} else {
throw new System . ArgumentException ( "Tiles are not adjacent (neither x or z coordinates match)" ) ;
}
if ( Math . Abs ( t1coord - t2coord ) ! = 1 ) {
throw new System . ArgumentException ( "Tiles are not adjacent (tile coordinates must differ by exactly 1. Got '" + t1coord + "' and '" + t2coord + "')" ) ;
}
// Midpoint between the two tiles
int midpoint = ( int ) Math . Round ( ( Math . Max ( t1coord , t2coord ) * tileWorldSize ) * Int3 . Precision ) ;
#if ASTARDEBUG
Vector3 v1 = new Vector3 ( - 100 , 0 , - 100 ) ;
Vector3 v2 = new Vector3 ( 100 , 0 , 100 ) ;
v1 [ coord ] = midpoint * Int3 . PrecisionFactor ;
v2 [ coord ] = midpoint * Int3 . PrecisionFactor ;
Debug . DrawLine ( v1 , v2 , Color . magenta ) ;
#endif
TriangleMeshNode [ ] nodes1 = tile1 . nodes ;
TriangleMeshNode [ ] nodes2 = tile2 . nodes ;
// Find all nodes of the second tile which are adjacent to the border between the tiles.
// This is used to speed up the matching process (the impact can be very significant for large tiles, but is insignificant for small ones).
TriangleMeshNode [ ] closeToEdge = ArrayPool < TriangleMeshNode > . Claim ( nodes2 . Length ) ;
int numCloseToEdge = 0 ;
for ( int j = 0 ; j < nodes2 . Length ; j + + ) {
TriangleMeshNode nodeB = nodes2 [ j ] ;
int bVertexCount = nodeB . GetVertexCount ( ) ;
for ( int b = 0 ; b < bVertexCount ; b + + ) {
Int3 bVertex1 = nodeB . GetVertexInGraphSpace ( b ) ;
Int3 bVertex2 = nodeB . GetVertexInGraphSpace ( ( b + 1 ) % bVertexCount ) ;
if ( Math . Abs ( bVertex1 [ coord ] - midpoint ) < 2 & & Math . Abs ( bVertex2 [ coord ] - midpoint ) < 2 ) {
closeToEdge [ numCloseToEdge ] = nodes2 [ j ] ;
numCloseToEdge + + ;
break ;
}
}
}
// Find adjacent nodes on the border between the tiles
for ( int i = 0 ; i < nodes1 . Length ; i + + ) {
TriangleMeshNode nodeA = nodes1 [ i ] ;
int aVertexCount = nodeA . GetVertexCount ( ) ;
// Loop through all *sides* of the node
for ( int a = 0 ; a < aVertexCount ; a + + ) {
// Vertices that the segment consists of
Int3 aVertex1 = nodeA . GetVertexInGraphSpace ( a ) ;
Int3 aVertex2 = nodeA . GetVertexInGraphSpace ( ( a + 1 ) % aVertexCount ) ;
// Check if it is really close to the tile border
if ( Math . Abs ( aVertex1 [ coord ] - midpoint ) < 2 & & Math . Abs ( aVertex2 [ coord ] - midpoint ) < 2 ) {
int minalt = Math . Min ( aVertex1 [ altcoord ] , aVertex2 [ altcoord ] ) ;
int maxalt = Math . Max ( aVertex1 [ altcoord ] , aVertex2 [ altcoord ] ) ;
// Degenerate edge
if ( minalt = = maxalt ) continue ;
for ( int j = 0 ; j < numCloseToEdge ; j + + ) {
TriangleMeshNode nodeB = closeToEdge [ j ] ;
int bVertexCount = nodeB . GetVertexCount ( ) ;
for ( int b = 0 ; b < bVertexCount ; b + + ) {
Int3 bVertex1 = nodeB . GetVertexInGraphSpace ( b ) ;
Int3 bVertex2 = nodeB . GetVertexInGraphSpace ( ( b + 1 ) % bVertexCount ) ;
if ( Math . Abs ( bVertex1 [ coord ] - midpoint ) < 2 & & Math . Abs ( bVertex2 [ coord ] - midpoint ) < 2 ) {
int minalt2 = Math . Min ( bVertex1 [ altcoord ] , bVertex2 [ altcoord ] ) ;
int maxalt2 = Math . Max ( bVertex1 [ altcoord ] , bVertex2 [ altcoord ] ) ;
// Degenerate edge
if ( minalt2 = = maxalt2 ) continue ;
if ( maxalt > minalt2 & & minalt < maxalt2 ) {
// The two nodes seem to be adjacent
// Test shortest distance between the segments (first test if they are equal since that is much faster and pretty common)
if ( ( aVertex1 = = bVertex1 & & aVertex2 = = bVertex2 ) | | ( aVertex1 = = bVertex2 & & aVertex2 = = bVertex1 ) | |
VectorMath . SqrDistanceSegmentSegment ( ( Vector3 ) aVertex1 , ( Vector3 ) aVertex2 , ( Vector3 ) bVertex1 , ( Vector3 ) bVertex2 ) < MaxTileConnectionEdgeDistance * MaxTileConnectionEdgeDistance ) {
uint cost = ( uint ) ( nodeA . position - nodeB . position ) . costMagnitude ;
nodeA . AddConnection ( nodeB , cost , ( byte ) a ) ;
nodeB . AddConnection ( nodeA , cost , ( byte ) b ) ;
}
}
}
}
}
}
}
}
ArrayPool < TriangleMeshNode > . Release ( ref closeToEdge ) ;
}
/// <summary>
/// Start batch updating of tiles.
/// During batch updating, tiles will not be connected if they are updating with ReplaceTile.
/// When ending batching, all affected tiles will be connected.
/// This is faster than not using batching.
/// </summary>
public void StartBatchTileUpdate ( ) {
if ( batchTileUpdate ) throw new System . InvalidOperationException ( "Calling StartBatchLoad when batching is already enabled" ) ;
batchTileUpdate = true ;
}
/// <summary>
/// Destroy several nodes simultaneously.
/// This is faster than simply looping through the nodes and calling the node.Destroy method because some optimizations
/// relating to how connections are removed can be optimized.
/// </summary>
void DestroyNodes ( List < MeshNode > nodes ) {
for ( int i = 0 ; i < batchNodesToDestroy . Count ; i + + ) {
batchNodesToDestroy [ i ] . TemporaryFlag1 = true ;
}
for ( int i = 0 ; i < batchNodesToDestroy . Count ; i + + ) {
var node = batchNodesToDestroy [ i ] ;
for ( int j = 0 ; j < node . connections . Length ; j + + ) {
var neighbour = node . connections [ j ] . node ;
if ( ! neighbour . TemporaryFlag1 ) {
neighbour . RemoveConnection ( node ) ;
}
}
// Remove the connections array explicitly for performance.
// Otherwise the Destroy method will try to remove the connections in both directions one by one which is slow.
ArrayPool < Connection > . Release ( ref node . connections , true ) ;
node . Destroy ( ) ;
}
}
void TryConnect ( int tileIdx1 , int tileIdx2 ) {
// If both tiles were flagged, then only connect if tileIdx1 < tileIdx2 to make sure we don't connect the tiles twice
// as this method will be called with swapped arguments as well.
if ( tiles [ tileIdx1 ] . flag & & tiles [ tileIdx2 ] . flag & & tileIdx1 > = tileIdx2 ) return ;
ConnectTiles ( tiles [ tileIdx1 ] , tiles [ tileIdx2 ] ) ;
}
/// <summary>
/// End batch updating of tiles.
/// During batch updating, tiles will not be connected if they are updating with ReplaceTile.
/// When ending batching, all affected tiles will be connected.
/// This is faster than not using batching.
/// </summary>
public void EndBatchTileUpdate ( ) {
if ( ! batchTileUpdate ) throw new System . InvalidOperationException ( "Calling EndBatchTileUpdate when batching had not yet been started" ) ;
batchTileUpdate = false ;
DestroyNodes ( batchNodesToDestroy ) ;
batchNodesToDestroy . ClearFast ( ) ;
for ( int i = 0 ; i < batchUpdatedTiles . Count ; i + + ) tiles [ batchUpdatedTiles [ i ] ] . flag = true ;
for ( int i = 0 ; i < batchUpdatedTiles . Count ; i + + ) {
int x = batchUpdatedTiles [ i ] % tileXCount , z = batchUpdatedTiles [ i ] / tileXCount ;
if ( x > 0 ) TryConnect ( batchUpdatedTiles [ i ] , batchUpdatedTiles [ i ] - 1 ) ;
if ( x < tileXCount - 1 ) TryConnect ( batchUpdatedTiles [ i ] , batchUpdatedTiles [ i ] + 1 ) ;
if ( z > 0 ) TryConnect ( batchUpdatedTiles [ i ] , batchUpdatedTiles [ i ] - tileXCount ) ;
if ( z < tileZCount - 1 ) TryConnect ( batchUpdatedTiles [ i ] , batchUpdatedTiles [ i ] + tileXCount ) ;
}
for ( int i = 0 ; i < batchUpdatedTiles . Count ; i + + ) tiles [ batchUpdatedTiles [ i ] ] . flag = false ;
batchUpdatedTiles . ClearFast ( ) ;
}
/// <summary>
/// Clear the tile at the specified coordinate.
/// Must be called during a batch update, see <see cref="StartBatchTileUpdate"/>.
/// </summary>
protected void ClearTile ( int x , int z ) {
if ( ! batchTileUpdate ) throw new System . Exception ( "Must be called during a batch update. See StartBatchTileUpdate" ) ;
var tile = GetTile ( x , z ) ;
if ( tile = = null ) return ;
var nodes = tile . nodes ;
for ( int i = 0 ; i < nodes . Length ; i + + ) {
if ( nodes [ i ] ! = null ) batchNodesToDestroy . Add ( nodes [ i ] ) ;
}
ObjectPool < BBTree > . Release ( ref tile . bbTree ) ;
// TODO: Pool tile object and various arrays in it?
tiles [ x + z * tileXCount ] = null ;
}
/// <summary>Temporary buffer used in <see cref="PrepareNodeRecycling"/></summary>
Dictionary < int , int > nodeRecyclingHashBuffer = new Dictionary < int , int > ( ) ;
/// <summary>
/// Reuse nodes that keep the exact same vertices after a tile replacement.
/// The reused nodes will be added to the recycledNodeBuffer array at the index corresponding to the
/// indices in the triangle array that its vertices uses.
///
/// All connections on the reused nodes will be removed except ones that go to other graphs.
/// The reused nodes will be removed from the tile by replacing it with a null slot in the node array.
///
/// See: <see cref="ReplaceTile"/>
/// </summary>
void PrepareNodeRecycling ( int x , int z , Int3 [ ] verts , int [ ] tris , TriangleMeshNode [ ] recycledNodeBuffer ) {
NavmeshTile tile = GetTile ( x , z ) ;
if ( tile = = null | | tile . nodes . Length = = 0 ) return ;
var nodes = tile . nodes ;
var recycling = nodeRecyclingHashBuffer ;
for ( int i = 0 , j = 0 ; i < tris . Length ; i + = 3 , j + + ) {
recycling [ verts [ tris [ i + 0 ] ] . GetHashCode ( ) + verts [ tris [ i + 1 ] ] . GetHashCode ( ) + verts [ tris [ i + 2 ] ] . GetHashCode ( ) ] = j ;
}
var connectionsToKeep = ListPool < Connection > . Claim ( ) ;
for ( int i = 0 ; i < nodes . Length ; i + + ) {
var node = nodes [ i ] ;
Int3 v0 , v1 , v2 ;
node . GetVerticesInGraphSpace ( out v0 , out v1 , out v2 ) ;
var hash = v0 . GetHashCode ( ) + v1 . GetHashCode ( ) + v2 . GetHashCode ( ) ;
int newNodeIndex ;
if ( recycling . TryGetValue ( hash , out newNodeIndex ) ) {
// Technically we should check for a cyclic permutations of the vertices (e.g node a,b,c could become node b,c,a)
// but in almost all cases the vertices will keep the same order. Allocating one or two extra nodes isn't such a big deal.
if ( verts [ tris [ 3 * newNodeIndex + 0 ] ] = = v0 & & verts [ tris [ 3 * newNodeIndex + 1 ] ] = = v1 & & verts [ tris [ 3 * newNodeIndex + 2 ] ] = = v2 ) {
recycledNodeBuffer [ newNodeIndex ] = node ;
// Remove the node from the tile
nodes [ i ] = null ;
// Only keep connections to nodes on other graphs
// Usually there are no connections to nodes to other graphs and this is faster than removing all connections them one by one
for ( int j = 0 ; j < node . connections . Length ; j + + ) {
if ( node . connections [ j ] . node . GraphIndex ! = node . GraphIndex ) {
connectionsToKeep . Add ( node . connections [ j ] ) ;
}
}
ArrayPool < Connection > . Release ( ref node . connections , true ) ;
if ( connectionsToKeep . Count > 0 ) {
node . connections = connectionsToKeep . ToArrayFromPool ( ) ;
node . SetConnectivityDirty ( ) ;
connectionsToKeep . Clear ( ) ;
}
}
}
}
recycling . Clear ( ) ;
ListPool < Connection > . Release ( ref connectionsToKeep ) ;
}
/// <summary>
/// Replace tile at index with nodes created from specified navmesh.
/// This will create new nodes and link them to the adjacent tile (unless batching has been started in which case that will be done when batching ends).
///
/// The vertices are assumed to be in 'tile space', that is being in a rectangle with
/// one corner at the origin and one at (<see cref="TileWorldSizeX"/>, 0, <see cref="TileWorldSizeZ)"/>.
///
/// Note: The vertex and triangle arrays may be modified and will be stored with the tile data.
/// do not modify them after this method has been called.
///
/// See: <see cref="StartBatchTileUpdate"/>
/// </summary>
public void ReplaceTile ( int x , int z , Int3 [ ] verts , int [ ] tris ) {
int w = 1 , d = 1 ;
if ( x + w > tileXCount | | z + d > tileZCount | | x < 0 | | z < 0 ) {
throw new System . ArgumentException ( "Tile is placed at an out of bounds position or extends out of the graph bounds (" + x + ", " + z + " [" + w + ", " + d + "] " + tileXCount + " " + tileZCount + ")" ) ;
}
if ( tris . Length % 3 ! = 0 ) throw new System . ArgumentException ( "Triangle array's length must be a multiple of 3 (tris)" ) ;
if ( verts . Length > VertexIndexMask ) {
Debug . LogError ( "Too many vertices in the tile (" + verts . Length + " > " + VertexIndexMask + ")\nYou can enable ASTAR_RECAST_LARGER_TILES under the 'Optimizations' tab in the A* Inspector to raise this limit. Or you can use a smaller tile size to reduce the likelihood of this happening." ) ;
verts = new Int3 [ 0 ] ;
tris = new int [ 0 ] ;
}
var wasNotBatching = ! batchTileUpdate ;
if ( wasNotBatching ) StartBatchTileUpdate ( ) ;
Profiler . BeginSample ( "Tile Initialization" ) ;
//Create a new navmesh tile and assign its settings
var tile = new NavmeshTile {
x = x ,
z = z ,
w = w ,
d = d ,
tris = tris ,
bbTree = ObjectPool < BBTree > . Claim ( ) ,
graph = this ,
} ;
if ( ! Mathf . Approximately ( x * TileWorldSizeX * Int3 . FloatPrecision , ( float ) Math . Round ( x * TileWorldSizeX * Int3 . FloatPrecision ) ) ) Debug . LogWarning ( "Possible numerical imprecision. Consider adjusting tileSize and/or cellSize" ) ;
if ( ! Mathf . Approximately ( z * TileWorldSizeZ * Int3 . FloatPrecision , ( float ) Math . Round ( z * TileWorldSizeZ * Int3 . FloatPrecision ) ) ) Debug . LogWarning ( "Possible numerical imprecision. Consider adjusting tileSize and/or cellSize" ) ;
var offset = ( Int3 ) new Vector3 ( ( x * TileWorldSizeX ) , 0 , ( z * TileWorldSizeZ ) ) ;
for ( int i = 0 ; i < verts . Length ; i + + ) {
verts [ i ] + = offset ;
}
tile . vertsInGraphSpace = verts ;
tile . verts = ( Int3 [ ] ) verts . Clone ( ) ;
transform . Transform ( tile . verts ) ;
Profiler . BeginSample ( "Clear Previous Tiles" ) ;
// Create a backing array for the new nodes
var nodes = tile . nodes = new TriangleMeshNode [ tris . Length / 3 ] ;
// Recycle any nodes that are in the exact same spot after replacing the tile.
// This also keeps e.g penalties and tags and other connections which might be useful.
// It also avoids trashing the paths for the RichAI component (as it will have to immediately recalculate its path
// if it discovers that its path contains destroyed nodes).
PrepareNodeRecycling ( x , z , tile . vertsInGraphSpace , tris , tile . nodes ) ;
// Remove previous tiles (except the nodes that were recycled above)
ClearTile ( x , z ) ;
Profiler . EndSample ( ) ;
Profiler . EndSample ( ) ;
Profiler . BeginSample ( "Assign Node Data" ) ;
// Set tile
tiles [ x + z * tileXCount ] = tile ;
batchUpdatedTiles . Add ( x + z * tileXCount ) ;
// Create nodes and assign triangle indices
CreateNodes ( nodes , tile . tris , x + z * tileXCount , ( uint ) active . data . GetGraphIndex ( this ) ) ;
Profiler . EndSample ( ) ;
Profiler . BeginSample ( "AABBTree Rebuild" ) ;
tile . bbTree . RebuildFrom ( nodes ) ;
Profiler . EndSample ( ) ;
Profiler . BeginSample ( "Create Node Connections" ) ;
CreateNodeConnections ( tile . nodes ) ;
Profiler . EndSample ( ) ;
Profiler . BeginSample ( "Connect With Neighbours" ) ;
if ( wasNotBatching ) EndBatchTileUpdate ( ) ;
Profiler . EndSample ( ) ;
}
protected void CreateNodes ( TriangleMeshNode [ ] buffer , int [ ] tris , int tileIndex , uint graphIndex ) {
if ( buffer = = null | | buffer . Length < tris . Length / 3 ) throw new System . ArgumentException ( "buffer must be non null and at least as large as tris.Length/3" ) ;
// This index will be ORed to the triangle indices
tileIndex < < = TileIndexOffset ;
// Create nodes and assign vertex indices
for ( int i = 0 ; i < buffer . Length ; i + + ) {
var node = buffer [ i ] ;
// Allow the buffer to be partially filled in already to allow for recycling nodes
if ( node = = null ) node = buffer [ i ] = new TriangleMeshNode ( active ) ;
// Reset all relevant fields on the node (even on recycled nodes to avoid exposing internal implementation details)
node . Walkable = true ;
node . Tag = 0 ;
node . Penalty = initialPenalty ;
node . GraphIndex = graphIndex ;
// The vertices stored on the node are composed
// out of the triangle index and the tile index
node . v0 = tris [ i * 3 + 0 ] | tileIndex ;
node . v1 = tris [ i * 3 + 1 ] | tileIndex ;
node . v2 = tris [ i * 3 + 2 ] | tileIndex ;
// Make sure the triangle is clockwise in graph space (it may not be in world space since the graphs can be rotated)
// Note that we also modify the original triangle array because if the graph is cached then we will re-initialize the nodes from that array and assume all triangles are clockwise.
if ( RecalculateNormals & & ! VectorMath . IsClockwiseXZ ( node . GetVertexInGraphSpace ( 0 ) , node . GetVertexInGraphSpace ( 1 ) , node . GetVertexInGraphSpace ( 2 ) ) ) {
Memory . Swap ( ref tris [ i * 3 + 0 ] , ref tris [ i * 3 + 2 ] ) ;
Memory . Swap ( ref node . v0 , ref node . v2 ) ;
}
node . UpdatePositionFromVertices ( ) ;
}
}
public NavmeshBase ( ) {
navmeshUpdateData = new NavmeshUpdates . NavmeshUpdateSettings ( this ) ;
}
/// <summary>
/// Returns if there is an obstacle between origin and end on the graph.
/// This is not the same as Physics.Linecast, this function traverses the \b graph and looks for collisions instead of checking for collider intersection.
///
/// [Open online documentation to see images]
/// </summary>
public bool Linecast ( Vector3 origin , Vector3 end ) {
return Linecast ( origin , end , null ) ;
}
/// <summary>
/// Returns if there is an obstacle between origin and end on the graph.
///
/// This is not the same as Physics.Linecast, this function traverses the \b graph and looks for collisions instead of checking for collider intersection.
///
/// [Open online documentation to see images]
/// </summary>
/// <param name="origin">Point to linecast from.</param>
/// <param name="end">Point to linecast to.</param>
/// <param name="hit">Contains info on what was hit, see GraphHitInfo.</param>
/// <param name="hint">You may pass the node closest to the start point if you already know it for a minor performance boost.
/// If null, a search for the closest node will be done. This parameter is mostly deprecated and should be avoided. Pass null instead.</param>
public bool Linecast ( Vector3 origin , Vector3 end , GraphNode hint , out GraphHitInfo hit ) {
return Linecast ( this , origin , end , hint , out hit , null ) ;
}
/// <summary>
/// Returns if there is an obstacle between origin and end on the graph.
///
/// This is not the same as Physics.Linecast, this function traverses the \b graph and looks for collisions instead of checking for collider intersection.
///
/// [Open online documentation to see images]
/// </summary>
/// <param name="origin">Point to linecast from.</param>
/// <param name="end">Point to linecast to.</param>
/// <param name="hint">You may pass the node closest to the start point if you already know it for a minor performance boost.
/// If null, a search for the closest node will be done. This parameter is mostly deprecated and should be avoided. Pass null instead.</param>
public bool Linecast ( Vector3 origin , Vector3 end , GraphNode hint ) {
GraphHitInfo hit ;
return Linecast ( this , origin , end , hint , out hit , null ) ;
}
/// <summary>
/// Returns if there is an obstacle between origin and end on the graph.
///
/// This is not the same as Physics.Linecast, this function traverses the \b graph and looks for collisions instead of checking for collider intersection.
///
/// [Open online documentation to see images]
/// </summary>
/// <param name="origin">Point to linecast from.</param>
/// <param name="end">Point to linecast to.</param>
/// <param name="hit">Contains info on what was hit, see GraphHitInfo.</param>
/// <param name="hint">You may pass the node closest to the start point if you already know it for a minor performance boost.
/// If null, a search for the closest node will be done. This parameter is mostly deprecated and should be avoided. Pass null instead.</param>
/// <param name="trace">If a list is passed, then it will be filled with all nodes the linecast traverses.</param>
public bool Linecast ( Vector3 origin , Vector3 end , GraphNode hint , out GraphHitInfo hit , List < GraphNode > trace ) {
return Linecast ( this , origin , end , hint , out hit , trace ) ;
}
/// <summary>
/// Returns if there is an obstacle between origin and end on the graph.
///
/// This is not the same as Physics.Linecast, this function traverses the \b graph and looks for collisions instead of checking for collider intersection.
///
/// [Open online documentation to see images]
/// </summary>
/// <param name="origin">Point to linecast from.</param>
/// <param name="end">Point to linecast to.</param>
/// <param name="hit">Contains info on what was hit, see GraphHitInfo.</param>
/// <param name="trace">If a list is passed, then it will be filled with all nodes the linecast traverses.</param>
/// <param name="filter">If not null then the delegate will be called for each node and if it returns false the node will be treated as unwalkable and a hit will be returned.
/// Note that unwalkable nodes are always treated as unwalkable regardless of what this filter returns.</param>
public bool Linecast ( Vector3 origin , Vector3 end , out GraphHitInfo hit , List < GraphNode > trace , System . Func < GraphNode , bool > filter ) {
return Linecast ( this , origin , end , null , out hit , trace , filter ) ;
}
/// <summary>
/// Returns if there is an obstacle between origin and end on the graph.
///
/// This is not the same as Physics.Linecast, this function traverses the \b graph and looks for collisions instead of checking for collider intersection.
///
/// [Open online documentation to see images]
/// </summary>
/// <param name="graph">The graph to perform the search on.</param>
/// <param name="origin">Point to start from.</param>
/// <param name="end">Point to linecast to.</param>
/// <param name="hit">Contains info on what was hit, see GraphHitInfo.</param>
/// <param name="hint">You may pass the node closest to the start point if you already know it for a minor performance boost.
/// If null, a search for the closest node will be done. This parameter is mostly deprecated and should be avoided. Pass null instead.</param>
public static bool Linecast ( NavmeshBase graph , Vector3 origin , Vector3 end , GraphNode hint , out GraphHitInfo hit ) {
return Linecast ( graph , origin , end , hint , out hit , null ) ;
}
/// <summary>Cached <see cref="Pathfinding.NNConstraint.None"/> with distanceXZ=true to reduce allocations</summary>
static readonly NNConstraint NNConstraintNoneXZ = new NNConstraint {
constrainWalkability = false ,
constrainArea = false ,
constrainTags = false ,
constrainDistance = false ,
graphMask = - 1 ,
distanceXZ = true ,
} ;
/// <summary>Used to optimize linecasts by precomputing some values</summary>
static readonly byte [ ] LinecastShapeEdgeLookup ;
static NavmeshBase ( ) {
// Want want to figure out which side of a triangle that a ray exists using.
// There are only 3*3*3 = 27 different options for the [left/right/colinear] options for the 3 vertices of a triangle.
// So we can precompute the result to improve the performance of linecasts.
// For simplicity we reserve 2 bits for each side which means that we have 4*4*4 = 64 entries in the lookup table.
LinecastShapeEdgeLookup = new byte [ 64 ] ;
Side [ ] sideOfLine = new Side [ 3 ] ;
for ( int i = 0 ; i < LinecastShapeEdgeLookup . Length ; i + + ) {
sideOfLine [ 0 ] = ( Side ) ( ( i > > 0 ) & 0x3 ) ;
sideOfLine [ 1 ] = ( Side ) ( ( i > > 2 ) & 0x3 ) ;
sideOfLine [ 2 ] = ( Side ) ( ( i > > 4 ) & 0x3 ) ;
LinecastShapeEdgeLookup [ i ] = 0xFF ;
// Value 3 is an invalid value. So we just skip it.
if ( sideOfLine [ 0 ] ! = ( Side ) 3 & & sideOfLine [ 1 ] ! = ( Side ) 3 & & sideOfLine [ 2 ] ! = ( Side ) 3 ) {
// Figure out the side of the triangle that the line exits.
// In case the line passes through one of the vertices of the triangle
// there may be multiple alternatives. In that case pick the edge
// which contains the fewest vertices that lie on the line.
// This prevents a potential infinite loop when a linecast is done colinear
// to the edge of a triangle.
int bestBadness = int . MaxValue ;
for ( int j = 0 ; j < 3 ; j + + ) {
if ( ( sideOfLine [ j ] = = Side . Left | | sideOfLine [ j ] = = Side . Colinear ) & & ( sideOfLine [ ( j + 1 ) % 3 ] = = Side . Right | | sideOfLine [ ( j + 1 ) % 3 ] = = Side . Colinear ) ) {
var badness = ( sideOfLine [ j ] = = Side . Colinear ? 1 : 0 ) + ( sideOfLine [ ( j + 1 ) % 3 ] = = Side . Colinear ? 1 : 0 ) ;
if ( badness < bestBadness ) {
LinecastShapeEdgeLookup [ i ] = ( byte ) j ;
bestBadness = badness ;
}
}
}
}
}
}
/// <summary>
/// Returns if there is an obstacle between origin and end on the graph.
///
/// This is not the same as Physics.Linecast, this function traverses the \b graph and looks for collisions instead of checking for collider intersections.
///
/// Note: This method only makes sense for graphs in which there is a definite 'up' direction. For example it does not make sense for e.g spherical graphs,
/// navmeshes in which characters can walk on walls/ceilings or other curved worlds. If you try to use this method on such navmeshes it may output nonsense.
///
/// [Open online documentation to see images]
/// </summary>
/// <param name="graph">The graph to perform the search on</param>
/// <param name="origin">Point to start from. This point should be on the navmesh. It will be snapped to the closest point on the navmesh otherwise.</param>
/// <param name="end">Point to linecast to</param>
/// <param name="hit">Contains info on what was hit, see GraphHitInfo</param>
/// <param name="hint">If you already know the node which contains the origin point, you may pass it here for slighly improved performance. If null, a search for the closest node will be done.</param>
/// <param name="trace">If a list is passed, then it will be filled with all nodes along the line up until it hits an obstacle or reaches the end.</param>
/// <param name="filter">If not null then the delegate will be called for each node and if it returns false the node will be treated as unwalkable and a hit will be returned.
/// Note that unwalkable nodes are always treated as unwalkable regardless of what this filter returns.</param>
public static bool Linecast ( NavmeshBase graph , Vector3 origin , Vector3 end , GraphNode hint , out GraphHitInfo hit , List < GraphNode > trace , System . Func < GraphNode , bool > filter = null ) {
hit = new GraphHitInfo ( ) ;
if ( float . IsNaN ( origin . x + origin . y + origin . z ) ) throw new System . ArgumentException ( "origin is NaN" ) ;
if ( float . IsNaN ( end . x + end . y + end . z ) ) throw new System . ArgumentException ( "end is NaN" ) ;
var node = hint as TriangleMeshNode ;
if ( node = = null ) {
node = graph . GetNearest ( origin , NNConstraintNoneXZ ) . node as TriangleMeshNode ;
if ( node = = null ) {
Debug . LogError ( "Could not find a valid node to start from" ) ;
hit . origin = origin ;
hit . point = origin ;
return true ;
}
}
// Snap the origin to the navmesh
var i3originInGraphSpace = node . ClosestPointOnNodeXZInGraphSpace ( origin ) ;
hit . origin = graph . transform . Transform ( ( Vector3 ) i3originInGraphSpace ) ;
if ( ! node . Walkable | | ( filter ! = null & & ! filter ( node ) ) ) {
hit . node = node ;
hit . point = hit . origin ;
hit . tangentOrigin = hit . origin ;
return true ;
}
var endInGraphSpace = graph . transform . InverseTransform ( end ) ;
var i3endInGraphSpace = ( Int3 ) endInGraphSpace ;
// Fast early out check
if ( i3originInGraphSpace = = i3endInGraphSpace ) {
hit . point = hit . origin ;
hit . node = node ;
if ( trace ! = null ) trace . Add ( node ) ;
return false ;
}
int counter = 0 ;
while ( true ) {
counter + + ;
if ( counter > 2000 ) {
Debug . LogError ( "Linecast was stuck in infinite loop. Breaking." ) ;
return true ;
}
if ( trace ! = null ) trace . Add ( node ) ;
Int3 a0 , a1 , a2 ;
node . GetVerticesInGraphSpace ( out a0 , out a1 , out a2 ) ;
int sideOfLine = ( byte ) VectorMath . SideXZ ( i3originInGraphSpace , i3endInGraphSpace , a0 ) ;
sideOfLine | = ( byte ) VectorMath . SideXZ ( i3originInGraphSpace , i3endInGraphSpace , a1 ) < < 2 ;
sideOfLine | = ( byte ) VectorMath . SideXZ ( i3originInGraphSpace , i3endInGraphSpace , a2 ) < < 4 ;
// Use a lookup table to figure out which side of this triangle that the ray exits
int shapeEdgeA = ( int ) LinecastShapeEdgeLookup [ sideOfLine ] ;
// The edge consists of the vertex with index 'sharedEdgeA' and the next vertex after that (index '(sharedEdgeA+1)%3')
var sideNodeExit = VectorMath . SideXZ ( shapeEdgeA = = 0 ? a0 : ( shapeEdgeA = = 1 ? a1 : a2 ) , shapeEdgeA = = 0 ? a1 : ( shapeEdgeA = = 1 ? a2 : a0 ) , i3endInGraphSpace ) ;
if ( sideNodeExit ! = Side . Left ) {
// Ray stops before it leaves the current node.
// The endpoint must be inside the current node.
hit . point = end ;
hit . node = node ;
var endNode = graph . GetNearest ( end , NNConstraintNoneXZ ) . node as TriangleMeshNode ;
if ( endNode = = node | | endNode = = null ) {
// We ended up at the right node.
// If endNode == null we also take this branch.
// That case may happen if a linecast is made to a point, but the point way a very large distance straight up into the air.
// The linecast may indeed reach the right point, but it's so far away up into the air that the GetNearest method will stop searching.
return false ;
} else {
// The closest node to the end point was not the node we ended up at.
// This can happen if a linecast is done between two floors of a building.
// The linecast may reach the right location when seen from above
// but it will have ended up on the wrong floor of the building.
// This indicates that the start and end points cannot be connected by a valid straight line on the navmesh.
return true ;
}
}
if ( shapeEdgeA = = 0xFF ) {
// Line does not intersect node at all?
// This may theoretically happen if the origin was not properly snapped to the inside of the triangle, but is instead a tiny distance outside the node.
Debug . LogError ( "Line does not intersect node at all" ) ;
hit . node = node ;
hit . point = hit . tangentOrigin = hit . origin ;
return true ;
} else {
bool success = false ;
var nodeConnections = node . connections ;
// Check all node connetions to see which one is the next node along the ray's path
for ( int i = 0 ; i < nodeConnections . Length ; i + + ) {
if ( nodeConnections [ i ] . shapeEdge = = shapeEdgeA ) {
// This might be the next node that we enter
var neighbour = nodeConnections [ i ] . node as TriangleMeshNode ;
if ( neighbour = = null | | ! neighbour . Walkable | | ( filter ! = null & & ! filter ( neighbour ) ) ) continue ;
var neighbourConnections = neighbour . connections ;
int shapeEdgeB = - 1 ;
for ( int j = 0 ; j < neighbourConnections . Length ; j + + ) {
if ( neighbourConnections [ j ] . node = = node ) {
shapeEdgeB = neighbourConnections [ j ] . shapeEdge ;
break ;
}
}
if ( shapeEdgeB = = - 1 ) {
// Connection was mono-directional!
// This shouldn't normally happen on navmeshes (when the shapeEdge matches at least) unless a user has done something strange to the navmesh.
continue ;
}
var side1 = VectorMath . SideXZ ( i3originInGraphSpace , i3endInGraphSpace , neighbour . GetVertexInGraphSpace ( shapeEdgeB ) ) ;
var side2 = VectorMath . SideXZ ( i3originInGraphSpace , i3endInGraphSpace , neighbour . GetVertexInGraphSpace ( ( shapeEdgeB + 1 ) % 3 ) ) ;
// Check if the line enters this edge
success = ( side1 = = Side . Right | | side1 = = Side . Colinear ) & & ( side2 = = Side . Left | | side2 = = Side . Colinear ) ;
if ( ! success ) continue ;
// Ray has entered the neighbouring node.
// After the first node, it is possible to prove the loop invariant that shapeEdgeA will *never* end up as -1 (checked above)
// Since side = Colinear acts essentially as a wildcard. side1 and side2 can be the most restricted if they are side1=right, side2=left.
// Then when we get to the next node we know that the sideOfLine array is either [*, Right, Left], [Left, *, Right] or [Right, Left, *], where * is unknown.
// We are looking for the sequence [Left, Right] (possibly including Colinear as wildcard). We will always find this sequence regardless of the value of *.
node = neighbour ;
break ;
}
}
if ( ! success ) {
// Node did not enter any neighbours
// It must have hit the border of the navmesh
var hitEdgeStartInGraphSpace = ( Vector3 ) ( shapeEdgeA = = 0 ? a0 : ( shapeEdgeA = = 1 ? a1 : a2 ) ) ;
var hitEdgeEndInGraphSpace = ( Vector3 ) ( shapeEdgeA = = 0 ? a1 : ( shapeEdgeA = = 1 ? a2 : a0 ) ) ;
var intersectionInGraphSpace = VectorMath . LineIntersectionPointXZ ( hitEdgeStartInGraphSpace , hitEdgeEndInGraphSpace , ( Vector3 ) i3originInGraphSpace , ( Vector3 ) i3endInGraphSpace ) ;
hit . point = graph . transform . Transform ( intersectionInGraphSpace ) ;
hit . node = node ;
var hitEdgeStart = graph . transform . Transform ( hitEdgeStartInGraphSpace ) ;
var hitEdgeEnd = graph . transform . Transform ( hitEdgeEndInGraphSpace ) ;
hit . tangent = hitEdgeEnd - hitEdgeStart ;
hit . tangentOrigin = hitEdgeStart ;
return true ;
}
}
}
}
public override void OnDrawGizmos ( Pathfinding . Util . RetainedGizmos gizmos , bool drawNodes ) {
if ( ! drawNodes ) {
return ;
}
using ( var helper = gizmos . GetSingleFrameGizmoHelper ( active ) ) {
var bounds = new Bounds ( ) ;
bounds . SetMinMax ( Vector3 . zero , forcedBoundsSize ) ;
// Draw a write cube using the latest transform
// (this makes the bounds update immediately if some field is changed in the editor)
helper . builder . DrawWireCube ( CalculateTransform ( ) , bounds , Color . white ) ;
}
if ( tiles ! = null & & ( showMeshSurface | | showMeshOutline | | showNodeConnections ) ) {
var baseHasher = new RetainedGizmos . Hasher ( active ) ;
baseHasher . AddHash ( showMeshOutline ? 1 : 0 ) ;
baseHasher . AddHash ( showMeshSurface ? 1 : 0 ) ;
baseHasher . AddHash ( showNodeConnections ? 1 : 0 ) ;
int startTileIndex = 0 ;
var hasher = baseHasher ;
var hashedNodes = 0 ;
// Update navmesh vizualizations for
// the tiles that have been changed
for ( int i = 0 ; i < tiles . Length ; i + + ) {
// This may happen if an exception has been thrown when the graph was scanned.
// We don't want the gizmo code to start to throw exceptions as well then as
// that would obscure the actual source of the error.
if ( tiles [ i ] = = null ) continue ;
// Calculate a hash of the tile
var nodes = tiles [ i ] . nodes ;
for ( int j = 0 ; j < nodes . Length ; j + + ) {
hasher . HashNode ( nodes [ j ] ) ;
}
hashedNodes + = nodes . Length ;
// Note: do not batch more than some large number of nodes at a time.
// Also do not batch more than a single "row" of the graph at once
// because otherwise a small change in one part of the graph could invalidate
// the caches almost everywhere else.
// When restricting the caches to row by row a change in a row
// will never invalidate the cache in another row.
if ( hashedNodes > 1024 | | ( i % tileXCount ) = = tileXCount - 1 | | i = = tiles . Length - 1 ) {
if ( ! gizmos . Draw ( hasher ) ) {
using ( var helper = gizmos . GetGizmoHelper ( active , hasher ) ) {
if ( showMeshSurface | | showMeshOutline ) {
CreateNavmeshSurfaceVisualization ( tiles , startTileIndex , i + 1 , helper ) ;
CreateNavmeshOutlineVisualization ( tiles , startTileIndex , i + 1 , helper ) ;
}
if ( showNodeConnections ) {
for ( int ti = startTileIndex ; ti < = i ; ti + + ) {
if ( tiles [ ti ] = = null ) continue ;
var tileNodes = tiles [ ti ] . nodes ;
for ( int j = 0 ; j < tileNodes . Length ; j + + ) {
helper . DrawConnections ( tileNodes [ j ] ) ;
}
}
}
}
}
gizmos . Draw ( hasher ) ;
startTileIndex = i + 1 ;
hasher = baseHasher ;
hashedNodes = 0 ;
}
}
}
if ( active . showUnwalkableNodes ) DrawUnwalkableNodes ( active . unwalkableNodeDebugSize ) ;
}
/// <summary>Creates a mesh of the surfaces of the navmesh for use in OnDrawGizmos in the editor</summary>
void CreateNavmeshSurfaceVisualization ( NavmeshTile [ ] tiles , int startTile , int endTile , GraphGizmoHelper helper ) {
int numNodes = 0 ;
for ( int i = startTile ; i < endTile ; i + + ) if ( tiles [ i ] ! = null ) numNodes + = tiles [ i ] . nodes . Length ;
// Vertex array might be a bit larger than necessary, but that's ok
var vertices = ArrayPool < Vector3 > . Claim ( numNodes * 3 ) ;
var colors = ArrayPool < Color > . Claim ( numNodes * 3 ) ;
int offset = 0 ;
for ( int i = startTile ; i < endTile ; i + + ) {
var tile = tiles [ i ] ;
if ( tile = = null ) continue ;
for ( int j = 0 ; j < tile . nodes . Length ; j + + ) {
var node = tile . nodes [ j ] ;
Int3 v0 , v1 , v2 ;
node . GetVertices ( out v0 , out v1 , out v2 ) ;
int index = offset + j * 3 ;
vertices [ index + 0 ] = ( Vector3 ) v0 ;
vertices [ index + 1 ] = ( Vector3 ) v1 ;
vertices [ index + 2 ] = ( Vector3 ) v2 ;
var color = helper . NodeColor ( node ) ;
colors [ index + 0 ] = colors [ index + 1 ] = colors [ index + 2 ] = color ;
}
offset + = tile . nodes . Length * 3 ;
}
if ( showMeshSurface ) helper . DrawTriangles ( vertices , colors , numNodes ) ;
if ( showMeshOutline ) helper . DrawWireTriangles ( vertices , colors , numNodes ) ;
// Return lists to the pool
ArrayPool < Vector3 > . Release ( ref vertices ) ;
ArrayPool < Color > . Release ( ref colors ) ;
}
/// <summary>Creates an outline of the navmesh for use in OnDrawGizmos in the editor</summary>
static void CreateNavmeshOutlineVisualization ( NavmeshTile [ ] tiles , int startTile , int endTile , GraphGizmoHelper helper ) {
var sharedEdges = new bool [ 3 ] ;
for ( int i = startTile ; i < endTile ; i + + ) {
var tile = tiles [ i ] ;
if ( tile = = null ) continue ;
for ( int j = 0 ; j < tile . nodes . Length ; j + + ) {
sharedEdges [ 0 ] = sharedEdges [ 1 ] = sharedEdges [ 2 ] = false ;
var node = tile . nodes [ j ] ;
2024-02-20 18:34:40 +00:00
if ( node . connections ! = null ) {
for ( int c = 0 ; c < node . connections . Length ; c + + ) {
var other = node . connections [ c ] . node as TriangleMeshNode ;
// Loop through neighbours to figure out which edges are shared
if ( other ! = null & & other . GraphIndex = = node . GraphIndex ) {
for ( int v = 0 ; v < 3 ; v + + ) {
for ( int v2 = 0 ; v2 < 3 ; v2 + + ) {
if ( node . GetVertexIndex ( v ) = = other . GetVertexIndex ( ( v2 + 1 ) % 3 ) & & node . GetVertexIndex ( ( v + 1 ) % 3 ) = = other . GetVertexIndex ( v2 ) ) {
// Found a shared edge with the other node
sharedEdges [ v ] = true ;
v = 3 ;
break ;
}
2023-08-01 06:49:57 +00:00
}
}
}
}
}
var color = helper . NodeColor ( node ) ;
for ( int v = 0 ; v < 3 ; v + + ) {
if ( ! sharedEdges [ v ] ) {
helper . builder . DrawLine ( ( Vector3 ) node . GetVertex ( v ) , ( Vector3 ) node . GetVertex ( ( v + 1 ) % 3 ) , color ) ;
}
}
}
}
}
/// <summary>
/// Serializes Node Info.
/// Should serialize:
/// - Base
/// - Node Flags
/// - Node Penalties
/// - Node
/// - Node Positions (if applicable)
/// - Any other information necessary to load the graph in-game
/// All settings marked with json attributes (e.g JsonMember) have already been
/// saved as graph settings and do not need to be handled here.
///
/// It is not necessary for this implementation to be forward or backwards compatible.
///
/// See:
/// </summary>
protected override void SerializeExtraInfo ( GraphSerializationContext ctx ) {
BinaryWriter writer = ctx . writer ;
if ( tiles = = null ) {
writer . Write ( - 1 ) ;
return ;
}
writer . Write ( tileXCount ) ;
writer . Write ( tileZCount ) ;
for ( int z = 0 ; z < tileZCount ; z + + ) {
for ( int x = 0 ; x < tileXCount ; x + + ) {
NavmeshTile tile = tiles [ x + z * tileXCount ] ;
if ( tile = = null ) {
throw new System . Exception ( "NULL Tile" ) ;
//writer.Write (-1);
//continue;
}
writer . Write ( tile . x ) ;
writer . Write ( tile . z ) ;
if ( tile . x ! = x | | tile . z ! = z ) continue ;
writer . Write ( tile . w ) ;
writer . Write ( tile . d ) ;
writer . Write ( tile . tris . Length ) ;
for ( int i = 0 ; i < tile . tris . Length ; i + + ) writer . Write ( tile . tris [ i ] ) ;
writer . Write ( tile . verts . Length ) ;
for ( int i = 0 ; i < tile . verts . Length ; i + + ) {
ctx . SerializeInt3 ( tile . verts [ i ] ) ;
}
writer . Write ( tile . vertsInGraphSpace . Length ) ;
for ( int i = 0 ; i < tile . vertsInGraphSpace . Length ; i + + ) {
ctx . SerializeInt3 ( tile . vertsInGraphSpace [ i ] ) ;
}
writer . Write ( tile . nodes . Length ) ;
for ( int i = 0 ; i < tile . nodes . Length ; i + + ) {
tile . nodes [ i ] . SerializeNode ( ctx ) ;
}
}
}
}
protected override void DeserializeExtraInfo ( GraphSerializationContext ctx ) {
BinaryReader reader = ctx . reader ;
tileXCount = reader . ReadInt32 ( ) ;
if ( tileXCount < 0 ) return ;
tileZCount = reader . ReadInt32 ( ) ;
transform = CalculateTransform ( ) ;
tiles = new NavmeshTile [ tileXCount * tileZCount ] ;
//Make sure mesh nodes can reference this graph
TriangleMeshNode . SetNavmeshHolder ( ( int ) ctx . graphIndex , this ) ;
for ( int z = 0 ; z < tileZCount ; z + + ) {
for ( int x = 0 ; x < tileXCount ; x + + ) {
int tileIndex = x + z * tileXCount ;
int tx = reader . ReadInt32 ( ) ;
if ( tx < 0 ) throw new System . Exception ( "Invalid tile coordinates (x < 0)" ) ;
int tz = reader . ReadInt32 ( ) ;
if ( tz < 0 ) throw new System . Exception ( "Invalid tile coordinates (z < 0)" ) ;
// This is not the origin of a large tile. Refer back to that tile.
if ( tx ! = x | | tz ! = z ) {
tiles [ tileIndex ] = tiles [ tz * tileXCount + tx ] ;
continue ;
}
var tile = tiles [ tileIndex ] = new NavmeshTile {
x = tx ,
z = tz ,
w = reader . ReadInt32 ( ) ,
d = reader . ReadInt32 ( ) ,
bbTree = ObjectPool < BBTree > . Claim ( ) ,
graph = this ,
} ;
int trisCount = reader . ReadInt32 ( ) ;
if ( trisCount % 3 ! = 0 ) throw new System . Exception ( "Corrupt data. Triangle indices count must be divisable by 3. Read " + trisCount ) ;
tile . tris = new int [ trisCount ] ;
for ( int i = 0 ; i < tile . tris . Length ; i + + ) tile . tris [ i ] = reader . ReadInt32 ( ) ;
tile . verts = new Int3 [ reader . ReadInt32 ( ) ] ;
for ( int i = 0 ; i < tile . verts . Length ; i + + ) {
tile . verts [ i ] = ctx . DeserializeInt3 ( ) ;
}
if ( ctx . meta . version . Major > = 4 ) {
tile . vertsInGraphSpace = new Int3 [ reader . ReadInt32 ( ) ] ;
if ( tile . vertsInGraphSpace . Length ! = tile . verts . Length ) throw new System . Exception ( "Corrupt data. Array lengths did not match" ) ;
for ( int i = 0 ; i < tile . verts . Length ; i + + ) {
tile . vertsInGraphSpace [ i ] = ctx . DeserializeInt3 ( ) ;
}
} else {
// Compatibility
tile . vertsInGraphSpace = new Int3 [ tile . verts . Length ] ;
tile . verts . CopyTo ( tile . vertsInGraphSpace , 0 ) ;
transform . InverseTransform ( tile . vertsInGraphSpace ) ;
}
int nodeCount = reader . ReadInt32 ( ) ;
tile . nodes = new TriangleMeshNode [ nodeCount ] ;
// Prepare for storing in vertex indices
tileIndex < < = TileIndexOffset ;
for ( int i = 0 ; i < tile . nodes . Length ; i + + ) {
var node = new TriangleMeshNode ( active ) ;
tile . nodes [ i ] = node ;
node . DeserializeNode ( ctx ) ;
node . v0 = tile . tris [ i * 3 + 0 ] | tileIndex ;
node . v1 = tile . tris [ i * 3 + 1 ] | tileIndex ;
node . v2 = tile . tris [ i * 3 + 2 ] | tileIndex ;
node . UpdatePositionFromVertices ( ) ;
}
tile . bbTree . RebuildFrom ( tile . nodes ) ;
}
}
}
protected override void PostDeserialization ( GraphSerializationContext ctx ) {
// Compatibility
if ( ctx . meta . version < AstarSerializer . V4_1_0 & & tiles ! = null ) {
Dictionary < TriangleMeshNode , Connection [ ] > conns = tiles . SelectMany ( s = > s . nodes ) . ToDictionary ( n = > n , n = > n . connections ? ? new Connection [ 0 ] ) ;
// We need to recalculate all connections when upgrading data from earlier than 4.1.0
// as the connections now need information about which edge was used.
// This may remove connections for e.g off-mesh links.
foreach ( var tile in tiles ) CreateNodeConnections ( tile . nodes ) ;
foreach ( var tile in tiles ) ConnectTileWithNeighbours ( tile ) ;
// Restore any connections that were contained in the serialized file but didn't get added by the method calls above
GetNodes ( node = > {
var triNode = node as TriangleMeshNode ;
foreach ( var conn in conns [ triNode ] . Where ( conn = > ! triNode . ContainsConnection ( conn . node ) ) . ToList ( ) ) {
triNode . AddConnection ( conn . node , conn . cost , conn . shapeEdge ) ;
}
} ) ;
}
// Make sure that the transform is up to date.
// It is assumed that the current graph settings correspond to the correct
// transform as it is not serialized itself.
transform = CalculateTransform ( ) ;
}
}
}