task/0023-0028-navmesh #7

Merged
horooko merged 13 commits from task/0023-0026-navmesh into master 2026-04-09 05:54:39 +03:00
3 changed files with 173 additions and 2 deletions
Showing only changes of commit 289d5f783b - Show all commits
@@ -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);
}