task/0023-0028-navmesh #7
@@ -17,6 +17,12 @@ namespace InfiniteWorld.VoxelWorld.Contracts
|
||||
void GetInterestPoints(List<WorldInterestPoint> results);
|
||||
}
|
||||
|
||||
public interface INavCoverageReader
|
||||
{
|
||||
bool IsPositionCovered(Vector3 worldPosition);
|
||||
void GetCoverageWindows(List<NavCoverageWindowSnapshot> results);
|
||||
}
|
||||
|
||||
public readonly struct ChunkNavSourceSnapshot
|
||||
{
|
||||
public ChunkNavSourceSnapshot(Vector2Int coord, int version, ChunkNavBuildSourceDescriptor[] sources)
|
||||
@@ -77,7 +83,31 @@ namespace InfiniteWorld.VoxelWorld.Contracts
|
||||
{
|
||||
PlayerActor = 0,
|
||||
ActiveNpc = 1,
|
||||
Other = 2
|
||||
SpawnAnchor = 2,
|
||||
Other = 3
|
||||
}
|
||||
|
||||
public readonly struct NavCoverageWindowSnapshot
|
||||
{
|
||||
public NavCoverageWindowSnapshot(int id, Bounds bounds, NavCoverageState state, int interestCount)
|
||||
{
|
||||
Id = id;
|
||||
Bounds = bounds;
|
||||
State = state;
|
||||
InterestCount = interestCount;
|
||||
}
|
||||
|
||||
public int Id { get; }
|
||||
public Bounds Bounds { get; }
|
||||
public NavCoverageState State { get; }
|
||||
public int InterestCount { get; }
|
||||
}
|
||||
|
||||
public enum NavCoverageState
|
||||
{
|
||||
Pending = 0,
|
||||
Building = 1,
|
||||
Ready = 2
|
||||
}
|
||||
|
||||
public readonly struct ChunkNavGeometryReadyMessage
|
||||
|
||||
@@ -83,8 +83,12 @@ MonoBehaviour:
|
||||
worldGenerator: {fileID: 2927522923773808063}
|
||||
config:
|
||||
agentTypeId: 0
|
||||
navRegionSizeInChunks: 2
|
||||
maxNavMeshBuildsPerFrame: 1
|
||||
navBoundsHorizontalPadding: 1
|
||||
navBoundsVerticalPadding: 2
|
||||
navWarmupRadiusInRegions: 1
|
||||
maxActiveCoverageWindows: 3
|
||||
clusterMergeDistanceInChunks: 4
|
||||
coveragePaddingInChunks: 2
|
||||
coverageQuantizationInChunks: 1
|
||||
minCoverageWindowSizeInChunks: 4
|
||||
chunkCollectionMarginInChunks: 1
|
||||
|
||||
@@ -255,6 +255,7 @@ GameObject:
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 171707223}
|
||||
- component: {fileID: 171707224}
|
||||
m_Layer: 0
|
||||
m_Name: SpawnPoint
|
||||
m_TagString: Untagged
|
||||
@@ -277,6 +278,19 @@ Transform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &171707224
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 171707222}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 7a0a7758ae4541b39ed0b5d1fe912869, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::VoxelWorldScene.VoxelWorldSpawnAnchor
|
||||
priority: 2
|
||||
--- !u!1001 &1165873058
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@@ -7,10 +7,14 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||
public sealed class VoxelWorldNavMeshConfig
|
||||
{
|
||||
[Min(0)] public int agentTypeId;
|
||||
[Min(1)] public int navRegionSizeInChunks = 2;
|
||||
[Min(1)] public int maxNavMeshBuildsPerFrame = 1;
|
||||
[Min(0f)] public float navBoundsHorizontalPadding = 1f;
|
||||
[Min(0f)] public float navBoundsVerticalPadding = 2f;
|
||||
[Min(0)] public int navWarmupRadiusInRegions = 1;
|
||||
[Min(1)] public int maxActiveCoverageWindows = 3;
|
||||
[Min(0f)] public float clusterMergeDistanceInChunks = 4f;
|
||||
[Min(0f)] public float coveragePaddingInChunks = 2f;
|
||||
[Min(0.25f)] public float coverageQuantizationInChunks = 1f;
|
||||
[Min(1f)] public float minCoverageWindowSizeInChunks = 4f;
|
||||
[Min(0)] public int chunkCollectionMarginInChunks = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ using UnityNavMeshBuilder = UnityEngine.AI.NavMeshBuilder;
|
||||
|
||||
namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||
{
|
||||
public sealed class VoxelWorldNavMeshService : IStartable, ITickable, IDisposable
|
||||
public sealed class VoxelWorldNavMeshService : IStartable, ITickable, IDisposable, INavCoverageReader
|
||||
{
|
||||
private readonly IChunkNavSourceReader chunkNavSourceReader;
|
||||
private readonly IWorldInterestReader worldInterestReader;
|
||||
@@ -18,18 +18,20 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||
private readonly ISubscriber<ChunkNavGeometryRemovedMessage> chunkRemovedSubscriber;
|
||||
private readonly ISubscriber<WorldInterestChangedMessage> worldInterestChangedSubscriber;
|
||||
private readonly VoxelWorldNavMeshConfig config;
|
||||
private readonly Dictionary<Vector2Int, NavRegionRuntime> navRegions = new Dictionary<Vector2Int, NavRegionRuntime>();
|
||||
private readonly Queue<Vector2Int> dirtyNavRegions = new Queue<Vector2Int>();
|
||||
private readonly HashSet<Vector2Int> queuedNavRegions = new HashSet<Vector2Int>();
|
||||
private readonly List<Vector2Int> loadedChunkCoords = new List<Vector2Int>(64);
|
||||
private readonly List<WorldInterestPoint> interestPoints = new List<WorldInterestPoint>(4);
|
||||
private readonly List<Vector2Int> dirtyRegionCandidates = new List<Vector2Int>(16);
|
||||
private readonly List<NavMeshBuildSource> buildSources = new List<NavMeshBuildSource>(64);
|
||||
private readonly HashSet<Vector2Int> currentInterestRegions = new HashSet<Vector2Int>();
|
||||
private readonly HashSet<Vector2Int> previousInterestRegions = new HashSet<Vector2Int>();
|
||||
private readonly Dictionary<int, NavCoverageWindowRuntime> coverageWindows = new Dictionary<int, NavCoverageWindowRuntime>();
|
||||
private readonly Queue<int> dirtyCoverageWindowIds = new Queue<int>();
|
||||
private readonly HashSet<int> queuedCoverageWindowIds = new HashSet<int>();
|
||||
private readonly List<int> dirtyCoverageWindowCandidates = new List<int>(16);
|
||||
private readonly List<WorldInterestPoint> interestPoints = new List<WorldInterestPoint>(8);
|
||||
private readonly List<Vector2Int> loadedChunkCoords = new List<Vector2Int>(128);
|
||||
private readonly List<NavMeshBuildSource> buildSources = new List<NavMeshBuildSource>(256);
|
||||
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 Vector2Int? activeBuildRegion;
|
||||
private int nextCoverageWindowId = 1;
|
||||
private int? activeBuildWindowId;
|
||||
|
||||
public VoxelWorldNavMeshService(
|
||||
IChunkNavSourceReader chunkNavSourceReader,
|
||||
@@ -54,33 +56,27 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||
subscriptions.Add(worldInterestChangedSubscriber.Subscribe(OnWorldInterestChanged));
|
||||
|
||||
RefreshInterestPoints();
|
||||
|
||||
loadedChunkCoords.Clear();
|
||||
chunkNavSourceReader.GetLoadedChunkCoords(loadedChunkCoords);
|
||||
for (int i = 0; i < loadedChunkCoords.Count; i++)
|
||||
{
|
||||
MarkDirtyForChunk(loadedChunkCoords[i]);
|
||||
}
|
||||
|
||||
MarkWarmupRegionsDirty();
|
||||
SyncCoverageWindows();
|
||||
MarkAllCoverageWindowsDirty();
|
||||
}
|
||||
|
||||
public void Tick()
|
||||
{
|
||||
RefreshInterestPoints();
|
||||
SyncCoverageWindows();
|
||||
CompleteFinishedBuild();
|
||||
|
||||
int startedBuilds = 0;
|
||||
int maxBuilds = Mathf.Max(1, config.maxNavMeshBuildsPerFrame);
|
||||
while (startedBuilds < maxBuilds)
|
||||
{
|
||||
if (activeBuildRegion.HasValue || dirtyNavRegions.Count == 0)
|
||||
if (activeBuildWindowId.HasValue || dirtyCoverageWindowIds.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Vector2Int regionCoord = DequeueBestDirtyRegion();
|
||||
if (!TryStartRegionBuild(regionCoord))
|
||||
int windowId = DequeueBestDirtyCoverageWindow();
|
||||
if (!TryStartCoverageBuild(windowId))
|
||||
{
|
||||
startedBuilds++;
|
||||
continue;
|
||||
@@ -90,6 +86,34 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsPositionCovered(Vector3 worldPosition)
|
||||
{
|
||||
foreach (KeyValuePair<int, NavCoverageWindowRuntime> pair in coverageWindows)
|
||||
{
|
||||
NavCoverageWindowRuntime window = pair.Value;
|
||||
if (window.State == NavCoverageState.Ready && ContainsXZ(window.CoverageBounds, worldPosition))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void GetCoverageWindows(List<NavCoverageWindowSnapshot> results)
|
||||
{
|
||||
if (results == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(results));
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<int, NavCoverageWindowRuntime> pair in coverageWindows)
|
||||
{
|
||||
NavCoverageWindowRuntime window = pair.Value;
|
||||
results.Add(new NavCoverageWindowSnapshot(window.Id, window.CoverageBounds, window.State, window.InterestCount));
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
for (int i = 0; i < subscriptions.Count; i++)
|
||||
@@ -99,130 +123,315 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||
|
||||
subscriptions.Clear();
|
||||
|
||||
foreach (KeyValuePair<Vector2Int, NavRegionRuntime> pair in navRegions)
|
||||
foreach (KeyValuePair<int, NavCoverageWindowRuntime> pair in coverageWindows)
|
||||
{
|
||||
pair.Value.Dispose();
|
||||
}
|
||||
|
||||
navRegions.Clear();
|
||||
queuedNavRegions.Clear();
|
||||
dirtyNavRegions.Clear();
|
||||
currentInterestRegions.Clear();
|
||||
previousInterestRegions.Clear();
|
||||
activeBuildRegion = null;
|
||||
coverageWindows.Clear();
|
||||
queuedCoverageWindowIds.Clear();
|
||||
dirtyCoverageWindowIds.Clear();
|
||||
desiredCoverageWindows.Clear();
|
||||
clusterAccumulators.Clear();
|
||||
coverageWindowSnapshots.Clear();
|
||||
activeBuildWindowId = null;
|
||||
}
|
||||
|
||||
private void OnChunkNavGeometryReady(ChunkNavGeometryReadyMessage message)
|
||||
{
|
||||
MarkDirtyForChunk(message.Coord);
|
||||
MarkCoverageWindowsDirtyForChunk(message.Coord);
|
||||
}
|
||||
|
||||
private void OnChunkNavGeometryRemoved(ChunkNavGeometryRemovedMessage message)
|
||||
{
|
||||
MarkDirtyForChunk(message.Coord);
|
||||
MarkCoverageWindowsDirtyForChunk(message.Coord);
|
||||
}
|
||||
|
||||
private void OnWorldInterestChanged(WorldInterestChangedMessage message)
|
||||
{
|
||||
RefreshInterestPoints();
|
||||
MarkWarmupRegionsDirty();
|
||||
SyncCoverageWindows();
|
||||
MarkAllCoverageWindowsDirty();
|
||||
}
|
||||
|
||||
private void RefreshInterestPoints()
|
||||
{
|
||||
interestPoints.Clear();
|
||||
worldInterestReader.GetInterestPoints(interestPoints);
|
||||
}
|
||||
|
||||
previousInterestRegions.Clear();
|
||||
foreach (Vector2Int region in currentInterestRegions)
|
||||
private void SyncCoverageWindows()
|
||||
{
|
||||
BuildDesiredCoverageWindows();
|
||||
|
||||
foreach (KeyValuePair<int, NavCoverageWindowRuntime> pair in coverageWindows)
|
||||
{
|
||||
previousInterestRegions.Add(region);
|
||||
pair.Value.MatchedThisFrame = false;
|
||||
}
|
||||
|
||||
currentInterestRegions.Clear();
|
||||
for (int i = 0; i < desiredCoverageWindows.Count; i++)
|
||||
{
|
||||
DesiredCoverageWindow desiredWindow = desiredCoverageWindows[i];
|
||||
NavCoverageWindowRuntime runtime = FindBestMatchingCoverageWindow(desiredWindow);
|
||||
if (runtime == null)
|
||||
{
|
||||
runtime = new NavCoverageWindowRuntime(nextCoverageWindowId++, desiredWindow.CoverageBounds, desiredWindow.Priority, desiredWindow.InterestCount);
|
||||
runtime.MatchedThisFrame = true;
|
||||
coverageWindows.Add(runtime.Id, runtime);
|
||||
EnqueueDirtyCoverageWindow(runtime.Id);
|
||||
continue;
|
||||
}
|
||||
|
||||
runtime.MatchedThisFrame = true;
|
||||
runtime.Priority = desiredWindow.Priority;
|
||||
runtime.InterestCount = desiredWindow.InterestCount;
|
||||
|
||||
if (!BoundsApproximatelyEqual(runtime.CoverageBounds, desiredWindow.CoverageBounds))
|
||||
{
|
||||
runtime.CoverageBounds = desiredWindow.CoverageBounds;
|
||||
runtime.State = NavCoverageState.Pending;
|
||||
EnqueueDirtyCoverageWindow(runtime.Id);
|
||||
}
|
||||
}
|
||||
|
||||
coverageWindowSnapshots.Clear();
|
||||
foreach (KeyValuePair<int, NavCoverageWindowRuntime> pair in coverageWindows)
|
||||
{
|
||||
if (pair.Value.MatchedThisFrame)
|
||||
{
|
||||
coverageWindowSnapshots.Add(new NavCoverageWindowSnapshot(pair.Value.Id, pair.Value.CoverageBounds, pair.Value.State, pair.Value.InterestCount));
|
||||
}
|
||||
}
|
||||
|
||||
RemoveUnmatchedCoverageWindows();
|
||||
}
|
||||
|
||||
private void BuildDesiredCoverageWindows()
|
||||
{
|
||||
desiredCoverageWindows.Clear();
|
||||
clusterAccumulators.Clear();
|
||||
|
||||
if (interestPoints.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float chunkWorldSize = Mathf.Max(1f, chunkNavSourceReader.ChunkWorldSize);
|
||||
float mergeDistance = Mathf.Max(0f, config.clusterMergeDistanceInChunks) * chunkWorldSize;
|
||||
float quantizationStep = Mathf.Max(0.25f, config.coverageQuantizationInChunks) * chunkWorldSize;
|
||||
float padding = Mathf.Max(0f, config.coveragePaddingInChunks) * chunkWorldSize;
|
||||
float minWindowSize = Mathf.Max(1f, config.minCoverageWindowSizeInChunks) * chunkWorldSize;
|
||||
|
||||
for (int i = 0; i < interestPoints.Count; i++)
|
||||
{
|
||||
currentInterestRegions.Add(ChunkToRegion(WorldToChunk(interestPoints[i].Position)));
|
||||
WorldInterestPoint point = interestPoints[i];
|
||||
int bestClusterIndex = -1;
|
||||
float bestDistance = float.MaxValue;
|
||||
|
||||
for (int clusterIndex = 0; clusterIndex < clusterAccumulators.Count; clusterIndex++)
|
||||
{
|
||||
float distance = DistanceToBoundsXZ(clusterAccumulators[clusterIndex].RawBounds, point.Position);
|
||||
if (distance <= mergeDistance && distance < bestDistance)
|
||||
{
|
||||
bestDistance = distance;
|
||||
bestClusterIndex = clusterIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestClusterIndex >= 0)
|
||||
{
|
||||
ClusterAccumulator cluster = clusterAccumulators[bestClusterIndex];
|
||||
cluster.Add(point);
|
||||
clusterAccumulators[bestClusterIndex] = cluster;
|
||||
}
|
||||
else
|
||||
{
|
||||
clusterAccumulators.Add(new ClusterAccumulator(point));
|
||||
}
|
||||
}
|
||||
|
||||
if (!AreSetsEqual(previousInterestRegions, currentInterestRegions))
|
||||
MergeNearbyClusters(mergeDistance);
|
||||
|
||||
for (int i = 0; i < clusterAccumulators.Count; i++)
|
||||
{
|
||||
MarkWarmupRegionsDirty();
|
||||
ClusterAccumulator cluster = clusterAccumulators[i];
|
||||
Bounds coverageBounds = CreateQuantizedCoverageBounds(cluster.RawBounds, padding, minWindowSize, quantizationStep);
|
||||
desiredCoverageWindows.Add(new DesiredCoverageWindow(coverageBounds, cluster.Priority, cluster.InterestCount));
|
||||
}
|
||||
|
||||
desiredCoverageWindows.Sort((left, right) =>
|
||||
{
|
||||
int priorityCompare = right.Priority.CompareTo(left.Priority);
|
||||
if (priorityCompare != 0)
|
||||
{
|
||||
return priorityCompare;
|
||||
}
|
||||
|
||||
int interestCompare = right.InterestCount.CompareTo(left.InterestCount);
|
||||
if (interestCompare != 0)
|
||||
{
|
||||
return interestCompare;
|
||||
}
|
||||
|
||||
return left.CoverageBounds.center.sqrMagnitude.CompareTo(right.CoverageBounds.center.sqrMagnitude);
|
||||
});
|
||||
|
||||
int maxWindows = Mathf.Max(1, config.maxActiveCoverageWindows);
|
||||
if (desiredCoverageWindows.Count > maxWindows)
|
||||
{
|
||||
desiredCoverageWindows.RemoveRange(maxWindows, desiredCoverageWindows.Count - maxWindows);
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkWarmupRegionsDirty()
|
||||
private void MergeNearbyClusters(float mergeDistance)
|
||||
{
|
||||
int radius = Mathf.Max(0, config.navWarmupRadiusInRegions);
|
||||
foreach (Vector2Int region in currentInterestRegions)
|
||||
if (clusterAccumulators.Count < 2)
|
||||
{
|
||||
for (int y = -radius; y <= radius; y++)
|
||||
return;
|
||||
}
|
||||
|
||||
bool merged;
|
||||
do
|
||||
{
|
||||
merged = false;
|
||||
for (int i = 0; i < clusterAccumulators.Count; i++)
|
||||
{
|
||||
for (int x = -radius; x <= radius; x++)
|
||||
for (int j = i + 1; j < clusterAccumulators.Count; j++)
|
||||
{
|
||||
EnqueueDirtyRegion(new Vector2Int(region.x + x, region.y + y));
|
||||
if (DistanceBetweenBoundsXZ(clusterAccumulators[i].RawBounds, clusterAccumulators[j].RawBounds) > mergeDistance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ClusterAccumulator combined = clusterAccumulators[i];
|
||||
combined.Merge(clusterAccumulators[j]);
|
||||
clusterAccumulators[i] = combined;
|
||||
clusterAccumulators.RemoveAt(j);
|
||||
merged = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (merged)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (merged);
|
||||
}
|
||||
|
||||
private NavCoverageWindowRuntime FindBestMatchingCoverageWindow(DesiredCoverageWindow desiredWindow)
|
||||
{
|
||||
NavCoverageWindowRuntime bestMatch = null;
|
||||
float bestDistance = float.MaxValue;
|
||||
float chunkWorldSize = Mathf.Max(1f, chunkNavSourceReader.ChunkWorldSize);
|
||||
float matchThreshold = Mathf.Max(config.minCoverageWindowSizeInChunks, config.clusterMergeDistanceInChunks + config.coveragePaddingInChunks) * chunkWorldSize;
|
||||
|
||||
foreach (KeyValuePair<int, NavCoverageWindowRuntime> pair in coverageWindows)
|
||||
{
|
||||
NavCoverageWindowRuntime candidate = pair.Value;
|
||||
if (candidate.MatchedThisFrame)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float distance = Vector2.Distance(
|
||||
new Vector2(candidate.CoverageBounds.center.x, candidate.CoverageBounds.center.z),
|
||||
new Vector2(desiredWindow.CoverageBounds.center.x, desiredWindow.CoverageBounds.center.z));
|
||||
|
||||
if (distance > matchThreshold || distance >= bestDistance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bestDistance = distance;
|
||||
bestMatch = candidate;
|
||||
}
|
||||
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
private void RemoveUnmatchedCoverageWindows()
|
||||
{
|
||||
List<int> windowsToRemove = null;
|
||||
|
||||
foreach (KeyValuePair<int, NavCoverageWindowRuntime> pair in coverageWindows)
|
||||
{
|
||||
if (pair.Value.MatchedThisFrame)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
windowsToRemove ??= new List<int>();
|
||||
windowsToRemove.Add(pair.Key);
|
||||
}
|
||||
|
||||
if (windowsToRemove == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < windowsToRemove.Count; i++)
|
||||
{
|
||||
RemoveCoverageWindow(windowsToRemove[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkAllCoverageWindowsDirty()
|
||||
{
|
||||
foreach (KeyValuePair<int, NavCoverageWindowRuntime> pair in coverageWindows)
|
||||
{
|
||||
EnqueueDirtyCoverageWindow(pair.Key);
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkCoverageWindowsDirtyForChunk(Vector2Int chunkCoord)
|
||||
{
|
||||
Bounds chunkBounds = ExpandChunkBounds(GetChunkWorldBounds(chunkCoord), Mathf.Max(0, config.chunkCollectionMarginInChunks));
|
||||
foreach (KeyValuePair<int, NavCoverageWindowRuntime> pair in coverageWindows)
|
||||
{
|
||||
Bounds invalidationBounds = ExpandCoverageBounds(pair.Value.CoverageBounds, Mathf.Max(0, config.chunkCollectionMarginInChunks));
|
||||
if (IntersectsXZ(invalidationBounds, chunkBounds))
|
||||
{
|
||||
EnqueueDirtyCoverageWindow(pair.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkDirtyForChunk(Vector2Int chunkCoord)
|
||||
private void EnqueueDirtyCoverageWindow(int windowId)
|
||||
{
|
||||
int regionSize = Mathf.Max(1, config.navRegionSizeInChunks);
|
||||
Vector2Int regionCoord = ChunkToRegion(chunkCoord);
|
||||
EnqueueDirtyRegion(regionCoord);
|
||||
|
||||
int localX = PositiveModulo(chunkCoord.x, regionSize);
|
||||
int localY = PositiveModulo(chunkCoord.y, regionSize);
|
||||
if (localX == 0)
|
||||
if (!queuedCoverageWindowIds.Add(windowId))
|
||||
{
|
||||
EnqueueDirtyRegion(regionCoord + Vector2Int.left);
|
||||
}
|
||||
|
||||
if (localX == regionSize - 1)
|
||||
{
|
||||
EnqueueDirtyRegion(regionCoord + Vector2Int.right);
|
||||
}
|
||||
|
||||
if (localY == 0)
|
||||
{
|
||||
EnqueueDirtyRegion(regionCoord + Vector2Int.down);
|
||||
}
|
||||
|
||||
if (localY == regionSize - 1)
|
||||
{
|
||||
EnqueueDirtyRegion(regionCoord + Vector2Int.up);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnqueueDirtyRegion(Vector2Int regionCoord)
|
||||
{
|
||||
if (!queuedNavRegions.Add(regionCoord))
|
||||
{
|
||||
if (activeBuildRegion.HasValue && activeBuildRegion.Value == regionCoord && navRegions.TryGetValue(regionCoord, out NavRegionRuntime activeRegion))
|
||||
if (activeBuildWindowId.HasValue && activeBuildWindowId.Value == windowId && coverageWindows.TryGetValue(windowId, out NavCoverageWindowRuntime activeWindow))
|
||||
{
|
||||
activeRegion.BuildRequestedWhileRunning = true;
|
||||
activeWindow.BuildRequestedWhileRunning = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
dirtyNavRegions.Enqueue(regionCoord);
|
||||
dirtyCoverageWindowIds.Enqueue(windowId);
|
||||
}
|
||||
|
||||
private Vector2Int DequeueBestDirtyRegion()
|
||||
private int DequeueBestDirtyCoverageWindow()
|
||||
{
|
||||
dirtyRegionCandidates.Clear();
|
||||
while (dirtyNavRegions.Count > 0)
|
||||
dirtyCoverageWindowCandidates.Clear();
|
||||
while (dirtyCoverageWindowIds.Count > 0)
|
||||
{
|
||||
dirtyRegionCandidates.Add(dirtyNavRegions.Dequeue());
|
||||
dirtyCoverageWindowCandidates.Add(dirtyCoverageWindowIds.Dequeue());
|
||||
}
|
||||
|
||||
int bestIndex = 0;
|
||||
float bestScore = float.MaxValue;
|
||||
for (int i = 0; i < dirtyRegionCandidates.Count; i++)
|
||||
for (int i = 0; i < dirtyCoverageWindowCandidates.Count; i++)
|
||||
{
|
||||
float score = GetRegionPriorityScore(dirtyRegionCandidates[i]);
|
||||
int windowId = dirtyCoverageWindowCandidates[i];
|
||||
if (!coverageWindows.TryGetValue(windowId, out NavCoverageWindowRuntime window))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float score = GetCoveragePriorityScore(window);
|
||||
if (score < bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
@@ -230,36 +439,37 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||
}
|
||||
}
|
||||
|
||||
Vector2Int best = dirtyRegionCandidates[bestIndex];
|
||||
queuedNavRegions.Remove(best);
|
||||
int bestWindowId = dirtyCoverageWindowCandidates[bestIndex];
|
||||
queuedCoverageWindowIds.Remove(bestWindowId);
|
||||
|
||||
for (int i = 0; i < dirtyRegionCandidates.Count; i++)
|
||||
for (int i = 0; i < dirtyCoverageWindowCandidates.Count; i++)
|
||||
{
|
||||
if (i == bestIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
dirtyNavRegions.Enqueue(dirtyRegionCandidates[i]);
|
||||
dirtyCoverageWindowIds.Enqueue(dirtyCoverageWindowCandidates[i]);
|
||||
}
|
||||
|
||||
dirtyRegionCandidates.Clear();
|
||||
return best;
|
||||
dirtyCoverageWindowCandidates.Clear();
|
||||
return bestWindowId;
|
||||
}
|
||||
|
||||
private float GetRegionPriorityScore(Vector2Int regionCoord)
|
||||
private float GetCoveragePriorityScore(NavCoverageWindowRuntime window)
|
||||
{
|
||||
if (interestPoints.Count == 0)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
Vector3 regionCenter = GetRegionCenter(regionCoord);
|
||||
Vector3 center = window.CoverageBounds.center;
|
||||
float bestDistance = float.MaxValue;
|
||||
for (int i = 0; i < interestPoints.Count; i++)
|
||||
{
|
||||
float priority = Mathf.Max(0.01f, interestPoints[i].Priority);
|
||||
float distance = Vector3.SqrMagnitude(regionCenter - interestPoints[i].Position) / priority;
|
||||
float distance = Vector2.SqrMagnitude(
|
||||
new Vector2(center.x, center.z) - new Vector2(interestPoints[i].Position.x, interestPoints[i].Position.z)) / priority;
|
||||
if (distance < bestDistance)
|
||||
{
|
||||
bestDistance = distance;
|
||||
@@ -269,66 +479,70 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||
return bestDistance;
|
||||
}
|
||||
|
||||
private bool TryStartRegionBuild(Vector2Int regionCoord)
|
||||
private bool TryStartCoverageBuild(int windowId)
|
||||
{
|
||||
buildSources.Clear();
|
||||
bool hasCoreChunk = CollectBuildSources(regionCoord, buildSources);
|
||||
if (!hasCoreChunk || buildSources.Count == 0)
|
||||
if (!coverageWindows.TryGetValue(windowId, out NavCoverageWindowRuntime window))
|
||||
{
|
||||
RemoveRegion(regionCoord);
|
||||
return false;
|
||||
}
|
||||
|
||||
buildSources.Clear();
|
||||
window.CollectionBounds = ExpandCoverageBounds(window.CoverageBounds, Mathf.Max(0, config.chunkCollectionMarginInChunks));
|
||||
|
||||
bool hasSources = CollectBuildSources(window.CollectionBounds, buildSources);
|
||||
if (!hasSources || buildSources.Count == 0)
|
||||
{
|
||||
window.State = NavCoverageState.Pending;
|
||||
RemoveCoverageData(window);
|
||||
return false;
|
||||
}
|
||||
|
||||
Bounds buildBounds = CalculateBounds(buildSources);
|
||||
ExpandBounds(ref buildBounds, config.navBoundsHorizontalPadding, config.navBoundsVerticalPadding);
|
||||
window.BuildBounds = buildBounds;
|
||||
window.BuildRequestedWhileRunning = false;
|
||||
|
||||
NavRegionRuntime region = GetOrCreateRegion(regionCoord);
|
||||
region.BuildRequestedWhileRunning = false;
|
||||
region.BuildBounds = buildBounds;
|
||||
|
||||
if (region.NavMeshData == null)
|
||||
if (window.NavMeshData == null)
|
||||
{
|
||||
region.NavMeshData = new NavMeshData(config.agentTypeId);
|
||||
window.NavMeshData = new NavMeshData(config.agentTypeId);
|
||||
}
|
||||
|
||||
if (!region.Instance.valid)
|
||||
if (!window.Instance.valid)
|
||||
{
|
||||
region.Instance = UnityNavMesh.AddNavMeshData(region.NavMeshData);
|
||||
window.Instance = UnityNavMesh.AddNavMeshData(window.NavMeshData);
|
||||
}
|
||||
|
||||
NavMeshBuildSettings buildSettings = UnityNavMesh.GetSettingsByID(config.agentTypeId);
|
||||
region.ActiveBuild = UnityNavMeshBuilder.UpdateNavMeshDataAsync(region.NavMeshData, buildSettings, buildSources, buildBounds);
|
||||
activeBuildRegion = regionCoord;
|
||||
window.ActiveBuild = UnityNavMeshBuilder.UpdateNavMeshDataAsync(window.NavMeshData, buildSettings, buildSources, buildBounds);
|
||||
window.State = NavCoverageState.Building;
|
||||
activeBuildWindowId = windowId;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool CollectBuildSources(Vector2Int regionCoord, List<NavMeshBuildSource> results)
|
||||
private bool CollectBuildSources(Bounds coverageBounds, List<NavMeshBuildSource> results)
|
||||
{
|
||||
int regionSize = Mathf.Max(1, config.navRegionSizeInChunks);
|
||||
int baseChunkX = regionCoord.x * regionSize;
|
||||
int baseChunkY = regionCoord.y * regionSize;
|
||||
bool hasCoreChunk = false;
|
||||
loadedChunkCoords.Clear();
|
||||
chunkNavSourceReader.GetLoadedChunkCoords(loadedChunkCoords);
|
||||
|
||||
for (int y = -1; y <= regionSize; y++)
|
||||
bool hasSources = false;
|
||||
for (int i = 0; i < loadedChunkCoords.Count; i++)
|
||||
{
|
||||
for (int x = -1; x <= regionSize; x++)
|
||||
Vector2Int chunkCoord = loadedChunkCoords[i];
|
||||
if (!IntersectsXZ(GetChunkWorldBounds(chunkCoord), coverageBounds))
|
||||
{
|
||||
Vector2Int chunkCoord = new Vector2Int(baseChunkX + x, baseChunkY + y);
|
||||
if (!chunkNavSourceReader.TryGetChunkNavSourceSnapshot(chunkCoord, out ChunkNavSourceSnapshot snapshot) || snapshot.Sources == null || snapshot.Sources.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (x >= 0 && x < regionSize && y >= 0 && y < regionSize)
|
||||
{
|
||||
hasCoreChunk = true;
|
||||
}
|
||||
|
||||
AppendBuildSources(snapshot.Sources, results);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!chunkNavSourceReader.TryGetChunkNavSourceSnapshot(chunkCoord, out ChunkNavSourceSnapshot snapshot) || snapshot.Sources == null || snapshot.Sources.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
hasSources = true;
|
||||
AppendBuildSources(snapshot.Sources, results);
|
||||
}
|
||||
|
||||
return hasCoreChunk;
|
||||
return hasSources;
|
||||
}
|
||||
|
||||
private static void AppendBuildSources(ChunkNavBuildSourceDescriptor[] descriptors, List<NavMeshBuildSource> results)
|
||||
@@ -356,58 +570,87 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||
|
||||
private void CompleteFinishedBuild()
|
||||
{
|
||||
if (!activeBuildRegion.HasValue)
|
||||
if (!activeBuildWindowId.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!navRegions.TryGetValue(activeBuildRegion.Value, out NavRegionRuntime region))
|
||||
if (!coverageWindows.TryGetValue(activeBuildWindowId.Value, out NavCoverageWindowRuntime window))
|
||||
{
|
||||
activeBuildRegion = null;
|
||||
activeBuildWindowId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (region.ActiveBuild != null && !region.ActiveBuild.isDone)
|
||||
if (window.ActiveBuild != null && !window.ActiveBuild.isDone)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
region.ActiveBuild = null;
|
||||
Vector2Int completedRegion = activeBuildRegion.Value;
|
||||
activeBuildRegion = null;
|
||||
window.ActiveBuild = null;
|
||||
window.State = NavCoverageState.Ready;
|
||||
int completedWindowId = activeBuildWindowId.Value;
|
||||
activeBuildWindowId = null;
|
||||
|
||||
if (region.BuildRequestedWhileRunning)
|
||||
if (window.BuildRequestedWhileRunning)
|
||||
{
|
||||
region.BuildRequestedWhileRunning = false;
|
||||
EnqueueDirtyRegion(completedRegion);
|
||||
window.BuildRequestedWhileRunning = false;
|
||||
EnqueueDirtyCoverageWindow(completedWindowId);
|
||||
}
|
||||
}
|
||||
|
||||
private NavRegionRuntime GetOrCreateRegion(Vector2Int regionCoord)
|
||||
private void RemoveCoverageWindow(int windowId)
|
||||
{
|
||||
if (!navRegions.TryGetValue(regionCoord, out NavRegionRuntime region))
|
||||
{
|
||||
region = new NavRegionRuntime();
|
||||
navRegions.Add(regionCoord, region);
|
||||
}
|
||||
|
||||
return region;
|
||||
}
|
||||
|
||||
private void RemoveRegion(Vector2Int regionCoord)
|
||||
{
|
||||
if (!navRegions.TryGetValue(regionCoord, out NavRegionRuntime region))
|
||||
if (!coverageWindows.TryGetValue(windowId, out NavCoverageWindowRuntime window))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeBuildRegion.HasValue && activeBuildRegion.Value == regionCoord)
|
||||
if (activeBuildWindowId.HasValue && activeBuildWindowId.Value == windowId)
|
||||
{
|
||||
activeBuildRegion = null;
|
||||
activeBuildWindowId = null;
|
||||
}
|
||||
|
||||
region.Dispose();
|
||||
navRegions.Remove(regionCoord);
|
||||
window.Dispose();
|
||||
coverageWindows.Remove(windowId);
|
||||
queuedCoverageWindowIds.Remove(windowId);
|
||||
}
|
||||
|
||||
private static void RemoveCoverageData(NavCoverageWindowRuntime window)
|
||||
{
|
||||
if (window.ActiveBuild != null && !window.ActiveBuild.isDone && window.NavMeshData != null)
|
||||
{
|
||||
UnityNavMeshBuilder.Cancel(window.NavMeshData);
|
||||
}
|
||||
|
||||
if (window.Instance.valid)
|
||||
{
|
||||
UnityNavMesh.RemoveNavMeshData(window.Instance);
|
||||
window.Instance = default;
|
||||
}
|
||||
|
||||
window.ActiveBuild = null;
|
||||
window.NavMeshData = null;
|
||||
}
|
||||
|
||||
private Bounds GetChunkWorldBounds(Vector2Int chunkCoord)
|
||||
{
|
||||
float chunkSize = Mathf.Max(1f, chunkNavSourceReader.ChunkWorldSize);
|
||||
Vector3 min = new Vector3(chunkCoord.x * chunkSize, -500f, chunkCoord.y * chunkSize);
|
||||
Vector3 size = new Vector3(chunkSize, 1000f, chunkSize);
|
||||
return new Bounds(min + new Vector3(chunkSize * 0.5f, 0f, chunkSize * 0.5f), size);
|
||||
}
|
||||
|
||||
private Bounds ExpandCoverageBounds(Bounds bounds, int chunkMargin)
|
||||
{
|
||||
return ExpandChunkBounds(bounds, chunkMargin);
|
||||
}
|
||||
|
||||
private Bounds ExpandChunkBounds(Bounds bounds, int chunkMargin)
|
||||
{
|
||||
float chunkSize = Mathf.Max(1f, chunkNavSourceReader.ChunkWorldSize);
|
||||
float horizontalPadding = chunkMargin * chunkSize;
|
||||
bounds.Expand(new Vector3(horizontalPadding * 2f, 0f, horizontalPadding * 2f));
|
||||
return bounds;
|
||||
}
|
||||
|
||||
private static Bounds CalculateBounds(List<NavMeshBuildSource> sources)
|
||||
@@ -472,79 +715,145 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||
bounds.size = size;
|
||||
}
|
||||
|
||||
private Vector2Int WorldToChunk(Vector3 position)
|
||||
private static Bounds CreateQuantizedCoverageBounds(Bounds rawBounds, float padding, float minSize, float quantizationStep)
|
||||
{
|
||||
float chunkSize = Mathf.Max(1f, chunkNavSourceReader.ChunkWorldSize);
|
||||
return new Vector2Int(
|
||||
Mathf.FloorToInt(position.x / chunkSize),
|
||||
Mathf.FloorToInt(position.z / chunkSize));
|
||||
Vector3 min = rawBounds.min;
|
||||
Vector3 max = rawBounds.max;
|
||||
|
||||
min.x -= padding;
|
||||
min.z -= padding;
|
||||
max.x += padding;
|
||||
max.z += padding;
|
||||
|
||||
EnsureMinimumSpan(ref min.x, ref max.x, minSize);
|
||||
EnsureMinimumSpan(ref min.z, ref max.z, minSize);
|
||||
|
||||
min.x = quantizationStep * Mathf.Floor(min.x / quantizationStep);
|
||||
min.z = quantizationStep * Mathf.Floor(min.z / quantizationStep);
|
||||
max.x = quantizationStep * Mathf.Ceil(max.x / quantizationStep);
|
||||
max.z = quantizationStep * Mathf.Ceil(max.z / quantizationStep);
|
||||
|
||||
Vector3 center = new Vector3((min.x + max.x) * 0.5f, 0f, (min.z + max.z) * 0.5f);
|
||||
Vector3 size = new Vector3(Mathf.Max(max.x - min.x, minSize), 0.1f, Mathf.Max(max.z - min.z, minSize));
|
||||
return new Bounds(center, size);
|
||||
}
|
||||
|
||||
private Vector2Int ChunkToRegion(Vector2Int chunkCoord)
|
||||
private static void EnsureMinimumSpan(ref float min, ref float max, float minimumSize)
|
||||
{
|
||||
int size = Mathf.Max(1, config.navRegionSizeInChunks);
|
||||
return new Vector2Int(
|
||||
Mathf.FloorToInt(chunkCoord.x / (float)size),
|
||||
Mathf.FloorToInt(chunkCoord.y / (float)size));
|
||||
}
|
||||
|
||||
private Vector3 GetRegionCenter(Vector2Int regionCoord)
|
||||
{
|
||||
float chunkSize = Mathf.Max(1f, chunkNavSourceReader.ChunkWorldSize);
|
||||
float regionSize = Mathf.Max(1, config.navRegionSizeInChunks) * chunkSize;
|
||||
return new Vector3(
|
||||
(regionCoord.x + 0.5f) * regionSize,
|
||||
0f,
|
||||
(regionCoord.y + 0.5f) * regionSize);
|
||||
}
|
||||
|
||||
private static bool AreSetsEqual(HashSet<Vector2Int> left, HashSet<Vector2Int> right)
|
||||
{
|
||||
if (left.Count != right.Count)
|
||||
float currentSize = max - min;
|
||||
if (currentSize >= minimumSize)
|
||||
{
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Vector2Int value in left)
|
||||
float halfPadding = (minimumSize - currentSize) * 0.5f;
|
||||
min -= halfPadding;
|
||||
max += halfPadding;
|
||||
}
|
||||
|
||||
private static float DistanceToBoundsXZ(Bounds bounds, Vector3 point)
|
||||
{
|
||||
float dx = Mathf.Max(bounds.min.x - point.x, 0f, point.x - bounds.max.x);
|
||||
float dz = Mathf.Max(bounds.min.z - point.z, 0f, point.z - bounds.max.z);
|
||||
return Mathf.Sqrt(dx * dx + dz * dz);
|
||||
}
|
||||
|
||||
private static float DistanceBetweenBoundsXZ(Bounds left, Bounds right)
|
||||
{
|
||||
float dx = Mathf.Max(left.min.x - right.max.x, 0f, right.min.x - left.max.x);
|
||||
float dz = Mathf.Max(left.min.z - right.max.z, 0f, right.min.z - left.max.z);
|
||||
return Mathf.Sqrt(dx * dx + dz * dz);
|
||||
}
|
||||
|
||||
private static bool ContainsXZ(Bounds bounds, Vector3 position)
|
||||
{
|
||||
return position.x >= bounds.min.x && position.x <= bounds.max.x
|
||||
&& position.z >= bounds.min.z && position.z <= bounds.max.z;
|
||||
}
|
||||
|
||||
private static bool IntersectsXZ(Bounds left, Bounds right)
|
||||
{
|
||||
return left.min.x <= right.max.x && left.max.x >= right.min.x
|
||||
&& left.min.z <= right.max.z && left.max.z >= right.min.z;
|
||||
}
|
||||
|
||||
private static bool BoundsApproximatelyEqual(Bounds left, Bounds right)
|
||||
{
|
||||
return Vector3.SqrMagnitude(left.center - right.center) < 0.0001f
|
||||
&& Vector3.SqrMagnitude(left.size - right.size) < 0.0001f;
|
||||
}
|
||||
|
||||
private readonly struct DesiredCoverageWindow
|
||||
{
|
||||
public DesiredCoverageWindow(Bounds coverageBounds, float priority, int interestCount)
|
||||
{
|
||||
if (!right.Contains(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
CoverageBounds = coverageBounds;
|
||||
Priority = priority;
|
||||
InterestCount = interestCount;
|
||||
}
|
||||
|
||||
return true;
|
||||
public Bounds CoverageBounds { get; }
|
||||
public float Priority { get; }
|
||||
public int InterestCount { get; }
|
||||
}
|
||||
|
||||
private static int PositiveModulo(int value, int modulus)
|
||||
private struct ClusterAccumulator
|
||||
{
|
||||
int result = value % modulus;
|
||||
return result < 0 ? result + modulus : result;
|
||||
public ClusterAccumulator(WorldInterestPoint point)
|
||||
{
|
||||
RawBounds = new Bounds(new Vector3(point.Position.x, 0f, point.Position.z), new Vector3(0.1f, 0.1f, 0.1f));
|
||||
Priority = point.Priority;
|
||||
InterestCount = 1;
|
||||
}
|
||||
|
||||
public Bounds RawBounds;
|
||||
public float Priority;
|
||||
public int InterestCount;
|
||||
|
||||
public void Add(WorldInterestPoint point)
|
||||
{
|
||||
RawBounds.Encapsulate(new Vector3(point.Position.x, 0f, point.Position.z));
|
||||
Priority = Mathf.Max(Priority, point.Priority);
|
||||
InterestCount++;
|
||||
}
|
||||
|
||||
public void Merge(ClusterAccumulator other)
|
||||
{
|
||||
RawBounds.Encapsulate(other.RawBounds.min);
|
||||
RawBounds.Encapsulate(other.RawBounds.max);
|
||||
Priority = Mathf.Max(Priority, other.Priority);
|
||||
InterestCount += other.InterestCount;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NavRegionRuntime : IDisposable
|
||||
private sealed class NavCoverageWindowRuntime : IDisposable
|
||||
{
|
||||
public NavCoverageWindowRuntime(int id, Bounds coverageBounds, float priority, int interestCount)
|
||||
{
|
||||
Id = id;
|
||||
CoverageBounds = coverageBounds;
|
||||
Priority = priority;
|
||||
InterestCount = interestCount;
|
||||
State = NavCoverageState.Pending;
|
||||
}
|
||||
|
||||
public int Id { get; }
|
||||
public Bounds CoverageBounds;
|
||||
public Bounds CollectionBounds;
|
||||
public Bounds BuildBounds;
|
||||
public float Priority;
|
||||
public int InterestCount;
|
||||
public NavCoverageState State;
|
||||
public NavMeshData NavMeshData;
|
||||
public NavMeshDataInstance Instance;
|
||||
public AsyncOperation ActiveBuild;
|
||||
public bool BuildRequestedWhileRunning;
|
||||
public Bounds BuildBounds;
|
||||
public bool MatchedThisFrame;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (ActiveBuild != null && !ActiveBuild.isDone && NavMeshData != null)
|
||||
{
|
||||
UnityNavMeshBuilder.Cancel(NavMeshData);
|
||||
}
|
||||
|
||||
if (Instance.valid)
|
||||
{
|
||||
UnityNavMesh.RemoveNavMeshData(Instance);
|
||||
Instance = default;
|
||||
}
|
||||
|
||||
ActiveBuild = null;
|
||||
NavMeshData = null;
|
||||
RemoveCoverageData(this);
|
||||
State = NavCoverageState.Pending;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.Collections.Generic;
|
||||
using InfiniteWorld.VoxelWorld;
|
||||
using InfiniteWorld.VoxelWorld.Contracts;
|
||||
using UnityEngine;
|
||||
|
||||
namespace VoxelWorldScene
|
||||
{
|
||||
public sealed class SceneWorldInterestReader : IWorldInterestReader
|
||||
{
|
||||
private readonly VoxelWorldGenerator worldGenerator;
|
||||
private VoxelWorldSpawnAnchor[] spawnAnchors;
|
||||
private int lastAnchorRefreshFrame = -1;
|
||||
|
||||
public SceneWorldInterestReader(VoxelWorldGenerator worldGenerator)
|
||||
{
|
||||
this.worldGenerator = worldGenerator;
|
||||
}
|
||||
|
||||
public int InterestVersion => worldGenerator != null ? worldGenerator.InterestVersion : 0;
|
||||
|
||||
public void GetInterestPoints(List<WorldInterestPoint> results)
|
||||
{
|
||||
if (results == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
worldGenerator?.GetInterestPoints(results);
|
||||
RefreshSpawnAnchors();
|
||||
|
||||
if (spawnAnchors == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < spawnAnchors.Length; i++)
|
||||
{
|
||||
VoxelWorldSpawnAnchor anchor = spawnAnchors[i];
|
||||
if (anchor == null || !anchor.isActiveAndEnabled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
results.Add(new WorldInterestPoint(anchor.transform.position, anchor.Priority, WorldInterestKind.SpawnAnchor));
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshSpawnAnchors()
|
||||
{
|
||||
if (lastAnchorRefreshFrame == Time.frameCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lastAnchorRefreshFrame = Time.frameCount;
|
||||
spawnAnchors = Object.FindObjectsByType<VoxelWorldSpawnAnchor>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f1f0155f1e6452486d2f44f9dcefd5a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -30,8 +30,9 @@ namespace VoxelWorldScene
|
||||
|
||||
builder.RegisterMessagePipe();
|
||||
builder.RegisterInstance(config);
|
||||
builder.RegisterInstance(worldGenerator).As<IChunkNavSourceReader>().As<IWorldInterestReader>().AsSelf();
|
||||
builder.RegisterEntryPoint<VoxelWorldNavMeshService>();
|
||||
builder.RegisterInstance(worldGenerator).As<IChunkNavSourceReader>().AsSelf();
|
||||
builder.Register<SceneWorldInterestReader>(Lifetime.Singleton).As<IWorldInterestReader>();
|
||||
builder.RegisterEntryPoint<VoxelWorldNavMeshService>().AsSelf();
|
||||
builder.RegisterBuildCallback(ResolvePublishers);
|
||||
}
|
||||
|
||||
@@ -48,4 +49,5 @@ namespace VoxelWorldScene
|
||||
resolver.Resolve<IPublisher<WorldInterestChangedMessage>>());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,11 +40,6 @@ namespace VoxelWorldScene
|
||||
return explicitStreamTarget;
|
||||
}
|
||||
|
||||
if (currentStreamTarget != null)
|
||||
{
|
||||
return currentStreamTarget;
|
||||
}
|
||||
|
||||
CameraFollow[] cameraFollows = FindObjectsByType<CameraFollow>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||||
for (int i = 0; i < cameraFollows.Length; i++)
|
||||
{
|
||||
@@ -55,6 +50,16 @@ namespace VoxelWorldScene
|
||||
}
|
||||
}
|
||||
|
||||
VoxelWorldSpawnAnchor[] spawnAnchors = FindObjectsByType<VoxelWorldSpawnAnchor>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||||
for (int i = 0; i < spawnAnchors.Length; i++)
|
||||
{
|
||||
VoxelWorldSpawnAnchor anchor = spawnAnchors[i];
|
||||
if (anchor != null && anchor.isActiveAndEnabled)
|
||||
{
|
||||
return anchor.transform;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace VoxelWorldScene
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class VoxelWorldSpawnAnchor : MonoBehaviour
|
||||
{
|
||||
[SerializeField, Min(0.01f)] private float priority = 2f;
|
||||
|
||||
public float Priority => priority;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a0a7758ae4541b39ed0b5d1fe912869
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user