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:
@@ -23,6 +23,18 @@ namespace InfiniteWorld.VoxelWorld.Contracts
|
||||
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 ChunkNavSourceSnapshot(Vector2Int coord, int version, ChunkNavBuildSourceDescriptor[] sources)
|
||||
@@ -84,7 +96,8 @@ namespace InfiniteWorld.VoxelWorld.Contracts
|
||||
PlayerActor = 0,
|
||||
ActiveNpc = 1,
|
||||
SpawnAnchor = 2,
|
||||
Other = 3
|
||||
TransientNavHint = 3,
|
||||
Other = 4
|
||||
}
|
||||
|
||||
public readonly struct NavCoverageWindowSnapshot
|
||||
@@ -143,4 +156,14 @@ namespace InfiniteWorld.VoxelWorld.Contracts
|
||||
|
||||
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 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>();
|
||||
@@ -28,7 +30,7 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||
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>(3);
|
||||
private readonly List<IDisposable> subscriptions = new List<IDisposable>(4);
|
||||
|
||||
private int nextCoverageWindowId = 1;
|
||||
private int? activeBuildWindowId;
|
||||
@@ -36,16 +38,20 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -54,6 +60,7 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||
subscriptions.Add(chunkReadySubscriber.Subscribe(OnChunkNavGeometryReady));
|
||||
subscriptions.Add(chunkRemovedSubscriber.Subscribe(OnChunkNavGeometryRemoved));
|
||||
subscriptions.Add(worldInterestChangedSubscriber.Subscribe(OnWorldInterestChanged));
|
||||
subscriptions.Add(navCoverageHintChangedSubscriber.Subscribe(OnNavCoverageHintChanged));
|
||||
|
||||
RefreshInterestPoints();
|
||||
SyncCoverageWindows();
|
||||
@@ -154,10 +161,18 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||
MarkAllCoverageWindowsDirty();
|
||||
}
|
||||
|
||||
private void OnNavCoverageHintChanged(NavCoverageHintChangedMessage message)
|
||||
{
|
||||
RefreshInterestPoints();
|
||||
SyncCoverageWindows();
|
||||
MarkAllCoverageWindowsDirty();
|
||||
}
|
||||
|
||||
private void RefreshInterestPoints()
|
||||
{
|
||||
interestPoints.Clear();
|
||||
worldInterestReader.GetInterestPoints(interestPoints);
|
||||
navCoverageHintReader.GetHintPoints(interestPoints);
|
||||
}
|
||||
|
||||
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(worldGenerator).As<IChunkNavSourceReader>().AsSelf();
|
||||
builder.Register<SceneWorldInterestReader>(Lifetime.Singleton).As<IWorldInterestReader>();
|
||||
builder.RegisterEntryPoint<NavCoverageHintService>().AsSelf();
|
||||
builder.RegisterEntryPoint<VoxelWorldNavMeshService>().AsSelf();
|
||||
builder.RegisterBuildCallback(ResolvePublishers);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user