From 211c9758892566f91d5b637f17ce29d3373d0733 Mon Sep 17 00:00:00 2001 From: Konstantin Dyachenko Date: Wed, 8 Apr 2026 09:41:17 +0700 Subject: [PATCH] [Fix] Flatten Terrain --- .../Runtime/VoxelWorldGenerator.Spawns.cs | 232 ++++++++++++++++++ .../VoxelWorldGenerator.Spawns.cs.meta | 2 + .../VoxelWorld/Runtime/VoxelWorldGenerator.cs | 7 + 3 files changed, 241 insertions(+) create mode 100644 Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.Spawns.cs create mode 100644 Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.Spawns.cs.meta diff --git a/Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.Spawns.cs b/Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.Spawns.cs new file mode 100644 index 00000000..d480bdf4 --- /dev/null +++ b/Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.Spawns.cs @@ -0,0 +1,232 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace InfiniteWorld.VoxelWorld +{ + public sealed partial class VoxelWorldGenerator + { + private readonly Dictionary> spawnedPlacementsByChunk = new Dictionary>(); + private readonly Dictionary spawnedPlacementsById = new Dictionary(); + private readonly HashSet disabledSpawnIds = new HashSet(); + + private Transform placementRoot; + + public IReadOnlyCollection DisabledSpawnIds => disabledSpawnIds; + + public bool IsSpawnDisabled(long spawnId) + { + return disabledSpawnIds.Contains(spawnId); + } + + public bool DisableSpawn(long spawnId) + { + if (!disabledSpawnIds.Add(spawnId)) + { + return false; + } + + if (spawnedPlacementsById.TryGetValue(spawnId, out SpawnedPlacementRuntime runtime)) + { + RemoveSpawnedPlacement(runtime); + } + + return true; + } + + public bool EnableSpawn(long spawnId) + { + return disabledSpawnIds.Remove(spawnId); + } + + public void ClearDisabledSpawns() + { + disabledSpawnIds.Clear(); + } + + private void EnsurePlacementRoot() + { + if (placementRoot != null) + { + return; + } + + Transform existing = transform.Find("WorldPlacements"); + if (existing != null) + { + placementRoot = existing; + return; + } + + GameObject root = new GameObject("WorldPlacements"); + root.transform.SetParent(transform, false); + placementRoot = root.transform; + } + + private void TrySpawnChunkPlacements(Vector2Int chunkCoord) + { + EnsurePlacementRoot(); + if (placementRoot == null || spawnedPlacementsByChunk.ContainsKey(chunkCoord)) + { + return; + } + + WorldChunkPlacementPlan plan = GetChunkPlacementPlan(chunkCoord); + IReadOnlyList spawnPoints = plan.SpawnPoints; + if (spawnPoints == null || spawnPoints.Count == 0) + { + return; + } + + List chunkPlacements = new List(spawnPoints.Count); + for (int i = 0; i < spawnPoints.Count; i++) + { + WorldSpawnPoint spawnPoint = spawnPoints[i]; + if (disabledSpawnIds.Contains(spawnPoint.SpawnId) || spawnedPlacementsById.ContainsKey(spawnPoint.SpawnId)) + { + continue; + } + + if (!TryResolveSpawnPrefab(spawnPoint, out GameObject prefab) || prefab == null) + { + continue; + } + + GameObject instance = Object.Instantiate(prefab, spawnPoint.Position, spawnPoint.Rotation, placementRoot); + instance.name = $"{prefab.name}_{spawnPoint.ChunkCoord.x}_{spawnPoint.ChunkCoord.y}_{spawnPoint.SpawnOrdinalInChunk}"; + SpawnedPlacementRuntime runtime = new SpawnedPlacementRuntime(spawnPoint.SpawnId, chunkCoord, spawnPoint.CollectionIndex, spawnPoint.EntryIndex, instance); + chunkPlacements.Add(runtime); + spawnedPlacementsById.Add(runtime.SpawnId, runtime); + } + + if (chunkPlacements.Count > 0) + { + spawnedPlacementsByChunk[chunkCoord] = chunkPlacements; + } + } + + private bool TryResolveSpawnPrefab(WorldSpawnPoint spawnPoint, out GameObject prefab) + { + prefab = null; + if (config == null || config.placementCollections == null) + { + return false; + } + + if (spawnPoint.CollectionIndex < 0 || spawnPoint.CollectionIndex >= config.placementCollections.Count) + { + return false; + } + + WorldPrefabCollection collection = config.placementCollections[spawnPoint.CollectionIndex]; + if (collection == null || collection.entries == null || spawnPoint.EntryIndex < 0 || spawnPoint.EntryIndex >= collection.entries.Count) + { + return false; + } + + WorldPrefabEntry entry = collection.entries[spawnPoint.EntryIndex]; + if (entry == null) + { + return false; + } + + prefab = entry.prefab; + return prefab != null; + } + + private void DespawnChunkPlacements(Vector2Int chunkCoord) + { + if (!spawnedPlacementsByChunk.TryGetValue(chunkCoord, out List chunkPlacements)) + { + return; + } + + for (int i = 0; i < chunkPlacements.Count; i++) + { + SpawnedPlacementRuntime runtime = chunkPlacements[i]; + spawnedPlacementsById.Remove(runtime.SpawnId); + DestroyPlacementInstance(runtime.Instance); + } + + spawnedPlacementsByChunk.Remove(chunkCoord); + } + + private void RemoveSpawnedPlacement(SpawnedPlacementRuntime runtime) + { + spawnedPlacementsById.Remove(runtime.SpawnId); + if (spawnedPlacementsByChunk.TryGetValue(runtime.ChunkCoord, out List chunkPlacements)) + { + chunkPlacements.RemoveAll(item => item.SpawnId == runtime.SpawnId); + if (chunkPlacements.Count == 0) + { + spawnedPlacementsByChunk.Remove(runtime.ChunkCoord); + } + } + + DestroyPlacementInstance(runtime.Instance); + } + + private void CleanupSpawnedPlacements() + { + foreach (KeyValuePair> pair in spawnedPlacementsByChunk) + { + List chunkPlacements = pair.Value; + for (int i = 0; i < chunkPlacements.Count; i++) + { + DestroyPlacementInstance(chunkPlacements[i].Instance); + } + } + + spawnedPlacementsByChunk.Clear(); + spawnedPlacementsById.Clear(); + + if (placementRoot != null) + { + if (Application.isPlaying) + { + Object.Destroy(placementRoot.gameObject); + } + else + { + Object.DestroyImmediate(placementRoot.gameObject); + } + + placementRoot = null; + } + } + + private static void DestroyPlacementInstance(GameObject instance) + { + if (instance == null) + { + return; + } + + if (Application.isPlaying) + { + Object.Destroy(instance); + } + else + { + Object.DestroyImmediate(instance); + } + } + + private sealed class SpawnedPlacementRuntime + { + public SpawnedPlacementRuntime(long spawnId, Vector2Int chunkCoord, int collectionIndex, int entryIndex, GameObject instance) + { + SpawnId = spawnId; + ChunkCoord = chunkCoord; + CollectionIndex = collectionIndex; + EntryIndex = entryIndex; + Instance = instance; + } + + public long SpawnId { get; } + public Vector2Int ChunkCoord { get; } + public int CollectionIndex { get; } + public int EntryIndex { get; } + public GameObject Instance { get; } + } + } +} diff --git a/Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.Spawns.cs.meta b/Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.Spawns.cs.meta new file mode 100644 index 00000000..e5663157 --- /dev/null +++ b/Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.Spawns.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f7d0d16ba43223c41bd21fbe9333d819 \ No newline at end of file diff --git a/Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.cs b/Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.cs index a3b269bf..ac1591cc 100644 --- a/Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.cs +++ b/Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.cs @@ -75,6 +75,7 @@ namespace InfiniteWorld.VoxelWorld EnsureRuntimeData(); EnsureChunkRoot(); EnsureRegionRoot(); + EnsurePlacementRoot(); TryResolveStreamTarget(); } @@ -83,6 +84,7 @@ namespace InfiniteWorld.VoxelWorld EnsureRuntimeData(); EnsureChunkRoot(); EnsureRegionRoot(); + EnsurePlacementRoot(); if (!TryResolveStreamTarget()) { return; @@ -121,6 +123,7 @@ namespace InfiniteWorld.VoxelWorld CleanupChunks(); CleanupRegions(); CleanupPlacementPlans(); + CleanupSpawnedPlacements(); atlas?.Dispose(); atlas = null; } @@ -290,6 +293,7 @@ namespace InfiniteWorld.VoxelWorld { chunkPlacementPlans.Remove(coord); } + DespawnChunkPlacements(coord); runtime.Dispose(); TryDisposeRegionIfEmpty(regionCoord); QueueNeighborRefresh(coord); @@ -804,6 +808,8 @@ namespace InfiniteWorld.VoxelWorld runtime.State = ChunkState.ReadyToRender; } + TrySpawnChunkPlacements(result.Coord); + return true; } @@ -890,6 +896,7 @@ namespace InfiniteWorld.VoxelWorld } chunks.Clear(); + CleanupSpawnedPlacements(); dirtyChunkMeshes.Clear(); queuedChunkMeshes.Clear(); pendingNeighborRefreshes.Clear();