2024-06-03 18:26:03 +00:00
using UnityEngine ;
using System.Collections.Generic ;
namespace Pathfinding {
using UnityEngine.Profiling ;
using Pathfinding.Util ;
using Pathfinding.Serialization ;
using Unity.Collections ;
using Unity.Jobs ;
using Pathfinding.Graphs.Navmesh.Jobs ;
using Pathfinding.Graphs.Navmesh ;
using Unity.Mathematics ;
/// <summary>
/// Generates graphs based on navmeshes.
/// [Open online documentation to see images]
///
/// Navmeshes are meshes in which each triangle defines a walkable area.
/// These are great because the AI can get so much more information on how it can walk.
/// Polygons instead of points mean that the <see cref="FunnelModifier"/> can produce really nice looking paths, and the graphs are also really fast to search
/// and have a low memory footprint because fewer nodes are usually needed to describe the same area compared to grid graphs.
///
/// The navmesh graph requires that you create a navmesh manually. The package also has support for generating navmeshes automatically using the <see cref="RecastGraph"/>.
///
/// For a tutorial on how to configure a navmesh graph, take a look at getstarted2 (view in online documentation for working links).
///
/// [Open online documentation to see images]
2024-11-28 23:07:50 +00:00
///
/// \section navmeshgraph-inspector Inspector
2024-06-03 18:26:03 +00:00
/// [Open online documentation to see images]
///
2024-11-28 23:07:50 +00:00
/// \inspectorField{Source Mesh, sourceMesh}
/// \inspectorField{Offset, offset}
/// \inspectorField{Rotation, rotation}
/// \inspectorField{Scale, scale}
/// \inspectorField{Recalculate Normals, recalculateNormals}
/// \inspectorField{Affected By Navmesh Cuts, enableNavmeshCutting}
/// \inspectorField{Agent Radius, navmeshCuttingCharacterRadius}
/// \inspectorField{Initial Penalty, initialPenalty}
///
/// See: <see cref="RecastGraph"/>
2024-06-03 18:26:03 +00:00
/// </summary>
[JsonOptIn]
[Pathfinding.Util.Preserve]
public class NavMeshGraph : NavmeshBase , IUpdatableGraph {
/// <summary>Mesh to construct navmesh from</summary>
[JsonMember]
public Mesh sourceMesh ;
/// <summary>Offset in world space</summary>
[JsonMember]
public Vector3 offset ;
/// <summary>Rotation in degrees</summary>
[JsonMember]
public Vector3 rotation ;
/// <summary>Scale of the graph</summary>
[JsonMember]
public float scale = 1 ;
/// <summary>
/// Determines how normals are calculated.
/// Disable for spherical graphs or other complicated surfaces that allow the agents to e.g walk on walls or ceilings.
///
/// By default the normals of the mesh will be flipped so that they point as much as possible in the upwards direction.
/// The normals are important when connecting adjacent nodes. Two adjacent nodes will only be connected if they are oriented the same way.
/// This is particularly important if you have a navmesh on the walls or even on the ceiling of a room. Or if you are trying to make a spherical navmesh.
/// If you do one of those things then you should set disable this setting and make sure the normals in your source mesh are properly set.
///
/// If you for example take a look at the image below. In the upper case then the nodes on the bottom half of the
/// mesh haven't been connected with the nodes on the upper half because the normals on the lower half will have been
/// modified to point inwards (as that is the direction that makes them face upwards the most) while the normals on
/// the upper half point outwards. This causes the nodes to not connect properly along the seam. When this option
/// is set to false instead the nodes are connected properly as in the original mesh all normals point outwards.
/// [Open online documentation to see images]
///
/// The default value of this field is true to reduce the risk for errors in the common case. If a mesh is supplied that
/// has all normals pointing downwards and this option is false, then some methods like <see cref="PointOnNavmesh"/> will not work correctly
/// as they assume that the normals point upwards. For a more complicated surface like a spherical graph those methods make no sense anyway
/// as there is no clear definition of what it means to be "inside" a triangle when there is no clear up direction.
/// </summary>
[JsonMember]
public bool recalculateNormals = true ;
/// <summary>
/// Cached bounding box minimum of <see cref="sourceMesh"/>.
/// This is important when the graph has been saved to a file and is later loaded again, but the original mesh does not exist anymore (or has been moved).
/// In that case we still need to be able to find the bounding box since the <see cref="CalculateTransform"/> method uses it.
/// </summary>
[JsonMember]
Vector3 cachedSourceMeshBoundsMin ;
/// <summary>
/// Radius to use when expanding navmesh cuts.
///
/// See: <see cref="NavmeshCut.radiusExpansionMode"/>
/// </summary>
[JsonMember]
public float navmeshCuttingCharacterRadius = 0.5f ;
public override float NavmeshCuttingCharacterRadius = > navmeshCuttingCharacterRadius ;
public override bool RecalculateNormals = > recalculateNormals ;
public override float TileWorldSizeX = > forcedBoundsSize . x ;
public override float TileWorldSizeZ = > forcedBoundsSize . z ;
// Tiles are not supported, so this is irrelevant
public override float MaxTileConnectionEdgeDistance = > 0f ;
/// <summary>
/// True if the point is inside the bounding box of this graph.
///
/// Warning: If your input mesh is entirely flat, the bounding box will also end up entirely flat (with a height of zero), this will make this function return false for almost all points, unless they are at exactly the right y-coordinate.
///
/// Note: For an unscanned graph, this will always return false.
/// </summary>
public override bool IsInsideBounds ( Vector3 point ) {
if ( this . tiles = = null | | this . tiles . Length = = 0 | | sourceMesh = = null ) return false ;
var local = transform . InverseTransform ( point ) ;
var size = sourceMesh . bounds . size * scale ;
// Allow a small margin
const float EPS = 0.0001f ;
return local . x > = - EPS & & local . y > = - EPS & & local . z > = - EPS & & local . x < = size . x + EPS & & local . y < = size . y + EPS & & local . z < = size . z + EPS ;
}
/// <summary>
/// World bounding box for the graph.
///
/// This always contains the whole graph.
///
/// Note: Since this is an axis-aligned bounding box, it may not be particularly tight if the graph is significantly rotated.
///
/// If no mesh has been assigned, this will return a zero sized bounding box at the origin.
///
/// [Open online documentation to see images]
/// </summary>
public override Bounds bounds {
get {
if ( sourceMesh = = null ) return default ;
var m = ( float4x4 ) CalculateTransform ( ) . matrix ;
var b = new ToWorldMatrix ( new float3x3 ( m . c0 . xyz , m . c1 . xyz , m . c2 . xyz ) ) . ToWorld ( new Bounds ( Vector3 . zero , sourceMesh . bounds . size * scale ) ) ;
return b ;
}
}
public override GraphTransform CalculateTransform ( ) {
return new GraphTransform ( Matrix4x4 . TRS ( offset , Quaternion . Euler ( rotation ) , Vector3 . one ) * Matrix4x4 . TRS ( sourceMesh ! = null ? sourceMesh . bounds . min * scale : cachedSourceMeshBoundsMin * scale , Quaternion . identity , Vector3 . one ) ) ;
}
class NavMeshGraphUpdatePromise : IGraphUpdatePromise {
public NavMeshGraph graph ;
public List < GraphUpdateObject > graphUpdates ;
public void Apply ( IGraphUpdateContext ctx ) {
for ( int i = 0 ; i < graphUpdates . Count ; i + + ) {
var graphUpdate = graphUpdates [ i ] ;
UpdateArea ( graphUpdate , graph ) ;
// TODO: Not strictly accurate, since the update may affect node that have a surface that extends
// outside of the bounds.
ctx . DirtyBounds ( graphUpdate . bounds ) ;
}
}
}
IGraphUpdatePromise IUpdatableGraph . ScheduleGraphUpdates ( List < GraphUpdateObject > graphUpdates ) = > new NavMeshGraphUpdatePromise { graph = this , graphUpdates = graphUpdates } ;
public static void UpdateArea ( GraphUpdateObject o , INavmeshHolder graph ) {
Bounds bounds = graph . transform . InverseTransform ( o . bounds ) ;
// Bounding rectangle with integer coordinates
var irect = new IntRect (
Mathf . FloorToInt ( bounds . min . x * Int3 . Precision ) ,
Mathf . FloorToInt ( bounds . min . z * Int3 . Precision ) ,
Mathf . CeilToInt ( bounds . max . x * Int3 . Precision ) ,
Mathf . CeilToInt ( bounds . max . z * Int3 . Precision )
) ;
// Corners of the bounding rectangle
var a = new Int3 ( irect . xmin , 0 , irect . ymin ) ;
var b = new Int3 ( irect . xmin , 0 , irect . ymax ) ;
var c = new Int3 ( irect . xmax , 0 , irect . ymin ) ;
var d = new Int3 ( irect . xmax , 0 , irect . ymax ) ;
var ymin = ( ( Int3 ) bounds . min ) . y ;
var ymax = ( ( Int3 ) bounds . max ) . y ;
// Loop through all nodes and check if they intersect the bounding box
graph . GetNodes ( _node = > {
var node = _node as TriangleMeshNode ;
bool inside = false ;
int allLeft = 0 ;
int allRight = 0 ;
int allTop = 0 ;
int allBottom = 0 ;
// Check bounding box rect in XZ plane
for ( int v = 0 ; v < 3 ; v + + ) {
Int3 p = node . GetVertexInGraphSpace ( v ) ;
if ( irect . Contains ( p . x , p . z ) ) {
inside = true ;
break ;
}
if ( p . x < irect . xmin ) allLeft + + ;
if ( p . x > irect . xmax ) allRight + + ;
if ( p . z < irect . ymin ) allTop + + ;
if ( p . z > irect . ymax ) allBottom + + ;
}
if ( ! inside & & ( allLeft = = 3 | | allRight = = 3 | | allTop = = 3 | | allBottom = = 3 ) ) {
return ;
}
// Check if the polygon edges intersect the bounding rect
for ( int v = 0 ; v < 3 ; v + + ) {
int v2 = v > 1 ? 0 : v + 1 ;
Int3 vert1 = node . GetVertexInGraphSpace ( v ) ;
Int3 vert2 = node . GetVertexInGraphSpace ( v2 ) ;
if ( VectorMath . SegmentsIntersectXZ ( a , b , vert1 , vert2 ) ) { inside = true ; break ; }
if ( VectorMath . SegmentsIntersectXZ ( a , c , vert1 , vert2 ) ) { inside = true ; break ; }
if ( VectorMath . SegmentsIntersectXZ ( c , d , vert1 , vert2 ) ) { inside = true ; break ; }
if ( VectorMath . SegmentsIntersectXZ ( d , b , vert1 , vert2 ) ) { inside = true ; break ; }
}
// Check if the node contains any corner of the bounding rect
if ( inside | | node . ContainsPointInGraphSpace ( a ) | | node . ContainsPointInGraphSpace ( b ) | | node . ContainsPointInGraphSpace ( c ) | | node . ContainsPointInGraphSpace ( d ) ) {
inside = true ;
}
if ( ! inside ) {
return ;
}
int allAbove = 0 ;
int allBelow = 0 ;
// Check y coordinate
for ( int v = 0 ; v < 3 ; v + + ) {
Int3 p = node . GetVertexInGraphSpace ( v ) ;
if ( p . y < ymin ) allBelow + + ;
if ( p . y > ymax ) allAbove + + ;
}
// Polygon is either completely above the bounding box or completely below it
if ( allBelow = = 3 | | allAbove = = 3 ) return ;
// Triangle is inside the bounding box!
// Update it!
o . WillUpdateNode ( node ) ;
o . Apply ( node ) ;
} ) ;
}
class NavMeshGraphScanPromise : IGraphUpdatePromise {
public NavMeshGraph graph ;
bool emptyGraph ;
GraphTransform transform ;
NavmeshTile [ ] tiles ;
Vector3 forcedBoundsSize ;
IntRect tileRect ;
2024-11-28 23:07:50 +00:00
NavmeshUpdates . NavmeshUpdateSettings cutSettings ;
2024-06-03 18:26:03 +00:00
public IEnumerator < JobHandle > Prepare ( ) {
var sourceMesh = graph . sourceMesh ;
graph . cachedSourceMeshBoundsMin = sourceMesh ! = null ? sourceMesh . bounds . min : Vector3 . zero ;
transform = graph . CalculateTransform ( ) ;
if ( sourceMesh = = null ) {
emptyGraph = true ;
yield break ;
}
if ( ! sourceMesh . isReadable ) {
Debug . LogError ( "The source mesh " + sourceMesh . name + " is not readable. Enable Read/Write in the mesh's import settings." , sourceMesh ) ;
emptyGraph = true ;
yield break ;
}
Profiler . BeginSample ( "GetMeshData" ) ;
var meshDatas = Mesh . AcquireReadOnlyMeshData ( sourceMesh ) ;
MeshUtility . GetMeshData ( meshDatas , 0 , out var vertices , out var indices ) ;
meshDatas . Dispose ( ) ;
Profiler . EndSample ( ) ;
// Convert the vertices to graph space
// so that the minimum of the bounding box of the mesh is at the origin
// (the vertices will later be transformed to world space)
2024-11-28 23:07:50 +00:00
var scale = graph . scale ;
var meshToGraphSpace = Matrix4x4 . TRS ( - sourceMesh . bounds . min * scale , Quaternion . identity , Vector3 . one * scale ) ;
2024-06-03 18:26:03 +00:00
var promise = JobBuildTileMeshFromVertices . Schedule ( vertices , indices , meshToGraphSpace , graph . RecalculateNormals ) ;
2024-11-28 23:07:50 +00:00
forcedBoundsSize = sourceMesh . bounds . size * scale ;
2024-06-03 18:26:03 +00:00
tileRect = new IntRect ( 0 , 0 , 0 , 0 ) ;
tiles = new NavmeshTile [ tileRect . Area ] ;
var tilesGCHandle = System . Runtime . InteropServices . GCHandle . Alloc ( tiles ) ;
2024-11-28 23:07:50 +00:00
var tileLayout = new TileLayout ( new Bounds ( transform . Transform ( forcedBoundsSize * 0.5f ) , forcedBoundsSize ) , Quaternion . Euler ( graph . rotation ) , 0.001f , 0 , false ) ;
cutSettings = new NavmeshUpdates . NavmeshUpdateSettings ( graph , tileLayout ) ;
var cutPromise = RecastBuilder . CutTiles ( graph , cutSettings . clipperLookup , tileLayout ) . Schedule ( promise ) ;
2024-06-03 18:26:03 +00:00
var tileNodeConnections = new NativeArray < JobCalculateTriangleConnections . TileNodeConnectionsUnsafe > ( tiles . Length , Allocator . Persistent ) ;
2024-11-28 23:07:50 +00:00
var postCutInput = cutPromise . GetValue ( ) ;
var preCutInput = promise . GetValue ( ) ;
NativeArray < TileMesh . TileMeshUnsafe > finalTileMeshes ;
if ( postCutInput . tileMeshes . tileMeshes . IsCreated ) {
UnityEngine . Assertions . Assert . AreEqual ( postCutInput . tileMeshes . tileMeshes . Length , tileRect . Area ) ;
finalTileMeshes = postCutInput . tileMeshes . tileMeshes ;
} else {
finalTileMeshes = preCutInput . tileMeshes . tileMeshes ;
}
2024-06-03 18:26:03 +00:00
var calculateConnectionsJob = new JobCalculateTriangleConnections {
2024-11-28 23:07:50 +00:00
tileMeshes = finalTileMeshes ,
2024-06-03 18:26:03 +00:00
nodeConnections = tileNodeConnections ,
2024-11-28 23:07:50 +00:00
} . Schedule ( cutPromise . handle ) ;
2024-06-03 18:26:03 +00:00
var createTilesJob = new JobCreateTiles {
2024-11-28 23:07:50 +00:00
// If any cutting is done, we need to save the pre-cut data to be able to re-cut tiles later
preCutTileMeshes = postCutInput . tileMeshes . tileMeshes . IsCreated ? preCutInput . tileMeshes . tileMeshes : default ,
tileMeshes = finalTileMeshes ,
2024-06-03 18:26:03 +00:00
tiles = tilesGCHandle ,
tileRect = tileRect ,
2024-11-28 23:07:50 +00:00
graphTileCount = new Vector2Int ( tileRect . Width , tileRect . Height ) ,
2024-06-03 18:26:03 +00:00
graphIndex = graph . graphIndex ,
initialPenalty = graph . initialPenalty ,
recalculateNormals = graph . recalculateNormals ,
graphToWorldSpace = transform . matrix ,
2024-11-28 23:07:50 +00:00
tileWorldSize = tileLayout . TileWorldSize ,
} . Schedule ( cutPromise . handle ) ;
2024-06-03 18:26:03 +00:00
var applyConnectionsJob = new JobWriteNodeConnections {
tiles = tilesGCHandle ,
nodeConnections = tileNodeConnections ,
} . Schedule ( JobHandle . CombineDependencies ( createTilesJob , calculateConnectionsJob ) ) ;
yield return applyConnectionsJob ;
// This has already been used in the createTilesJob
2024-11-28 23:07:50 +00:00
promise . Complete ( ) . Dispose ( ) ;
cutPromise . Complete ( ) . Dispose ( ) ;
2024-06-03 18:26:03 +00:00
tileNodeConnections . Dispose ( ) ;
vertices . Dispose ( ) ;
indices . Dispose ( ) ;
tilesGCHandle . Free ( ) ;
}
public void Apply ( IGraphUpdateContext ctx ) {
if ( emptyGraph ) {
graph . forcedBoundsSize = Vector3 . zero ;
graph . transform = transform ;
graph . tileZCount = graph . tileXCount = 1 ;
TriangleMeshNode . SetNavmeshHolder ( AstarPath . active . data . GetGraphIndex ( graph ) , graph ) ;
graph . FillWithEmptyTiles ( ) ;
2024-11-28 23:07:50 +00:00
graph . navmeshUpdateData . Dispose ( ) ;
2024-06-03 18:26:03 +00:00
return ;
}
// Destroy all previous nodes (if any)
graph . DestroyAllNodes ( ) ;
// Initialize all nodes that were created in the jobs
for ( int j = 0 ; j < tiles . Length ; j + + ) AstarPath . active . InitializeNodes ( tiles [ j ] . nodes ) ;
// Assign all data as one atomic operation (from the viewpoint of the main thread)
graph . forcedBoundsSize = forcedBoundsSize ;
graph . transform = transform ;
graph . tileXCount = tileRect . Width ;
graph . tileZCount = tileRect . Height ;
graph . tiles = tiles ;
TriangleMeshNode . SetNavmeshHolder ( graph . active . data . GetGraphIndex ( graph ) , graph ) ;
2024-11-28 23:07:50 +00:00
cutSettings . AttachToGraph ( ) ;
2024-06-03 18:26:03 +00:00
if ( graph . OnRecalculatedTiles ! = null ) graph . OnRecalculatedTiles ( tiles . Clone ( ) as NavmeshTile [ ] ) ;
}
}
protected override IGraphUpdatePromise ScanInternal ( bool async ) = > new NavMeshGraphScanPromise { graph = this } ;
protected override void PostDeserialization ( GraphSerializationContext ctx ) {
if ( ctx . meta . version < AstarSerializer . V4_3_74 ) {
this . navmeshCuttingCharacterRadius = 0 ;
}
base . PostDeserialization ( ctx ) ;
}
}
}