diff --git a/Assets/Features/VoxelWorld/Contracts/NavMeshWorldContracts.cs b/Assets/Features/VoxelWorld/Contracts/NavMeshWorldContracts.cs index f4a8bda3..c0661a44 100644 --- a/Assets/Features/VoxelWorld/Contracts/NavMeshWorldContracts.cs +++ b/Assets/Features/VoxelWorld/Contracts/NavMeshWorldContracts.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using UnityEngine; -using UnityEngine.AI; namespace InfiniteWorld.VoxelWorld.Contracts { @@ -34,136 +33,4 @@ namespace InfiniteWorld.VoxelWorld.Contracts int HintVersion { get; } void GetHintPoints(List results); } - - public readonly struct ChunkNavSourceSnapshot - { - public ChunkNavSourceSnapshot(Vector2Int coord, int version, ChunkNavBuildSourceDescriptor[] sources) - { - Coord = coord; - Version = version; - Sources = sources; - } - - public Vector2Int Coord { get; } - public int Version { get; } - public ChunkNavBuildSourceDescriptor[] Sources { get; } - } - - public readonly struct ChunkNavBuildSourceDescriptor - { - public ChunkNavBuildSourceDescriptor(NavMeshBuildSourceShape shape, Matrix4x4 transform, Vector3 size, Mesh mesh, int area) - { - Shape = shape; - Transform = transform; - Size = size; - Mesh = mesh; - Area = area; - } - - public NavMeshBuildSourceShape Shape { get; } - public Matrix4x4 Transform { get; } - public Vector3 Size { get; } - public Mesh Mesh { get; } - public int Area { get; } - - public static ChunkNavBuildSourceDescriptor CreateBox(Matrix4x4 transform, Vector3 size, int area = 0) - { - return new ChunkNavBuildSourceDescriptor(NavMeshBuildSourceShape.Box, transform, size, null, area); - } - - public static ChunkNavBuildSourceDescriptor CreateMesh(Matrix4x4 transform, Mesh mesh, int area = 0) - { - return new ChunkNavBuildSourceDescriptor(NavMeshBuildSourceShape.Mesh, transform, Vector3.zero, mesh, area); - } - } - - public readonly struct WorldInterestPoint - { - public WorldInterestPoint(Vector3 position, float priority, WorldInterestKind kind) - { - Position = position; - Priority = priority; - Kind = kind; - } - - public Vector3 Position { get; } - public float Priority { get; } - public WorldInterestKind Kind { get; } - } - - public enum WorldInterestKind - { - PlayerActor = 0, - ActiveNpc = 1, - SpawnAnchor = 2, - TransientNavHint = 3, - Other = 4 - } - - public readonly struct NavCoverageWindowSnapshot - { - public NavCoverageWindowSnapshot(int id, Bounds bounds, NavCoverageState state, int interestCount) - { - Id = id; - Bounds = bounds; - State = state; - InterestCount = interestCount; - } - - public int Id { get; } - public Bounds Bounds { get; } - public NavCoverageState State { get; } - public int InterestCount { get; } - } - - public enum NavCoverageState - { - Pending = 0, - Building = 1, - Ready = 2 - } - - public readonly struct ChunkNavGeometryReadyMessage - { - public ChunkNavGeometryReadyMessage(Vector2Int coord, int version) - { - Coord = coord; - Version = version; - } - - public Vector2Int Coord { get; } - public int Version { get; } - } - - public readonly struct ChunkNavGeometryRemovedMessage - { - public ChunkNavGeometryRemovedMessage(Vector2Int coord, int version) - { - Coord = coord; - Version = version; - } - - public Vector2Int Coord { get; } - public int Version { get; } - } - - public readonly struct WorldInterestChangedMessage - { - public WorldInterestChangedMessage(int version) - { - Version = version; - } - - public int Version { get; } - } - - public readonly struct NavCoverageHintChangedMessage - { - public NavCoverageHintChangedMessage(int version) - { - Version = version; - } - - public int Version { get; } - } } diff --git a/Assets/Features/VoxelWorld/Contracts/NavMeshWorldEnums.cs b/Assets/Features/VoxelWorld/Contracts/NavMeshWorldEnums.cs new file mode 100644 index 00000000..0f6a3281 --- /dev/null +++ b/Assets/Features/VoxelWorld/Contracts/NavMeshWorldEnums.cs @@ -0,0 +1,18 @@ +namespace InfiniteWorld.VoxelWorld.Contracts +{ + public enum WorldInterestKind + { + PlayerActor = 0, + ActiveNpc = 1, + SpawnAnchor = 2, + TransientNavHint = 3, + Other = 4 + } + + public enum NavCoverageState + { + Pending = 0, + Building = 1, + Ready = 2 + } +} diff --git a/Assets/Features/VoxelWorld/Contracts/NavMeshWorldEnums.cs.meta b/Assets/Features/VoxelWorld/Contracts/NavMeshWorldEnums.cs.meta new file mode 100644 index 00000000..4b921164 --- /dev/null +++ b/Assets/Features/VoxelWorld/Contracts/NavMeshWorldEnums.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4112a97dd67e45aca6f2c0928de438bd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Features/VoxelWorld/Contracts/NavMeshWorldMessages.cs b/Assets/Features/VoxelWorld/Contracts/NavMeshWorldMessages.cs new file mode 100644 index 00000000..8c52cd1d --- /dev/null +++ b/Assets/Features/VoxelWorld/Contracts/NavMeshWorldMessages.cs @@ -0,0 +1,48 @@ +using UnityEngine; + +namespace InfiniteWorld.VoxelWorld.Contracts +{ + public readonly struct ChunkNavGeometryReadyMessage + { + public ChunkNavGeometryReadyMessage(Vector2Int coord, int version) + { + Coord = coord; + Version = version; + } + + public Vector2Int Coord { get; } + public int Version { get; } + } + + public readonly struct ChunkNavGeometryRemovedMessage + { + public ChunkNavGeometryRemovedMessage(Vector2Int coord, int version) + { + Coord = coord; + Version = version; + } + + public Vector2Int Coord { get; } + public int Version { get; } + } + + public readonly struct WorldInterestChangedMessage + { + public WorldInterestChangedMessage(int version) + { + Version = version; + } + + public int Version { get; } + } + + public readonly struct NavCoverageHintChangedMessage + { + public NavCoverageHintChangedMessage(int version) + { + Version = version; + } + + public int Version { get; } + } +} diff --git a/Assets/Features/VoxelWorld/Contracts/NavMeshWorldMessages.cs.meta b/Assets/Features/VoxelWorld/Contracts/NavMeshWorldMessages.cs.meta new file mode 100644 index 00000000..f03292a0 --- /dev/null +++ b/Assets/Features/VoxelWorld/Contracts/NavMeshWorldMessages.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e2ea3cb8fdd545019f666d378bc8eaaa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Features/VoxelWorld/Contracts/NavMeshWorldSnapshots.cs b/Assets/Features/VoxelWorld/Contracts/NavMeshWorldSnapshots.cs new file mode 100644 index 00000000..39a564f5 --- /dev/null +++ b/Assets/Features/VoxelWorld/Contracts/NavMeshWorldSnapshots.cs @@ -0,0 +1,77 @@ +using UnityEngine; +using UnityEngine.AI; + +namespace InfiniteWorld.VoxelWorld.Contracts +{ + public readonly struct ChunkNavSourceSnapshot + { + public ChunkNavSourceSnapshot(Vector2Int coord, int version, ChunkNavBuildSourceDescriptor[] sources) + { + Coord = coord; + Version = version; + Sources = sources; + } + + public Vector2Int Coord { get; } + public int Version { get; } + public ChunkNavBuildSourceDescriptor[] Sources { get; } + } + + public readonly struct ChunkNavBuildSourceDescriptor + { + public ChunkNavBuildSourceDescriptor(NavMeshBuildSourceShape shape, Matrix4x4 transform, Vector3 size, Mesh mesh, int area) + { + Shape = shape; + Transform = transform; + Size = size; + Mesh = mesh; + Area = area; + } + + public NavMeshBuildSourceShape Shape { get; } + public Matrix4x4 Transform { get; } + public Vector3 Size { get; } + public Mesh Mesh { get; } + public int Area { get; } + + public static ChunkNavBuildSourceDescriptor CreateBox(Matrix4x4 transform, Vector3 size, int area = 0) + { + return new ChunkNavBuildSourceDescriptor(NavMeshBuildSourceShape.Box, transform, size, null, area); + } + + public static ChunkNavBuildSourceDescriptor CreateMesh(Matrix4x4 transform, Mesh mesh, int area = 0) + { + return new ChunkNavBuildSourceDescriptor(NavMeshBuildSourceShape.Mesh, transform, Vector3.zero, mesh, area); + } + } + + public readonly struct WorldInterestPoint + { + public WorldInterestPoint(Vector3 position, float priority, WorldInterestKind kind) + { + Position = position; + Priority = priority; + Kind = kind; + } + + public Vector3 Position { get; } + public float Priority { get; } + public WorldInterestKind Kind { get; } + } + + public readonly struct NavCoverageWindowSnapshot + { + public NavCoverageWindowSnapshot(int id, Bounds bounds, NavCoverageState state, int interestCount) + { + Id = id; + Bounds = bounds; + State = state; + InterestCount = interestCount; + } + + public int Id { get; } + public Bounds Bounds { get; } + public NavCoverageState State { get; } + public int InterestCount { get; } + } +} diff --git a/Assets/Features/VoxelWorld/Contracts/NavMeshWorldSnapshots.cs.meta b/Assets/Features/VoxelWorld/Contracts/NavMeshWorldSnapshots.cs.meta new file mode 100644 index 00000000..7bd0913b --- /dev/null +++ b/Assets/Features/VoxelWorld/Contracts/NavMeshWorldSnapshots.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 91e1b6896fdd4f7a9968cc4af4bf7550 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Features/VoxelWorldNavMesh/Runtime/NavBuildSourceCollector.cs b/Assets/Features/VoxelWorldNavMesh/Runtime/NavBuildSourceCollector.cs new file mode 100644 index 00000000..15e419c5 --- /dev/null +++ b/Assets/Features/VoxelWorldNavMesh/Runtime/NavBuildSourceCollector.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using InfiniteWorld.VoxelWorld.Contracts; +using UnityEngine; +using UnityEngine.AI; + +namespace InfiniteWorld.VoxelWorld.NavMesh +{ + internal static class NavBuildSourceCollector + { + public static bool CollectBuildSources( + IChunkNavSourceReader chunkNavSourceReader, + Bounds coverageBounds, + List loadedChunkCoords, + List results) + { + loadedChunkCoords.Clear(); + chunkNavSourceReader.GetLoadedChunkCoords(loadedChunkCoords); + + bool hasSources = false; + for (int i = 0; i < loadedChunkCoords.Count; i++) + { + Vector2Int chunkCoord = loadedChunkCoords[i]; + if (!NavMeshBoundsUtility.IntersectsXZ(GetChunkWorldBounds(chunkNavSourceReader, 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; + } + + public static Bounds GetChunkWorldBounds(IChunkNavSourceReader chunkNavSourceReader, 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); + } + + public static Bounds ExpandCoverageBounds(IChunkNavSourceReader chunkNavSourceReader, Bounds bounds, int chunkMargin) + { + return ExpandChunkBounds(chunkNavSourceReader, bounds, chunkMargin); + } + + public static Bounds ExpandChunkBounds(IChunkNavSourceReader chunkNavSourceReader, 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 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); + } + } + } +} diff --git a/Assets/Features/VoxelWorldNavMesh/Runtime/NavBuildSourceCollector.cs.meta b/Assets/Features/VoxelWorldNavMesh/Runtime/NavBuildSourceCollector.cs.meta new file mode 100644 index 00000000..5b6ae614 --- /dev/null +++ b/Assets/Features/VoxelWorldNavMesh/Runtime/NavBuildSourceCollector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9f2d97479ccb4401bc37fd6481d83304 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Features/VoxelWorldNavMesh/Runtime/NavCoverageHintService.cs b/Assets/Features/VoxelWorldNavMesh/Runtime/NavCoverageHintService.cs new file mode 100644 index 00000000..0662ede7 --- /dev/null +++ b/Assets/Features/VoxelWorldNavMesh/Runtime/NavCoverageHintService.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using InfiniteWorld.VoxelWorld.Contracts; +using MessagePipe; +using UnityEngine; +using VContainer.Unity; + +namespace InfiniteWorld.VoxelWorld.NavMesh +{ + public sealed class NavCoverageHintService : ITickable, INavCoverageHintRegistry, INavCoverageHintReader + { + private readonly IChunkNavSourceReader chunkNavSourceReader; + private readonly VoxelWorldNavMeshConfig config; + private readonly IPublisher hintChangedPublisher; + private readonly Dictionary hints = new Dictionary(); + private readonly List expiredHintOwnerIds = new List(8); + + private int hintVersion; + + public NavCoverageHintService( + IChunkNavSourceReader chunkNavSourceReader, + VoxelWorldNavMeshConfig config, + IPublisher hintChangedPublisher) + { + this.chunkNavSourceReader = chunkNavSourceReader; + this.config = config ?? new VoxelWorldNavMeshConfig(); + this.hintChangedPublisher = hintChangedPublisher; + } + + public int HintVersion => hintVersion; + + public void Tick() + { + if (hints.Count == 0) + { + return; + } + + float now = Time.time; + expiredHintOwnerIds.Clear(); + foreach (KeyValuePair pair in hints) + { + if (pair.Value.ExpireAt > now) + { + continue; + } + + expiredHintOwnerIds.Add(pair.Key); + } + + if (expiredHintOwnerIds.Count == 0) + { + return; + } + + for (int i = 0; i < expiredHintOwnerIds.Count; i++) + { + hints.Remove(expiredHintOwnerIds[i]); + } + + NotifyHintsChanged(); + } + + public void SetLinearHint(int ownerId, Vector3 from, Vector3 to, float priority, float ttlSeconds) + { + if (ownerId == 0) + { + ownerId = 1; + } + + float expireAt = Time.time + Mathf.Max(0.1f, ttlSeconds); + WorldInterestPoint[] points = BuildLinearHintPoints(from, to, Mathf.Max(0.01f, priority)); + hints[ownerId] = new HintEntry(points, expireAt); + NotifyHintsChanged(); + } + + public void ClearHint(int ownerId) + { + if (!hints.Remove(ownerId)) + { + return; + } + + NotifyHintsChanged(); + } + + public void GetHintPoints(List results) + { + if (results == null) + { + throw new ArgumentNullException(nameof(results)); + } + + foreach (KeyValuePair pair in hints) + { + WorldInterestPoint[] points = pair.Value.Points; + for (int i = 0; i < points.Length; i++) + { + results.Add(points[i]); + } + } + } + + private WorldInterestPoint[] BuildLinearHintPoints(Vector3 from, Vector3 to, float priority) + { + float chunkWorldSize = Mathf.Max(1f, chunkNavSourceReader.ChunkWorldSize); + float spacing = Mathf.Max(chunkWorldSize, config.clusterMergeDistanceInChunks * chunkWorldSize * 0.75f); + float distance = Vector3.Distance(from, to); + int segmentCount = Mathf.Max(1, Mathf.CeilToInt(distance / Mathf.Max(0.01f, spacing))); + int pointCount = segmentCount + 1; + WorldInterestPoint[] points = new WorldInterestPoint[pointCount]; + + for (int i = 0; i < pointCount; i++) + { + float t = pointCount == 1 ? 1f : i / (float)(pointCount - 1); + Vector3 position = Vector3.Lerp(from, to, t); + points[i] = new WorldInterestPoint(position, priority, WorldInterestKind.TransientNavHint); + } + + return points; + } + + private void NotifyHintsChanged() + { + hintVersion++; + hintChangedPublisher?.Publish(new NavCoverageHintChangedMessage(hintVersion)); + } + + private readonly struct HintEntry + { + public HintEntry(WorldInterestPoint[] points, float expireAt) + { + Points = points; + ExpireAt = expireAt; + } + + public WorldInterestPoint[] Points { get; } + public float ExpireAt { get; } + } + } +} diff --git a/Assets/Features/VoxelWorldNavMesh/Runtime/NavCoverageHintService.cs.meta b/Assets/Features/VoxelWorldNavMesh/Runtime/NavCoverageHintService.cs.meta new file mode 100644 index 00000000..763071dc --- /dev/null +++ b/Assets/Features/VoxelWorldNavMesh/Runtime/NavCoverageHintService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4c6dcd38712d499fb48ec43c0ec77031 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Features/VoxelWorldNavMesh/Runtime/NavCoveragePlanning.cs b/Assets/Features/VoxelWorldNavMesh/Runtime/NavCoveragePlanning.cs new file mode 100644 index 00000000..71a0cf4f --- /dev/null +++ b/Assets/Features/VoxelWorldNavMesh/Runtime/NavCoveragePlanning.cs @@ -0,0 +1,227 @@ +using System.Collections.Generic; +using InfiniteWorld.VoxelWorld.Contracts; +using UnityEngine; + +namespace InfiniteWorld.VoxelWorld.NavMesh +{ + internal static class NavCoveragePlanning + { + public static void BuildDesiredCoverageWindows( + List interestPoints, + VoxelWorldNavMeshConfig config, + float chunkWorldSize, + List desiredCoverageWindows, + List clusterAccumulators) + { + desiredCoverageWindows.Clear(); + clusterAccumulators.Clear(); + + if (interestPoints.Count == 0) + { + return; + } + + 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 = NavMeshBoundsUtility.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(clusterAccumulators, mergeDistance); + + for (int i = 0; i < clusterAccumulators.Count; i++) + { + ClusterAccumulator cluster = clusterAccumulators[i]; + Bounds coverageBounds = NavMeshBoundsUtility.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); + } + } + + public static NavCoverageWindowRuntime FindBestMatchingCoverageWindow( + DesiredCoverageWindow desiredWindow, + Dictionary coverageWindows, + float chunkWorldSize, + VoxelWorldNavMeshConfig config) + { + NavCoverageWindowRuntime bestMatch = null; + float bestDistance = float.MaxValue; + 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; + } + + public static float GetCoveragePriorityScore(NavCoverageWindowRuntime window, List interestPoints) + { + 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 static void MergeNearbyClusters(List clusterAccumulators, 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 (NavMeshBoundsUtility.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); + } + } + + internal 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; } + } + + internal 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; + } + } +} diff --git a/Assets/Features/VoxelWorldNavMesh/Runtime/NavCoveragePlanning.cs.meta b/Assets/Features/VoxelWorldNavMesh/Runtime/NavCoveragePlanning.cs.meta new file mode 100644 index 00000000..6302b006 --- /dev/null +++ b/Assets/Features/VoxelWorldNavMesh/Runtime/NavCoveragePlanning.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 74d2bb8418be4671a146c0949637163c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Features/VoxelWorldNavMesh/Runtime/NavCoverageWindowRuntime.cs b/Assets/Features/VoxelWorldNavMesh/Runtime/NavCoverageWindowRuntime.cs new file mode 100644 index 00000000..17936cdc --- /dev/null +++ b/Assets/Features/VoxelWorldNavMesh/Runtime/NavCoverageWindowRuntime.cs @@ -0,0 +1,57 @@ +using System; +using InfiniteWorld.VoxelWorld.Contracts; +using UnityEngine; +using UnityEngine.AI; +using UnityNavMesh = UnityEngine.AI.NavMesh; +using UnityNavMeshBuilder = UnityEngine.AI.NavMeshBuilder; + +namespace InfiniteWorld.VoxelWorld.NavMesh +{ + internal 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 ResetCoverageData() + { + if (ActiveBuild != null && !ActiveBuild.isDone && NavMeshData != null) + { + UnityNavMeshBuilder.Cancel(NavMeshData); + } + + if (Instance.valid) + { + UnityNavMesh.RemoveNavMeshData(Instance); + Instance = default; + } + + ActiveBuild = null; + NavMeshData = null; + } + + public void Dispose() + { + ResetCoverageData(); + State = NavCoverageState.Pending; + } + } +} diff --git a/Assets/Features/VoxelWorldNavMesh/Runtime/NavCoverageWindowRuntime.cs.meta b/Assets/Features/VoxelWorldNavMesh/Runtime/NavCoverageWindowRuntime.cs.meta new file mode 100644 index 00000000..7369a69d --- /dev/null +++ b/Assets/Features/VoxelWorldNavMesh/Runtime/NavCoverageWindowRuntime.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: caa4b87bcf874133b155e44475c58ca3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Features/VoxelWorldNavMesh/Runtime/NavMeshBoundsUtility.cs b/Assets/Features/VoxelWorldNavMesh/Runtime/NavMeshBoundsUtility.cs new file mode 100644 index 00000000..e8723cc1 --- /dev/null +++ b/Assets/Features/VoxelWorldNavMesh/Runtime/NavMeshBoundsUtility.cs @@ -0,0 +1,139 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.AI; + +namespace InfiniteWorld.VoxelWorld.NavMesh +{ + internal static class NavMeshBoundsUtility + { + public 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; + } + + public 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; + } + + public 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); + } + + public 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); + } + + public 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); + } + + public 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; + } + + public 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; + } + + public 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 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 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; + } + } +} diff --git a/Assets/Features/VoxelWorldNavMesh/Runtime/NavMeshBoundsUtility.cs.meta b/Assets/Features/VoxelWorldNavMesh/Runtime/NavMeshBoundsUtility.cs.meta new file mode 100644 index 00000000..acbfc077 --- /dev/null +++ b/Assets/Features/VoxelWorldNavMesh/Runtime/NavMeshBoundsUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b94f04d9597e4174b88035a1751b84fe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Features/VoxelWorldNavMesh/Runtime/VoxelWorldNavMeshService.cs b/Assets/Features/VoxelWorldNavMesh/Runtime/VoxelWorldNavMeshService.cs index 29f5942f..e63975df 100644 --- a/Assets/Features/VoxelWorldNavMesh/Runtime/VoxelWorldNavMeshService.cs +++ b/Assets/Features/VoxelWorldNavMesh/Runtime/VoxelWorldNavMeshService.cs @@ -27,7 +27,6 @@ namespace InfiniteWorld.VoxelWorld.NavMesh 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(4); @@ -98,7 +97,7 @@ namespace InfiniteWorld.VoxelWorld.NavMesh foreach (KeyValuePair pair in coverageWindows) { NavCoverageWindowRuntime window = pair.Value; - if (window.State == NavCoverageState.Ready && ContainsXZ(window.CoverageBounds, worldPosition)) + if (window.State == NavCoverageState.Ready && NavMeshBoundsUtility.ContainsXZ(window.CoverageBounds, worldPosition)) { return true; } @@ -140,7 +139,6 @@ namespace InfiniteWorld.VoxelWorld.NavMesh dirtyCoverageWindowIds.Clear(); desiredCoverageWindows.Clear(); clusterAccumulators.Clear(); - coverageWindowSnapshots.Clear(); activeBuildWindowId = null; } @@ -177,7 +175,12 @@ namespace InfiniteWorld.VoxelWorld.NavMesh private void SyncCoverageWindows() { - BuildDesiredCoverageWindows(); + NavCoveragePlanning.BuildDesiredCoverageWindows( + interestPoints, + config, + Mathf.Max(1f, chunkNavSourceReader.ChunkWorldSize), + desiredCoverageWindows, + clusterAccumulators); foreach (KeyValuePair pair in coverageWindows) { @@ -187,7 +190,11 @@ namespace InfiniteWorld.VoxelWorld.NavMesh for (int i = 0; i < desiredCoverageWindows.Count; i++) { DesiredCoverageWindow desiredWindow = desiredCoverageWindows[i]; - NavCoverageWindowRuntime runtime = FindBestMatchingCoverageWindow(desiredWindow); + NavCoverageWindowRuntime runtime = NavCoveragePlanning.FindBestMatchingCoverageWindow( + desiredWindow, + coverageWindows, + Mathf.Max(1f, chunkNavSourceReader.ChunkWorldSize), + config); if (runtime == null) { runtime = new NavCoverageWindowRuntime(nextCoverageWindowId++, desiredWindow.CoverageBounds, desiredWindow.Priority, desiredWindow.InterestCount); @@ -201,7 +208,7 @@ namespace InfiniteWorld.VoxelWorld.NavMesh runtime.Priority = desiredWindow.Priority; runtime.InterestCount = desiredWindow.InterestCount; - if (!BoundsApproximatelyEqual(runtime.CoverageBounds, desiredWindow.CoverageBounds)) + if (!NavMeshBoundsUtility.BoundsApproximatelyEqual(runtime.CoverageBounds, desiredWindow.CoverageBounds)) { runtime.CoverageBounds = desiredWindow.CoverageBounds; runtime.State = NavCoverageState.Pending; @@ -209,163 +216,9 @@ namespace InfiniteWorld.VoxelWorld.NavMesh } } - 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; @@ -402,11 +255,19 @@ namespace InfiniteWorld.VoxelWorld.NavMesh private void MarkCoverageWindowsDirtyForChunk(Vector2Int chunkCoord) { - Bounds chunkBounds = ExpandChunkBounds(GetChunkWorldBounds(chunkCoord), Mathf.Max(0, config.chunkCollectionMarginInChunks)); + Bounds chunkBounds = NavBuildSourceCollector.ExpandChunkBounds( + chunkNavSourceReader, + NavBuildSourceCollector.GetChunkWorldBounds(chunkNavSourceReader, 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)) + Bounds invalidationBounds = NavBuildSourceCollector.ExpandCoverageBounds( + chunkNavSourceReader, + pair.Value.CoverageBounds, + Mathf.Max(0, config.chunkCollectionMarginInChunks)); + + if (NavMeshBoundsUtility.IntersectsXZ(invalidationBounds, chunkBounds)) { EnqueueDirtyCoverageWindow(pair.Key); } @@ -446,7 +307,7 @@ namespace InfiniteWorld.VoxelWorld.NavMesh continue; } - float score = GetCoveragePriorityScore(window); + float score = NavCoveragePlanning.GetCoveragePriorityScore(window, interestPoints); if (score < bestScore) { bestScore = score; @@ -471,29 +332,6 @@ namespace InfiniteWorld.VoxelWorld.NavMesh 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)) @@ -502,18 +340,26 @@ namespace InfiniteWorld.VoxelWorld.NavMesh } buildSources.Clear(); - window.CollectionBounds = ExpandCoverageBounds(window.CoverageBounds, Mathf.Max(0, config.chunkCollectionMarginInChunks)); + window.CollectionBounds = NavBuildSourceCollector.ExpandCoverageBounds( + chunkNavSourceReader, + window.CoverageBounds, + Mathf.Max(0, config.chunkCollectionMarginInChunks)); + + bool hasSources = NavBuildSourceCollector.CollectBuildSources( + chunkNavSourceReader, + window.CollectionBounds, + loadedChunkCoords, + buildSources); - bool hasSources = CollectBuildSources(window.CollectionBounds, buildSources); if (!hasSources || buildSources.Count == 0) { window.State = NavCoverageState.Pending; - RemoveCoverageData(window); + window.ResetCoverageData(); return false; } - Bounds buildBounds = CalculateBounds(buildSources); - ExpandBounds(ref buildBounds, config.navBoundsHorizontalPadding, config.navBoundsVerticalPadding); + Bounds buildBounds = NavMeshBoundsUtility.CalculateBounds(buildSources); + NavMeshBoundsUtility.ExpandBounds(ref buildBounds, config.navBoundsHorizontalPadding, config.navBoundsVerticalPadding); window.BuildBounds = buildBounds; window.BuildRequestedWhileRunning = false; @@ -534,55 +380,6 @@ namespace InfiniteWorld.VoxelWorld.NavMesh 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) @@ -629,379 +426,5 @@ namespace InfiniteWorld.VoxelWorld.NavMesh 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; - } - } - } - - public sealed class NavCoverageHintService : ITickable, INavCoverageHintRegistry, INavCoverageHintReader - { - private readonly IChunkNavSourceReader chunkNavSourceReader; - private readonly VoxelWorldNavMeshConfig config; - private readonly IPublisher hintChangedPublisher; - private readonly Dictionary hints = new Dictionary(); - private readonly List expiredHintOwnerIds = new List(8); - - private int hintVersion; - - public NavCoverageHintService( - IChunkNavSourceReader chunkNavSourceReader, - VoxelWorldNavMeshConfig config, - IPublisher hintChangedPublisher) - { - this.chunkNavSourceReader = chunkNavSourceReader; - this.config = config ?? new VoxelWorldNavMeshConfig(); - this.hintChangedPublisher = hintChangedPublisher; - } - - public int HintVersion => hintVersion; - - public void Tick() - { - if (hints.Count == 0) - { - return; - } - - float now = Time.time; - expiredHintOwnerIds.Clear(); - foreach (KeyValuePair pair in hints) - { - if (pair.Value.ExpireAt > now) - { - continue; - } - - expiredHintOwnerIds.Add(pair.Key); - } - - if (expiredHintOwnerIds.Count == 0) - { - return; - } - - for (int i = 0; i < expiredHintOwnerIds.Count; i++) - { - hints.Remove(expiredHintOwnerIds[i]); - } - - NotifyHintsChanged(); - } - - public void SetLinearHint(int ownerId, Vector3 from, Vector3 to, float priority, float ttlSeconds) - { - if (ownerId == 0) - { - ownerId = 1; - } - - float expireAt = Time.time + Mathf.Max(0.1f, ttlSeconds); - WorldInterestPoint[] points = BuildLinearHintPoints(from, to, Mathf.Max(0.01f, priority)); - hints[ownerId] = new HintEntry(points, expireAt); - NotifyHintsChanged(); - } - - public void ClearHint(int ownerId) - { - if (!hints.Remove(ownerId)) - { - return; - } - - NotifyHintsChanged(); - } - - public void GetHintPoints(List results) - { - if (results == null) - { - throw new ArgumentNullException(nameof(results)); - } - - foreach (KeyValuePair pair in hints) - { - WorldInterestPoint[] points = pair.Value.Points; - for (int i = 0; i < points.Length; i++) - { - results.Add(points[i]); - } - } - } - - private WorldInterestPoint[] BuildLinearHintPoints(Vector3 from, Vector3 to, float priority) - { - float chunkWorldSize = Mathf.Max(1f, chunkNavSourceReader.ChunkWorldSize); - float spacing = Mathf.Max(chunkWorldSize, config.clusterMergeDistanceInChunks * chunkWorldSize * 0.75f); - float distance = Vector3.Distance(from, to); - int segmentCount = Mathf.Max(1, Mathf.CeilToInt(distance / Mathf.Max(0.01f, spacing))); - int pointCount = segmentCount + 1; - WorldInterestPoint[] points = new WorldInterestPoint[pointCount]; - - for (int i = 0; i < pointCount; i++) - { - float t = pointCount == 1 ? 1f : i / (float)(pointCount - 1); - Vector3 position = Vector3.Lerp(from, to, t); - points[i] = new WorldInterestPoint(position, priority, WorldInterestKind.TransientNavHint); - } - - return points; - } - - private void NotifyHintsChanged() - { - hintVersion++; - hintChangedPublisher?.Publish(new NavCoverageHintChangedMessage(hintVersion)); - } - - private readonly struct HintEntry - { - public HintEntry(WorldInterestPoint[] points, float expireAt) - { - Points = points; - ExpireAt = expireAt; - } - - public WorldInterestPoint[] Points { get; } - public float ExpireAt { get; } - } } }