add voxel world runtime navmesh sidecar
Introduce a DI-wired NavMesh sidecar for voxel chunks so world streaming stays actor-driven and world state remains the canonical source for navigation rebuilds.
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b8ddf3935be4c6da0df53dfe0792909
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,116 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
|
||||
namespace InfiniteWorld.VoxelWorld.Contracts
|
||||
{
|
||||
public interface IChunkNavSourceReader
|
||||
{
|
||||
float ChunkWorldSize { get; }
|
||||
void GetLoadedChunkCoords(List<Vector2Int> results);
|
||||
bool TryGetChunkNavSourceSnapshot(Vector2Int coord, out ChunkNavSourceSnapshot snapshot);
|
||||
}
|
||||
|
||||
public interface IWorldInterestReader
|
||||
{
|
||||
int InterestVersion { get; }
|
||||
void GetInterestPoints(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,
|
||||
Other = 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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 55dd8e2a1b2d458aa96895a54d53e6ca
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "VoxelWorld.Contracts",
|
||||
"rootNamespace": "InfiniteWorld.VoxelWorld.Contracts",
|
||||
"references": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96b902ea5b554a1b8a9e0c29e03118f2
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -10,6 +10,8 @@ GameObject:
|
||||
m_Component:
|
||||
- component: {fileID: 74135865886311664}
|
||||
- component: {fileID: 2927522923773808063}
|
||||
- component: {fileID: 6182401849027620011}
|
||||
- component: {fileID: 6182401849027620012}
|
||||
m_Layer: 0
|
||||
m_Name: VoxelWorld
|
||||
m_TagString: Untagged
|
||||
@@ -47,3 +49,42 @@ MonoBehaviour:
|
||||
streamTarget: {fileID: 0}
|
||||
config: {fileID: 11400000, guid: b8cf28a5522134b479c23f017234070c, type: 2}
|
||||
_terrainShader: {fileID: 4800000, guid: ec80aebd8cb61f44cbfa6b7d5f087211, type: 3}
|
||||
--- !u!114 &6182401849027620011
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 797018065588400165}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 0c52a16bd6e44739b6bb1b4471a7a5a9, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::VoxelWorldScene.VoxelWorldPlayerStreamTargetBinding
|
||||
worldGenerator: {fileID: 2927522923773808063}
|
||||
explicitStreamTarget: {fileID: 0}
|
||||
--- !u!114 &6182401849027620012
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 797018065588400165}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 2dfd0b7ddf3a419f91ce891210f85d4b, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::VoxelWorldScene.VoxelWorldNavMeshLifetimeScope
|
||||
parentReference:
|
||||
TypeName:
|
||||
autoRun: 1
|
||||
autoInjectGameObjects: []
|
||||
enableRuntimeNavMesh: 1
|
||||
worldGenerator: {fileID: 2927522923773808063}
|
||||
config:
|
||||
agentTypeId: 0
|
||||
navRegionSizeInChunks: 2
|
||||
maxNavMeshBuildsPerFrame: 1
|
||||
navBoundsHorizontalPadding: 1
|
||||
navBoundsVerticalPadding: 2
|
||||
navWarmupRadiusInRegions: 1
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"name": "VoxelWorld.Runtime",
|
||||
"rootNamespace": "InfiniteWorld.VoxelWorld",
|
||||
"references": [
|
||||
"UniTask"
|
||||
"UniTask",
|
||||
"VoxelWorld.Contracts",
|
||||
"MessagePipe"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using InfiniteWorld.VoxelWorld.Contracts;
|
||||
using MessagePipe;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
|
||||
namespace InfiniteWorld.VoxelWorld
|
||||
{
|
||||
public sealed partial class VoxelWorldGenerator : MonoBehaviour
|
||||
public sealed partial class VoxelWorldGenerator : MonoBehaviour, IChunkNavSourceReader, IWorldInterestReader
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] private Transform streamTarget;
|
||||
@@ -36,8 +39,12 @@ namespace InfiniteWorld.VoxelWorld
|
||||
private int regionRebuildSession;
|
||||
private VoxelWorldAtlas atlas;
|
||||
private int atlasBiomeCount;
|
||||
private int interestVersion;
|
||||
private bool regionRebuildLoopRunning;
|
||||
private VoxelWorldResolvedSettings settings = VoxelWorldResolvedSettings.Default;
|
||||
private IPublisher<ChunkNavGeometryReadyMessage> chunkNavGeometryReadyPublisher;
|
||||
private IPublisher<ChunkNavGeometryRemovedMessage> chunkNavGeometryRemovedPublisher;
|
||||
private IPublisher<WorldInterestChangedMessage> worldInterestChangedPublisher;
|
||||
|
||||
private int chunkSize => settings.ChunkSize;
|
||||
private int generationRadius => settings.GenerationRadius;
|
||||
@@ -67,6 +74,8 @@ namespace InfiniteWorld.VoxelWorld
|
||||
private int maxNeighborRefreshesPerFrame => settings.MaxNeighborRefreshesPerFrame;
|
||||
private int renderRegionSizeInChunks => settings.RenderRegionSizeInChunks;
|
||||
private int maxRegionBuildsPerFrame => settings.MaxRegionBuildsPerFrame;
|
||||
public float ChunkWorldSize => chunkSize;
|
||||
public int InterestVersion => interestVersion;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -75,7 +84,6 @@ namespace InfiniteWorld.VoxelWorld
|
||||
EnsureRuntimeData();
|
||||
EnsureChunkRoot();
|
||||
EnsureRegionRoot();
|
||||
TryResolveStreamTarget();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
@@ -196,21 +204,100 @@ namespace InfiniteWorld.VoxelWorld
|
||||
|
||||
private bool TryResolveStreamTarget()
|
||||
{
|
||||
if (streamTarget != null)
|
||||
return streamTarget != null;
|
||||
}
|
||||
|
||||
public void BindWorldContracts(
|
||||
IPublisher<ChunkNavGeometryReadyMessage> chunkNavGeometryReadyPublisher,
|
||||
IPublisher<ChunkNavGeometryRemovedMessage> chunkNavGeometryRemovedPublisher,
|
||||
IPublisher<WorldInterestChangedMessage> worldInterestChangedPublisher)
|
||||
{
|
||||
this.chunkNavGeometryReadyPublisher = chunkNavGeometryReadyPublisher;
|
||||
this.chunkNavGeometryRemovedPublisher = chunkNavGeometryRemovedPublisher;
|
||||
this.worldInterestChangedPublisher = worldInterestChangedPublisher;
|
||||
}
|
||||
|
||||
public void SetStreamTarget(Transform target)
|
||||
{
|
||||
if (streamTarget == target)
|
||||
{
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
Camera mainCamera = Camera.main;
|
||||
if (mainCamera == null)
|
||||
streamTarget = target;
|
||||
interestVersion++;
|
||||
worldInterestChangedPublisher?.Publish(new WorldInterestChangedMessage(interestVersion));
|
||||
}
|
||||
|
||||
public void GetInterestPoints(List<WorldInterestPoint> results)
|
||||
{
|
||||
if (results == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(results));
|
||||
}
|
||||
|
||||
if (streamTarget == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
results.Add(new WorldInterestPoint(streamTarget.position, 1f, WorldInterestKind.PlayerActor));
|
||||
}
|
||||
|
||||
public void GetLoadedChunkCoords(List<Vector2Int> results)
|
||||
{
|
||||
if (results == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(results));
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<Vector2Int, ChunkRuntime> pair in chunks)
|
||||
{
|
||||
if (HasNavGeometry(pair.Value))
|
||||
{
|
||||
results.Add(pair.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetChunkNavSourceSnapshot(Vector2Int coord, out ChunkNavSourceSnapshot snapshot)
|
||||
{
|
||||
if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !HasNavGeometry(runtime))
|
||||
{
|
||||
snapshot = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
streamTarget = mainCamera.transform;
|
||||
ChunkNavBuildSourceDescriptor groundSource = ChunkNavBuildSourceDescriptor.CreateBox(
|
||||
Matrix4x4.TRS(
|
||||
runtime.GroundCollider.transform.TransformPoint(runtime.GroundCollider.center),
|
||||
runtime.GroundCollider.transform.rotation,
|
||||
runtime.GroundCollider.transform.lossyScale),
|
||||
runtime.GroundCollider.size);
|
||||
|
||||
if (runtime.ColliderMesh != null && runtime.ColliderMesh.vertexCount > 0)
|
||||
{
|
||||
snapshot = new ChunkNavSourceSnapshot(
|
||||
coord,
|
||||
runtime.Version,
|
||||
new[]
|
||||
{
|
||||
groundSource,
|
||||
ChunkNavBuildSourceDescriptor.CreateMesh(runtime.MountainCollider.transform.localToWorldMatrix, runtime.ColliderMesh)
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
snapshot = new ChunkNavSourceSnapshot(coord, runtime.Version, new[] { groundSource });
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool HasNavGeometry(ChunkRuntime runtime)
|
||||
{
|
||||
return runtime != null && runtime.Root != null && runtime.GroundCollider != null && runtime.State == ChunkState.Rendered;
|
||||
}
|
||||
|
||||
private void ScheduleChunkGeneration(Vector2Int centerChunk)
|
||||
{
|
||||
List<Vector2Int> coords = GetCoordsByPriority(centerChunk, generationRadius);
|
||||
@@ -284,6 +371,7 @@ namespace InfiniteWorld.VoxelWorld
|
||||
|
||||
Vector2Int regionCoord = ChunkToRegion(coord);
|
||||
MarkRegionDirty(coord);
|
||||
PublishChunkNavGeometryRemoved(coord, runtime.Version);
|
||||
chunks.Remove(coord);
|
||||
runtime.Dispose();
|
||||
TryDisposeRegionIfEmpty(regionCoord);
|
||||
@@ -415,10 +503,21 @@ namespace InfiniteWorld.VoxelWorld
|
||||
}
|
||||
|
||||
runtime.ApplyColliderMesh(pending.ColliderMesh);
|
||||
PublishChunkNavGeometryReady(pending.Coord, runtime.Version);
|
||||
applies++;
|
||||
}
|
||||
}
|
||||
|
||||
private void PublishChunkNavGeometryReady(Vector2Int coord, int version)
|
||||
{
|
||||
chunkNavGeometryReadyPublisher?.Publish(new ChunkNavGeometryReadyMessage(coord, version));
|
||||
}
|
||||
|
||||
private void PublishChunkNavGeometryRemoved(Vector2Int coord, int version)
|
||||
{
|
||||
chunkNavGeometryRemovedPublisher?.Publish(new ChunkNavGeometryRemovedMessage(coord, version));
|
||||
}
|
||||
|
||||
private void QueueNeighborRefresh(Vector2Int coord)
|
||||
{
|
||||
if (!queuedNeighborRefreshes.Add(coord))
|
||||
|
||||
Reference in New Issue
Block a user