Files
TheDeclineOfWarriors/Assets/Features/VoxelWorldNavMesh/Runtime/VoxelWorldNavMeshService.cs
T
Alexander Borisov 289d5f783b add transient nav coverage hints
Introduce nav-only corridor hints and feed them into clustered coverage scheduling so runtime NavMesh can prewarm ahead of active movement paths.
2026-04-08 15:37:37 +03:00

1008 lines
38 KiB
C#

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 INavCoverageHintReader navCoverageHintReader;
private readonly ISubscriber<ChunkNavGeometryReadyMessage> chunkReadySubscriber;
private readonly ISubscriber<ChunkNavGeometryRemovedMessage> chunkRemovedSubscriber;
private readonly ISubscriber<WorldInterestChangedMessage> worldInterestChangedSubscriber;
private readonly ISubscriber<NavCoverageHintChangedMessage> navCoverageHintChangedSubscriber;
private readonly VoxelWorldNavMeshConfig config;
private readonly Dictionary<int, NavCoverageWindowRuntime> coverageWindows = new Dictionary<int, NavCoverageWindowRuntime>();
private readonly Queue<int> dirtyCoverageWindowIds = new Queue<int>();
private readonly HashSet<int> queuedCoverageWindowIds = new HashSet<int>();
private readonly List<int> dirtyCoverageWindowCandidates = new List<int>(16);
private readonly List<WorldInterestPoint> interestPoints = new List<WorldInterestPoint>(8);
private readonly List<Vector2Int> loadedChunkCoords = new List<Vector2Int>(128);
private readonly List<NavMeshBuildSource> buildSources = new List<NavMeshBuildSource>(256);
private readonly List<NavCoverageWindowSnapshot> coverageWindowSnapshots = new List<NavCoverageWindowSnapshot>(8);
private readonly List<DesiredCoverageWindow> desiredCoverageWindows = new List<DesiredCoverageWindow>(8);
private readonly List<ClusterAccumulator> clusterAccumulators = new List<ClusterAccumulator>(8);
private readonly List<IDisposable> subscriptions = new List<IDisposable>(4);
private int nextCoverageWindowId = 1;
private int? activeBuildWindowId;
public VoxelWorldNavMeshService(
IChunkNavSourceReader chunkNavSourceReader,
IWorldInterestReader worldInterestReader,
INavCoverageHintReader navCoverageHintReader,
ISubscriber<ChunkNavGeometryReadyMessage> chunkReadySubscriber,
ISubscriber<ChunkNavGeometryRemovedMessage> chunkRemovedSubscriber,
ISubscriber<WorldInterestChangedMessage> worldInterestChangedSubscriber,
ISubscriber<NavCoverageHintChangedMessage> navCoverageHintChangedSubscriber,
VoxelWorldNavMeshConfig config)
{
this.chunkNavSourceReader = chunkNavSourceReader;
this.worldInterestReader = worldInterestReader;
this.navCoverageHintReader = navCoverageHintReader;
this.chunkReadySubscriber = chunkReadySubscriber;
this.chunkRemovedSubscriber = chunkRemovedSubscriber;
this.worldInterestChangedSubscriber = worldInterestChangedSubscriber;
this.navCoverageHintChangedSubscriber = navCoverageHintChangedSubscriber;
this.config = config ?? new VoxelWorldNavMeshConfig();
}
public void Start()
{
subscriptions.Add(chunkReadySubscriber.Subscribe(OnChunkNavGeometryReady));
subscriptions.Add(chunkRemovedSubscriber.Subscribe(OnChunkNavGeometryRemoved));
subscriptions.Add(worldInterestChangedSubscriber.Subscribe(OnWorldInterestChanged));
subscriptions.Add(navCoverageHintChangedSubscriber.Subscribe(OnNavCoverageHintChanged));
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<int, NavCoverageWindowRuntime> pair in coverageWindows)
{
NavCoverageWindowRuntime window = pair.Value;
if (window.State == NavCoverageState.Ready && ContainsXZ(window.CoverageBounds, worldPosition))
{
return true;
}
}
return false;
}
public void GetCoverageWindows(List<NavCoverageWindowSnapshot> results)
{
if (results == null)
{
throw new ArgumentNullException(nameof(results));
}
foreach (KeyValuePair<int, NavCoverageWindowRuntime> 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<int, NavCoverageWindowRuntime> 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 OnNavCoverageHintChanged(NavCoverageHintChangedMessage message)
{
RefreshInterestPoints();
SyncCoverageWindows();
MarkAllCoverageWindowsDirty();
}
private void RefreshInterestPoints()
{
interestPoints.Clear();
worldInterestReader.GetInterestPoints(interestPoints);
navCoverageHintReader.GetHintPoints(interestPoints);
}
private void SyncCoverageWindows()
{
BuildDesiredCoverageWindows();
foreach (KeyValuePair<int, NavCoverageWindowRuntime> 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<int, NavCoverageWindowRuntime> 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<int, NavCoverageWindowRuntime> 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<int> windowsToRemove = null;
foreach (KeyValuePair<int, NavCoverageWindowRuntime> pair in coverageWindows)
{
if (pair.Value.MatchedThisFrame)
{
continue;
}
windowsToRemove ??= new List<int>();
windowsToRemove.Add(pair.Key);
}
if (windowsToRemove == null)
{
return;
}
for (int i = 0; i < windowsToRemove.Count; i++)
{
RemoveCoverageWindow(windowsToRemove[i]);
}
}
private void MarkAllCoverageWindowsDirty()
{
foreach (KeyValuePair<int, NavCoverageWindowRuntime> pair in coverageWindows)
{
EnqueueDirtyCoverageWindow(pair.Key);
}
}
private void MarkCoverageWindowsDirtyForChunk(Vector2Int chunkCoord)
{
Bounds chunkBounds = ExpandChunkBounds(GetChunkWorldBounds(chunkCoord), Mathf.Max(0, config.chunkCollectionMarginInChunks));
foreach (KeyValuePair<int, NavCoverageWindowRuntime> 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<NavMeshBuildSource> 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<NavMeshBuildSource> 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<NavMeshBuildSource> 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<NavCoverageHintChangedMessage> hintChangedPublisher;
private readonly Dictionary<int, HintEntry> hints = new Dictionary<int, HintEntry>();
private readonly List<int> expiredHintOwnerIds = new List<int>(8);
private int hintVersion;
public NavCoverageHintService(
IChunkNavSourceReader chunkNavSourceReader,
VoxelWorldNavMeshConfig config,
IPublisher<NavCoverageHintChangedMessage> 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<int, HintEntry> 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<WorldInterestPoint> results)
{
if (results == null)
{
throw new ArgumentNullException(nameof(results));
}
foreach (KeyValuePair<int, HintEntry> 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; }
}
}
}