160 lines
5.5 KiB
C#
160 lines
5.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using InfiniteWorld.VoxelWorld.Contracts;
|
|
using MessagePipe;
|
|
using UnityEngine;
|
|
using VContainer.Unity;
|
|
|
|
namespace InfiniteWorld.VoxelWorld.NavMesh
|
|
{
|
|
/// <summary>
|
|
/// Stores short-lived route hints and expands them into interest points so nav coverage can prewarm ahead of movement.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Increments whenever the effective set of active hints changes and cached coverage planning should be invalidated.
|
|
/// </summary>
|
|
public int HintVersion => hintVersion;
|
|
|
|
/// <summary>
|
|
/// Expires hints whose time-to-live has elapsed so stale route bias does not keep shaping coverage forever.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers or refreshes a temporary linear corridor for one owner so coverage can be biased along an upcoming route.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a previously registered route hint once the owner no longer needs prewarmed coverage.
|
|
/// </summary>
|
|
public void ClearHint(int ownerId)
|
|
{
|
|
if (!hints.Remove(ownerId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
NotifyHintsChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Appends the currently active hint points so the main coverage scheduler can treat them like supplemental interest.
|
|
/// </summary>
|
|
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; }
|
|
}
|
|
}
|
|
}
|