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);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user