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:
Alexander Borisov
2026-04-08 11:28:39 +03:00
parent 4e1cf273fa
commit 055b87a85c
22 changed files with 1095 additions and 8 deletions
+2
View File
@@ -14,6 +14,8 @@ namespace Players
private float _mouseOrbitAngle;
public Transform Target => _target != null ? _target : transform;
public override void OnStartClient()
{
base.OnStartClient();
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3c4e0cf9d3254f8bbb320e52c9a67bd0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,51 @@
using InfiniteWorld.VoxelWorld;
using InfiniteWorld.VoxelWorld.Contracts;
using InfiniteWorld.VoxelWorld.NavMesh;
using MessagePipe;
using UnityEngine;
using VContainer;
using VContainer.Unity;
namespace VoxelWorldScene
{
[DisallowMultipleComponent]
[RequireComponent(typeof(VoxelWorldGenerator))]
public sealed class VoxelWorldNavMeshLifetimeScope : LifetimeScope
{
[SerializeField] private bool enableRuntimeNavMesh = true;
[SerializeField] private VoxelWorldGenerator worldGenerator;
[SerializeField] private VoxelWorldNavMeshConfig config = new VoxelWorldNavMeshConfig();
protected override void Configure(IContainerBuilder builder)
{
if (!enableRuntimeNavMesh)
{
return;
}
if (worldGenerator == null)
{
worldGenerator = GetComponent<VoxelWorldGenerator>();
}
builder.RegisterMessagePipe();
builder.RegisterInstance(config);
builder.RegisterInstance(worldGenerator).As<IChunkNavSourceReader>().As<IWorldInterestReader>().AsSelf();
builder.RegisterEntryPoint<VoxelWorldNavMeshService>();
builder.RegisterBuildCallback(ResolvePublishers);
}
private void ResolvePublishers(IObjectResolver resolver)
{
if (!enableRuntimeNavMesh || worldGenerator == null)
{
return;
}
worldGenerator.BindWorldContracts(
resolver.Resolve<IPublisher<ChunkNavGeometryReadyMessage>>(),
resolver.Resolve<IPublisher<ChunkNavGeometryRemovedMessage>>(),
resolver.Resolve<IPublisher<WorldInterestChangedMessage>>());
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2dfd0b7ddf3a419f91ce891210f85d4b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,75 @@
using InfiniteWorld.VoxelWorld;
using Players;
using UnityEngine;
namespace VoxelWorldScene
{
[DisallowMultipleComponent]
[RequireComponent(typeof(VoxelWorldGenerator))]
public sealed class VoxelWorldPlayerStreamTargetBinding : MonoBehaviour
{
[SerializeField] private VoxelWorldGenerator worldGenerator;
[SerializeField] private Transform explicitStreamTarget;
private Transform currentStreamTarget;
private void Awake()
{
if (worldGenerator == null)
{
worldGenerator = GetComponent<VoxelWorldGenerator>();
}
ApplyResolvedTarget(ResolveTarget());
}
private void Update()
{
ApplyResolvedTarget(ResolveTarget());
}
private void OnDisable()
{
ApplyResolvedTarget(null);
}
private Transform ResolveTarget()
{
if (explicitStreamTarget != null)
{
return explicitStreamTarget;
}
if (currentStreamTarget != null)
{
return currentStreamTarget;
}
CameraFollow[] cameraFollows = FindObjectsByType<CameraFollow>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
for (int i = 0; i < cameraFollows.Length; i++)
{
CameraFollow follow = cameraFollows[i];
if (follow != null && follow.IsOwner)
{
return follow.Target;
}
}
return null;
}
private void ApplyResolvedTarget(Transform resolvedTarget)
{
if (currentStreamTarget == resolvedTarget)
{
return;
}
currentStreamTarget = resolvedTarget;
if (worldGenerator != null)
{
worldGenerator.SetStreamTarget(currentStreamTarget);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0c52a16bd6e44739b6bb1b4471a7a5a9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: