using System; using System.Collections; using System.Collections.Generic; using Swihoni.Components; using Swihoni.Util.Math; using Unity.Profiling; using UnityEngine; using UnityEngine.SceneManagement; using Voxels.Map; using Random = UnityEngine.Random; namespace Voxels { public delegate void MapProgressCallback(in MapProgressInfo mapProgressInfoCallback); public enum MapLoadingStage { Waiting, CleaningUp, SettingUp, Generating, UpdatingMesh, Completed, Failed } public struct MapProgressInfo { public MapLoadingStage stage; public float progress; } public enum ChunkActionType { Commission, Generate, UpdateMesh } public class ChunkManager : MonoBehaviour { [SerializeField] private GameObject m_ChunkPrefab = default; [SerializeField] private int m_ChunkSize = default; private readonly Stack m_ChunkPool = new(); private int m_PoolSize; private MapContainer m_LoadedMap; public int ChunkSize => m_ChunkSize; public MapContainer Map { get; private set; } public Dictionary Chunks { get; } = new(); public MapProgressInfo ProgressInfo { get; set; } = new() {stage = MapLoadingStage.Completed}; public IEnumerator LoadMap(MapContainer map) { Map = map; m_LoadedMap = map.Clone(); SetPoolSize(map); // Decommission all current chunks yield return DecommissionAllChunks(); if (map.terrainHeight.WithoutValue) yield break; yield return ManagePoolSize(); yield return ChunkActionForAllStaticMapChunks(map, ChunkActionType.Commission); yield return ChunkActionForAllStaticMapChunks(map, ChunkActionType.Generate); yield return ChunkActionForAllStaticMapChunks(map, ChunkActionType.UpdateMesh); ProgressInfo = new MapProgressInfo {stage = MapLoadingStage.Completed}; } private IEnumerator DecommissionAllChunks() { var progress = 0.0f; int commissionedChunks = Chunks.Count; var chunksInPositionToRemove = new List(Chunks.Keys); foreach (Position3Int chunkPosition in chunksInPositionToRemove) { DecommissionChunkInPosition(chunkPosition); progress += 1.0f / commissionedChunks; ProgressInfo = new MapProgressInfo {stage = MapLoadingStage.CleaningUp, progress = progress}; yield return null; } } private IEnumerator ChunkActionForAllStaticMapChunks(MapContainer map, ChunkActionType actionType) { var progress = 0.0f; Position3Int lower = map.dimension.lowerBound, upper = map.dimension.upperBound; for (int x = lower.x; x <= upper.x; x++) for (int y = lower.y; y <= upper.y; y++) for (int z = lower.z; z <= upper.z; z++) { var chunkPosition = new Position3Int(x, y, z); progress += 1.0f / m_PoolSize; var progressInfo = new MapProgressInfo {progress = progress}; switch (actionType) { case ChunkActionType.Commission: { CommissionChunkFromPoolIntoPosition(chunkPosition); progressInfo.stage = MapLoadingStage.SettingUp; break; } case ChunkActionType.Generate: { GetChunkFromPosition(chunkPosition).CreateTerrainFromSave(map); progressInfo.stage = MapLoadingStage.Generating; break; } case ChunkActionType.UpdateMesh: { UpdateChunkMesh(GetChunkFromPosition(chunkPosition)); progressInfo.stage = MapLoadingStage.UpdatingMesh; break; } default: { Debug.LogWarning($"Unrecognized chunk action type: {actionType}"); break; } } ProgressInfo = progressInfo; yield return null; } if (actionType == ChunkActionType.Generate) foreach (VoxelChange change in map.voxelChanges.List) ApplyVoxelChanges(change, false, TouchedChunks); } /// /// Manage the size of the pool, commission chunks if there are too little, /// and decommission if there are too much. /// This will only change things if chunk pool and the chunks are free. /// private IEnumerator ManagePoolSize() { var adjust = true; while (adjust) { int totalAmountOfChunks = m_ChunkPool.Count + Chunks.Count; if (totalAmountOfChunks < m_PoolSize) { GameObject chunkInstance = Instantiate(m_ChunkPrefab); SceneManager.MoveGameObjectToScene(chunkInstance, gameObject.scene); chunkInstance.name = "DecommissionedChunk"; // chunkInstance.hideFlags = HideFlags.HideInHierarchy; var chunk = chunkInstance.GetComponent(); chunk.Initialize(this, m_ChunkSize); m_ChunkPool.Push(chunk); } else { // if (totalAmountOfChunks > m_PoolSize) // Destroy(m_ChunkPool.Pop().gameObject); adjust = false; } yield return null; } } private static readonly TouchedChunks TouchedChunks = new(); private static readonly ProfilerMarker ApplyVoxelChangeMarker = new("Apply Voxel Change"); /// /// Change the data of a chunk in the array. /// /// Data to change on voxel /// Whether or not to update map save component /// /// public void ApplyVoxelChanges(in VoxelChange change, bool updateSave = true, TouchedChunks existingTouched = null, bool overrideBreakable = false) { using (ApplyVoxelChangeMarker.Auto()) { TouchedChunks touched = existingTouched ?? TouchedChunks; if (change.isUndo) { foreach ((Chunk chunk, Position3Int position, Voxel voxel) in change.undo) { chunk.SetVoxelDataRawNoCheck(position, voxel); AddChunksToUpdateFromVoxel(position, chunk, touched); } if (updateSave) Map.voxelChanges.RemoveEnd(); } else { Position3Int worldPosition = change.position.Value; Random.InitState(worldPosition.GetHashCode()); VoxelChange GetEvaluated(in VoxelChange originalChange, Chunk chunk, in Position3Int voxelChunkPosition, bool mergeOriginal = true) { VoxelChange evaluatedChange = originalChange.revert.GetValueOrDefault() ? chunk.GetChangeFromMap(voxelChunkPosition, Map) : default; if (mergeOriginal) evaluatedChange.Merge(originalChange); return evaluatedChange; } void SetEvaluatedVoxel(in VoxelChange originalChange, in VoxelChange evaluatedChange, Chunk chunk, in Position3Int voxelChunkPosition) { // ReSharper disable once PossibleInvalidOperationException originalChange.undo?.Add((chunk, voxelChunkPosition, chunk.GetVoxelNoCheck(voxelChunkPosition))); chunk.SetVoxelDataNoCheck(voxelChunkPosition, evaluatedChange); AddChunksToUpdateFromVoxel(voxelChunkPosition, chunk, touched); } VoxelVolumeForm form = change.form.Value; switch (form) { case VoxelVolumeForm.Single: { Chunk chunk = GetChunkFromWorldPosition(worldPosition); if (!chunk) return; Position3Int voxelChunkPosition = WorldVoxelToChunkVoxel(worldPosition, chunk); VoxelChange evaluatedChange = GetEvaluated(change, chunk, voxelChunkPosition); SetEvaluatedVoxel(change, evaluatedChange, chunk, voxelChunkPosition); break; } case VoxelVolumeForm.Prism: { Position3Int l = worldPosition, u = change.upperBound.Value; int lx = Math.Min(l.x, u.x), ly = Math.Min(l.y, u.y), lz = Math.Min(l.z, u.z); for (int x = lx; x <= Math.Max(l.x, u.x); x++) for (int y = ly; y <= Math.Max(l.y, u.y); y++) for (int z = lz; z <= Math.Max(l.z, u.z); z++) { var voxelWorldPosition = new Position3Int(x, y, z); Chunk chunk = GetChunkFromWorldPosition(voxelWorldPosition); if (!chunk) continue; Position3Int voxelChunkPosition = WorldVoxelToChunkVoxel(voxelWorldPosition, chunk); ref Voxel voxel = ref chunk.GetVoxelNoCheck(voxelChunkPosition); /* Evaluated Change */ VoxelChange evaluatedChange = GetEvaluated(change, chunk, voxelChunkPosition); evaluatedChange.form = VoxelVolumeForm.Single; if (change.density.HasValue && (x == lx || y == ly || z == lz)) evaluatedChange.density = null; else if (evaluatedChange.density is { } density && !change.noRandom.GetValueOrDefault()) evaluatedChange.density = checked((byte) Mathf.RoundToInt(density * Random.Range(0.75f, 1.0f))); if (!change.modifiesBlocks.GetValueOrDefault() && voxel.HasBlock) { evaluatedChange.color = null; evaluatedChange.texture = null; } SetEvaluatedVoxel(change, evaluatedChange, chunk, voxelChunkPosition); } break; } case VoxelVolumeForm.Spherical: case VoxelVolumeForm.Cylindrical: { float radius = change.magnitude.Value; bool isAdditive = radius > 0.0f; float absoluteRadius = Mathf.Abs(radius); int roundedRadius = Mathf.CeilToInt(absoluteRadius); for (int ix = -roundedRadius; ix <= roundedRadius; ix++) for (int iy = -roundedRadius; iy <= roundedRadius; iy++) for (int iz = -roundedRadius; iz <= roundedRadius; iz++) { Position3Int voxelWorldPosition = worldPosition + new Position3Int(ix, iy, iz); Chunk chunk = GetChunkFromWorldPosition(voxelWorldPosition); if (!chunk) continue; Position3Int voxelChunkPosition = WorldVoxelToChunkVoxel(voxelWorldPosition, chunk); ref Voxel voxel = ref chunk.GetVoxelNoCheck(voxelChunkPosition); static float GetDistance(VoxelVolumeForm _form, in Vector3 _center, in Vector3 _voxel) => _form == VoxelVolumeForm.Cylindrical ? Mathf.Sqrt((_center.x - _voxel.x).Square() + (_center.z - _voxel.z).Square()) : Vector3.Distance(_center, _voxel); float distance = GetDistance(form, worldPosition, voxelWorldPosition), floatDensity = distance / absoluteRadius * 0.5f; if (!change.noRandom.GetValueOrDefault()) floatDensity *= Random.Range(0.85f, 1.0f); byte newDensity = checked((byte) Mathf.RoundToInt(Mathf.Clamp01(floatDensity) * byte.MaxValue)), currentDensity = voxel.density; if (!overrideBreakable && voxel.IsUnbreakable) continue; /* Evaluated Change */ VoxelChange evaluatedChange = GetEvaluated(change, chunk, voxelChunkPosition, false); var changedDensity = false; if (isAdditive) { newDensity = checked((byte) (byte.MaxValue - newDensity)); if (newDensity > currentDensity) { evaluatedChange.density = newDensity; changedDensity = true; } } else { if (newDensity < currentDensity) { evaluatedChange.density = newDensity; changedDensity = true; } } if (changedDensity && voxel.OnlySmooth) { evaluatedChange.Merge(change); if (change.color is { } color) evaluatedChange.color = Color32.Lerp(color, voxel.color, Mathf.Clamp01(distance / absoluteRadius) * 0.1f); evaluatedChange.natural = false; } bool isInside = GetDistance(form, worldPosition, voxelWorldPosition + new Vector3(0.5f, 0.5f, 0.5f)) < absoluteRadius * 1.5f; if (isInside && !isAdditive && change.modifiesBlocks.GetValueOrDefault() && voxel.HasBlock) { evaluatedChange.Merge(change); if (change.color is { } color) evaluatedChange.color = Color32.Lerp(color, voxel.color, Mathf.Clamp01(distance / absoluteRadius) * 0.1f); evaluatedChange.natural = false; evaluatedChange.hasBlock = false; } evaluatedChange.form = VoxelVolumeForm.Single; SetEvaluatedVoxel(change, evaluatedChange, chunk, voxelChunkPosition); } break; } case VoxelVolumeForm.Wall: break; default: throw new ArgumentOutOfRangeException(nameof(form), form, null); } if (updateSave) Map.voxelChanges.Append(change); } if (existingTouched is null) TouchedChunks.UpdateMesh(); } } public VoxelChange? GetMapSaveVoxel(in Position3Int worldPosition) { Chunk chunk = GetChunkFromWorldPosition(worldPosition); if (!chunk) return null; Position3Int voxelChunkPosition = WorldVoxelToChunkVoxel(worldPosition, chunk); VoxelChange change = chunk.GetChangeFromMap(voxelChunkPosition, m_LoadedMap); change.position = worldPosition; return change; } /// /// Given a world position, find the chunk and then the voxel in that chunk. /// /// Position of voxel in world space /// Chunk we know it is in. If it is null, we will try to find it /// Voxel in that chunk, or null if it does not exist public Voxel? GetVoxel(in Position3Int worldPosition, Chunk chunk = null) { if (!chunk) chunk = GetChunkFromWorldPosition(worldPosition); // ReSharper disable once PossibleNullReferenceException if (chunk) return chunk.GetVoxelNoCheck(WorldVoxelToChunkVoxel(worldPosition, chunk)); return null; } /// /// Given a world position, determine if a chunk is there. /// /// World position of chunk /// Chunk instance, or null if it doe not exist public Chunk GetChunkFromWorldPosition(in Position3Int worldPosition) { Position3Int chunkPosition = WorldToChunk(worldPosition); return GetChunkFromPosition(chunkPosition); } public static void UpdateChunkMesh(Chunk chunk) => chunk.UpdateAndApply(); public void AddChunksToUpdateFromVoxel(in Position3Int voxelChunkPosition, Chunk originatingChunk, ICollection chunksToUpdate) { chunksToUpdate.Add(originatingChunk); void AddIfNeeded(int voxelChunkPositionSingle, in Position3Int axis) { int sign; if (voxelChunkPositionSingle == 0) sign = -1; else if (voxelChunkPositionSingle == m_ChunkSize - 1) sign = 1; else return; Chunk chunk = GetChunkFromPosition(originatingChunk.Position + axis * sign); if (chunk) chunksToUpdate.Add(chunk); } AddIfNeeded(voxelChunkPosition.x, new Position3Int {x = 1}); AddIfNeeded(voxelChunkPosition.y, new Position3Int {y = 1}); AddIfNeeded(voxelChunkPosition.z, new Position3Int {z = 1}); } private static readonly HashSet UpdateAdjacentChunks = new(); private void UpdateChunkMesh(Chunk chunk, in Position3Int voxelChunkPosition) { AddChunksToUpdateFromVoxel(voxelChunkPosition, chunk, UpdateAdjacentChunks); foreach (Chunk chunkToUpdate in UpdateAdjacentChunks) UpdateChunkMesh(chunkToUpdate); UpdateAdjacentChunks.Clear(); } public Chunk GetChunkFromPosition(in Position3Int chunkPosition) { Chunks.TryGetValue(chunkPosition, out Chunk containerChunk); return containerChunk; } private void SetPoolSize(MapContainer save) { Position3Int upper = save.dimension.upperBound, lower = save.dimension.lowerBound; m_PoolSize = (upper.x - lower.x + 1) * (upper.y - lower.y + 1) * (upper.z - lower.z + 1); } private void CommissionChunkFromPoolIntoPosition(in Position3Int newChunkPosition) { if (Chunks.ContainsKey(newChunkPosition)) return; Chunk chunk = m_ChunkPool.Pop(); Chunks.Add(newChunkPosition, chunk); chunk.Commission(newChunkPosition); } private void DecommissionChunkInPosition(in Position3Int chunkPosition) { Chunk chunk = GetChunkFromPosition(chunkPosition); if (!chunk) return; chunk.Decommission(); Chunks.Remove(chunk.Position); m_ChunkPool.Push(chunk); } public Position3Int WorldVoxelToChunkVoxel(in Position3Int worldPosition, Chunk chunk) => worldPosition - chunk.Position * m_ChunkSize; /// /// Given a world position, return the position of the chunk that would contain it. /// /// World position inside of chunk /// Position of chunk in respect to chunks dictionary private Position3Int WorldToChunk(in Vector3 worldPosition) { float chunkSize = m_ChunkSize; return new Position3Int(Mathf.FloorToInt(worldPosition.x / chunkSize), Mathf.FloorToInt(worldPosition.y / chunkSize), Mathf.FloorToInt(worldPosition.z / chunkSize)); } } }