using DotRecast.Detour; using DotRecast.Detour.Crowd; using System.Runtime.CompilerServices; namespace Sandbox.Navigation; /// /// Navigation Mesh - allowing AI to navigate a world /// [Expose] public sealed partial class NavMesh : IDisposable { internal DtNavMesh navmeshInternal; internal DtCrowd crowd; internal DtNavMeshQuery query; // Making this only work from Scene.NavMesh for now. There's no real reason we can't let // then create these and manage them themselves. But for now, early days, I want to lock // it down to only required functionality. internal NavMesh() { navmeshInternal = new DtNavMesh(); } ~NavMesh() { Dispose(); } public void Dispose() { tileCache.Dispose(); GC.SuppressFinalize( this ); } /// /// Determines wether the navigation mesh is enabled and should be generated /// public bool IsEnabled { get; set; } = false; /// /// The navigation mesh is generating /// [Hide] public bool IsGenerating { get; private set; } = false; /// /// The navigation mesh is dirty and needs a complete rebuild /// [Hide] public bool IsDirty { get; private set; } = true; /// /// Should the generator include static bodies /// [Group( "Generation Input" )] public bool IncludeStaticBodies { get; set; } = true; /// /// Should the generator include keyframed bodies /// [Group( "Generation Input" )] public bool IncludeKeyframedBodies { get; set; } = true; /// /// Don't include these bodies in the generation /// [Group( "Generation Input" )] public TagSet ExcludedBodies { get; set; } = new(); /// /// If any, we'll only include bodies with this tag /// [Group( "Generation Input" )] public TagSet IncludedBodies { get; set; } = new(); /// /// Constantly update the navigation mesh in the editor /// [Group( "Editor" )] public bool EditorAutoUpdate { get; set; } = true; /// /// Draw the navigation mesh in the editor /// [Group( "Editor" )] public bool DrawMesh { get; set; } /// /// Height of the agent /// [Group( "Agent" )] public float AgentHeight { get; set; } = 64.0f; /// /// The radius of the agent. This will change how much gap is left on the edges of surfaces, so they don't clip into walls. /// [Group( "Agent" )] public float AgentRadius { get; set; } = 16.0f; /// /// The maximum height an agent can climb (step) /// [Group( "Agent" )] public float AgentStepSize { get; set; } = 18.0f; /// /// The maximum slope an agent can walk up (in degrees) /// [Group( "Agent" )] public float AgentMaxSlope { get; set; } = 40.0f; // Tiling props not exposed until we are sure we want to expose them /// /// The xz-plane cell size to use for fields. [Limit: > 0] [Units: wu] /// private float CellSize = 4.0f; /// /// The y-axis cell size to use for fields. [Limit: > 0] [Units: wu] /// private float CellHeight = 4.0f; /// /// The width/height size of tile's on the xy-plane. [Limit: >= 0] [Units: vx] /// private int TileSizeXYVoxels { get; set; } = 128; private float TileSizeXYWorldSpace { get => TileSizeXYVoxels * CellSize; } // We have DT_TILE_BITS(28) bits for tiles and DT_POLY_BITS(20) for poly's, so we can have 2^28 tiles and 2^20 polys internal Vector2Int TileCount { get; set; } = new Vector2Int( 256, 256 ); // Sqrt( 1<< 28 ) = 16384 internal int MaxPolys = 1 << 20; internal Action OnInit; internal BBox WorldBounds; private float TileHeightWorldSpace { get; set; } = 1048576f; // The origin of the tile grid private Vector3 TileOrigin { get => -0.5f * TileSizeWorldSpace.WithZ( 0 ) * new Vector3( TileCount.x, TileCount.y ) - new Vector3( 0, 0, 0.5f * TileSizeWorldSpace.z ); } private Vector3 TileSizeWorldSpace { get => new Vector3( TileSizeXYWorldSpace, TileSizeXYWorldSpace, TileHeightWorldSpace ); } /// /// Set the navgiation a dirty, so it will rebuild next update /// public void SetDirty() { IsDirty = true; } internal void Init() { ThreadSafe.AssertIsMainThread(); var navMeshParams = new DtNavMeshParams { tileHeight = TileSizeXYWorldSpace, tileWidth = TileSizeXYWorldSpace, maxTiles = TileCount.x * TileCount.y, maxPolys = MaxPolys, orig = ToNav( TileOrigin ), }; navmeshInternal.Init( navMeshParams, 6 ); DtCrowdConfig crowdConfig = new DtCrowdConfig( AgentRadius, AgentHeight ); crowdConfig.topologyOptimizationTimeThreshold = 1f; crowd = new DtCrowd( crowdConfig, navmeshInternal ); DtObstacleAvoidanceParams obsParams = new DtObstacleAvoidanceParams(); obsParams.VelocityBias = 0.4f; obsParams.DesiredVelocityWeight = 2.0f; obsParams.CurrentVelocityWeight = 0.75f; obsParams.SideBiasWeight = 0.75f; obsParams.TimeOfImpactWeight = 2.5f; obsParams.HorizonTime = 2.5f; obsParams.GridResolution = 33; obsParams.AdaptiveDivisions = 7; obsParams.AdaptiveRings = 2; obsParams.AdaptiveRefinementDepth = 5; obsParams.VelocityBias = 0.5f; obsParams.AdaptiveDivisions = 16; obsParams.AdaptiveRings = 4; obsParams.AdaptiveRefinementDepth = 16; crowd.SetObstacleAvoidanceParams( 0, obsParams ); query = new DtNavMeshQuery( navmeshInternal ); OnInit?.Invoke(); } internal void HandleEditorAutoUpdate( PhysicsWorld world ) { WorldBounds = CalculateWorldBounds( world ); // accountf or a border incase world shrinks WorldBounds = WorldBounds.Grow( TileSizeXYWorldSpace * 2 ); Gizmo.Draw.LineBBox( WorldBounds ); var minMaxBounds = CalculateMinMaxTileCoords( WorldBounds ); // request full rebuild for every tile in bounds for ( int x = minMaxBounds.Left; x <= minMaxBounds.Right; x++ ) { for ( int y = minMaxBounds.Top; y <= minMaxBounds.Bottom; y++ ) { var tile = tileCache.GetOrAddTile( new Vector2Int( x, y ) ); tile.RequestFullRebuild(); } } } public async Task Generate( PhysicsWorld world ) { if ( IsGenerating ) { Log.Warning( "NavMesh is already generating" ); return false; } try { IsGenerating = true; Init(); WorldBounds = CalculateWorldBounds( world ); await GenerateTiles( world, WorldBounds ); } finally { IsGenerating = false; IsDirty = false; } return true; } private BBox CalculateWorldBounds( PhysicsWorld world ) { // Iterate over all bodies and create world bounds BBox? result = null; foreach ( var body in world.Bodies ) { if ( !IsBodyRelevantForNavmesh( body ) ) { continue; } result = result == null ? body.GetBounds() : result?.AddBBox( body.GetBounds() ); } if ( result != null ) { result?.Grow( CellSize * 2.0f ); // Grow the bounds a bit to make sure we don't have any precission issues with the edges return (BBox)result; } return new BBox( Vector3.Zero, Vector3.Zero ); } internal bool IsBodyRelevantForNavmesh( PhysicsBody body ) { var navmeshBodyType = body.NavmeshBodyTypeOverride ?? body.BodyType; if ( body.ShapeCount == 0 ) return false; if ( navmeshBodyType == PhysicsBodyType.Dynamic ) return false; // never include dynamic bodies if ( navmeshBodyType == PhysicsBodyType.Static && !IncludeStaticBodies ) return false; if ( navmeshBodyType == PhysicsBodyType.Keyframed && !IncludeKeyframedBodies ) return false; // Excluded by tags if ( ExcludedBodies is not null && !ExcludedBodies.IsEmpty && body.Shapes.Any( shape => shape.Tags.HasAny( ExcludedBodies ) ) ) return false; // Inlcuded by tags if ( IncludedBodies is not null && !IncludedBodies.IsEmpty && !body.Shapes.Any( shape => shape.Tags.HasAny( IncludedBodies ) ) ) return false; return true; } internal int GetPolyCount( Vector2Int tilePosition ) { var tile = navmeshInternal.GetTileAt( tilePosition.x, tilePosition.y, 0 ); if ( tile == null || tile.data.header == null ) { return default; } return tile == null ? 0 : tile.data.header.polyCount; } internal DtPoly GetPoly( Vector2Int tilePosition, int index ) { var tile = navmeshInternal.GetTileAt( tilePosition.x, tilePosition.y, 0 ); if ( tile == null || tile.data.header == null || index >= tile.data.header.polyCount ) { return default; } return tile.data.polys[index]; } internal int GetPolyVertCount( Vector2Int tilePosition, int index ) { var tile = navmeshInternal.GetTileAt( tilePosition.x, tilePosition.y, 0 ); if ( tile == null || tile.data.header == null || index >= tile.data.header.polyCount ) { return default; } return tile.data.header.vertCount; } internal IEnumerable GetPolyVerts( Vector2Int tilePosition, int polyIndex ) { var tile = navmeshInternal.GetTileAt( tilePosition.x, tilePosition.y, 0 ); if ( tile == null || tile.data.header == null || polyIndex >= tile.data.header.polyCount ) { return []; } var poly = tile.data.polys[polyIndex]; return poly.verts.Select( vertexIndex => FromNav( tile.data.verts[vertexIndex] ) ); } [MethodImpl( MethodImplOptions.AggressiveInlining )] internal static Vector3 FromNav( Vector3 v ) { return new Vector3( v.x, v.z, v.y ); } [MethodImpl( MethodImplOptions.AggressiveInlining )] internal static Vector3 ToNav( Vector3 v ) { return new Vector3( v.x, v.z, v.y ); } [MethodImpl( MethodImplOptions.AggressiveInlining )] internal static BBox ToNav( BBox b ) { return new BBox( ToNav( b.Mins ), ToNav( b.Maxs ) ); } [MethodImpl( MethodImplOptions.AggressiveInlining )] internal static Sphere ToNav( Sphere s ) { return new Sphere( ToNav( s.Center ), s.Radius ); } [MethodImpl( MethodImplOptions.AggressiveInlining )] internal static Capsule ToNav( Capsule c ) { return new Capsule( ToNav( c.CenterA ), ToNav( c.CenterB ), c.Radius ); } // Quaternion/Rotation conversion between world space and nav space. // Mapping chosen to match existing position (Vector3) axis swizzle (Y<->Z) plus handedness adjustments: // World (x, y, z, w) -> Nav ( -x, -z, y, w ) [MethodImpl( MethodImplOptions.AggressiveInlining )] internal static Rotation ToNav( Rotation r ) { return new Rotation( -r.x, -r.z, -r.y, r.w ); } [MethodImpl( MethodImplOptions.AggressiveInlining )] internal static Transform ToNav( in Transform t ) { return new Transform( ToNav( t.Position ), ToNav( t.Rotation ), ToNav( t.Scale ) ); } }