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))
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 814b46557fef4e36a0cba9242dd1feea
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ce0e93eaf54c45e8bff2ff3770aad24d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "VoxelWorld.NavMesh.Runtime",
|
||||
"rootNamespace": "InfiniteWorld.VoxelWorld.NavMesh",
|
||||
"references": [
|
||||
"VoxelWorld.Contracts",
|
||||
"UniTask",
|
||||
"VContainer",
|
||||
"MessagePipe"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c0bf4204de447d69095f9f1fa208e2e
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||
{
|
||||
[Serializable]
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15bfc8bcd2594a3193c1bcd7eff3e770
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,551 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using InfiniteWorld.VoxelWorld.Contracts;
|
||||
using MessagePipe;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
using VContainer.Unity;
|
||||
using UnityNavMesh = UnityEngine.AI.NavMesh;
|
||||
using UnityNavMeshBuilder = UnityEngine.AI.NavMeshBuilder;
|
||||
|
||||
namespace InfiniteWorld.VoxelWorld.NavMesh
|
||||
{
|
||||
public sealed class VoxelWorldNavMeshService : IStartable, ITickable, IDisposable
|
||||
{
|
||||
private readonly IChunkNavSourceReader chunkNavSourceReader;
|
||||
private readonly IWorldInterestReader worldInterestReader;
|
||||
private readonly ISubscriber<ChunkNavGeometryReadyMessage> chunkReadySubscriber;
|
||||
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 List<IDisposable> subscriptions = new List<IDisposable>(3);
|
||||
|
||||
private Vector2Int? activeBuildRegion;
|
||||
|
||||
public VoxelWorldNavMeshService(
|
||||
IChunkNavSourceReader chunkNavSourceReader,
|
||||
IWorldInterestReader worldInterestReader,
|
||||
ISubscriber<ChunkNavGeometryReadyMessage> chunkReadySubscriber,
|
||||
ISubscriber<ChunkNavGeometryRemovedMessage> chunkRemovedSubscriber,
|
||||
ISubscriber<WorldInterestChangedMessage> worldInterestChangedSubscriber,
|
||||
VoxelWorldNavMeshConfig config)
|
||||
{
|
||||
this.chunkNavSourceReader = chunkNavSourceReader;
|
||||
this.worldInterestReader = worldInterestReader;
|
||||
this.chunkReadySubscriber = chunkReadySubscriber;
|
||||
this.chunkRemovedSubscriber = chunkRemovedSubscriber;
|
||||
this.worldInterestChangedSubscriber = worldInterestChangedSubscriber;
|
||||
this.config = config ?? new VoxelWorldNavMeshConfig();
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
subscriptions.Add(chunkReadySubscriber.Subscribe(OnChunkNavGeometryReady));
|
||||
subscriptions.Add(chunkRemovedSubscriber.Subscribe(OnChunkNavGeometryRemoved));
|
||||
subscriptions.Add(worldInterestChangedSubscriber.Subscribe(OnWorldInterestChanged));
|
||||
|
||||
RefreshInterestPoints();
|
||||
|
||||
loadedChunkCoords.Clear();
|
||||
chunkNavSourceReader.GetLoadedChunkCoords(loadedChunkCoords);
|
||||
for (int i = 0; i < loadedChunkCoords.Count; i++)
|
||||
{
|
||||
MarkDirtyForChunk(loadedChunkCoords[i]);
|
||||
}
|
||||
|
||||
MarkWarmupRegionsDirty();
|
||||
}
|
||||
|
||||
public void Tick()
|
||||
{
|
||||
RefreshInterestPoints();
|
||||
CompleteFinishedBuild();
|
||||
|
||||
int startedBuilds = 0;
|
||||
int maxBuilds = Mathf.Max(1, config.maxNavMeshBuildsPerFrame);
|
||||
while (startedBuilds < maxBuilds)
|
||||
{
|
||||
if (activeBuildRegion.HasValue || dirtyNavRegions.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Vector2Int regionCoord = DequeueBestDirtyRegion();
|
||||
if (!TryStartRegionBuild(regionCoord))
|
||||
{
|
||||
startedBuilds++;
|
||||
continue;
|
||||
}
|
||||
|
||||
startedBuilds++;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
for (int i = 0; i < subscriptions.Count; i++)
|
||||
{
|
||||
subscriptions[i]?.Dispose();
|
||||
}
|
||||
|
||||
subscriptions.Clear();
|
||||
|
||||
foreach (KeyValuePair<Vector2Int, NavRegionRuntime> pair in navRegions)
|
||||
{
|
||||
pair.Value.Dispose();
|
||||
}
|
||||
|
||||
navRegions.Clear();
|
||||
queuedNavRegions.Clear();
|
||||
dirtyNavRegions.Clear();
|
||||
currentInterestRegions.Clear();
|
||||
previousInterestRegions.Clear();
|
||||
activeBuildRegion = null;
|
||||
}
|
||||
|
||||
private void OnChunkNavGeometryReady(ChunkNavGeometryReadyMessage message)
|
||||
{
|
||||
MarkDirtyForChunk(message.Coord);
|
||||
}
|
||||
|
||||
private void OnChunkNavGeometryRemoved(ChunkNavGeometryRemovedMessage message)
|
||||
{
|
||||
MarkDirtyForChunk(message.Coord);
|
||||
}
|
||||
|
||||
private void OnWorldInterestChanged(WorldInterestChangedMessage message)
|
||||
{
|
||||
RefreshInterestPoints();
|
||||
MarkWarmupRegionsDirty();
|
||||
}
|
||||
|
||||
private void RefreshInterestPoints()
|
||||
{
|
||||
interestPoints.Clear();
|
||||
worldInterestReader.GetInterestPoints(interestPoints);
|
||||
|
||||
previousInterestRegions.Clear();
|
||||
foreach (Vector2Int region in currentInterestRegions)
|
||||
{
|
||||
previousInterestRegions.Add(region);
|
||||
}
|
||||
|
||||
currentInterestRegions.Clear();
|
||||
for (int i = 0; i < interestPoints.Count; i++)
|
||||
{
|
||||
currentInterestRegions.Add(ChunkToRegion(WorldToChunk(interestPoints[i].Position)));
|
||||
}
|
||||
|
||||
if (!AreSetsEqual(previousInterestRegions, currentInterestRegions))
|
||||
{
|
||||
MarkWarmupRegionsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkWarmupRegionsDirty()
|
||||
{
|
||||
int radius = Mathf.Max(0, config.navWarmupRadiusInRegions);
|
||||
foreach (Vector2Int region in currentInterestRegions)
|
||||
{
|
||||
for (int y = -radius; y <= radius; y++)
|
||||
{
|
||||
for (int x = -radius; x <= radius; x++)
|
||||
{
|
||||
EnqueueDirtyRegion(new Vector2Int(region.x + x, region.y + y));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkDirtyForChunk(Vector2Int chunkCoord)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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))
|
||||
{
|
||||
activeRegion.BuildRequestedWhileRunning = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
dirtyNavRegions.Enqueue(regionCoord);
|
||||
}
|
||||
|
||||
private Vector2Int DequeueBestDirtyRegion()
|
||||
{
|
||||
dirtyRegionCandidates.Clear();
|
||||
while (dirtyNavRegions.Count > 0)
|
||||
{
|
||||
dirtyRegionCandidates.Add(dirtyNavRegions.Dequeue());
|
||||
}
|
||||
|
||||
int bestIndex = 0;
|
||||
float bestScore = float.MaxValue;
|
||||
for (int i = 0; i < dirtyRegionCandidates.Count; i++)
|
||||
{
|
||||
float score = GetRegionPriorityScore(dirtyRegionCandidates[i]);
|
||||
if (score < bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
Vector2Int best = dirtyRegionCandidates[bestIndex];
|
||||
queuedNavRegions.Remove(best);
|
||||
|
||||
for (int i = 0; i < dirtyRegionCandidates.Count; i++)
|
||||
{
|
||||
if (i == bestIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
dirtyNavRegions.Enqueue(dirtyRegionCandidates[i]);
|
||||
}
|
||||
|
||||
dirtyRegionCandidates.Clear();
|
||||
return best;
|
||||
}
|
||||
|
||||
private float GetRegionPriorityScore(Vector2Int regionCoord)
|
||||
{
|
||||
if (interestPoints.Count == 0)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
Vector3 regionCenter = GetRegionCenter(regionCoord);
|
||||
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;
|
||||
if (distance < bestDistance)
|
||||
{
|
||||
bestDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
return bestDistance;
|
||||
}
|
||||
|
||||
private bool TryStartRegionBuild(Vector2Int regionCoord)
|
||||
{
|
||||
buildSources.Clear();
|
||||
bool hasCoreChunk = CollectBuildSources(regionCoord, buildSources);
|
||||
if (!hasCoreChunk || buildSources.Count == 0)
|
||||
{
|
||||
RemoveRegion(regionCoord);
|
||||
return false;
|
||||
}
|
||||
|
||||
Bounds buildBounds = CalculateBounds(buildSources);
|
||||
ExpandBounds(ref buildBounds, config.navBoundsHorizontalPadding, config.navBoundsVerticalPadding);
|
||||
|
||||
NavRegionRuntime region = GetOrCreateRegion(regionCoord);
|
||||
region.BuildRequestedWhileRunning = false;
|
||||
region.BuildBounds = buildBounds;
|
||||
|
||||
if (region.NavMeshData == null)
|
||||
{
|
||||
region.NavMeshData = new NavMeshData(config.agentTypeId);
|
||||
}
|
||||
|
||||
if (!region.Instance.valid)
|
||||
{
|
||||
region.Instance = UnityNavMesh.AddNavMeshData(region.NavMeshData);
|
||||
}
|
||||
|
||||
NavMeshBuildSettings buildSettings = UnityNavMesh.GetSettingsByID(config.agentTypeId);
|
||||
region.ActiveBuild = UnityNavMeshBuilder.UpdateNavMeshDataAsync(region.NavMeshData, buildSettings, buildSources, buildBounds);
|
||||
activeBuildRegion = regionCoord;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool CollectBuildSources(Vector2Int regionCoord, List<NavMeshBuildSource> results)
|
||||
{
|
||||
int regionSize = Mathf.Max(1, config.navRegionSizeInChunks);
|
||||
int baseChunkX = regionCoord.x * regionSize;
|
||||
int baseChunkY = regionCoord.y * regionSize;
|
||||
bool hasCoreChunk = false;
|
||||
|
||||
for (int y = -1; y <= regionSize; y++)
|
||||
{
|
||||
for (int x = -1; x <= regionSize; x++)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return hasCoreChunk;
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
if (!activeBuildRegion.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!navRegions.TryGetValue(activeBuildRegion.Value, out NavRegionRuntime region))
|
||||
{
|
||||
activeBuildRegion = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (region.ActiveBuild != null && !region.ActiveBuild.isDone)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
region.ActiveBuild = null;
|
||||
Vector2Int completedRegion = activeBuildRegion.Value;
|
||||
activeBuildRegion = null;
|
||||
|
||||
if (region.BuildRequestedWhileRunning)
|
||||
{
|
||||
region.BuildRequestedWhileRunning = false;
|
||||
EnqueueDirtyRegion(completedRegion);
|
||||
}
|
||||
}
|
||||
|
||||
private NavRegionRuntime GetOrCreateRegion(Vector2Int regionCoord)
|
||||
{
|
||||
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))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeBuildRegion.HasValue && activeBuildRegion.Value == regionCoord)
|
||||
{
|
||||
activeBuildRegion = null;
|
||||
}
|
||||
|
||||
region.Dispose();
|
||||
navRegions.Remove(regionCoord);
|
||||
}
|
||||
|
||||
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 Vector2Int WorldToChunk(Vector3 position)
|
||||
{
|
||||
float chunkSize = Mathf.Max(1f, chunkNavSourceReader.ChunkWorldSize);
|
||||
return new Vector2Int(
|
||||
Mathf.FloorToInt(position.x / chunkSize),
|
||||
Mathf.FloorToInt(position.z / chunkSize));
|
||||
}
|
||||
|
||||
private Vector2Int ChunkToRegion(Vector2Int chunkCoord)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (Vector2Int value in left)
|
||||
{
|
||||
if (!right.Contains(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int PositiveModulo(int value, int modulus)
|
||||
{
|
||||
int result = value % modulus;
|
||||
return result < 0 ? result + modulus : result;
|
||||
}
|
||||
|
||||
private sealed class NavRegionRuntime : IDisposable
|
||||
{
|
||||
public NavMeshData NavMeshData;
|
||||
public NavMeshDataInstance Instance;
|
||||
public AsyncOperation ActiveBuild;
|
||||
public bool BuildRequestedWhileRunning;
|
||||
public Bounds BuildBounds;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1f16cad74f034aa899a965d1ff0ef8aa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -14,6 +14,8 @@ namespace Players
|
||||
|
||||
private float _mouseOrbitAngle;
|
||||
|
||||
public Transform Target => _target != null ? _target : transform;
|
||||
|
||||
public override void OnStartClient()
|
||||
{
|
||||
base.OnStartClient();
|
||||
|
||||
@@ -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:
|
||||
Reference in New Issue
Block a user