using System; using System.Collections; using System.Collections.Generic; using BlockSpace.Gamemodes; using Photon.Pun; using UnityEngine; namespace BlockSpace.Voxels { [DisallowMultipleComponent] public sealed class VoxelGroupManager : MonoBehaviour { private enum UndoOperationType : byte { Stroke = 0, Erase = 1, Merge = 2 } private sealed class GroupSnapshot { public ushort id; public Vector3Int position; public byte packedRotation; public Vector3Int size; public byte[] voxels; } public struct SavedGroupData { public Vector3Int position; public byte packedRotation; public Vector3Int size; public byte[] voxels; public bool IsValid { get { if (voxels != null && voxels.Length != 0 && size.x > 0 && size.y > 0) { return size.z > 0; } return false; } } } private sealed class UndoRecord { public UndoOperationType type; public readonly List before = new List(); public readonly List after = new List(); } private const string SpawnClipPath = "Assets/Spawn.mp3"; private const string DeleteClipPath = "Assets/Delete.mp3"; private const string SpawnDestroyParticlePrefabPath = "Assets/Spawn_Destroy Particle.prefab"; public const float DefaultVoxelSize = 0.125f; [SerializeField] private float voxelSize = 0.125f; [SerializeField] private int maxRebuildsPerFrame = 2; [SerializeField] private bool creatorMode = true; [SerializeField] private Material opaqueMaterial; [SerializeField] private Material unlitMaterial; [SerializeField] private Material transparentMaterial; [SerializeField] private Material invisibleMaterial; [SerializeField] private AudioClip spawnClip; [SerializeField] private AudioClip deleteClip; [SerializeField] private GameObject spawnDestroyParticlePrefab; private readonly Dictionary groups = new Dictionary(); private readonly Queue rebuildQueue = new Queue(); private readonly HashSet queuedIds = new HashSet(); private readonly List deleteBuffer = new List(); private readonly List groupBuffer = new List(); private readonly Stack undoStack = new Stack(); private readonly Dictionary activeEraseBeforeSnapshots = new Dictionary(); private readonly HashSet activeEraseAffectedIds = new HashSet(); private ushort nextId = 1; private Material[] sharedMaterials; private bool eraseUndoSessionActive; private bool isUndoing; private bool suppressNetworkBroadcast; private bool editingLocked; private VoxelGroupPhotonSync networkSync; public const ushort AvatarGroupIdMin = 60000; public static VoxelGroupManager Instance { get; private set; } public float VoxelSize => voxelSize; public bool CreatorMode => creatorMode; public bool EditingLocked => editingLocked; internal Material[] SharedMaterials => sharedMaterials; internal void RegisterNetworkSync(VoxelGroupPhotonSync sync) { networkSync = sync; } public VoxelGroup CreateGroupFromStroke(ICollection worldCells, byte voxelValue, byte rotation = 0) { if (worldCells == null || worldCells.Count == 0 || PackedVoxel.IsAir(voxelValue)) { return null; } Vector3Int vector3Int = new Vector3Int(int.MaxValue, int.MaxValue, int.MaxValue); Vector3Int vector3Int2 = new Vector3Int(int.MinValue, int.MinValue, int.MinValue); foreach (Vector3Int worldCell in worldCells) { if (worldCell.x < vector3Int.x) { vector3Int.x = worldCell.x; } if (worldCell.y < vector3Int.y) { vector3Int.y = worldCell.y; } if (worldCell.z < vector3Int.z) { vector3Int.z = worldCell.z; } if (worldCell.x > vector3Int2.x) { vector3Int2.x = worldCell.x; } if (worldCell.y > vector3Int2.y) { vector3Int2.y = worldCell.y; } if (worldCell.z > vector3Int2.z) { vector3Int2.z = worldCell.z; } } Vector3Int size = new Vector3Int(vector3Int2.x - vector3Int.x + 1, vector3Int2.y - vector3Int.y + 1, vector3Int2.z - vector3Int.z + 1); byte[] array = new byte[size.x * size.y * size.z]; foreach (Vector3Int worldCell2 in worldCells) { Vector3Int vector3Int3 = worldCell2 - vector3Int; int num = vector3Int3.x + vector3Int3.y * size.x + vector3Int3.z * size.x * size.y; array[num] = voxelValue; } VoxelGroup voxelGroup = CreateGroup(vector3Int, rotation, size, array, queueInitialRebuild: true, playSpawnAnimation: true, playSpawnParticles: true); if (voxelGroup != null) { voxelGroup.zoneID = GetCurrentZoneIDForNewGroups(); } return voxelGroup; } public VoxelGroup BeginStrokeGroup(byte rotation = 0) { VoxelGroup voxelGroup = CreateGroup(Vector3Int.zero, rotation, Vector3Int.zero, new byte[0], queueInitialRebuild: false, playSpawnAnimation: false, playSpawnParticles: false); voxelGroup.zoneID = GetCurrentZoneIDForNewGroups(); voxelGroup.SetGreedyRenderMeshing(enabled: false); return voxelGroup; } public bool AddVoxelToStrokeGroup(VoxelGroup group, Vector3Int worldCell, byte voxelValue) { if (!ContainsGroup(group) || PackedVoxel.IsAir(voxelValue)) { return false; } return group.SetWorldVoxel(worldCell, voxelValue, markMeshDirty: false, markColliderDirty: false); } public void RefreshStrokeGroup(VoxelGroup group) { if (ContainsGroup(group)) { group.RebuildMesh(); } } internal bool RasterizeStrokeSegment(VoxelGroup group, Vector3 start, Vector3 end, float radius, byte voxelValue, ISet trackedCells = null, bool refreshMesh = true) { if (!ContainsGroup(group) || PackedVoxel.IsAir(voxelValue)) { return false; } float num = Mathf.Max(voxelSize * 0.5f, radius * 0.5f); float num2 = Vector3.Distance(start, end); int num3 = Mathf.Max(1, Mathf.CeilToInt(num2 / num)); bool flag = false; for (int i = 0; i <= num3; i++) { float t = ((num3 == 0) ? 0f : ((float)i / (float)num3)); Vector3 worldCenter = Vector3.Lerp(start, end, t); flag |= AddSphereToStrokeGroup(group, worldCenter, radius, voxelValue, trackedCells); } if (flag && refreshMesh) { RefreshStrokeGroup(group); } return flag; } internal void DiscardTransientStrokeGroup(VoxelGroup group) { if (ContainsGroup(group)) { RemoveGroupInternal(group, sendRemoveHook: false, playDestroyAnimation: false, playDestroyParticles: false); } } internal void BroadcastStrokePreviewBegin(byte voxelValue, float radius, float forwardOffset, Vector3 startPoint) { if (!suppressNetworkBroadcast) { ResolveNetworkSync()?.BroadcastStrokePreviewBegin(voxelValue, radius, forwardOffset, startPoint); } } internal void BroadcastStrokePreviewUpdate(byte voxelValue, float radius, float forwardOffset, Vector3 point) { if (!suppressNetworkBroadcast) { ResolveNetworkSync()?.BroadcastStrokePreviewUpdate(voxelValue, radius, forwardOffset, point); } } internal void BroadcastStrokePreviewEnd() { if (!suppressNetworkBroadcast) { ResolveNetworkSync()?.BroadcastStrokePreviewEnd(); } } internal void BroadcastErasePreviewBegin(float radius, float forwardOffset, Vector3 startPoint) { if (!suppressNetworkBroadcast) { ResolveNetworkSync()?.BroadcastErasePreviewBegin(radius, forwardOffset, startPoint); } } internal void BroadcastErasePreviewUpdate(float radius, float forwardOffset, Vector3 point) { if (!suppressNetworkBroadcast) { ResolveNetworkSync()?.BroadcastErasePreviewUpdate(radius, forwardOffset, point); } } internal void BroadcastErasePreviewEnd() { if (!suppressNetworkBroadcast) { ResolveNetworkSync()?.BroadcastErasePreviewEnd(); } } internal bool TryRestoreTransientGroup(ushort id, SavedGroupData data) { if (!data.IsValid) { ApplyNetworkDestroy(id, playDestroyAnimation: false, playDestroyParticles: false); return true; } ApplyNetworkCreateOrReplace(id, data.position, data.packedRotation, data.size, data.voxels, playSpawnAnimation: false, playSpawnParticles: false); return true; } public void FinalizeStrokeGroup(VoxelGroup group) { if (ContainsGroup(group)) { FinalizeLiveEditedGroup(group, sendNetworkUpdate: false); RecordStrokeUndo(group, playSpawnAnimationOnRemote: true); } } public void CancelStrokeGroup(VoxelGroup group) { if (ContainsGroup(group)) { DeleteGroup(group); } } private bool AddSphereToStrokeGroup(VoxelGroup group, Vector3 worldCenter, float radius, byte voxelValue, ISet trackedCells) { float num = radius * radius; int num2 = Mathf.FloorToInt((worldCenter.x - radius) / voxelSize); int num3 = Mathf.FloorToInt((worldCenter.y - radius) / voxelSize); int num4 = Mathf.FloorToInt((worldCenter.z - radius) / voxelSize); int num5 = Mathf.FloorToInt((worldCenter.x + radius) / voxelSize); int num6 = Mathf.FloorToInt((worldCenter.y + radius) / voxelSize); int num7 = Mathf.FloorToInt((worldCenter.z + radius) / voxelSize); bool flag = false; for (int i = num4; i <= num7; i++) { for (int j = num3; j <= num6; j++) { for (int k = num2; k <= num5; k++) { if (!((new Vector3(((float)k + 0.5f) * voxelSize, ((float)j + 0.5f) * voxelSize, ((float)i + 0.5f) * voxelSize) - worldCenter).sqrMagnitude > num)) { Vector3Int vector3Int = new Vector3Int(k, j, i); if (trackedCells == null || trackedCells.Add(vector3Int)) { flag |= AddVoxelToStrokeGroup(group, vector3Int, voxelValue); } } } } } return flag; } public void DeleteGroup(ushort id) { if (groups.TryGetValue(id, out var value)) { RemoveGroup(value); } } public void DeleteGroup(VoxelGroup group) { if (!(group == null)) { RemoveGroup(group); } } public VoxelGroup MergeGroups(IReadOnlyCollection groupsToMerge) { if (groupsToMerge == null || groupsToMerge.Count < 2) { return null; } List list = new List(groupsToMerge.Count); foreach (VoxelGroup item in groupsToMerge) { if (ContainsGroup(item) && item.size.x > 0 && item.size.y > 0 && item.size.z > 0) { list.Add(item); } } if (list.Count < 2) { return null; } UndoRecord undoRecord = (isUndoing ? null : new UndoRecord { type = UndoOperationType.Merge }); list.Sort((VoxelGroup a, VoxelGroup b) => a.id.CompareTo(b.id)); Vector3Int vector3Int = new Vector3Int(int.MaxValue, int.MaxValue, int.MaxValue); Vector3Int vector3Int2 = new Vector3Int(int.MinValue, int.MinValue, int.MinValue); bool flag = false; for (int num = 0; num < list.Count; num++) { VoxelGroup voxelGroup = list[num]; for (int num2 = 0; num2 < voxelGroup.size.z; num2++) { for (int num3 = 0; num3 < voxelGroup.size.y; num3++) { for (int num4 = 0; num4 < voxelGroup.size.x; num4++) { if (!PackedVoxel.IsAir(voxelGroup.GetVoxel(num4, num3, num2))) { Vector3Int vector3Int3 = voxelGroup.position + GridRotationUtility.Rotate(new Vector3Int(num4, num3, num2), voxelGroup.packedRotation); if (vector3Int3.x < vector3Int.x) { vector3Int.x = vector3Int3.x; } if (vector3Int3.y < vector3Int.y) { vector3Int.y = vector3Int3.y; } if (vector3Int3.z < vector3Int.z) { vector3Int.z = vector3Int3.z; } if (vector3Int3.x > vector3Int2.x) { vector3Int2.x = vector3Int3.x; } if (vector3Int3.y > vector3Int2.y) { vector3Int2.y = vector3Int3.y; } if (vector3Int3.z > vector3Int2.z) { vector3Int2.z = vector3Int3.z; } flag = true; } } } } } if (!flag) { return null; } Vector3Int groupSize = new Vector3Int(vector3Int2.x - vector3Int.x + 1, vector3Int2.y - vector3Int.y + 1, vector3Int2.z - vector3Int.z + 1); byte[] array = new byte[groupSize.x * groupSize.y * groupSize.z]; for (int num5 = 0; num5 < list.Count; num5++) { VoxelGroup voxelGroup2 = list[num5]; for (int num6 = 0; num6 < voxelGroup2.size.z; num6++) { for (int num7 = 0; num7 < voxelGroup2.size.y; num7++) { for (int num8 = 0; num8 < voxelGroup2.size.x; num8++) { byte voxel = voxelGroup2.GetVoxel(num8, num7, num6); if (!PackedVoxel.IsAir(voxel)) { Vector3Int vector3Int4 = voxelGroup2.position + GridRotationUtility.Rotate(new Vector3Int(num8, num7, num6), voxelGroup2.packedRotation) - vector3Int; int num9 = vector3Int4.x + vector3Int4.y * groupSize.x + vector3Int4.z * groupSize.x * groupSize.y; array[num9] = voxel; } } } } } VoxelGroup voxelGroup3 = list[list.Count - 1]; if (undoRecord != null) { for (int num10 = 0; num10 < list.Count; num10++) { GroupSnapshot groupSnapshot = CaptureSnapshot(list[num10]); if (groupSnapshot != null) { undoRecord.before.Add(groupSnapshot); } } } for (int num11 = 0; num11 < list.Count; num11++) { VoxelGroup voxelGroup4 = list[num11]; if (!(voxelGroup4 == voxelGroup3)) { RemoveGroupInternal(voxelGroup4, !suppressNetworkBroadcast, playDestroyAnimation: false, playDestroyParticles: false); } } voxelGroup3.OverwriteData(vector3Int, 0, groupSize, array); if (undoRecord != null) { GroupSnapshot groupSnapshot2 = CaptureSnapshot(voxelGroup3); if (groupSnapshot2 != null) { undoRecord.after.Add(groupSnapshot2); PushUndoRecord(undoRecord); } } SendUpdateGroupAfterMergeStub(voxelGroup3); return voxelGroup3; } public bool TryCaptureGroupData(VoxelGroup group, out SavedGroupData data) { data = default(SavedGroupData); if (!ContainsGroup(group) || group.voxels == null || group.voxels.Length == 0) { return false; } data = new SavedGroupData { position = group.position, packedRotation = group.packedRotation, size = group.size, voxels = (byte[])group.voxels.Clone() }; return data.IsValid; } public bool TryBuildCombinedGroupData(IReadOnlyCollection groupsToCombine, out SavedGroupData data) { data = default(SavedGroupData); if (groupsToCombine == null || groupsToCombine.Count == 0) { return false; } List list = new List(groupsToCombine.Count); foreach (VoxelGroup item in groupsToCombine) { if (ContainsGroup(item) && item.size.x > 0 && item.size.y > 0 && item.size.z > 0) { list.Add(item); } } if (list.Count == 0) { return false; } if (list.Count == 1) { return TryCaptureGroupData(list[0], out data); } list.Sort((VoxelGroup a, VoxelGroup b) => a.id.CompareTo(b.id)); Vector3Int vector3Int = new Vector3Int(int.MaxValue, int.MaxValue, int.MaxValue); Vector3Int vector3Int2 = new Vector3Int(int.MinValue, int.MinValue, int.MinValue); bool flag = false; for (int num = 0; num < list.Count; num++) { VoxelGroup voxelGroup = list[num]; for (int num2 = 0; num2 < voxelGroup.size.z; num2++) { for (int num3 = 0; num3 < voxelGroup.size.y; num3++) { for (int num4 = 0; num4 < voxelGroup.size.x; num4++) { if (!PackedVoxel.IsAir(voxelGroup.GetVoxel(num4, num3, num2))) { Vector3Int vector3Int3 = voxelGroup.position + GridRotationUtility.Rotate(new Vector3Int(num4, num3, num2), voxelGroup.packedRotation); if (vector3Int3.x < vector3Int.x) { vector3Int.x = vector3Int3.x; } if (vector3Int3.y < vector3Int.y) { vector3Int.y = vector3Int3.y; } if (vector3Int3.z < vector3Int.z) { vector3Int.z = vector3Int3.z; } if (vector3Int3.x > vector3Int2.x) { vector3Int2.x = vector3Int3.x; } if (vector3Int3.y > vector3Int2.y) { vector3Int2.y = vector3Int3.y; } if (vector3Int3.z > vector3Int2.z) { vector3Int2.z = vector3Int3.z; } flag = true; } } } } } if (!flag) { return false; } Vector3Int size = new Vector3Int(vector3Int2.x - vector3Int.x + 1, vector3Int2.y - vector3Int.y + 1, vector3Int2.z - vector3Int.z + 1); byte[] array = new byte[size.x * size.y * size.z]; for (int num5 = 0; num5 < list.Count; num5++) { VoxelGroup voxelGroup2 = list[num5]; for (int num6 = 0; num6 < voxelGroup2.size.z; num6++) { for (int num7 = 0; num7 < voxelGroup2.size.y; num7++) { for (int num8 = 0; num8 < voxelGroup2.size.x; num8++) { byte voxel = voxelGroup2.GetVoxel(num8, num7, num6); if (!PackedVoxel.IsAir(voxel)) { Vector3Int vector3Int4 = voxelGroup2.position + GridRotationUtility.Rotate(new Vector3Int(num8, num7, num6), voxelGroup2.packedRotation) - vector3Int; int num9 = vector3Int4.x + vector3Int4.y * size.x + vector3Int4.z * size.x * size.y; array[num9] = voxel; } } } } } data = new SavedGroupData { position = vector3Int, packedRotation = 0, size = size, voxels = array }; return true; } public VoxelGroup SpawnSavedGroup(SavedGroupData data, Vector3Int gridPosition) { if (!data.IsValid) { return null; } VoxelGroup voxelGroup = CreateGroup(gridPosition, data.packedRotation, data.size, data.voxels, queueInitialRebuild: true, playSpawnAnimation: true, playSpawnParticles: true); if (voxelGroup != null) { voxelGroup.zoneID = GetCurrentZoneIDForNewGroups(); } RecordStrokeUndo(voxelGroup, playSpawnAnimationOnRemote: false, playSpawnParticlesOnRemote: false); return voxelGroup; } public VoxelGroup DuplicateGroup(VoxelGroup source, Vector3Int gridOffset) { if (!ContainsGroup(source) || source.voxels == null || source.voxels.Length == 0) { return null; } VoxelGroup voxelGroup = CreateGroup(source.position + gridOffset, source.packedRotation, source.size, (byte[])source.voxels.Clone(), queueInitialRebuild: true, playSpawnAnimation: true, playSpawnParticles: true); if (voxelGroup != null) { voxelGroup.zoneID = source.zoneID; RecordStrokeUndo(voxelGroup, playSpawnAnimationOnRemote: false, playSpawnParticlesOnRemote: false); } return voxelGroup; } public bool TransformGroup(VoxelGroup group, Vector3Int gridPosition, byte rotation, bool sendNetworkUpdate = true) { if (!ContainsGroup(group)) { return false; } if (group.position == gridPosition && group.packedRotation == rotation) { return true; } group.SetGridPose(gridPosition, rotation); if (sendNetworkUpdate) { SendUpdateGroupStub(group); } return true; } public void SetGroupColliderEnabled(VoxelGroup group, bool enabled) { if (ContainsGroup(group) && !(group.meshCollider == null)) { group.meshCollider.enabled = enabled; if (!suppressNetworkBroadcast) { ResolveNetworkSync()?.BroadcastSetColliderEnabled(group, enabled); } } } public void BroadcastMovePreview(VoxelGroup group) { if (ContainsGroup(group) && !suppressNetworkBroadcast) { ResolveNetworkSync()?.BroadcastMovePreview(group); } } public void BroadcastMoveFinal(VoxelGroup group) { if (ContainsGroup(group) && !suppressNetworkBroadcast) { ResolveNetworkSync()?.BroadcastMoveFinal(group); } } public bool PaintWholeGroup(VoxelGroup group, byte voxelValue) { if (!ContainsGroup(group) || PackedVoxel.IsAir(voxelValue)) { return false; } if (!group.RepaintAll(voxelValue)) { return false; } SendUpdateGroupStub(group); return true; } public void BeginEraseUndoSession() { if (!isUndoing) { activeEraseBeforeSnapshots.Clear(); activeEraseAffectedIds.Clear(); eraseUndoSessionActive = true; } } public void BeginPaintUndoSession() { BeginEraseUndoSession(); } public void CompleteEraseUndoSession() { if (!eraseUndoSessionActive) { return; } eraseUndoSessionActive = false; if (activeEraseBeforeSnapshots.Count == 0) { activeEraseBeforeSnapshots.Clear(); activeEraseAffectedIds.Clear(); return; } UndoRecord undoRecord = new UndoRecord { type = UndoOperationType.Erase }; foreach (KeyValuePair activeEraseBeforeSnapshot in activeEraseBeforeSnapshots) { undoRecord.before.Add(activeEraseBeforeSnapshot.Value); } foreach (ushort activeEraseAffectedId in activeEraseAffectedIds) { if (groups.TryGetValue(activeEraseAffectedId, out var value)) { GroupSnapshot groupSnapshot = CaptureSnapshot(value); if (groupSnapshot != null) { undoRecord.after.Add(groupSnapshot); } } } PushUndoRecord(undoRecord); activeEraseBeforeSnapshots.Clear(); activeEraseAffectedIds.Clear(); } public void CompletePaintUndoSession() { CompleteEraseUndoSession(); } public bool UndoLastOperation() { if (undoStack.Count == 0) { return false; } UndoRecord undoRecord = undoStack.Pop(); isUndoing = true; try { for (int i = 0; i < undoRecord.after.Count; i++) { GroupSnapshot groupSnapshot = undoRecord.after[i]; if (groups.TryGetValue(groupSnapshot.id, out var value)) { RemoveGroupInternal(value, !suppressNetworkBroadcast, playDestroyAnimation: true, playDestroyParticles: false); } } for (int j = 0; j < undoRecord.before.Count; j++) { RestoreSnapshot(undoRecord.before[j]); } } finally { isUndoing = false; } return true; } public void EraseSphere(Vector3 worldCenter, float radius) { if (radius <= 0f || groups.Count == 0) { return; } groupBuffer.Clear(); foreach (VoxelGroup value2 in groups.Values) { groupBuffer.Add(value2); } deleteBuffer.Clear(); for (int i = 0; i < groupBuffer.Count; i++) { VoxelGroup voxelGroup = groupBuffer[i]; if (!(voxelGroup == null) && voxelGroup.IntersectsSphere(worldCenter, radius) && voxelGroup.EraseSphere(worldCenter, radius)) { if (!voxelGroup.TrimToContents()) { deleteBuffer.Add(voxelGroup.id); continue; } QueueRebuild(voxelGroup); SendUpdateGroupStub(voxelGroup); } } for (int j = 0; j < deleteBuffer.Count; j++) { if (groups.TryGetValue(deleteBuffer[j], out var value)) { RemoveGroupInternal(value, !suppressNetworkBroadcast, playDestroyAnimation: false, playDestroyParticles: false); } } } public bool EraseSphereRealtime(Vector3 worldCenter, float radius, ISet editedGroups) { if (radius <= 0f || groups.Count == 0) { return false; } groupBuffer.Clear(); foreach (VoxelGroup value2 in groups.Values) { groupBuffer.Add(value2); } deleteBuffer.Clear(); bool result = false; for (int i = 0; i < groupBuffer.Count; i++) { VoxelGroup voxelGroup = groupBuffer[i]; if (voxelGroup == null || !voxelGroup.IntersectsSphere(worldCenter, radius)) { continue; } GroupSnapshot groupSnapshot = null; if (eraseUndoSessionActive && !activeEraseBeforeSnapshots.ContainsKey(voxelGroup.id)) { groupSnapshot = CaptureSnapshot(voxelGroup); } if (voxelGroup.EraseSphere(worldCenter, radius, markMeshDirty: false, markColliderDirty: false)) { result = true; if (groupSnapshot != null) { activeEraseBeforeSnapshots[voxelGroup.id] = groupSnapshot; } if (eraseUndoSessionActive) { activeEraseAffectedIds.Add(voxelGroup.id); } if (!voxelGroup.TrimToContents()) { deleteBuffer.Add(voxelGroup.id); editedGroups?.Remove(voxelGroup); } else { voxelGroup.SetGreedyRenderMeshing(enabled: false); voxelGroup.RebuildMesh(); editedGroups?.Add(voxelGroup); } } } for (int j = 0; j < deleteBuffer.Count; j++) { if (groups.TryGetValue(deleteBuffer[j], out var value)) { RemoveGroupInternal(value, !suppressNetworkBroadcast, playDestroyAnimation: false, playDestroyParticles: false); } } return result; } public bool PaintSphereRealtime(Vector3 worldCenter, float radius, byte voxelValue, ISet editedGroups) { if (radius <= 0f || groups.Count == 0 || PackedVoxel.IsAir(voxelValue)) { return false; } groupBuffer.Clear(); foreach (VoxelGroup value in groups.Values) { groupBuffer.Add(value); } bool result = false; for (int i = 0; i < groupBuffer.Count; i++) { VoxelGroup voxelGroup = groupBuffer[i]; if (voxelGroup == null || !voxelGroup.IntersectsSphere(worldCenter, radius)) { continue; } GroupSnapshot groupSnapshot = null; if (eraseUndoSessionActive && !activeEraseBeforeSnapshots.ContainsKey(voxelGroup.id)) { groupSnapshot = CaptureSnapshot(voxelGroup); } if (voxelGroup.PaintSphere(worldCenter, radius, voxelValue, markMeshDirty: false, markColliderDirty: false)) { result = true; if (groupSnapshot != null) { activeEraseBeforeSnapshots[voxelGroup.id] = groupSnapshot; } if (eraseUndoSessionActive) { activeEraseAffectedIds.Add(voxelGroup.id); } voxelGroup.SetGreedyRenderMeshing(enabled: false); voxelGroup.RebuildMesh(); editedGroups?.Add(voxelGroup); } } return result; } internal bool EraseSpherePreview(Vector3 worldCenter, float radius, ISet editedGroups) { bool flag = isUndoing; bool flag2 = suppressNetworkBroadcast; isUndoing = true; suppressNetworkBroadcast = true; try { return EraseSphereRealtime(worldCenter, radius, editedGroups); } finally { isUndoing = flag; suppressNetworkBroadcast = flag2; } } public void FinalizeLiveEditedGroup(VoxelGroup group, bool sendNetworkUpdate = true, bool rebuildCollider = true, bool playDestroyAnimationIfEmpty = true) { if (!ContainsGroup(group)) { return; } if (!group.TrimToContents()) { RemoveGroupInternal(group, !suppressNetworkBroadcast, playDestroyAnimationIfEmpty, playDestroyAnimationIfEmpty); return; } group.SetGreedyRenderMeshing(enabled: true); group.RebuildMesh(); if (rebuildCollider) { group.RebuildCollider(); } else { group.dirtyCollider = false; } if (sendNetworkUpdate) { SendUpdateGroupStub(group); } } public void SetCreatorMode(bool enabled) { if (enabled && editingLocked) { enabled = false; } if (creatorMode == enabled) { return; } creatorMode = enabled; foreach (VoxelGroup value in groups.Values) { if (!(value == null)) { value.SetCreatorMode(enabled); } } } public void SetEditingLocked(bool locked) { if (editingLocked != locked) { editingLocked = locked; if (editingLocked && creatorMode) { SetCreatorMode(enabled: false); } } } internal int GetCurrentZoneIDForNewGroups() { GamemodeZone gamemodeZone = ((GamemodeZoneManager.Instance != null) ? GamemodeZoneManager.Instance.CurrentZone : null); if (!(gamemodeZone != null)) { return -1; } return gamemodeZone.zoneID; } public void RemoveGroupsByZoneID(int zoneID) { if (groups.Count == 0) { return; } groupBuffer.Clear(); foreach (VoxelGroup value in groups.Values) { if (value != null && value.zoneID == zoneID) { groupBuffer.Add(value); } } for (int i = 0; i < groupBuffer.Count; i++) { RemoveGroup(groupBuffer[i]); } groupBuffer.Clear(); } public int RemoveGroupsForZone(GamemodeZone zone) { if (zone == null || groups.Count == 0) { return 0; } groupBuffer.Clear(); foreach (VoxelGroup value in groups.Values) { if (!(value == null) && (value.zoneID == zone.zoneID || zone.ContainsGroup(value))) { groupBuffer.Add(value); } } int count = groupBuffer.Count; for (int i = 0; i < groupBuffer.Count; i++) { RemoveGroup(groupBuffer[i]); } groupBuffer.Clear(); return count; } public bool TryGetGroup(ushort id, out VoxelGroup group) { return groups.TryGetValue(id, out group); } public bool TryPickNearestVoxelValue(Vector3 worldPoint, float maxRadius, out byte voxelValue) { voxelValue = 0; if (groups.Count == 0 || maxRadius <= 0f) { return false; } float num = maxRadius * maxRadius; bool result = false; groupBuffer.Clear(); foreach (VoxelGroup value in groups.Values) { groupBuffer.Add(value); } for (int i = 0; i < groupBuffer.Count; i++) { VoxelGroup voxelGroup = groupBuffer[i]; if (!(voxelGroup == null) && voxelGroup.IntersectsSphere(worldPoint, maxRadius) && voxelGroup.TryPickNearestVoxelValue(worldPoint, maxRadius, out var voxelValue2, out var _, out var pickedDistanceSquared) && !(pickedDistanceSquared > num)) { result = true; num = pickedDistanceSquared; voxelValue = voxelValue2; } } return result; } public List GetGroupsInRadius(Vector3 worldPos, float radius) { List list = new List(); float num = radius * radius; foreach (VoxelGroup value in groups.Values) { if (!(value == null) && value.size.x > 0 && value.size.y > 0 && value.size.z > 0 && value.GetWorldBounds().SqrDistance(worldPos) <= num) { list.Add(value); } } return list; } public List GetAllGroups() { List result = new List(groups.Count); GetAllGroups(result); return result; } public void GetAllGroups(List result) { if (result == null) { return; } result.Clear(); foreach (VoxelGroup value in groups.Values) { if (value != null) { result.Add(value); } } } public void ClearAllGroupsForNetworkSync() { bool flag = isUndoing; bool flag2 = suppressNetworkBroadcast; isUndoing = true; suppressNetworkBroadcast = true; try { undoStack.Clear(); activeEraseBeforeSnapshots.Clear(); activeEraseAffectedIds.Clear(); eraseUndoSessionActive = false; groupBuffer.Clear(); foreach (VoxelGroup value in groups.Values) { if (value != null) { groupBuffer.Add(value); } } for (int i = 0; i < groupBuffer.Count; i++) { if (!(groupBuffer[i] != null) || groupBuffer[i].id < 60000) { RemoveGroupInternal(groupBuffer[i], sendRemoveHook: false, playDestroyAnimation: false, playDestroyParticles: false); } } } finally { groupBuffer.Clear(); isUndoing = flag; suppressNetworkBroadcast = flag2; } } public VoxelGroup ApplyNetworkCreateOrReplace(ushort id, Vector3Int position, byte rotation, Vector3Int size, byte[] voxelData, bool playSpawnAnimation, bool playSpawnParticles) { return ApplyNetworkCreateOrReplace(id, position, rotation, size, voxelData, -1, playSpawnAnimation, playSpawnParticles); } public VoxelGroup ApplyNetworkCreateOrReplace(ushort id, Vector3Int position, byte rotation, Vector3Int size, byte[] voxelData, int zoneID, bool playSpawnAnimation, bool playSpawnParticles) { bool flag = isUndoing; bool flag2 = suppressNetworkBroadcast; isUndoing = true; suppressNetworkBroadcast = true; try { VoxelGroup voxelGroup = UpsertGroup(id, position, rotation, size, voxelData, queueInitialRebuild: true, playSpawnAnimation, playSpawnParticles); if (voxelGroup != null) { voxelGroup.zoneID = zoneID; } return voxelGroup; } finally { isUndoing = flag; suppressNetworkBroadcast = flag2; } } public void ApplyNetworkDestroy(ushort id, bool playDestroyAnimation, bool playDestroyParticles) { bool flag = isUndoing; bool flag2 = suppressNetworkBroadcast; isUndoing = true; suppressNetworkBroadcast = true; try { if (groups.TryGetValue(id, out var value)) { RemoveGroupInternal(value, sendRemoveHook: false, playDestroyAnimation, playDestroyParticles); } } finally { isUndoing = flag; suppressNetworkBroadcast = flag2; } } public void RemoveGroup(VoxelGroup group) { RemoveGroupInternal(group, !suppressNetworkBroadcast, playDestroyAnimation: true, playDestroyParticles: true); } public void RemoveGroupWithoutAnimation(VoxelGroup group) { RemoveGroupInternal(group, !suppressNetworkBroadcast, playDestroyAnimation: false, playDestroyParticles: false); } private void RemoveGroupInternal(VoxelGroup group, bool sendRemoveHook, bool playDestroyAnimation, bool playDestroyParticles) { if (!(group == null)) { if (groups.Remove(group.id)) { queuedIds.Remove(group.id); } group.SetSelected(selected: false); if (sendRemoveHook) { SendRemoveGroupStub(group.id, playDestroyAnimation, playDestroyParticles); } if (playDestroyAnimation) { StartCoroutine(AnimateAndDestroyGroup(group, playDestroyParticles)); } else { UnityEngine.Object.Destroy(group.gameObject); } } } public ushort GetNextGroupId() { return AllocateId(); } public void AddGroup(VoxelGroup group) { if (group == null) { return; } if (group.id == 0) { group.id = GetNextGroupId(); } if (!groups.ContainsKey(group.id)) { group.transform.SetParent(base.transform, worldPositionStays: false); group.SetOwner(this); groups.Add(group.id, group); AdvanceNextIdPast(group.id); if (group.dirtyMesh || group.dirtyCollider) { QueueRebuild(group); } } } internal void QueueRebuild(VoxelGroup group) { if (!(group == null) && queuedIds.Add(group.id)) { rebuildQueue.Enqueue(group); } } private void Awake() { if (Instance != null && Instance != this) { Debug.LogError("Multiple VoxelGroupManager instances found.", this); return; } Instance = this; voxelSize = Mathf.Max(0.001f, voxelSize); maxRebuildsPerFrame = Mathf.Max(1, maxRebuildsPerFrame); EnsureEffectAssetsAssigned(); BuildSharedMaterials(); RebindExistingGroups(); } private void Update() { int num = 0; while (num < maxRebuildsPerFrame && rebuildQueue.Count > 0) { VoxelGroup voxelGroup = rebuildQueue.Dequeue(); if (voxelGroup == null) { continue; } queuedIds.Remove(voxelGroup.id); if (groups.ContainsKey(voxelGroup.id)) { if (voxelGroup.dirtyMesh) { voxelGroup.RebuildMesh(); } if (voxelGroup.dirtyCollider) { voxelGroup.RebuildCollider(); } num++; if (voxelGroup.dirtyMesh || voxelGroup.dirtyCollider) { QueueRebuild(voxelGroup); } } } } private void OnDestroy() { if (Instance == this) { Instance = null; } } private void OnValidate() { EnsureEffectAssetsAssigned(); } private ushort AllocateId() { if (PhotonNetwork.InRoom && PhotonNetwork.LocalPlayer != null) { ushort num = (ushort)((byte)Mathf.Clamp(PhotonNetwork.LocalPlayer.ActorNumber, 1, 255) << 8); for (ushort num2 = 1; num2 < 255; num2++) { ushort num3 = (ushort)(num | num2); if (!groups.ContainsKey(num3)) { return num3; } } throw new InvalidOperationException("No free voxel group ids remain for the local network actor."); } ushort num4 = nextId; for (int i = 0; i < 65535; i++) { if (num4 == 0) { num4 = 1; } if (!groups.ContainsKey(num4)) { nextId = (ushort)(num4 + 1); if (nextId == 0) { nextId = 1; } return num4; } num4++; } throw new InvalidOperationException("No free voxel group ids remain."); } private VoxelGroup CreateGroup(Vector3Int gridPosition, byte rotation, Vector3Int size, byte[] voxels, bool queueInitialRebuild, bool playSpawnAnimation, bool playSpawnParticles) { ushort nextGroupId = GetNextGroupId(); return UpsertGroup(nextGroupId, gridPosition, rotation, size, voxels, queueInitialRebuild, playSpawnAnimation, playSpawnParticles); } private bool ContainsGroup(VoxelGroup group) { if (group != null && groups.TryGetValue(group.id, out var value)) { return value == group; } return false; } private void RebindExistingGroups() { groups.Clear(); queuedIds.Clear(); rebuildQueue.Clear(); VoxelGroup[] componentsInChildren = GetComponentsInChildren(includeInactive: true); ushort num = 0; foreach (VoxelGroup voxelGroup in componentsInChildren) { if (!(voxelGroup == null)) { if (voxelGroup.id == 0 || groups.ContainsKey(voxelGroup.id)) { voxelGroup.id = AllocateSceneId(groups); } num = ((voxelGroup.id > num) ? voxelGroup.id : num); groups[voxelGroup.id] = voxelGroup; voxelGroup.SetOwner(this); QueueRebuild(voxelGroup); } } nextId = (ushort)(num + 1); if (nextId == 0) { nextId = 1; } } private ushort AllocateSceneId(Dictionary existing) { for (ushort num = 1; num != 0; num++) { if (!existing.ContainsKey(num)) { return num; } } throw new InvalidOperationException("No free voxel group ids remain."); } private void BuildSharedMaterials() { opaqueMaterial = EnsureCompatibleMaterial(opaqueMaterial, "BlockSpace_Opaque", transparent: false, useLitShader: true); unlitMaterial = EnsureCompatibleMaterial(unlitMaterial, "BlockSpace_Unlit", transparent: false, useLitShader: false); transparentMaterial = EnsureCompatibleMaterial(transparentMaterial, "BlockSpace_Transparent", transparent: true, useLitShader: false); invisibleMaterial = EnsureCompatibleMaterial(invisibleMaterial, "BlockSpace_Invisible", transparent: true, useLitShader: false); sharedMaterials = new Material[4] { opaqueMaterial, unlitMaterial, transparentMaterial, invisibleMaterial }; } private static Material CreateDefaultMaterial(string materialName, bool transparent, bool useLitShader) { Shader shader = Shader.Find(GetBlockSpaceVertexColorShaderName(transparent, useLitShader)); if (shader != null) { Material obj = new Material(shader) { name = materialName }; ConfigureDefaultMaterial(obj, transparent); return obj; } Shader shader2 = (useLitShader ? (Shader.Find("Universal Render Pipeline/Particles/Lit") ?? Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard")) : (Shader.Find("Universal Render Pipeline/Particles/Unlit") ?? Shader.Find("Universal Render Pipeline/Unlit") ?? Shader.Find("Sprites/Default") ?? Shader.Find("Standard"))); if (shader2 == null) { Debug.LogError("BlockSpace could not find a fallback shader for voxel materials."); return null; } Material obj2 = new Material(shader2) { name = materialName }; ConfigureDefaultMaterial(obj2, transparent); return obj2; } private static Material EnsureCompatibleMaterial(Material material, string materialName, bool transparent, bool useLitShader) { bool flag = UsesBlockSpaceVertexColorShader(material); bool flag2 = UsesBlockSpaceVertexColorShader(material, transparent, useLitShader); if (material == null || !SupportsVertexColorParticles(material) || (flag && !flag2) || (!flag && useLitShader && !UsesLitShader(material)) || (!flag && !useLitShader && UsesLitShader(material))) { return CreateDefaultMaterial(materialName, transparent, useLitShader); } ConfigureDefaultMaterial(material, transparent); return material; } private static void ConfigureDefaultMaterial(Material material, bool transparent) { if (!(material == null)) { if (material.HasProperty("_BaseColor")) { material.SetColor("_BaseColor", Color.white); } if (material.HasProperty("_Color")) { material.SetColor("_Color", Color.white); } if (material.HasProperty("_Smoothness")) { material.SetFloat("_Smoothness", transparent ? 0.05f : 0.15f); } if (material.HasProperty("_Metallic")) { material.SetFloat("_Metallic", 0f); } if (material.HasProperty("_BaseMap")) { material.SetTexture("_BaseMap", Texture2D.whiteTexture); } if (material.HasProperty("_ColorMode")) { material.SetFloat("_ColorMode", 0f); } if (material.HasProperty("_BaseColorAddSubDiff")) { material.SetVector("_BaseColorAddSubDiff", Vector4.zero); } if (material.HasProperty("_Surface")) { material.SetFloat("_Surface", transparent ? 1f : 0f); } if (material.HasProperty("_Blend")) { material.SetFloat("_Blend", 0f); } if (material.HasProperty("_Cull")) { material.SetFloat("_Cull", 2f); } if (material.HasProperty("_SrcBlend")) { material.SetFloat("_SrcBlend", transparent ? 5f : 1f); } if (material.HasProperty("_DstBlend")) { material.SetFloat("_DstBlend", transparent ? 10f : 0f); } if (material.HasProperty("_SrcBlendAlpha")) { material.SetFloat("_SrcBlendAlpha", 1f); } if (material.HasProperty("_DstBlendAlpha")) { material.SetFloat("_DstBlendAlpha", transparent ? 10f : 0f); } if (material.HasProperty("_ZWrite")) { material.SetFloat("_ZWrite", transparent ? 0f : 1f); } if (material.HasProperty("_AlphaClip")) { material.SetFloat("_AlphaClip", 0f); } material.renderQueue = (transparent ? 3000 : 2000); material.enableInstancing = false; if (transparent) { material.SetOverrideTag("RenderType", "Transparent"); material.EnableKeyword("_SURFACE_TYPE_TRANSPARENT"); } else { material.SetOverrideTag("RenderType", "Opaque"); material.DisableKeyword("_SURFACE_TYPE_TRANSPARENT"); } material.DisableKeyword("_COLOROVERLAY_ON"); material.DisableKeyword("_COLORCOLOR_ON"); material.DisableKeyword("_COLORADDSUBDIFF_ON"); } } private static bool SupportsVertexColorParticles(Material material) { if (material == null || material.shader == null) { return false; } if (!material.HasProperty("_ColorMode") && !material.HasProperty("_BaseColorAddSubDiff") && !material.shader.name.Contains("Particles")) { return UsesBlockSpaceVertexColorShader(material); } return true; } private static bool UsesBlockSpaceVertexColorShader(Material material) { if (material == null || material.shader == null) { return false; } string text = material.shader.name; if (!(text == "BlockSpace/VertexColorURP") && !(text == "BlockSpace/VertexColorURP_Unlit")) { return text == "BlockSpace/VertexColorURP_Transparent"; } return true; } private static bool UsesBlockSpaceVertexColorShader(Material material, bool transparent, bool useLitShader) { if (material != null && material.shader != null) { return material.shader.name == GetBlockSpaceVertexColorShaderName(transparent, useLitShader); } return false; } private static string GetBlockSpaceVertexColorShaderName(bool transparent, bool useLitShader) { if (transparent) { return "BlockSpace/VertexColorURP_Transparent"; } if (!useLitShader) { return "BlockSpace/VertexColorURP_Unlit"; } return "BlockSpace/VertexColorURP"; } private static bool UsesLitShader(Material material) { if (material == null || material.shader == null) { return false; } string text = material.shader.name; if (text.Contains("Lit")) { return !text.Contains("Unlit"); } return false; } private void SendUpdateGroupAfterMergeStub(VoxelGroup newGroup) { if (!(newGroup == null) && !suppressNetworkBroadcast) { ResolveNetworkSync()?.BroadcastModify(newGroup); } } private void SendUpdateGroupStub(VoxelGroup group) { if (!(group == null) && !suppressNetworkBroadcast) { ResolveNetworkSync()?.BroadcastModify(group); } } public void BroadcastGroupModify(VoxelGroup group) { SendUpdateGroupStub(group); } private void SendRemoveGroupStub(ushort groupId, bool playDestroyAnimation, bool playDestroyParticles) { if (groupId != 0 && !suppressNetworkBroadcast) { ResolveNetworkSync()?.BroadcastDestroy(groupId, playDestroyAnimation, playDestroyParticles); } } private void RecordStrokeUndo(VoxelGroup group, bool playSpawnAnimationOnRemote, bool playSpawnParticlesOnRemote = true) { if (!isUndoing) { GroupSnapshot groupSnapshot = CaptureSnapshot(group); if (groupSnapshot != null) { UndoRecord undoRecord = new UndoRecord { type = UndoOperationType.Stroke }; undoRecord.after.Add(groupSnapshot); PushUndoRecord(undoRecord); ResolveNetworkSync()?.BroadcastCreate(group, playSpawnAnimationOnRemote, playSpawnParticlesOnRemote); } } } private GroupSnapshot CaptureSnapshot(VoxelGroup group) { if (group == null || group.voxels == null) { return null; } return new GroupSnapshot { id = group.id, position = group.position, packedRotation = group.packedRotation, size = group.size, voxels = (byte[])group.voxels.Clone() }; } private void RestoreSnapshot(GroupSnapshot snapshot) { if (snapshot == null) { return; } bool flag = groups.ContainsKey(snapshot.id); VoxelGroup voxelGroup = UpsertGroup(snapshot.id, snapshot.position, snapshot.packedRotation, snapshot.size, (byte[])snapshot.voxels.Clone(), queueInitialRebuild: true, !flag, playSpawnParticlesForNewGroup: false); if (!(voxelGroup == null)) { if (flag) { SendUpdateGroupStub(voxelGroup); } else { ResolveNetworkSync()?.BroadcastCreate(voxelGroup, playSpawnAnimation: true, playSpawnParticles: false); } } } private void PushUndoRecord(UndoRecord undoRecord) { if (!isUndoing && undoRecord != null && (undoRecord.before.Count != 0 || undoRecord.after.Count != 0)) { undoStack.Push(undoRecord); } } private VoxelGroup UpsertGroup(ushort id, Vector3Int position, byte rotation, Vector3Int size, byte[] voxels, bool queueInitialRebuild, bool playSpawnAnimationForNewGroup, bool playSpawnParticlesForNewGroup) { byte[] voxelData = ((voxels != null) ? ((byte[])voxels.Clone()) : new byte[0]); if (groups.TryGetValue(id, out var value)) { value.OverwriteData(position, rotation, size, voxelData, queueInitialRebuild); AdvanceNextIdPast(id); return value; } GameObject obj = new GameObject($"VoxelGroup_{id:D5}"); obj.transform.SetParent(base.transform, worldPositionStays: false); VoxelGroup voxelGroup = obj.AddComponent(); voxelGroup.Initialize(this, id, position, rotation, size, voxelData, queueInitialRebuild); AddGroup(voxelGroup); if (playSpawnAnimationForNewGroup) { StartCoroutine(AnimateSpawnedGroup(voxelGroup, playSpawnParticlesForNewGroup)); } return voxelGroup; } private void AdvanceNextIdPast(ushort id) { if (!PhotonNetwork.InRoom && id >= nextId) { nextId = (ushort)(id + 1); if (nextId == 0) { nextId = 1; } } } private void EnsureEffectAssetsAssigned() { } private IEnumerator AnimateSpawnedGroup(VoxelGroup group, bool playParticles) { if (!(group == null)) { if (group.dirtyMesh) { group.RebuildMesh(); } if (group.meshCollider != null) { group.meshCollider.enabled = false; } Vector3 center = group.GetWorldBounds().center; Quaternion rotation = group.transform.rotation; Vector3 localBoundsCenter = GetGroupLocalBoundsCenter(group); ApplyCenteredAnimatedScale(group, center, rotation, localBoundsCenter, 0.01f); PlayLifecycleClip(spawnClip, center, 1f); if (playParticles) { SpawnLifecycleParticle(center, rotation); } float elapsed = 0f; while (elapsed < 0.17f) { elapsed += Time.unscaledDeltaTime; float t = Mathf.Clamp01(elapsed / 0.17f); float uniformScale = Mathf.LerpUnclamped(0.01f, 1.2f, t); ApplyCenteredAnimatedScale(group, center, rotation, localBoundsCenter, uniformScale); yield return null; } elapsed = 0f; while (elapsed < 0.18f) { elapsed += Time.unscaledDeltaTime; float t2 = Mathf.Clamp01(elapsed / 0.18f); float uniformScale2 = Mathf.LerpUnclamped(1.2f, 1f, t2); ApplyCenteredAnimatedScale(group, center, rotation, localBoundsCenter, uniformScale2); yield return null; } ResetAnimatedGroupTransform(group); if (group != null && group.meshCollider != null) { group.meshCollider.enabled = true; } } } private IEnumerator AnimateAndDestroyGroup(VoxelGroup group, bool playParticles) { if (!(group == null)) { Vector3 center = group.GetWorldBounds().center; Quaternion rotation = group.transform.rotation; Vector3 localBoundsCenter = GetGroupLocalBoundsCenter(group); if (group.meshCollider != null) { group.meshCollider.enabled = false; } PlayLifecycleClip(deleteClip, center, 1f); float elapsed = 0f; while (elapsed < 0.18f) { elapsed += Time.unscaledDeltaTime; float t = Mathf.Clamp01(elapsed / 0.18f); float uniformScale = Mathf.LerpUnclamped(1f, 1.2f, t); ApplyCenteredAnimatedScale(group, center, rotation, localBoundsCenter, uniformScale); yield return null; } elapsed = 0f; while (elapsed < 0.17f) { elapsed += Time.unscaledDeltaTime; float t2 = Mathf.Clamp01(elapsed / 0.17f); float uniformScale2 = Mathf.LerpUnclamped(1.2f, 0.01f, t2); ApplyCenteredAnimatedScale(group, center, rotation, localBoundsCenter, uniformScale2); yield return null; } if (playParticles) { SpawnLifecycleParticle(center, rotation); } if (group != null) { UnityEngine.Object.Destroy(group.gameObject); } } } private Vector3 GetGroupLocalBoundsCenter(VoxelGroup group) { return new Vector3(group.size.x, group.size.y, group.size.z) * (voxelSize * 0.5f); } private void ApplyCenteredAnimatedScale(VoxelGroup group, Vector3 worldCenter, Quaternion rotation, Vector3 localBoundsCenter, float uniformScale) { if (!(group == null)) { Vector3 vector = localBoundsCenter * uniformScale; Vector3 position = worldCenter - rotation * vector; group.transform.SetPositionAndRotation(position, rotation); group.transform.localScale = Vector3.one * uniformScale; } } private void ResetAnimatedGroupTransform(VoxelGroup group) { if (!(group == null)) { group.transform.localScale = Vector3.one; group.transform.SetPositionAndRotation((Vector3)group.position * voxelSize, GridRotationUtility.ToQuaternion(group.packedRotation)); } } private void PlayLifecycleClip(AudioClip clip, Vector3 worldPosition, float volume) { if (!(clip == null)) { AudioSource.PlayClipAtPoint(clip, worldPosition, volume); } } private void SpawnLifecycleParticle(Vector3 worldPosition, Quaternion rotation) { if (!(spawnDestroyParticlePrefab == null)) { UnityEngine.Object.Destroy(UnityEngine.Object.Instantiate(spawnDestroyParticlePrefab, worldPosition, rotation), 0.7f); } } private VoxelGroupPhotonSync ResolveNetworkSync() { if (networkSync == null) { TryGetComponent(out networkSync); } return networkSync; } } }