/// <summary> /// Marks tiles that should be built according to how much their geometry affects the navigation mesh and the bounding boxes specified for building /// </summary> private void MarkTiles(NavigationMeshInputBuilder inputBuilder, ref NavigationMeshBuildSettings buildSettings, ref NavigationAgentSettings agentSettings, HashSet <Point> tilesToBuild) { // Extend bounding box for agent size BoundingBox boundingBoxToCheck = inputBuilder.BoundingBox; NavigationMeshBuildUtils.ExtendBoundingBox(ref boundingBoxToCheck, new Vector3(agentSettings.Radius)); List <Point> newTileList = NavigationMeshBuildUtils.GetOverlappingTiles(buildSettings, boundingBoxToCheck); foreach (Point p in newTileList) { tilesToBuild.Add(p); } }
/// <summary> /// Calculates X-Z span for a navigation mesh tile. The Y-axis will span from <see cref="float.MinValue"/> to <see cref="float.MaxValue"/> /// </summary> public static BoundingBox CalculateTileBoundingBox(NavigationMeshBuildSettings settings, Point tileCoord) { float tcs = settings.TileSize * settings.CellSize; Vector2 tileMin = new Vector2(tileCoord.X * tcs, tileCoord.Y * tcs); Vector2 tileMax = tileMin + new Vector2(tcs); BoundingBox boundingBox = BoundingBox.Empty; boundingBox.Minimum.X = tileMin.X; boundingBox.Minimum.Z = tileMin.Y; boundingBox.Maximum.X = tileMax.X; boundingBox.Maximum.Z = tileMax.Y; boundingBox.Minimum.Y = float.MinValue; boundingBox.Maximum.Y = float.MaxValue; return(boundingBox); }
/// <summary> /// Check which tiles overlap a given bounding box /// </summary> /// <param name="settings"></param> /// <param name="boundingBox"></param> /// <returns></returns> public static List <Point> GetOverlappingTiles(NavigationMeshBuildSettings settings, BoundingBox boundingBox) { List <Point> ret = new List <Point>(); float tcs = settings.TileSize * settings.CellSize; Vector2 start = boundingBox.Minimum.XZ() / tcs; Vector2 end = boundingBox.Maximum.XZ() / tcs; Point startTile = new Point( (int)Math.Floor(start.X), (int)Math.Floor(start.Y)); Point endTile = new Point( (int)Math.Ceiling(end.X), (int)Math.Ceiling(end.Y)); for (int y = startTile.Y; y < endTile.Y; y++) { for (int x = startTile.X; x < endTile.X; x++) { ret.Add(new Point(x, y)); } } return(ret); }
public bool Equals(NavigationMeshBuildSettings other) { return(CellHeight.Equals(other.CellHeight) && CellSize.Equals(other.CellSize) && TileSize == other.TileSize && MinRegionArea.Equals(other.MinRegionArea) && RegionMergeArea.Equals(other.RegionMergeArea) && MaxEdgeLen.Equals(other.MaxEdgeLen) && MaxEdgeError.Equals(other.MaxEdgeError) && DetailSamplingDistance.Equals(other.DetailSamplingDistance) && MaxDetailSamplingError.Equals(other.MaxDetailSamplingError)); }
/// <summary> /// Snaps a <see cref="BoundingBox"/>'s height according to the given <see cref="NavigationMeshBuildSettings"/> /// </summary> /// <param name="settings">The build settings</param> /// <param name="boundingBox">Reference to the bounding box to snap</param> public static void SnapBoundingBoxToCellHeight(NavigationMeshBuildSettings settings, ref BoundingBox boundingBox) { // Snap Y to tile height to avoid height differences between tiles boundingBox.Minimum.Y = (float)Math.Floor(boundingBox.Minimum.Y / settings.CellHeight) * settings.CellHeight; boundingBox.Maximum.Y = (float)Math.Ceiling(boundingBox.Maximum.Y / settings.CellHeight) * settings.CellHeight; }
/// <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); }
private NavigationMeshTile BuildTile(Point tileCoordinate, NavigationMeshBuildSettings buildSettings, NavigationAgentSettings agentSettings, ICollection <BoundingBox> boundingBoxes, Vector3[] inputVertices, int[] inputIndices, long buildTimeStamp) { NavigationMeshTile meshTile = null; // Include bounding boxes in tile height range BoundingBox tileBoundingBox = NavigationMeshBuildUtils.CalculateTileBoundingBox(buildSettings, tileCoordinate); float minimumHeight = float.MaxValue; float maximumHeight = float.MinValue; bool shouldBuildTile = false; foreach (var boundingBox in boundingBoxes) { if (boundingBox.Intersects(ref tileBoundingBox)) { maximumHeight = Math.Max(maximumHeight, boundingBox.Maximum.Y); minimumHeight = Math.Min(minimumHeight, boundingBox.Minimum.Y); shouldBuildTile = true; } } NavigationMeshBuildUtils.SnapBoundingBoxToCellHeight(buildSettings, ref tileBoundingBox); // Skip tiles that do not overlap with any bounding box if (shouldBuildTile) { // Set tile's minimum and maximum height tileBoundingBox.Minimum.Y = minimumHeight; tileBoundingBox.Maximum.Y = maximumHeight; unsafe { IntPtr builder = Navigation.CreateBuilder(); // Turn build settings into native structure format Navigation.BuildSettings internalBuildSettings = new Navigation.BuildSettings { // Tile settings BoundingBox = tileBoundingBox, TilePosition = tileCoordinate, TileSize = buildSettings.TileSize, // General build settings CellHeight = buildSettings.CellHeight, CellSize = buildSettings.CellSize, RegionMinArea = buildSettings.MinRegionArea, RegionMergeArea = buildSettings.RegionMergeArea, EdgeMaxLen = buildSettings.MaxEdgeLen, EdgeMaxError = buildSettings.MaxEdgeError, DetailSampleDist = buildSettings.DetailSamplingDistance, DetailSampleMaxError = buildSettings.MaxDetailSamplingError, // Agent settings AgentHeight = agentSettings.Height, AgentRadius = agentSettings.Radius, AgentMaxClimb = agentSettings.MaxClimb, AgentMaxSlope = agentSettings.MaxSlope.Degrees }; Navigation.SetSettings(builder, new IntPtr(&internalBuildSettings)); IntPtr buildResultPtr = Navigation.Build(builder, inputVertices, inputVertices.Length, inputIndices, inputIndices.Length); Navigation.GeneratedData *generatedDataPtr = (Navigation.GeneratedData *)buildResultPtr; if (generatedDataPtr->Success && generatedDataPtr->NavmeshDataLength > 0) { meshTile = new NavigationMeshTile(); // Copy the generated navigationMesh data meshTile.Data = new byte[generatedDataPtr->NavmeshDataLength + sizeof(long)]; Marshal.Copy(generatedDataPtr->NavmeshData, meshTile.Data, 0, generatedDataPtr->NavmeshDataLength); // Append time stamp byte[] timeStamp = BitConverter.GetBytes(buildTimeStamp); for (int i = 0; i < timeStamp.Length; i++) { meshTile.Data[meshTile.Data.Length - sizeof(long) + i] = timeStamp[i]; } List <Vector3> outputVerts = new List <Vector3>(); if (generatedDataPtr->NumNavmeshVertices > 0) { Vector3 *navmeshVerts = (Vector3 *)generatedDataPtr->NavmeshVertices; for (int j = 0; j < generatedDataPtr->NumNavmeshVertices; j++) { outputVerts.Add(navmeshVerts[j]); } } Navigation.DestroyBuilder(builder); } } } return(meshTile); }