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.
This commit is contained in:
Alexander Borisov
2026-04-08 15:37:37 +03:00
parent 0b380def78
commit 289d5f783b
3 changed files with 173 additions and 2 deletions
@@ -23,6 +23,18 @@ namespace InfiniteWorld.VoxelWorld.Contracts
void GetCoverageWindows(List<NavCoverageWindowSnapshot> results); void GetCoverageWindows(List<NavCoverageWindowSnapshot> results);
} }
public interface INavCoverageHintRegistry
{
void SetLinearHint(int ownerId, Vector3 from, Vector3 to, float priority, float ttlSeconds);
void ClearHint(int ownerId);
}
public interface INavCoverageHintReader
{
int HintVersion { get; }
void GetHintPoints(List<WorldInterestPoint> results);
}
public readonly struct ChunkNavSourceSnapshot public readonly struct ChunkNavSourceSnapshot
{ {
public ChunkNavSourceSnapshot(Vector2Int coord, int version, ChunkNavBuildSourceDescriptor[] sources) public ChunkNavSourceSnapshot(Vector2Int coord, int version, ChunkNavBuildSourceDescriptor[] sources)
@@ -84,7 +96,8 @@ namespace InfiniteWorld.VoxelWorld.Contracts
PlayerActor = 0, PlayerActor = 0,
ActiveNpc = 1, ActiveNpc = 1,
SpawnAnchor = 2, SpawnAnchor = 2,
Other = 3 TransientNavHint = 3,
Other = 4
} }
public readonly struct NavCoverageWindowSnapshot public readonly struct NavCoverageWindowSnapshot
@@ -143,4 +156,14 @@ namespace InfiniteWorld.VoxelWorld.Contracts
public int Version { get; } public int Version { get; }
} }
public readonly struct NavCoverageHintChangedMessage
{
public NavCoverageHintChangedMessage(int version)
{
Version = version;
}
public int Version { get; }
}
} }
@@ -14,9 +14,11 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
{ {
private readonly IChunkNavSourceReader chunkNavSourceReader; private readonly IChunkNavSourceReader chunkNavSourceReader;
private readonly IWorldInterestReader worldInterestReader; private readonly IWorldInterestReader worldInterestReader;
private readonly INavCoverageHintReader navCoverageHintReader;
private readonly ISubscriber<ChunkNavGeometryReadyMessage> chunkReadySubscriber; private readonly ISubscriber<ChunkNavGeometryReadyMessage> chunkReadySubscriber;
private readonly ISubscriber<ChunkNavGeometryRemovedMessage> chunkRemovedSubscriber; private readonly ISubscriber<ChunkNavGeometryRemovedMessage> chunkRemovedSubscriber;
private readonly ISubscriber<WorldInterestChangedMessage> worldInterestChangedSubscriber; private readonly ISubscriber<WorldInterestChangedMessage> worldInterestChangedSubscriber;
private readonly ISubscriber<NavCoverageHintChangedMessage> navCoverageHintChangedSubscriber;
private readonly VoxelWorldNavMeshConfig config; private readonly VoxelWorldNavMeshConfig config;
private readonly Dictionary<int, NavCoverageWindowRuntime> coverageWindows = new Dictionary<int, NavCoverageWindowRuntime>(); private readonly Dictionary<int, NavCoverageWindowRuntime> coverageWindows = new Dictionary<int, NavCoverageWindowRuntime>();
private readonly Queue<int> dirtyCoverageWindowIds = new Queue<int>(); private readonly Queue<int> dirtyCoverageWindowIds = new Queue<int>();
@@ -28,7 +30,7 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
private readonly List<NavCoverageWindowSnapshot> coverageWindowSnapshots = new List<NavCoverageWindowSnapshot>(8); private readonly List<NavCoverageWindowSnapshot> coverageWindowSnapshots = new List<NavCoverageWindowSnapshot>(8);
private readonly List<DesiredCoverageWindow> desiredCoverageWindows = new List<DesiredCoverageWindow>(8); private readonly List<DesiredCoverageWindow> desiredCoverageWindows = new List<DesiredCoverageWindow>(8);
private readonly List<ClusterAccumulator> clusterAccumulators = new List<ClusterAccumulator>(8); private readonly List<ClusterAccumulator> clusterAccumulators = new List<ClusterAccumulator>(8);
private readonly List<IDisposable> subscriptions = new List<IDisposable>(3); private readonly List<IDisposable> subscriptions = new List<IDisposable>(4);
private int nextCoverageWindowId = 1; private int nextCoverageWindowId = 1;
private int? activeBuildWindowId; private int? activeBuildWindowId;
@@ -36,16 +38,20 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
public VoxelWorldNavMeshService( public VoxelWorldNavMeshService(
IChunkNavSourceReader chunkNavSourceReader, IChunkNavSourceReader chunkNavSourceReader,
IWorldInterestReader worldInterestReader, IWorldInterestReader worldInterestReader,
INavCoverageHintReader navCoverageHintReader,
ISubscriber<ChunkNavGeometryReadyMessage> chunkReadySubscriber, ISubscriber<ChunkNavGeometryReadyMessage> chunkReadySubscriber,
ISubscriber<ChunkNavGeometryRemovedMessage> chunkRemovedSubscriber, ISubscriber<ChunkNavGeometryRemovedMessage> chunkRemovedSubscriber,
ISubscriber<WorldInterestChangedMessage> worldInterestChangedSubscriber, ISubscriber<WorldInterestChangedMessage> worldInterestChangedSubscriber,
ISubscriber<NavCoverageHintChangedMessage> navCoverageHintChangedSubscriber,
VoxelWorldNavMeshConfig config) VoxelWorldNavMeshConfig config)
{ {
this.chunkNavSourceReader = chunkNavSourceReader; this.chunkNavSourceReader = chunkNavSourceReader;
this.worldInterestReader = worldInterestReader; this.worldInterestReader = worldInterestReader;
this.navCoverageHintReader = navCoverageHintReader;
this.chunkReadySubscriber = chunkReadySubscriber; this.chunkReadySubscriber = chunkReadySubscriber;
this.chunkRemovedSubscriber = chunkRemovedSubscriber; this.chunkRemovedSubscriber = chunkRemovedSubscriber;
this.worldInterestChangedSubscriber = worldInterestChangedSubscriber; this.worldInterestChangedSubscriber = worldInterestChangedSubscriber;
this.navCoverageHintChangedSubscriber = navCoverageHintChangedSubscriber;
this.config = config ?? new VoxelWorldNavMeshConfig(); this.config = config ?? new VoxelWorldNavMeshConfig();
} }
@@ -54,6 +60,7 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
subscriptions.Add(chunkReadySubscriber.Subscribe(OnChunkNavGeometryReady)); subscriptions.Add(chunkReadySubscriber.Subscribe(OnChunkNavGeometryReady));
subscriptions.Add(chunkRemovedSubscriber.Subscribe(OnChunkNavGeometryRemoved)); subscriptions.Add(chunkRemovedSubscriber.Subscribe(OnChunkNavGeometryRemoved));
subscriptions.Add(worldInterestChangedSubscriber.Subscribe(OnWorldInterestChanged)); subscriptions.Add(worldInterestChangedSubscriber.Subscribe(OnWorldInterestChanged));
subscriptions.Add(navCoverageHintChangedSubscriber.Subscribe(OnNavCoverageHintChanged));
RefreshInterestPoints(); RefreshInterestPoints();
SyncCoverageWindows(); SyncCoverageWindows();
@@ -154,10 +161,18 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
MarkAllCoverageWindowsDirty(); MarkAllCoverageWindowsDirty();
} }
private void OnNavCoverageHintChanged(NavCoverageHintChangedMessage message)
{
RefreshInterestPoints();
SyncCoverageWindows();
MarkAllCoverageWindowsDirty();
}
private void RefreshInterestPoints() private void RefreshInterestPoints()
{ {
interestPoints.Clear(); interestPoints.Clear();
worldInterestReader.GetInterestPoints(interestPoints); worldInterestReader.GetInterestPoints(interestPoints);
navCoverageHintReader.GetHintPoints(interestPoints);
} }
private void SyncCoverageWindows() private void SyncCoverageWindows()
@@ -857,4 +872,136 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
} }
} }
} }
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; }
}
}
} }
@@ -32,6 +32,7 @@ namespace VoxelWorldScene
builder.RegisterInstance(config); builder.RegisterInstance(config);
builder.RegisterInstance(worldGenerator).As<IChunkNavSourceReader>().AsSelf(); builder.RegisterInstance(worldGenerator).As<IChunkNavSourceReader>().AsSelf();
builder.Register<SceneWorldInterestReader>(Lifetime.Singleton).As<IWorldInterestReader>(); builder.Register<SceneWorldInterestReader>(Lifetime.Singleton).As<IWorldInterestReader>();
builder.RegisterEntryPoint<NavCoverageHintService>().AsSelf();
builder.RegisterEntryPoint<VoxelWorldNavMeshService>().AsSelf(); builder.RegisterEntryPoint<VoxelWorldNavMeshService>().AsSelf();
builder.RegisterBuildCallback(ResolvePublishers); builder.RegisterBuildCallback(ResolvePublishers);
} }