reorganize navmesh contracts and services
Split VoxelWorld nav contracts into focused files and extract clustered coverage helpers so the navmesh service stays a coordinator instead of a catch-all runtime file.
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.AI;
|
|
||||||
|
|
||||||
namespace InfiniteWorld.VoxelWorld.Contracts
|
namespace InfiniteWorld.VoxelWorld.Contracts
|
||||||
{
|
{
|
||||||
@@ -34,136 +33,4 @@ namespace InfiniteWorld.VoxelWorld.Contracts
|
|||||||
int HintVersion { get; }
|
int HintVersion { get; }
|
||||||
void GetHintPoints(List<WorldInterestPoint> results);
|
void GetHintPoints(List<WorldInterestPoint> results);
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly struct ChunkNavSourceSnapshot
|
|
||||||
{
|
|
||||||
public ChunkNavSourceSnapshot(Vector2Int coord, int version, ChunkNavBuildSourceDescriptor[] sources)
|
|
||||||
{
|
|
||||||
Coord = coord;
|
|
||||||
Version = version;
|
|
||||||
Sources = sources;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector2Int Coord { get; }
|
|
||||||
public int Version { get; }
|
|
||||||
public ChunkNavBuildSourceDescriptor[] Sources { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly struct ChunkNavBuildSourceDescriptor
|
|
||||||
{
|
|
||||||
public ChunkNavBuildSourceDescriptor(NavMeshBuildSourceShape shape, Matrix4x4 transform, Vector3 size, Mesh mesh, int area)
|
|
||||||
{
|
|
||||||
Shape = shape;
|
|
||||||
Transform = transform;
|
|
||||||
Size = size;
|
|
||||||
Mesh = mesh;
|
|
||||||
Area = area;
|
|
||||||
}
|
|
||||||
|
|
||||||
public NavMeshBuildSourceShape Shape { get; }
|
|
||||||
public Matrix4x4 Transform { get; }
|
|
||||||
public Vector3 Size { get; }
|
|
||||||
public Mesh Mesh { get; }
|
|
||||||
public int Area { get; }
|
|
||||||
|
|
||||||
public static ChunkNavBuildSourceDescriptor CreateBox(Matrix4x4 transform, Vector3 size, int area = 0)
|
|
||||||
{
|
|
||||||
return new ChunkNavBuildSourceDescriptor(NavMeshBuildSourceShape.Box, transform, size, null, area);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ChunkNavBuildSourceDescriptor CreateMesh(Matrix4x4 transform, Mesh mesh, int area = 0)
|
|
||||||
{
|
|
||||||
return new ChunkNavBuildSourceDescriptor(NavMeshBuildSourceShape.Mesh, transform, Vector3.zero, mesh, area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly struct WorldInterestPoint
|
|
||||||
{
|
|
||||||
public WorldInterestPoint(Vector3 position, float priority, WorldInterestKind kind)
|
|
||||||
{
|
|
||||||
Position = position;
|
|
||||||
Priority = priority;
|
|
||||||
Kind = kind;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector3 Position { get; }
|
|
||||||
public float Priority { get; }
|
|
||||||
public WorldInterestKind Kind { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum WorldInterestKind
|
|
||||||
{
|
|
||||||
PlayerActor = 0,
|
|
||||||
ActiveNpc = 1,
|
|
||||||
SpawnAnchor = 2,
|
|
||||||
TransientNavHint = 3,
|
|
||||||
Other = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
public ChunkNavGeometryReadyMessage(Vector2Int coord, int version)
|
|
||||||
{
|
|
||||||
Coord = coord;
|
|
||||||
Version = version;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector2Int Coord { get; }
|
|
||||||
public int Version { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly struct ChunkNavGeometryRemovedMessage
|
|
||||||
{
|
|
||||||
public ChunkNavGeometryRemovedMessage(Vector2Int coord, int version)
|
|
||||||
{
|
|
||||||
Coord = coord;
|
|
||||||
Version = version;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector2Int Coord { get; }
|
|
||||||
public int Version { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly struct WorldInterestChangedMessage
|
|
||||||
{
|
|
||||||
public WorldInterestChangedMessage(int version)
|
|
||||||
{
|
|
||||||
Version = version;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Version { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly struct NavCoverageHintChangedMessage
|
|
||||||
{
|
|
||||||
public NavCoverageHintChangedMessage(int version)
|
|
||||||
{
|
|
||||||
Version = version;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Version { get; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace InfiniteWorld.VoxelWorld.Contracts
|
||||||
|
{
|
||||||
|
public enum WorldInterestKind
|
||||||
|
{
|
||||||
|
PlayerActor = 0,
|
||||||
|
ActiveNpc = 1,
|
||||||
|
SpawnAnchor = 2,
|
||||||
|
TransientNavHint = 3,
|
||||||
|
Other = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum NavCoverageState
|
||||||
|
{
|
||||||
|
Pending = 0,
|
||||||
|
Building = 1,
|
||||||
|
Ready = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4112a97dd67e45aca6f2c0928de438bd
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace InfiniteWorld.VoxelWorld.Contracts
|
||||||
|
{
|
||||||
|
public readonly struct ChunkNavGeometryReadyMessage
|
||||||
|
{
|
||||||
|
public ChunkNavGeometryReadyMessage(Vector2Int coord, int version)
|
||||||
|
{
|
||||||
|
Coord = coord;
|
||||||
|
Version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2Int Coord { get; }
|
||||||
|
public int Version { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct ChunkNavGeometryRemovedMessage
|
||||||
|
{
|
||||||
|
public ChunkNavGeometryRemovedMessage(Vector2Int coord, int version)
|
||||||
|
{
|
||||||
|
Coord = coord;
|
||||||
|
Version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2Int Coord { get; }
|
||||||
|
public int Version { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct WorldInterestChangedMessage
|
||||||
|
{
|
||||||
|
public WorldInterestChangedMessage(int version)
|
||||||
|
{
|
||||||
|
Version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Version { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct NavCoverageHintChangedMessage
|
||||||
|
{
|
||||||
|
public NavCoverageHintChangedMessage(int version)
|
||||||
|
{
|
||||||
|
Version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Version { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e2ea3cb8fdd545019f666d378bc8eaaa
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.AI;
|
||||||
|
|
||||||
|
namespace InfiniteWorld.VoxelWorld.Contracts
|
||||||
|
{
|
||||||
|
public readonly struct ChunkNavSourceSnapshot
|
||||||
|
{
|
||||||
|
public ChunkNavSourceSnapshot(Vector2Int coord, int version, ChunkNavBuildSourceDescriptor[] sources)
|
||||||
|
{
|
||||||
|
Coord = coord;
|
||||||
|
Version = version;
|
||||||
|
Sources = sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2Int Coord { get; }
|
||||||
|
public int Version { get; }
|
||||||
|
public ChunkNavBuildSourceDescriptor[] Sources { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct ChunkNavBuildSourceDescriptor
|
||||||
|
{
|
||||||
|
public ChunkNavBuildSourceDescriptor(NavMeshBuildSourceShape shape, Matrix4x4 transform, Vector3 size, Mesh mesh, int area)
|
||||||
|
{
|
||||||
|
Shape = shape;
|
||||||
|
Transform = transform;
|
||||||
|
Size = size;
|
||||||
|
Mesh = mesh;
|
||||||
|
Area = area;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NavMeshBuildSourceShape Shape { get; }
|
||||||
|
public Matrix4x4 Transform { get; }
|
||||||
|
public Vector3 Size { get; }
|
||||||
|
public Mesh Mesh { get; }
|
||||||
|
public int Area { get; }
|
||||||
|
|
||||||
|
public static ChunkNavBuildSourceDescriptor CreateBox(Matrix4x4 transform, Vector3 size, int area = 0)
|
||||||
|
{
|
||||||
|
return new ChunkNavBuildSourceDescriptor(NavMeshBuildSourceShape.Box, transform, size, null, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ChunkNavBuildSourceDescriptor CreateMesh(Matrix4x4 transform, Mesh mesh, int area = 0)
|
||||||
|
{
|
||||||
|
return new ChunkNavBuildSourceDescriptor(NavMeshBuildSourceShape.Mesh, transform, Vector3.zero, mesh, area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct WorldInterestPoint
|
||||||
|
{
|
||||||
|
public WorldInterestPoint(Vector3 position, float priority, WorldInterestKind kind)
|
||||||
|
{
|
||||||
|
Position = position;
|
||||||
|
Priority = priority;
|
||||||
|
Kind = kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3 Position { get; }
|
||||||
|
public float Priority { get; }
|
||||||
|
public WorldInterestKind Kind { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 91e1b6896fdd4f7a9968cc4af4bf7550
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using InfiniteWorld.VoxelWorld.Contracts;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.AI;
|
||||||
|
|
||||||
|
namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||||
|
{
|
||||||
|
internal static class NavBuildSourceCollector
|
||||||
|
{
|
||||||
|
public static bool CollectBuildSources(
|
||||||
|
IChunkNavSourceReader chunkNavSourceReader,
|
||||||
|
Bounds coverageBounds,
|
||||||
|
List<Vector2Int> loadedChunkCoords,
|
||||||
|
List<NavMeshBuildSource> results)
|
||||||
|
{
|
||||||
|
loadedChunkCoords.Clear();
|
||||||
|
chunkNavSourceReader.GetLoadedChunkCoords(loadedChunkCoords);
|
||||||
|
|
||||||
|
bool hasSources = false;
|
||||||
|
for (int i = 0; i < loadedChunkCoords.Count; i++)
|
||||||
|
{
|
||||||
|
Vector2Int chunkCoord = loadedChunkCoords[i];
|
||||||
|
if (!NavMeshBoundsUtility.IntersectsXZ(GetChunkWorldBounds(chunkNavSourceReader, chunkCoord), coverageBounds))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chunkNavSourceReader.TryGetChunkNavSourceSnapshot(chunkCoord, out ChunkNavSourceSnapshot snapshot) || snapshot.Sources == null || snapshot.Sources.Length == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSources = true;
|
||||||
|
AppendBuildSources(snapshot.Sources, results);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Bounds GetChunkWorldBounds(IChunkNavSourceReader chunkNavSourceReader, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Bounds ExpandCoverageBounds(IChunkNavSourceReader chunkNavSourceReader, Bounds bounds, int chunkMargin)
|
||||||
|
{
|
||||||
|
return ExpandChunkBounds(chunkNavSourceReader, bounds, chunkMargin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Bounds ExpandChunkBounds(IChunkNavSourceReader chunkNavSourceReader, 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 void AppendBuildSources(ChunkNavBuildSourceDescriptor[] descriptors, List<NavMeshBuildSource> results)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < descriptors.Length; i++)
|
||||||
|
{
|
||||||
|
ChunkNavBuildSourceDescriptor descriptor = descriptors[i];
|
||||||
|
if (descriptor.Shape == NavMeshBuildSourceShape.Mesh && descriptor.Mesh == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
NavMeshBuildSource source = new NavMeshBuildSource
|
||||||
|
{
|
||||||
|
area = descriptor.Area,
|
||||||
|
shape = descriptor.Shape,
|
||||||
|
transform = descriptor.Transform,
|
||||||
|
size = descriptor.Size,
|
||||||
|
sourceObject = descriptor.Shape == NavMeshBuildSourceShape.Mesh ? descriptor.Mesh : null
|
||||||
|
};
|
||||||
|
|
||||||
|
results.Add(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9f2d97479ccb4401bc37fd6481d83304
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using InfiniteWorld.VoxelWorld.Contracts;
|
||||||
|
using MessagePipe;
|
||||||
|
using UnityEngine;
|
||||||
|
using VContainer.Unity;
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4c6dcd38712d499fb48ec43c0ec77031
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using InfiniteWorld.VoxelWorld.Contracts;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||||
|
{
|
||||||
|
internal static class NavCoveragePlanning
|
||||||
|
{
|
||||||
|
public static void BuildDesiredCoverageWindows(
|
||||||
|
List<WorldInterestPoint> interestPoints,
|
||||||
|
VoxelWorldNavMeshConfig config,
|
||||||
|
float chunkWorldSize,
|
||||||
|
List<DesiredCoverageWindow> desiredCoverageWindows,
|
||||||
|
List<ClusterAccumulator> clusterAccumulators)
|
||||||
|
{
|
||||||
|
desiredCoverageWindows.Clear();
|
||||||
|
clusterAccumulators.Clear();
|
||||||
|
|
||||||
|
if (interestPoints.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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++)
|
||||||
|
{
|
||||||
|
WorldInterestPoint point = interestPoints[i];
|
||||||
|
int bestClusterIndex = -1;
|
||||||
|
float bestDistance = float.MaxValue;
|
||||||
|
|
||||||
|
for (int clusterIndex = 0; clusterIndex < clusterAccumulators.Count; clusterIndex++)
|
||||||
|
{
|
||||||
|
float distance = NavMeshBoundsUtility.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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MergeNearbyClusters(clusterAccumulators, mergeDistance);
|
||||||
|
|
||||||
|
for (int i = 0; i < clusterAccumulators.Count; i++)
|
||||||
|
{
|
||||||
|
ClusterAccumulator cluster = clusterAccumulators[i];
|
||||||
|
Bounds coverageBounds = NavMeshBoundsUtility.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NavCoverageWindowRuntime FindBestMatchingCoverageWindow(
|
||||||
|
DesiredCoverageWindow desiredWindow,
|
||||||
|
Dictionary<int, NavCoverageWindowRuntime> coverageWindows,
|
||||||
|
float chunkWorldSize,
|
||||||
|
VoxelWorldNavMeshConfig config)
|
||||||
|
{
|
||||||
|
NavCoverageWindowRuntime bestMatch = null;
|
||||||
|
float bestDistance = float.MaxValue;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float GetCoveragePriorityScore(NavCoverageWindowRuntime window, List<WorldInterestPoint> interestPoints)
|
||||||
|
{
|
||||||
|
if (interestPoints.Count == 0)
|
||||||
|
{
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = Vector2.SqrMagnitude(
|
||||||
|
new Vector2(center.x, center.z) - new Vector2(interestPoints[i].Position.x, interestPoints[i].Position.z)) / priority;
|
||||||
|
if (distance < bestDistance)
|
||||||
|
{
|
||||||
|
bestDistance = distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MergeNearbyClusters(List<ClusterAccumulator> clusterAccumulators, float mergeDistance)
|
||||||
|
{
|
||||||
|
if (clusterAccumulators.Count < 2)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool merged;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
merged = false;
|
||||||
|
for (int i = 0; i < clusterAccumulators.Count; i++)
|
||||||
|
{
|
||||||
|
for (int j = i + 1; j < clusterAccumulators.Count; j++)
|
||||||
|
{
|
||||||
|
if (NavMeshBoundsUtility.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal readonly struct DesiredCoverageWindow
|
||||||
|
{
|
||||||
|
public DesiredCoverageWindow(Bounds coverageBounds, float priority, int interestCount)
|
||||||
|
{
|
||||||
|
CoverageBounds = coverageBounds;
|
||||||
|
Priority = priority;
|
||||||
|
InterestCount = interestCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bounds CoverageBounds { get; }
|
||||||
|
public float Priority { get; }
|
||||||
|
public int InterestCount { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal struct ClusterAccumulator
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 74d2bb8418be4671a146c0949637163c
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using System;
|
||||||
|
using InfiniteWorld.VoxelWorld.Contracts;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.AI;
|
||||||
|
using UnityNavMesh = UnityEngine.AI.NavMesh;
|
||||||
|
using UnityNavMeshBuilder = UnityEngine.AI.NavMeshBuilder;
|
||||||
|
|
||||||
|
namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||||
|
{
|
||||||
|
internal 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 bool MatchedThisFrame;
|
||||||
|
|
||||||
|
public void ResetCoverageData()
|
||||||
|
{
|
||||||
|
if (ActiveBuild != null && !ActiveBuild.isDone && NavMeshData != null)
|
||||||
|
{
|
||||||
|
UnityNavMeshBuilder.Cancel(NavMeshData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Instance.valid)
|
||||||
|
{
|
||||||
|
UnityNavMesh.RemoveNavMeshData(Instance);
|
||||||
|
Instance = default;
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveBuild = null;
|
||||||
|
NavMeshData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
ResetCoverageData();
|
||||||
|
State = NavCoverageState.Pending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: caa4b87bcf874133b155e44475c58ca3
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.AI;
|
||||||
|
|
||||||
|
namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||||
|
{
|
||||||
|
internal static class NavMeshBoundsUtility
|
||||||
|
{
|
||||||
|
public static Bounds CalculateBounds(List<NavMeshBuildSource> sources)
|
||||||
|
{
|
||||||
|
Bounds bounds = GetSourceBounds(sources[0]);
|
||||||
|
for (int i = 1; i < sources.Count; i++)
|
||||||
|
{
|
||||||
|
bounds.Encapsulate(GetSourceBounds(sources[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ExpandBounds(ref Bounds bounds, float horizontalPadding, float verticalPadding)
|
||||||
|
{
|
||||||
|
Vector3 size = bounds.size;
|
||||||
|
size.x = Mathf.Max(size.x + horizontalPadding * 2f, 0.1f);
|
||||||
|
size.z = Mathf.Max(size.z + horizontalPadding * 2f, 0.1f);
|
||||||
|
size.y = Mathf.Max(size.y + verticalPadding * 2f, 0.1f);
|
||||||
|
bounds.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Bounds CreateQuantizedCoverageBounds(Bounds rawBounds, float padding, float minSize, float quantizationStep)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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 static Bounds GetSourceBounds(NavMeshBuildSource source)
|
||||||
|
{
|
||||||
|
if (source.shape == NavMeshBuildSourceShape.Box)
|
||||||
|
{
|
||||||
|
return TransformBounds(source.transform, new Bounds(Vector3.zero, source.size));
|
||||||
|
}
|
||||||
|
|
||||||
|
Mesh mesh = source.sourceObject as Mesh;
|
||||||
|
if (mesh != null)
|
||||||
|
{
|
||||||
|
return TransformBounds(source.transform, mesh.bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Bounds(source.transform.GetColumn(3), Vector3.zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Bounds TransformBounds(Matrix4x4 matrix, Bounds localBounds)
|
||||||
|
{
|
||||||
|
Vector3 center = localBounds.center;
|
||||||
|
Vector3 extents = localBounds.extents;
|
||||||
|
|
||||||
|
Vector3[] corners =
|
||||||
|
{
|
||||||
|
new Vector3(center.x - extents.x, center.y - extents.y, center.z - extents.z),
|
||||||
|
new Vector3(center.x - extents.x, center.y - extents.y, center.z + extents.z),
|
||||||
|
new Vector3(center.x - extents.x, center.y + extents.y, center.z - extents.z),
|
||||||
|
new Vector3(center.x - extents.x, center.y + extents.y, center.z + extents.z),
|
||||||
|
new Vector3(center.x + extents.x, center.y - extents.y, center.z - extents.z),
|
||||||
|
new Vector3(center.x + extents.x, center.y - extents.y, center.z + extents.z),
|
||||||
|
new Vector3(center.x + extents.x, center.y + extents.y, center.z - extents.z),
|
||||||
|
new Vector3(center.x + extents.x, center.y + extents.y, center.z + extents.z)
|
||||||
|
};
|
||||||
|
|
||||||
|
Bounds worldBounds = new Bounds(matrix.MultiplyPoint3x4(corners[0]), Vector3.zero);
|
||||||
|
for (int i = 1; i < corners.Length; i++)
|
||||||
|
{
|
||||||
|
worldBounds.Encapsulate(matrix.MultiplyPoint3x4(corners[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return worldBounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureMinimumSpan(ref float min, ref float max, float minimumSize)
|
||||||
|
{
|
||||||
|
float currentSize = max - min;
|
||||||
|
if (currentSize >= minimumSize)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float halfPadding = (minimumSize - currentSize) * 0.5f;
|
||||||
|
min -= halfPadding;
|
||||||
|
max += halfPadding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b94f04d9597e4174b88035a1751b84fe
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -27,7 +27,6 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
|||||||
private readonly List<WorldInterestPoint> interestPoints = new List<WorldInterestPoint>(8);
|
private readonly List<WorldInterestPoint> interestPoints = new List<WorldInterestPoint>(8);
|
||||||
private readonly List<Vector2Int> loadedChunkCoords = new List<Vector2Int>(128);
|
private readonly List<Vector2Int> loadedChunkCoords = new List<Vector2Int>(128);
|
||||||
private readonly List<NavMeshBuildSource> buildSources = new List<NavMeshBuildSource>(256);
|
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<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>(4);
|
private readonly List<IDisposable> subscriptions = new List<IDisposable>(4);
|
||||||
@@ -98,7 +97,7 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
|||||||
foreach (KeyValuePair<int, NavCoverageWindowRuntime> pair in coverageWindows)
|
foreach (KeyValuePair<int, NavCoverageWindowRuntime> pair in coverageWindows)
|
||||||
{
|
{
|
||||||
NavCoverageWindowRuntime window = pair.Value;
|
NavCoverageWindowRuntime window = pair.Value;
|
||||||
if (window.State == NavCoverageState.Ready && ContainsXZ(window.CoverageBounds, worldPosition))
|
if (window.State == NavCoverageState.Ready && NavMeshBoundsUtility.ContainsXZ(window.CoverageBounds, worldPosition))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -140,7 +139,6 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
|||||||
dirtyCoverageWindowIds.Clear();
|
dirtyCoverageWindowIds.Clear();
|
||||||
desiredCoverageWindows.Clear();
|
desiredCoverageWindows.Clear();
|
||||||
clusterAccumulators.Clear();
|
clusterAccumulators.Clear();
|
||||||
coverageWindowSnapshots.Clear();
|
|
||||||
activeBuildWindowId = null;
|
activeBuildWindowId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +175,12 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
|||||||
|
|
||||||
private void SyncCoverageWindows()
|
private void SyncCoverageWindows()
|
||||||
{
|
{
|
||||||
BuildDesiredCoverageWindows();
|
NavCoveragePlanning.BuildDesiredCoverageWindows(
|
||||||
|
interestPoints,
|
||||||
|
config,
|
||||||
|
Mathf.Max(1f, chunkNavSourceReader.ChunkWorldSize),
|
||||||
|
desiredCoverageWindows,
|
||||||
|
clusterAccumulators);
|
||||||
|
|
||||||
foreach (KeyValuePair<int, NavCoverageWindowRuntime> pair in coverageWindows)
|
foreach (KeyValuePair<int, NavCoverageWindowRuntime> pair in coverageWindows)
|
||||||
{
|
{
|
||||||
@@ -187,7 +190,11 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
|||||||
for (int i = 0; i < desiredCoverageWindows.Count; i++)
|
for (int i = 0; i < desiredCoverageWindows.Count; i++)
|
||||||
{
|
{
|
||||||
DesiredCoverageWindow desiredWindow = desiredCoverageWindows[i];
|
DesiredCoverageWindow desiredWindow = desiredCoverageWindows[i];
|
||||||
NavCoverageWindowRuntime runtime = FindBestMatchingCoverageWindow(desiredWindow);
|
NavCoverageWindowRuntime runtime = NavCoveragePlanning.FindBestMatchingCoverageWindow(
|
||||||
|
desiredWindow,
|
||||||
|
coverageWindows,
|
||||||
|
Mathf.Max(1f, chunkNavSourceReader.ChunkWorldSize),
|
||||||
|
config);
|
||||||
if (runtime == null)
|
if (runtime == null)
|
||||||
{
|
{
|
||||||
runtime = new NavCoverageWindowRuntime(nextCoverageWindowId++, desiredWindow.CoverageBounds, desiredWindow.Priority, desiredWindow.InterestCount);
|
runtime = new NavCoverageWindowRuntime(nextCoverageWindowId++, desiredWindow.CoverageBounds, desiredWindow.Priority, desiredWindow.InterestCount);
|
||||||
@@ -201,7 +208,7 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
|||||||
runtime.Priority = desiredWindow.Priority;
|
runtime.Priority = desiredWindow.Priority;
|
||||||
runtime.InterestCount = desiredWindow.InterestCount;
|
runtime.InterestCount = desiredWindow.InterestCount;
|
||||||
|
|
||||||
if (!BoundsApproximatelyEqual(runtime.CoverageBounds, desiredWindow.CoverageBounds))
|
if (!NavMeshBoundsUtility.BoundsApproximatelyEqual(runtime.CoverageBounds, desiredWindow.CoverageBounds))
|
||||||
{
|
{
|
||||||
runtime.CoverageBounds = desiredWindow.CoverageBounds;
|
runtime.CoverageBounds = desiredWindow.CoverageBounds;
|
||||||
runtime.State = NavCoverageState.Pending;
|
runtime.State = NavCoverageState.Pending;
|
||||||
@@ -209,163 +216,9 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
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++)
|
|
||||||
{
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MergeNearbyClusters(mergeDistance);
|
|
||||||
|
|
||||||
for (int i = 0; i < clusterAccumulators.Count; i++)
|
|
||||||
{
|
|
||||||
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 MergeNearbyClusters(float mergeDistance)
|
|
||||||
{
|
|
||||||
if (clusterAccumulators.Count < 2)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool merged;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
merged = false;
|
|
||||||
for (int i = 0; i < clusterAccumulators.Count; i++)
|
|
||||||
{
|
|
||||||
for (int j = i + 1; j < clusterAccumulators.Count; j++)
|
|
||||||
{
|
|
||||||
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()
|
private void RemoveUnmatchedCoverageWindows()
|
||||||
{
|
{
|
||||||
List<int> windowsToRemove = null;
|
List<int> windowsToRemove = null;
|
||||||
@@ -402,11 +255,19 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
|||||||
|
|
||||||
private void MarkCoverageWindowsDirtyForChunk(Vector2Int chunkCoord)
|
private void MarkCoverageWindowsDirtyForChunk(Vector2Int chunkCoord)
|
||||||
{
|
{
|
||||||
Bounds chunkBounds = ExpandChunkBounds(GetChunkWorldBounds(chunkCoord), Mathf.Max(0, config.chunkCollectionMarginInChunks));
|
Bounds chunkBounds = NavBuildSourceCollector.ExpandChunkBounds(
|
||||||
|
chunkNavSourceReader,
|
||||||
|
NavBuildSourceCollector.GetChunkWorldBounds(chunkNavSourceReader, chunkCoord),
|
||||||
|
Mathf.Max(0, config.chunkCollectionMarginInChunks));
|
||||||
|
|
||||||
foreach (KeyValuePair<int, NavCoverageWindowRuntime> pair in coverageWindows)
|
foreach (KeyValuePair<int, NavCoverageWindowRuntime> pair in coverageWindows)
|
||||||
{
|
{
|
||||||
Bounds invalidationBounds = ExpandCoverageBounds(pair.Value.CoverageBounds, Mathf.Max(0, config.chunkCollectionMarginInChunks));
|
Bounds invalidationBounds = NavBuildSourceCollector.ExpandCoverageBounds(
|
||||||
if (IntersectsXZ(invalidationBounds, chunkBounds))
|
chunkNavSourceReader,
|
||||||
|
pair.Value.CoverageBounds,
|
||||||
|
Mathf.Max(0, config.chunkCollectionMarginInChunks));
|
||||||
|
|
||||||
|
if (NavMeshBoundsUtility.IntersectsXZ(invalidationBounds, chunkBounds))
|
||||||
{
|
{
|
||||||
EnqueueDirtyCoverageWindow(pair.Key);
|
EnqueueDirtyCoverageWindow(pair.Key);
|
||||||
}
|
}
|
||||||
@@ -446,7 +307,7 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
float score = GetCoveragePriorityScore(window);
|
float score = NavCoveragePlanning.GetCoveragePriorityScore(window, interestPoints);
|
||||||
if (score < bestScore)
|
if (score < bestScore)
|
||||||
{
|
{
|
||||||
bestScore = score;
|
bestScore = score;
|
||||||
@@ -471,29 +332,6 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
|||||||
return bestWindowId;
|
return bestWindowId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private float GetCoveragePriorityScore(NavCoverageWindowRuntime window)
|
|
||||||
{
|
|
||||||
if (interestPoints.Count == 0)
|
|
||||||
{
|
|
||||||
return 0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = Vector2.SqrMagnitude(
|
|
||||||
new Vector2(center.x, center.z) - new Vector2(interestPoints[i].Position.x, interestPoints[i].Position.z)) / priority;
|
|
||||||
if (distance < bestDistance)
|
|
||||||
{
|
|
||||||
bestDistance = distance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bestDistance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryStartCoverageBuild(int windowId)
|
private bool TryStartCoverageBuild(int windowId)
|
||||||
{
|
{
|
||||||
if (!coverageWindows.TryGetValue(windowId, out NavCoverageWindowRuntime window))
|
if (!coverageWindows.TryGetValue(windowId, out NavCoverageWindowRuntime window))
|
||||||
@@ -502,18 +340,26 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildSources.Clear();
|
buildSources.Clear();
|
||||||
window.CollectionBounds = ExpandCoverageBounds(window.CoverageBounds, Mathf.Max(0, config.chunkCollectionMarginInChunks));
|
window.CollectionBounds = NavBuildSourceCollector.ExpandCoverageBounds(
|
||||||
|
chunkNavSourceReader,
|
||||||
|
window.CoverageBounds,
|
||||||
|
Mathf.Max(0, config.chunkCollectionMarginInChunks));
|
||||||
|
|
||||||
|
bool hasSources = NavBuildSourceCollector.CollectBuildSources(
|
||||||
|
chunkNavSourceReader,
|
||||||
|
window.CollectionBounds,
|
||||||
|
loadedChunkCoords,
|
||||||
|
buildSources);
|
||||||
|
|
||||||
bool hasSources = CollectBuildSources(window.CollectionBounds, buildSources);
|
|
||||||
if (!hasSources || buildSources.Count == 0)
|
if (!hasSources || buildSources.Count == 0)
|
||||||
{
|
{
|
||||||
window.State = NavCoverageState.Pending;
|
window.State = NavCoverageState.Pending;
|
||||||
RemoveCoverageData(window);
|
window.ResetCoverageData();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bounds buildBounds = CalculateBounds(buildSources);
|
Bounds buildBounds = NavMeshBoundsUtility.CalculateBounds(buildSources);
|
||||||
ExpandBounds(ref buildBounds, config.navBoundsHorizontalPadding, config.navBoundsVerticalPadding);
|
NavMeshBoundsUtility.ExpandBounds(ref buildBounds, config.navBoundsHorizontalPadding, config.navBoundsVerticalPadding);
|
||||||
window.BuildBounds = buildBounds;
|
window.BuildBounds = buildBounds;
|
||||||
window.BuildRequestedWhileRunning = false;
|
window.BuildRequestedWhileRunning = false;
|
||||||
|
|
||||||
@@ -534,55 +380,6 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CollectBuildSources(Bounds coverageBounds, List<NavMeshBuildSource> results)
|
|
||||||
{
|
|
||||||
loadedChunkCoords.Clear();
|
|
||||||
chunkNavSourceReader.GetLoadedChunkCoords(loadedChunkCoords);
|
|
||||||
|
|
||||||
bool hasSources = false;
|
|
||||||
for (int i = 0; i < loadedChunkCoords.Count; i++)
|
|
||||||
{
|
|
||||||
Vector2Int chunkCoord = loadedChunkCoords[i];
|
|
||||||
if (!IntersectsXZ(GetChunkWorldBounds(chunkCoord), coverageBounds))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!chunkNavSourceReader.TryGetChunkNavSourceSnapshot(chunkCoord, out ChunkNavSourceSnapshot snapshot) || snapshot.Sources == null || snapshot.Sources.Length == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasSources = true;
|
|
||||||
AppendBuildSources(snapshot.Sources, results);
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasSources;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AppendBuildSources(ChunkNavBuildSourceDescriptor[] descriptors, List<NavMeshBuildSource> results)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < descriptors.Length; i++)
|
|
||||||
{
|
|
||||||
ChunkNavBuildSourceDescriptor descriptor = descriptors[i];
|
|
||||||
if (descriptor.Shape == NavMeshBuildSourceShape.Mesh && descriptor.Mesh == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
NavMeshBuildSource source = new NavMeshBuildSource
|
|
||||||
{
|
|
||||||
area = descriptor.Area,
|
|
||||||
shape = descriptor.Shape,
|
|
||||||
transform = descriptor.Transform,
|
|
||||||
size = descriptor.Size,
|
|
||||||
sourceObject = descriptor.Shape == NavMeshBuildSourceShape.Mesh ? descriptor.Mesh : null
|
|
||||||
};
|
|
||||||
|
|
||||||
results.Add(source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CompleteFinishedBuild()
|
private void CompleteFinishedBuild()
|
||||||
{
|
{
|
||||||
if (!activeBuildWindowId.HasValue)
|
if (!activeBuildWindowId.HasValue)
|
||||||
@@ -629,379 +426,5 @@ namespace InfiniteWorld.VoxelWorld.NavMesh
|
|||||||
coverageWindows.Remove(windowId);
|
coverageWindows.Remove(windowId);
|
||||||
queuedCoverageWindowIds.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)
|
|
||||||
{
|
|
||||||
Bounds bounds = GetSourceBounds(sources[0]);
|
|
||||||
for (int i = 1; i < sources.Count; i++)
|
|
||||||
{
|
|
||||||
bounds.Encapsulate(GetSourceBounds(sources[i]));
|
|
||||||
}
|
|
||||||
|
|
||||||
return bounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Bounds GetSourceBounds(NavMeshBuildSource source)
|
|
||||||
{
|
|
||||||
if (source.shape == NavMeshBuildSourceShape.Box)
|
|
||||||
{
|
|
||||||
return TransformBounds(source.transform, new Bounds(Vector3.zero, source.size));
|
|
||||||
}
|
|
||||||
|
|
||||||
Mesh mesh = source.sourceObject as Mesh;
|
|
||||||
if (mesh != null)
|
|
||||||
{
|
|
||||||
return TransformBounds(source.transform, mesh.bounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Bounds(source.transform.GetColumn(3), Vector3.zero);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Bounds TransformBounds(Matrix4x4 matrix, Bounds localBounds)
|
|
||||||
{
|
|
||||||
Vector3 center = localBounds.center;
|
|
||||||
Vector3 extents = localBounds.extents;
|
|
||||||
|
|
||||||
Vector3[] corners =
|
|
||||||
{
|
|
||||||
new Vector3(center.x - extents.x, center.y - extents.y, center.z - extents.z),
|
|
||||||
new Vector3(center.x - extents.x, center.y - extents.y, center.z + extents.z),
|
|
||||||
new Vector3(center.x - extents.x, center.y + extents.y, center.z - extents.z),
|
|
||||||
new Vector3(center.x - extents.x, center.y + extents.y, center.z + extents.z),
|
|
||||||
new Vector3(center.x + extents.x, center.y - extents.y, center.z - extents.z),
|
|
||||||
new Vector3(center.x + extents.x, center.y - extents.y, center.z + extents.z),
|
|
||||||
new Vector3(center.x + extents.x, center.y + extents.y, center.z - extents.z),
|
|
||||||
new Vector3(center.x + extents.x, center.y + extents.y, center.z + extents.z)
|
|
||||||
};
|
|
||||||
|
|
||||||
Bounds worldBounds = new Bounds(matrix.MultiplyPoint3x4(corners[0]), Vector3.zero);
|
|
||||||
for (int i = 1; i < corners.Length; i++)
|
|
||||||
{
|
|
||||||
worldBounds.Encapsulate(matrix.MultiplyPoint3x4(corners[i]));
|
|
||||||
}
|
|
||||||
|
|
||||||
return worldBounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ExpandBounds(ref Bounds bounds, float horizontalPadding, float verticalPadding)
|
|
||||||
{
|
|
||||||
Vector3 size = bounds.size;
|
|
||||||
size.x = Mathf.Max(size.x + horizontalPadding * 2f, 0.1f);
|
|
||||||
size.z = Mathf.Max(size.z + horizontalPadding * 2f, 0.1f);
|
|
||||||
size.y = Mathf.Max(size.y + verticalPadding * 2f, 0.1f);
|
|
||||||
bounds.size = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Bounds CreateQuantizedCoverageBounds(Bounds rawBounds, float padding, float minSize, float quantizationStep)
|
|
||||||
{
|
|
||||||
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 static void EnsureMinimumSpan(ref float min, ref float max, float minimumSize)
|
|
||||||
{
|
|
||||||
float currentSize = max - min;
|
|
||||||
if (currentSize >= minimumSize)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
CoverageBounds = coverageBounds;
|
|
||||||
Priority = priority;
|
|
||||||
InterestCount = interestCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bounds CoverageBounds { get; }
|
|
||||||
public float Priority { get; }
|
|
||||||
public int InterestCount { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct ClusterAccumulator
|
|
||||||
{
|
|
||||||
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 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 bool MatchedThisFrame;
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
RemoveCoverageData(this);
|
|
||||||
State = NavCoverageState.Pending;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user