diff --git a/Assets/Features/VoxelWorld/Contracts/NavMeshWorldContracts.cs b/Assets/Features/VoxelWorld/Contracts/NavMeshWorldContracts.cs index 4715a285..f4a8bda3 100644 --- a/Assets/Features/VoxelWorld/Contracts/NavMeshWorldContracts.cs +++ b/Assets/Features/VoxelWorld/Contracts/NavMeshWorldContracts.cs @@ -23,6 +23,18 @@ namespace InfiniteWorld.VoxelWorld.Contracts void GetCoverageWindows(List 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 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; } + } } diff --git a/Assets/Features/VoxelWorldNavMesh/Runtime/VoxelWorldNavMeshService.cs b/Assets/Features/VoxelWorldNavMesh/Runtime/VoxelWorldNavMeshService.cs index 50412ec4..29f5942f 100644 --- a/Assets/Features/VoxelWorldNavMesh/Runtime/VoxelWorldNavMeshService.cs +++ b/Assets/Features/VoxelWorldNavMesh/Runtime/VoxelWorldNavMeshService.cs @@ -14,9 +14,11 @@ namespace InfiniteWorld.VoxelWorld.NavMesh { private readonly IChunkNavSourceReader chunkNavSourceReader; private readonly IWorldInterestReader worldInterestReader; + private readonly INavCoverageHintReader navCoverageHintReader; private readonly ISubscriber chunkReadySubscriber; private readonly ISubscriber chunkRemovedSubscriber; private readonly ISubscriber worldInterestChangedSubscriber; + private readonly ISubscriber navCoverageHintChangedSubscriber; private readonly VoxelWorldNavMeshConfig config; private readonly Dictionary coverageWindows = new Dictionary(); private readonly Queue dirtyCoverageWindowIds = new Queue(); @@ -28,7 +30,7 @@ namespace InfiniteWorld.VoxelWorld.NavMesh 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(3); + private readonly List subscriptions = new List(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 chunkReadySubscriber, ISubscriber chunkRemovedSubscriber, ISubscriber worldInterestChangedSubscriber, + ISubscriber 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 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/Scripts/VoxelWorld/VoxelWorldNavMeshLifetimeScope.cs b/Assets/Scripts/VoxelWorld/VoxelWorldNavMeshLifetimeScope.cs index b8ae77e2..425f0be3 100644 --- a/Assets/Scripts/VoxelWorld/VoxelWorldNavMeshLifetimeScope.cs +++ b/Assets/Scripts/VoxelWorld/VoxelWorldNavMeshLifetimeScope.cs @@ -32,6 +32,7 @@ namespace VoxelWorldScene builder.RegisterInstance(config); builder.RegisterInstance(worldGenerator).As().AsSelf(); builder.Register(Lifetime.Singleton).As(); + builder.RegisterEntryPoint().AsSelf(); builder.RegisterEntryPoint().AsSelf(); builder.RegisterBuildCallback(ResolvePublishers); }