using System; using System.Collections.Generic; using InfiniteWorld.VoxelWorld.Contracts; using MessagePipe; using UnityEngine; using UnityEngine.AI; using VContainer.Unity; using UnityNavMesh = UnityEngine.AI.NavMesh; using UnityNavMeshBuilder = UnityEngine.AI.NavMeshBuilder; namespace InfiniteWorld.VoxelWorld.NavMesh { public sealed class VoxelWorldNavMeshService : IStartable, ITickable, IDisposable, INavCoverageReader { private readonly IChunkNavSourceReader chunkNavSourceReader; private readonly IWorldInterestReader worldInterestReader; private readonly ISubscriber chunkReadySubscriber; private readonly ISubscriber chunkRemovedSubscriber; private readonly ISubscriber worldInterestChangedSubscriber; private readonly VoxelWorldNavMeshConfig config; private readonly Dictionary coverageWindows = new Dictionary(); private readonly Queue dirtyCoverageWindowIds = new Queue(); private readonly HashSet queuedCoverageWindowIds = new HashSet(); private readonly List dirtyCoverageWindowCandidates = new List(16); private readonly List interestPoints = new List(8); private readonly List loadedChunkCoords = new List(128); private readonly List buildSources = new List(256); private readonly List coverageWindowSnapshots = new List(8); private readonly List desiredCoverageWindows = new List(8); private readonly List clusterAccumulators = new List(8); private readonly List subscriptions = new List(3); private int nextCoverageWindowId = 1; private int? activeBuildWindowId; public VoxelWorldNavMeshService( IChunkNavSourceReader chunkNavSourceReader, IWorldInterestReader worldInterestReader, ISubscriber chunkReadySubscriber, ISubscriber chunkRemovedSubscriber, ISubscriber worldInterestChangedSubscriber, VoxelWorldNavMeshConfig config) { this.chunkNavSourceReader = chunkNavSourceReader; this.worldInterestReader = worldInterestReader; this.chunkReadySubscriber = chunkReadySubscriber; this.chunkRemovedSubscriber = chunkRemovedSubscriber; this.worldInterestChangedSubscriber = worldInterestChangedSubscriber; this.config = config ?? new VoxelWorldNavMeshConfig(); } public void Start() { subscriptions.Add(chunkReadySubscriber.Subscribe(OnChunkNavGeometryReady)); subscriptions.Add(chunkRemovedSubscriber.Subscribe(OnChunkNavGeometryRemoved)); subscriptions.Add(worldInterestChangedSubscriber.Subscribe(OnWorldInterestChanged)); RefreshInterestPoints(); SyncCoverageWindows(); MarkAllCoverageWindowsDirty(); } public void Tick() { RefreshInterestPoints(); SyncCoverageWindows(); CompleteFinishedBuild(); int startedBuilds = 0; int maxBuilds = Mathf.Max(1, config.maxNavMeshBuildsPerFrame); while (startedBuilds < maxBuilds) { if (activeBuildWindowId.HasValue || dirtyCoverageWindowIds.Count == 0) { break; } int windowId = DequeueBestDirtyCoverageWindow(); if (!TryStartCoverageBuild(windowId)) { startedBuilds++; continue; } startedBuilds++; } } public bool IsPositionCovered(Vector3 worldPosition) { foreach (KeyValuePair pair in coverageWindows) { NavCoverageWindowRuntime window = pair.Value; if (window.State == NavCoverageState.Ready && ContainsXZ(window.CoverageBounds, worldPosition)) { return true; } } return false; } public void GetCoverageWindows(List results) { if (results == null) { throw new ArgumentNullException(nameof(results)); } foreach (KeyValuePair pair in coverageWindows) { NavCoverageWindowRuntime window = pair.Value; results.Add(new NavCoverageWindowSnapshot(window.Id, window.CoverageBounds, window.State, window.InterestCount)); } } public void Dispose() { for (int i = 0; i < subscriptions.Count; i++) { subscriptions[i]?.Dispose(); } subscriptions.Clear(); foreach (KeyValuePair pair in coverageWindows) { pair.Value.Dispose(); } coverageWindows.Clear(); queuedCoverageWindowIds.Clear(); dirtyCoverageWindowIds.Clear(); desiredCoverageWindows.Clear(); clusterAccumulators.Clear(); coverageWindowSnapshots.Clear(); activeBuildWindowId = null; } private void OnChunkNavGeometryReady(ChunkNavGeometryReadyMessage message) { MarkCoverageWindowsDirtyForChunk(message.Coord); } private void OnChunkNavGeometryRemoved(ChunkNavGeometryRemovedMessage message) { MarkCoverageWindowsDirtyForChunk(message.Coord); } private void OnWorldInterestChanged(WorldInterestChangedMessage message) { RefreshInterestPoints(); SyncCoverageWindows(); MarkAllCoverageWindowsDirty(); } private void RefreshInterestPoints() { interestPoints.Clear(); worldInterestReader.GetInterestPoints(interestPoints); } private void SyncCoverageWindows() { BuildDesiredCoverageWindows(); foreach (KeyValuePair pair in coverageWindows) { pair.Value.MatchedThisFrame = false; } for (int i = 0; i < desiredCoverageWindows.Count; i++) { DesiredCoverageWindow desiredWindow = desiredCoverageWindows[i]; NavCoverageWindowRuntime runtime = FindBestMatchingCoverageWindow(desiredWindow); if (runtime == null) { runtime = new NavCoverageWindowRuntime(nextCoverageWindowId++, desiredWindow.CoverageBounds, desiredWindow.Priority, desiredWindow.InterestCount); runtime.MatchedThisFrame = true; coverageWindows.Add(runtime.Id, runtime); EnqueueDirtyCoverageWindow(runtime.Id); continue; } runtime.MatchedThisFrame = true; runtime.Priority = desiredWindow.Priority; runtime.InterestCount = desiredWindow.InterestCount; if (!BoundsApproximatelyEqual(runtime.CoverageBounds, desiredWindow.CoverageBounds)) { runtime.CoverageBounds = desiredWindow.CoverageBounds; runtime.State = NavCoverageState.Pending; EnqueueDirtyCoverageWindow(runtime.Id); } } coverageWindowSnapshots.Clear(); foreach (KeyValuePair pair in coverageWindows) { if (pair.Value.MatchedThisFrame) { coverageWindowSnapshots.Add(new NavCoverageWindowSnapshot(pair.Value.Id, pair.Value.CoverageBounds, pair.Value.State, pair.Value.InterestCount)); } } RemoveUnmatchedCoverageWindows(); } private void BuildDesiredCoverageWindows() { desiredCoverageWindows.Clear(); clusterAccumulators.Clear(); if (interestPoints.Count == 0) { return; } float chunkWorldSize = Mathf.Max(1f, chunkNavSourceReader.ChunkWorldSize); float mergeDistance = Mathf.Max(0f, config.clusterMergeDistanceInChunks) * chunkWorldSize; float quantizationStep = Mathf.Max(0.25f, config.coverageQuantizationInChunks) * chunkWorldSize; float padding = Mathf.Max(0f, config.coveragePaddingInChunks) * chunkWorldSize; float minWindowSize = Mathf.Max(1f, config.minCoverageWindowSizeInChunks) * chunkWorldSize; for (int i = 0; i < interestPoints.Count; i++) { WorldInterestPoint point = interestPoints[i]; int bestClusterIndex = -1; float bestDistance = float.MaxValue; for (int clusterIndex = 0; clusterIndex < clusterAccumulators.Count; clusterIndex++) { float distance = DistanceToBoundsXZ(clusterAccumulators[clusterIndex].RawBounds, point.Position); if (distance <= mergeDistance && distance < bestDistance) { bestDistance = distance; bestClusterIndex = clusterIndex; } } if (bestClusterIndex >= 0) { ClusterAccumulator cluster = clusterAccumulators[bestClusterIndex]; cluster.Add(point); clusterAccumulators[bestClusterIndex] = cluster; } else { clusterAccumulators.Add(new ClusterAccumulator(point)); } } MergeNearbyClusters(mergeDistance); for (int i = 0; i < clusterAccumulators.Count; i++) { ClusterAccumulator cluster = clusterAccumulators[i]; Bounds coverageBounds = CreateQuantizedCoverageBounds(cluster.RawBounds, padding, minWindowSize, quantizationStep); desiredCoverageWindows.Add(new DesiredCoverageWindow(coverageBounds, cluster.Priority, cluster.InterestCount)); } desiredCoverageWindows.Sort((left, right) => { int priorityCompare = right.Priority.CompareTo(left.Priority); if (priorityCompare != 0) { return priorityCompare; } int interestCompare = right.InterestCount.CompareTo(left.InterestCount); if (interestCompare != 0) { return interestCompare; } return left.CoverageBounds.center.sqrMagnitude.CompareTo(right.CoverageBounds.center.sqrMagnitude); }); int maxWindows = Mathf.Max(1, config.maxActiveCoverageWindows); if (desiredCoverageWindows.Count > maxWindows) { desiredCoverageWindows.RemoveRange(maxWindows, desiredCoverageWindows.Count - maxWindows); } } private void MergeNearbyClusters(float mergeDistance) { if (clusterAccumulators.Count < 2) { return; } bool merged; do { merged = false; for (int i = 0; i < clusterAccumulators.Count; i++) { for (int j = i + 1; j < clusterAccumulators.Count; j++) { if (DistanceBetweenBoundsXZ(clusterAccumulators[i].RawBounds, clusterAccumulators[j].RawBounds) > mergeDistance) { continue; } ClusterAccumulator combined = clusterAccumulators[i]; combined.Merge(clusterAccumulators[j]); clusterAccumulators[i] = combined; clusterAccumulators.RemoveAt(j); merged = true; break; } if (merged) { break; } } } while (merged); } private NavCoverageWindowRuntime FindBestMatchingCoverageWindow(DesiredCoverageWindow desiredWindow) { NavCoverageWindowRuntime bestMatch = null; float bestDistance = float.MaxValue; float chunkWorldSize = Mathf.Max(1f, chunkNavSourceReader.ChunkWorldSize); float matchThreshold = Mathf.Max(config.minCoverageWindowSizeInChunks, config.clusterMergeDistanceInChunks + config.coveragePaddingInChunks) * chunkWorldSize; foreach (KeyValuePair pair in coverageWindows) { NavCoverageWindowRuntime candidate = pair.Value; if (candidate.MatchedThisFrame) { continue; } float distance = Vector2.Distance( new Vector2(candidate.CoverageBounds.center.x, candidate.CoverageBounds.center.z), new Vector2(desiredWindow.CoverageBounds.center.x, desiredWindow.CoverageBounds.center.z)); if (distance > matchThreshold || distance >= bestDistance) { continue; } bestDistance = distance; bestMatch = candidate; } return bestMatch; } private void RemoveUnmatchedCoverageWindows() { List windowsToRemove = null; foreach (KeyValuePair pair in coverageWindows) { if (pair.Value.MatchedThisFrame) { continue; } windowsToRemove ??= new List(); windowsToRemove.Add(pair.Key); } if (windowsToRemove == null) { return; } for (int i = 0; i < windowsToRemove.Count; i++) { RemoveCoverageWindow(windowsToRemove[i]); } } private void MarkAllCoverageWindowsDirty() { foreach (KeyValuePair pair in coverageWindows) { EnqueueDirtyCoverageWindow(pair.Key); } } private void MarkCoverageWindowsDirtyForChunk(Vector2Int chunkCoord) { Bounds chunkBounds = ExpandChunkBounds(GetChunkWorldBounds(chunkCoord), Mathf.Max(0, config.chunkCollectionMarginInChunks)); foreach (KeyValuePair pair in coverageWindows) { Bounds invalidationBounds = ExpandCoverageBounds(pair.Value.CoverageBounds, Mathf.Max(0, config.chunkCollectionMarginInChunks)); if (IntersectsXZ(invalidationBounds, chunkBounds)) { EnqueueDirtyCoverageWindow(pair.Key); } } } private void EnqueueDirtyCoverageWindow(int windowId) { if (!queuedCoverageWindowIds.Add(windowId)) { if (activeBuildWindowId.HasValue && activeBuildWindowId.Value == windowId && coverageWindows.TryGetValue(windowId, out NavCoverageWindowRuntime activeWindow)) { activeWindow.BuildRequestedWhileRunning = true; } return; } dirtyCoverageWindowIds.Enqueue(windowId); } private int DequeueBestDirtyCoverageWindow() { dirtyCoverageWindowCandidates.Clear(); while (dirtyCoverageWindowIds.Count > 0) { dirtyCoverageWindowCandidates.Add(dirtyCoverageWindowIds.Dequeue()); } int bestIndex = 0; float bestScore = float.MaxValue; for (int i = 0; i < dirtyCoverageWindowCandidates.Count; i++) { int windowId = dirtyCoverageWindowCandidates[i]; if (!coverageWindows.TryGetValue(windowId, out NavCoverageWindowRuntime window)) { continue; } float score = GetCoveragePriorityScore(window); if (score < bestScore) { bestScore = score; bestIndex = i; } } int bestWindowId = dirtyCoverageWindowCandidates[bestIndex]; queuedCoverageWindowIds.Remove(bestWindowId); for (int i = 0; i < dirtyCoverageWindowCandidates.Count; i++) { if (i == bestIndex) { continue; } dirtyCoverageWindowIds.Enqueue(dirtyCoverageWindowCandidates[i]); } dirtyCoverageWindowCandidates.Clear(); return bestWindowId; } private float GetCoveragePriorityScore(NavCoverageWindowRuntime window) { if (interestPoints.Count == 0) { return 0f; } Vector3 center = window.CoverageBounds.center; float bestDistance = float.MaxValue; for (int i = 0; i < interestPoints.Count; i++) { float priority = Mathf.Max(0.01f, interestPoints[i].Priority); float distance = Vector2.SqrMagnitude( new Vector2(center.x, center.z) - new Vector2(interestPoints[i].Position.x, interestPoints[i].Position.z)) / priority; if (distance < bestDistance) { bestDistance = distance; } } return bestDistance; } private bool TryStartCoverageBuild(int windowId) { if (!coverageWindows.TryGetValue(windowId, out NavCoverageWindowRuntime window)) { return false; } buildSources.Clear(); window.CollectionBounds = ExpandCoverageBounds(window.CoverageBounds, Mathf.Max(0, config.chunkCollectionMarginInChunks)); bool hasSources = CollectBuildSources(window.CollectionBounds, buildSources); if (!hasSources || buildSources.Count == 0) { window.State = NavCoverageState.Pending; RemoveCoverageData(window); return false; } Bounds buildBounds = CalculateBounds(buildSources); ExpandBounds(ref buildBounds, config.navBoundsHorizontalPadding, config.navBoundsVerticalPadding); window.BuildBounds = buildBounds; window.BuildRequestedWhileRunning = false; if (window.NavMeshData == null) { window.NavMeshData = new NavMeshData(config.agentTypeId); } if (!window.Instance.valid) { window.Instance = UnityNavMesh.AddNavMeshData(window.NavMeshData); } NavMeshBuildSettings buildSettings = UnityNavMesh.GetSettingsByID(config.agentTypeId); window.ActiveBuild = UnityNavMeshBuilder.UpdateNavMeshDataAsync(window.NavMeshData, buildSettings, buildSources, buildBounds); window.State = NavCoverageState.Building; activeBuildWindowId = windowId; return true; } private bool CollectBuildSources(Bounds coverageBounds, List results) { loadedChunkCoords.Clear(); chunkNavSourceReader.GetLoadedChunkCoords(loadedChunkCoords); bool hasSources = false; for (int i = 0; i < loadedChunkCoords.Count; i++) { Vector2Int chunkCoord = loadedChunkCoords[i]; if (!IntersectsXZ(GetChunkWorldBounds(chunkCoord), coverageBounds)) { continue; } if (!chunkNavSourceReader.TryGetChunkNavSourceSnapshot(chunkCoord, out ChunkNavSourceSnapshot snapshot) || snapshot.Sources == null || snapshot.Sources.Length == 0) { continue; } hasSources = true; AppendBuildSources(snapshot.Sources, results); } return hasSources; } private static void AppendBuildSources(ChunkNavBuildSourceDescriptor[] descriptors, List results) { for (int i = 0; i < descriptors.Length; i++) { ChunkNavBuildSourceDescriptor descriptor = descriptors[i]; if (descriptor.Shape == NavMeshBuildSourceShape.Mesh && descriptor.Mesh == null) { continue; } NavMeshBuildSource source = new NavMeshBuildSource { area = descriptor.Area, shape = descriptor.Shape, transform = descriptor.Transform, size = descriptor.Size, sourceObject = descriptor.Shape == NavMeshBuildSourceShape.Mesh ? descriptor.Mesh : null }; results.Add(source); } } private void CompleteFinishedBuild() { if (!activeBuildWindowId.HasValue) { return; } if (!coverageWindows.TryGetValue(activeBuildWindowId.Value, out NavCoverageWindowRuntime window)) { activeBuildWindowId = null; return; } if (window.ActiveBuild != null && !window.ActiveBuild.isDone) { return; } window.ActiveBuild = null; window.State = NavCoverageState.Ready; int completedWindowId = activeBuildWindowId.Value; activeBuildWindowId = null; if (window.BuildRequestedWhileRunning) { window.BuildRequestedWhileRunning = false; EnqueueDirtyCoverageWindow(completedWindowId); } } private void RemoveCoverageWindow(int windowId) { if (!coverageWindows.TryGetValue(windowId, out NavCoverageWindowRuntime window)) { return; } if (activeBuildWindowId.HasValue && activeBuildWindowId.Value == windowId) { activeBuildWindowId = null; } window.Dispose(); coverageWindows.Remove(windowId); queuedCoverageWindowIds.Remove(windowId); } private static void RemoveCoverageData(NavCoverageWindowRuntime window) { if (window.ActiveBuild != null && !window.ActiveBuild.isDone && window.NavMeshData != null) { UnityNavMeshBuilder.Cancel(window.NavMeshData); } if (window.Instance.valid) { UnityNavMesh.RemoveNavMeshData(window.Instance); window.Instance = default; } window.ActiveBuild = null; window.NavMeshData = null; } private Bounds GetChunkWorldBounds(Vector2Int chunkCoord) { float chunkSize = Mathf.Max(1f, chunkNavSourceReader.ChunkWorldSize); Vector3 min = new Vector3(chunkCoord.x * chunkSize, -500f, chunkCoord.y * chunkSize); Vector3 size = new Vector3(chunkSize, 1000f, chunkSize); return new Bounds(min + new Vector3(chunkSize * 0.5f, 0f, chunkSize * 0.5f), size); } private Bounds ExpandCoverageBounds(Bounds bounds, int chunkMargin) { return ExpandChunkBounds(bounds, chunkMargin); } private Bounds ExpandChunkBounds(Bounds bounds, int chunkMargin) { float chunkSize = Mathf.Max(1f, chunkNavSourceReader.ChunkWorldSize); float horizontalPadding = chunkMargin * chunkSize; bounds.Expand(new Vector3(horizontalPadding * 2f, 0f, horizontalPadding * 2f)); return bounds; } private static Bounds CalculateBounds(List sources) { Bounds bounds = GetSourceBounds(sources[0]); for (int i = 1; i < sources.Count; i++) { bounds.Encapsulate(GetSourceBounds(sources[i])); } return bounds; } private static Bounds GetSourceBounds(NavMeshBuildSource source) { if (source.shape == NavMeshBuildSourceShape.Box) { return TransformBounds(source.transform, new Bounds(Vector3.zero, source.size)); } Mesh mesh = source.sourceObject as Mesh; if (mesh != null) { return TransformBounds(source.transform, mesh.bounds); } return new Bounds(source.transform.GetColumn(3), Vector3.zero); } private static Bounds TransformBounds(Matrix4x4 matrix, Bounds localBounds) { Vector3 center = localBounds.center; Vector3 extents = localBounds.extents; Vector3[] corners = { new Vector3(center.x - extents.x, center.y - extents.y, center.z - extents.z), new Vector3(center.x - extents.x, center.y - extents.y, center.z + extents.z), new Vector3(center.x - extents.x, center.y + extents.y, center.z - extents.z), new Vector3(center.x - extents.x, center.y + extents.y, center.z + extents.z), new Vector3(center.x + extents.x, center.y - extents.y, center.z - extents.z), new Vector3(center.x + extents.x, center.y - extents.y, center.z + extents.z), new Vector3(center.x + extents.x, center.y + extents.y, center.z - extents.z), new Vector3(center.x + extents.x, center.y + extents.y, center.z + extents.z) }; Bounds worldBounds = new Bounds(matrix.MultiplyPoint3x4(corners[0]), Vector3.zero); for (int i = 1; i < corners.Length; i++) { worldBounds.Encapsulate(matrix.MultiplyPoint3x4(corners[i])); } return worldBounds; } private static void ExpandBounds(ref Bounds bounds, float horizontalPadding, float verticalPadding) { Vector3 size = bounds.size; size.x = Mathf.Max(size.x + horizontalPadding * 2f, 0.1f); size.z = Mathf.Max(size.z + horizontalPadding * 2f, 0.1f); size.y = Mathf.Max(size.y + verticalPadding * 2f, 0.1f); bounds.size = size; } private static Bounds CreateQuantizedCoverageBounds(Bounds rawBounds, float padding, float minSize, float quantizationStep) { Vector3 min = rawBounds.min; Vector3 max = rawBounds.max; min.x -= padding; min.z -= padding; max.x += padding; max.z += padding; EnsureMinimumSpan(ref min.x, ref max.x, minSize); EnsureMinimumSpan(ref min.z, ref max.z, minSize); min.x = quantizationStep * Mathf.Floor(min.x / quantizationStep); min.z = quantizationStep * Mathf.Floor(min.z / quantizationStep); max.x = quantizationStep * Mathf.Ceil(max.x / quantizationStep); max.z = quantizationStep * Mathf.Ceil(max.z / quantizationStep); Vector3 center = new Vector3((min.x + max.x) * 0.5f, 0f, (min.z + max.z) * 0.5f); Vector3 size = new Vector3(Mathf.Max(max.x - min.x, minSize), 0.1f, Mathf.Max(max.z - min.z, minSize)); return new Bounds(center, size); } private static void EnsureMinimumSpan(ref float min, ref float max, float minimumSize) { float currentSize = max - min; if (currentSize >= minimumSize) { return; } float halfPadding = (minimumSize - currentSize) * 0.5f; min -= halfPadding; max += halfPadding; } private static float DistanceToBoundsXZ(Bounds bounds, Vector3 point) { float dx = Mathf.Max(bounds.min.x - point.x, 0f, point.x - bounds.max.x); float dz = Mathf.Max(bounds.min.z - point.z, 0f, point.z - bounds.max.z); return Mathf.Sqrt(dx * dx + dz * dz); } private static float DistanceBetweenBoundsXZ(Bounds left, Bounds right) { float dx = Mathf.Max(left.min.x - right.max.x, 0f, right.min.x - left.max.x); float dz = Mathf.Max(left.min.z - right.max.z, 0f, right.min.z - left.max.z); return Mathf.Sqrt(dx * dx + dz * dz); } private static bool ContainsXZ(Bounds bounds, Vector3 position) { return position.x >= bounds.min.x && position.x <= bounds.max.x && position.z >= bounds.min.z && position.z <= bounds.max.z; } private static bool IntersectsXZ(Bounds left, Bounds right) { return left.min.x <= right.max.x && left.max.x >= right.min.x && left.min.z <= right.max.z && left.max.z >= right.min.z; } private static bool BoundsApproximatelyEqual(Bounds left, Bounds right) { return Vector3.SqrMagnitude(left.center - right.center) < 0.0001f && Vector3.SqrMagnitude(left.size - right.size) < 0.0001f; } private readonly struct DesiredCoverageWindow { public DesiredCoverageWindow(Bounds coverageBounds, float priority, int interestCount) { CoverageBounds = coverageBounds; Priority = priority; InterestCount = interestCount; } public Bounds CoverageBounds { get; } public float Priority { get; } public int InterestCount { get; } } private struct ClusterAccumulator { public ClusterAccumulator(WorldInterestPoint point) { RawBounds = new Bounds(new Vector3(point.Position.x, 0f, point.Position.z), new Vector3(0.1f, 0.1f, 0.1f)); Priority = point.Priority; InterestCount = 1; } public Bounds RawBounds; public float Priority; public int InterestCount; public void Add(WorldInterestPoint point) { RawBounds.Encapsulate(new Vector3(point.Position.x, 0f, point.Position.z)); Priority = Mathf.Max(Priority, point.Priority); InterestCount++; } public void Merge(ClusterAccumulator other) { RawBounds.Encapsulate(other.RawBounds.min); RawBounds.Encapsulate(other.RawBounds.max); Priority = Mathf.Max(Priority, other.Priority); InterestCount += other.InterestCount; } } private sealed class NavCoverageWindowRuntime : IDisposable { public NavCoverageWindowRuntime(int id, Bounds coverageBounds, float priority, int interestCount) { Id = id; CoverageBounds = coverageBounds; Priority = priority; InterestCount = interestCount; State = NavCoverageState.Pending; } public int Id { get; } public Bounds CoverageBounds; public Bounds CollectionBounds; public Bounds BuildBounds; public float Priority; public int InterestCount; public NavCoverageState State; public NavMeshData NavMeshData; public NavMeshDataInstance Instance; public AsyncOperation ActiveBuild; public bool BuildRequestedWhileRunning; public bool MatchedThisFrame; public void Dispose() { RemoveCoverageData(this); State = NavCoverageState.Pending; } } } }