private void Cleanup() { UpdateScene(null); CurrentNavigationMesh = null; NavigationMeshUpdated?.Invoke(this, null); }
private void FinilizeRebuild(Task <NavigationMeshBuildResult> resultTask) { var result = resultTask.Result; if (result.Success) { var args = new NavigationMeshUpdatedEventArgs { OldNavigationMesh = CurrentNavigationMesh, BuildResult = result, }; CurrentNavigationMesh = result.NavigationMesh; NavigationMeshUpdated?.Invoke(this, args); } }
/// <summary> /// Performs the build of a navigation mesh /// </summary> /// <param name="buildSettings">The build settings to pass to recast</param> /// <param name="agentSettings">A collection of agent settings to use, this will generate a layer in the navigation mesh for every agent settings in this collection (in the same order)</param> /// <param name="includedCollisionGroups">The collision groups that will affect which colliders are considered solid</param> /// <param name="boundingBoxes">A collection of bounding boxes to use as the region for which to generate navigation mesh tiles</param> /// <param name="cancellationToken">A cancellation token to interrupt the build process</param> /// <returns>The build result</returns> public NavigationMeshBuildResult Build(NavigationMeshBuildSettings buildSettings, ICollection <NavigationMeshGroup> groups, CollisionFilterGroupFlags includedCollisionGroups, ICollection <BoundingBox> boundingBoxes, CancellationToken cancellationToken) { var lastCache = oldNavigationMesh?.Cache; var result = new NavigationMeshBuildResult(); if (groups.Count == 0) { Logger?.Warning("No group settings found"); result.Success = true; result.NavigationMesh = new NavigationMesh(); return(result); } if (boundingBoxes.Count == 0) { Logger?.Warning("No bounding boxes found"); } var settingsHash = groups?.ComputeHash() ?? 0; settingsHash = (settingsHash * 397) ^ buildSettings.GetHashCode(); if (lastCache != null && lastCache.SettingsHash != settingsHash) { // Start from scratch if settings changed oldNavigationMesh = null; Logger?.Info("Build settings changed, doing a full rebuild"); } // Copy colliders so the collection doesn't get modified StaticColliderData[] collidersLocal; lock (colliders) { collidersLocal = colliders.ToArray(); } BuildInput(collidersLocal, includedCollisionGroups); // Check if cache was cleared while building the input lastCache = oldNavigationMesh?.Cache; // The new navigation mesh that will be created result.NavigationMesh = new NavigationMesh(); result.NavigationMesh.CellSize = buildSettings.CellSize; result.NavigationMesh.TileSize = buildSettings.TileSize; // Tile cache for this new navigation mesh NavigationMeshCache newCache = result.NavigationMesh.Cache = new NavigationMeshCache(); newCache.SettingsHash = settingsHash; // Generate global bounding box for planes BoundingBox globalBoundingBox = BoundingBox.Empty; foreach (var boundingBox in boundingBoxes) { globalBoundingBox = BoundingBox.Merge(boundingBox, globalBoundingBox); } // Combine input and collect tiles to build NavigationMeshInputBuilder sceneNavigationMeshInputBuilder = new NavigationMeshInputBuilder(); foreach (var colliderData in collidersLocal) { if (colliderData.InputBuilder == null) { continue; } // Otherwise, skip building these tiles sceneNavigationMeshInputBuilder.AppendOther(colliderData.InputBuilder); newCache.Add(colliderData.Component, colliderData.InputBuilder, colliderData.Planes, colliderData.ParameterHash); // Generate geometry for planes foreach (var plane in colliderData.Planes) { sceneNavigationMeshInputBuilder.AppendOther(BuildPlaneGeometry(plane, globalBoundingBox)); } } // TODO: Generate tile local mesh input data var inputVertices = sceneNavigationMeshInputBuilder.Points.ToArray(); var inputIndices = sceneNavigationMeshInputBuilder.Indices.ToArray(); // Enumerate over every layer, and build tiles for each of those layers using the provided agent settings using (var groupEnumerator = groups.NotNull().GetEnumerator()) { for (int layerIndex = 0; layerIndex < groups.Count; layerIndex++) { groupEnumerator.MoveNext(); var currentGroup = groupEnumerator.Current; var currentAgentSettings = currentGroup.AgentSettings; if (result.NavigationMesh.LayersInternal.ContainsKey(currentGroup.Id)) { Logger.Error($"The same group can't be selected twice: {currentGroup}"); return(result); } HashSet <Point> tilesToBuild = new HashSet <Point>(); foreach (var colliderData in collidersLocal) { if (colliderData.InputBuilder == null) { continue; } if (colliderData.Processed) { MarkTiles(colliderData.InputBuilder, ref buildSettings, ref currentAgentSettings, tilesToBuild); if (colliderData.Previous != null) { MarkTiles(colliderData.Previous.InputBuilder, ref buildSettings, ref currentAgentSettings, tilesToBuild); } } } // Check for removed colliders if (lastCache != null) { foreach (var obj in lastCache.Objects) { if (!newCache.Objects.ContainsKey(obj.Key)) { MarkTiles(obj.Value.InputBuilder, ref buildSettings, ref currentAgentSettings, tilesToBuild); } } } // Calculate updated/added bounding boxes foreach (var boundingBox in boundingBoxes) { if (!lastCache?.BoundingBoxes.Contains(boundingBox) ?? true) // In the case of no case, mark all tiles in all bounding boxes to be rebuilt { var tiles = NavigationMeshBuildUtils.GetOverlappingTiles(buildSettings, boundingBox); foreach (var tile in tiles) { tilesToBuild.Add(tile); } } } // Check for removed bounding boxes if (lastCache != null) { foreach (var boundingBox in lastCache.BoundingBoxes) { if (!boundingBoxes.Contains(boundingBox)) { var tiles = NavigationMeshBuildUtils.GetOverlappingTiles(buildSettings, boundingBox); foreach (var tile in tiles) { tilesToBuild.Add(tile); } } } } long buildTimeStamp = DateTime.UtcNow.Ticks; ConcurrentCollector <Tuple <Point, NavigationMeshTile> > builtTiles = new ConcurrentCollector <Tuple <Point, NavigationMeshTile> >(tilesToBuild.Count); Dispatcher.ForEach(tilesToBuild.ToArray(), tileCoordinate => { // Allow cancellation while building tiles if (cancellationToken.IsCancellationRequested) { return; } // Builds the tile, or returns null when there is nothing generated for this tile (empty tile) NavigationMeshTile meshTile = BuildTile(tileCoordinate, buildSettings, currentAgentSettings, boundingBoxes, inputVertices, inputIndices, buildTimeStamp); // Add the result to the list of built tiles builtTiles.Add(new Tuple <Point, NavigationMeshTile>(tileCoordinate, meshTile)); }); if (cancellationToken.IsCancellationRequested) { Logger?.Warning("Operation cancelled"); return(result); } // Add layer to the navigation mesh var layer = new NavigationMeshLayer(); result.NavigationMesh.LayersInternal.Add(currentGroup.Id, layer); // Copy tiles from from the previous build into the current NavigationMeshLayer sourceLayer = null; if (oldNavigationMesh != null && oldNavigationMesh.LayersInternal.TryGetValue(currentGroup.Id, out sourceLayer)) { foreach (var sourceTile in sourceLayer.Tiles) { layer.TilesInternal.Add(sourceTile.Key, sourceTile.Value); } } foreach (var p in builtTiles) { if (p.Item2 == null) { // Remove a tile if (layer.TilesInternal.ContainsKey(p.Item1)) { layer.TilesInternal.Remove(p.Item1); } } else { // Set or update tile layer.TilesInternal[p.Item1] = p.Item2; } } // Add information about which tiles were updated to the result if (tilesToBuild.Count > 0) { var layerUpdateInfo = new NavigationMeshLayerUpdateInfo(); layerUpdateInfo.GroupId = currentGroup.Id; layerUpdateInfo.UpdatedTiles = tilesToBuild.ToList(); result.UpdatedLayers.Add(layerUpdateInfo); } } } // Check for removed layers if (oldNavigationMesh != null) { var newGroups = groups.ToLookup(x => x.Id); foreach (var oldLayer in oldNavigationMesh.Layers) { if (!newGroups.Contains(oldLayer.Key)) { var updateInfo = new NavigationMeshLayerUpdateInfo(); updateInfo.UpdatedTiles.Capacity = oldLayer.Value.Tiles.Count; foreach (var tile in oldLayer.Value.Tiles) { updateInfo.UpdatedTiles.Add(tile.Key); } result.UpdatedLayers.Add(updateInfo); } } } // Store bounding boxes in new tile cache newCache.BoundingBoxes = new List <BoundingBox>(boundingBoxes); // Update navigation mesh oldNavigationMesh = result.NavigationMesh; result.Success = true; return(result); }
/// <summary> /// Rebuilds outdated triangle data for colliders and recalculates hashes storing everything in StaticColliderData /// </summary> private void BuildInput(StaticColliderData[] collidersLocal, CollisionFilterGroupFlags includedCollisionGroups) { NavigationMeshCache lastCache = oldNavigationMesh?.Cache; bool clearCache = false; Dispatcher.ForEach(collidersLocal, colliderData => { var entity = colliderData.Component.Entity; TransformComponent entityTransform = entity.Transform; Matrix entityWorldMatrix = entityTransform.WorldMatrix; NavigationMeshInputBuilder entityNavigationMeshInputBuilder = colliderData.InputBuilder = new NavigationMeshInputBuilder(); // Compute hash of collider and compare it with the previous build if there is one colliderData.ParameterHash = NavigationMeshBuildUtils.HashEntityCollider(colliderData.Component, includedCollisionGroups); colliderData.Previous = null; if (lastCache?.Objects.TryGetValue(colliderData.Component.Id, out colliderData.Previous) ?? false) { if ((!colliderData.Component.AlwaysUpdateNaviMeshCache) && (colliderData.Previous.ParameterHash == colliderData.ParameterHash)) { // In this case, we don't need to recalculate the geometry for this shape, since it wasn't changed // here we take the triangle mesh from the previous build as the current colliderData.InputBuilder = colliderData.Previous.InputBuilder; colliderData.Planes.Clear(); colliderData.Planes.AddRange(colliderData.Previous.Planes); colliderData.Processed = false; return; } } // Clear cache on removal of infinite planes if (colliderData.Planes.Count > 0) { clearCache = true; } // Clear planes colliderData.Planes.Clear(); // Return empty data for disabled colliders, filtered out colliders or trigger colliders if (!colliderData.Component.Enabled || colliderData.Component.IsTrigger || !NavigationMeshBuildUtils.CheckColliderFilter(colliderData.Component, includedCollisionGroups)) { colliderData.Processed = true; return; } // Make sure shape is up to date if (!NavigationMeshBuildUtils.HasLatestColliderShape(colliderData.Component)) { colliderData.Component.ComposeShape(); } // Interate through all the colliders shapes while queueing all shapes in compound shapes to process those as well Queue <ColliderShape> shapesToProcess = new Queue <ColliderShape>(); if (colliderData.Component.ColliderShape != null) { shapesToProcess.Enqueue(colliderData.Component.ColliderShape); while (shapesToProcess.Count > 0) { var shape = shapesToProcess.Dequeue(); var shapeType = shape.GetType(); if (shapeType == typeof(BoxColliderShape)) { var box = (BoxColliderShape)shape; var boxDesc = GetColliderShapeDesc <BoxColliderShapeDesc>(box.Description); Matrix transform = box.PositiveCenterMatrix * entityWorldMatrix; var meshData = GeometricPrimitive.Cube.New(boxDesc.Size, toLeftHanded: true); entityNavigationMeshInputBuilder.AppendMeshData(meshData, transform); } else if (shapeType == typeof(SphereColliderShape)) { var sphere = (SphereColliderShape)shape; var sphereDesc = GetColliderShapeDesc <SphereColliderShapeDesc>(sphere.Description); Matrix transform = sphere.PositiveCenterMatrix * entityWorldMatrix; var meshData = GeometricPrimitive.Sphere.New(sphereDesc.Radius, toLeftHanded: true); entityNavigationMeshInputBuilder.AppendMeshData(meshData, transform); } else if (shapeType == typeof(CylinderColliderShape)) { var cylinder = (CylinderColliderShape)shape; var cylinderDesc = GetColliderShapeDesc <CylinderColliderShapeDesc>(cylinder.Description); Matrix transform = cylinder.PositiveCenterMatrix * entityWorldMatrix; var meshData = GeometricPrimitive.Cylinder.New(cylinderDesc.Height, cylinderDesc.Radius, toLeftHanded: true); entityNavigationMeshInputBuilder.AppendMeshData(meshData, transform); } else if (shapeType == typeof(CapsuleColliderShape)) { var capsule = (CapsuleColliderShape)shape; var capsuleDesc = GetColliderShapeDesc <CapsuleColliderShapeDesc>(capsule.Description); Matrix transform = capsule.PositiveCenterMatrix * entityWorldMatrix; var meshData = GeometricPrimitive.Capsule.New(capsuleDesc.Length, capsuleDesc.Radius, toLeftHanded: true); entityNavigationMeshInputBuilder.AppendMeshData(meshData, transform); } else if (shapeType == typeof(ConeColliderShape)) { var cone = (ConeColliderShape)shape; var coneDesc = GetColliderShapeDesc <ConeColliderShapeDesc>(cone.Description); Matrix transform = cone.PositiveCenterMatrix * entityWorldMatrix; var meshData = GeometricPrimitive.Cone.New(coneDesc.Radius, coneDesc.Height, toLeftHanded: true); entityNavigationMeshInputBuilder.AppendMeshData(meshData, transform); } else if (shapeType == typeof(StaticPlaneColliderShape)) { var planeShape = (StaticPlaneColliderShape)shape; var planeDesc = GetColliderShapeDesc <StaticPlaneColliderShapeDesc>(planeShape.Description); Matrix transform = entityWorldMatrix; Plane plane = new Plane(planeDesc.Normal, planeDesc.Offset); // Pre-Transform plane parameters plane.Normal = Vector3.TransformNormal(planeDesc.Normal, transform); plane.Normal.Normalize(); plane.D += Vector3.Dot(transform.TranslationVector, plane.Normal); colliderData.Planes.Add(plane); } else if (shapeType == typeof(ConvexHullColliderShape)) { var hull = (ConvexHullColliderShape)shape; Matrix transform = hull.PositiveCenterMatrix * entityWorldMatrix; // Convert hull indices to int int[] indices = new int[hull.Indices.Count]; if (hull.Indices.Count % 3 != 0) { throw new InvalidOperationException($"{shapeType} does not consist of triangles"); } for (int i = 0; i < hull.Indices.Count; i += 3) { indices[i] = (int)hull.Indices[i]; indices[i + 2] = (int)hull.Indices[i + 1]; // NOTE: Reversed winding to create left handed input indices[i + 1] = (int)hull.Indices[i + 2]; } entityNavigationMeshInputBuilder.AppendArrays(hull.Points.ToArray(), indices, transform); } else if (shapeType == typeof(StaticMeshColliderShape)) { var mesh = (StaticMeshColliderShape)shape; Matrix transform = mesh.PositiveCenterMatrix * entityWorldMatrix; // Convert hull indices to int int[] indices = new int[mesh.Indices.Count]; if (mesh.Indices.Count % 3 != 0) { throw new InvalidOperationException($"{shapeType} does not consist of triangles"); } for (int i = 0; i < mesh.Indices.Count; i += 3) { indices[i] = (int)mesh.Indices[i]; indices[i + 2] = (int)mesh.Indices[i + 1]; // NOTE: Reversed winding to create left handed input indices[i + 1] = (int)mesh.Indices[i + 2]; } entityNavigationMeshInputBuilder.AppendArrays(mesh.Vertices.ToArray(), indices, transform); } else if (shapeType == typeof(HeightfieldColliderShape)) { var heightfield = (HeightfieldColliderShape)shape; var halfRange = (heightfield.MaxHeight - heightfield.MinHeight) * 0.5f; var offset = -(heightfield.MinHeight + halfRange); Matrix transform = Matrix.Translation(new Vector3(0, offset, 0)) * heightfield.PositiveCenterMatrix * entityWorldMatrix; var width = heightfield.HeightStickWidth - 1; var length = heightfield.HeightStickLength - 1; var mesh = GeometricPrimitive.Plane.New(width, length, width, length, normalDirection: NormalDirection.UpY, toLeftHanded: true); var arrayLength = heightfield.HeightStickWidth * heightfield.HeightStickLength; using (heightfield.LockToReadHeights()) { switch (heightfield.HeightType) { case HeightfieldTypes.Short: if (heightfield.ShortArray == null) { continue; } for (int i = 0; i < arrayLength; ++i) { mesh.Vertices[i].Position.Y = heightfield.ShortArray[i] * heightfield.HeightScale; } break; case HeightfieldTypes.Byte: if (heightfield.ByteArray == null) { continue; } for (int i = 0; i < arrayLength; ++i) { mesh.Vertices[i].Position.Y = heightfield.ByteArray[i] * heightfield.HeightScale; } break; case HeightfieldTypes.Float: if (heightfield.FloatArray == null) { continue; } for (int i = 0; i < arrayLength; ++i) { mesh.Vertices[i].Position.Y = heightfield.FloatArray[i]; } break; } } entityNavigationMeshInputBuilder.AppendMeshData(mesh, transform); } else if (shapeType == typeof(CompoundColliderShape)) { // Unroll compound collider shapes var compound = (CompoundColliderShape)shape; for (int i = 0; i < compound.Count; i++) { shapesToProcess.Enqueue(compound[i]); } } } } // Clear cache on addition of infinite planes if (colliderData.Planes.Count > 0) { clearCache = true; } // Mark collider as processed colliderData.Processed = true; }); if (clearCache && oldNavigationMesh != null) { oldNavigationMesh = null; } }
/// <summary> /// Initializes the builder, optionally with a previous navigation mesh when building incrementally /// </summary> /// <param name="oldNavigationMesh">The previous navigation mesh, to allow incremental builds</param> public NavigationMeshBuilder(NavigationMesh oldNavigationMesh = null) { this.oldNavigationMesh = oldNavigationMesh; }
/// <summary> /// Rebuilds outdated triangle data for colliders and recalculates hashes storing everything in StaticColliderData /// </summary> private void BuildInput(StaticColliderData[] collidersLocal, CollisionFilterGroupFlags includedCollisionGroups) { NavigationMeshCache lastCache = oldNavigationMesh?.Cache; bool clearCache = false; Dispatcher.ForEach(collidersLocal, colliderData => { var entity = colliderData.Component.Entity; TransformComponent entityTransform = entity.Transform; Matrix entityWorldMatrix = entityTransform.WorldMatrix; NavigationMeshInputBuilder entityNavigationMeshInputBuilder = colliderData.InputBuilder = new NavigationMeshInputBuilder(); // Compute hash of collider and compare it with the previous build if there is one colliderData.ParameterHash = NavigationMeshBuildUtils.HashEntityCollider(colliderData.Component, includedCollisionGroups); colliderData.Previous = null; if (lastCache?.Objects.TryGetValue(colliderData.Component.Id, out colliderData.Previous) ?? false) { if ((!colliderData.Component.AlwaysUpdateNaviMeshCache) && (colliderData.Previous.ParameterHash == colliderData.ParameterHash)) { // In this case, we don't need to recalculate the geometry for this shape, since it wasn't changed // here we take the triangle mesh from the previous build as the current colliderData.InputBuilder = colliderData.Previous.InputBuilder; colliderData.Planes.Clear(); colliderData.Planes.AddRange(colliderData.Previous.Planes); colliderData.Processed = false; return; } } // Clear cache on removal of infinite planes if (colliderData.Planes.Count > 0) { clearCache = true; } // Clear planes colliderData.Planes.Clear(); // Return empty data for disabled colliders, filtered out colliders or trigger colliders if (!colliderData.Component.Enabled || colliderData.Component.GenerateOverlapEvents || !NavigationMeshBuildUtils.CheckColliderFilter(colliderData.Component, includedCollisionGroups)) { colliderData.Processed = true; return; } // Make sure shape is up to date if (!NavigationMeshBuildUtils.HasLatestColliderShape(colliderData.Component)) { colliderData.Component.ComposeShape(); } // Interate through all the colliders shapes while queueing all shapes in compound shapes to process those as well Queue <ColliderShape> shapesToProcess = new Queue <ColliderShape>(); if (colliderData.Component.ColliderShape != null) { shapesToProcess.Enqueue(colliderData.Component.ColliderShape); while (shapesToProcess.Count > 0) { var shape = shapesToProcess.Dequeue(); var shapeType = shape.GetType(); if (shapeType == typeof(BoxColliderShape)) { var box = (BoxColliderShape)shape; var boxDesc = GetColliderShapeDesc <BoxColliderShapeDesc>(box.Description); Matrix transform = box.PositiveCenterMatrix * entityWorldMatrix; var meshData = GeometricPrimitive.Cube.New(boxDesc.Size, toLeftHanded: true); entityNavigationMeshInputBuilder.AppendMeshData(meshData, transform); } else if (shapeType == typeof(SphereColliderShape)) { var sphere = (SphereColliderShape)shape; var sphereDesc = GetColliderShapeDesc <SphereColliderShapeDesc>(sphere.Description); Matrix transform = sphere.PositiveCenterMatrix * entityWorldMatrix; var meshData = GeometricPrimitive.Sphere.New(sphereDesc.Radius, toLeftHanded: true); entityNavigationMeshInputBuilder.AppendMeshData(meshData, transform); } else if (shapeType == typeof(CylinderColliderShape)) { var cylinder = (CylinderColliderShape)shape; var cylinderDesc = GetColliderShapeDesc <CylinderColliderShapeDesc>(cylinder.Description); Matrix transform = cylinder.PositiveCenterMatrix * entityWorldMatrix; var meshData = GeometricPrimitive.Cylinder.New(cylinderDesc.Length, cylinderDesc.Radius, toLeftHanded: true); entityNavigationMeshInputBuilder.AppendMeshData(meshData, transform); } else if (shapeType == typeof(CapsuleColliderShape)) { var capsule = (CapsuleColliderShape)shape; var capsuleDesc = GetColliderShapeDesc <CapsuleColliderShapeDesc>(capsule.Description); Matrix transform = capsule.PositiveCenterMatrix * entityWorldMatrix; var meshData = GeometricPrimitive.Capsule.New(capsuleDesc.Length, capsuleDesc.Radius, toLeftHanded: true); entityNavigationMeshInputBuilder.AppendMeshData(meshData, transform); } else if (shapeType == typeof(ConvexHullColliderShape)) { var hull = (ConvexHullColliderShape)shape; Matrix transform = hull.PositiveCenterMatrix * entityWorldMatrix; // Convert hull indices to int int[] indices = new int[hull.Indices.Count]; if (hull.Indices.Count % 3 != 0) { throw new InvalidOperationException($"{shapeType} does not consist of triangles"); } for (int i = 0; i < hull.Indices.Count; i += 3) { indices[i] = (int)hull.Indices[i]; indices[i + 2] = (int)hull.Indices[i + 1]; // NOTE: Reversed winding to create left handed input indices[i + 1] = (int)hull.Indices[i + 2]; } entityNavigationMeshInputBuilder.AppendArrays(hull.Points.ToArray(), indices, transform); } else if (shapeType == typeof(MeshColliderShape)) { var mesh = (MeshColliderShape)shape; Matrix transform = mesh.PositiveCenterMatrix * entityWorldMatrix; // Convert hull indices to int int[] indices = new int[mesh.Indices.Length]; if (mesh.Indices.Length % 3 != 0) { throw new InvalidOperationException($"{shapeType} does not consist of triangles"); } for (int i = 0; i < mesh.Indices.Length; i += 3) { indices[i] = (int)mesh.Indices[i]; indices[i + 2] = (int)mesh.Indices[i + 1]; // NOTE: Reversed winding to create left handed input indices[i + 1] = (int)mesh.Indices[i + 2]; } entityNavigationMeshInputBuilder.AppendArrays(mesh.Vertices.ToArray(), indices, transform); } } } // Clear cache on addition of infinite planes if (colliderData.Planes.Count > 0) { clearCache = true; } // Mark collider as processed colliderData.Processed = true; }); if (clearCache && oldNavigationMesh != null) { oldNavigationMesh = null; } }