using System; using System.Collections; using System.Collections.Generic; using KD.Destro2D; using UnityEngine; public class TrackCrashController : MonoBehaviour { public static TrackCrashController Instance { get; private set; } public enum TrackCrashShape { Auto, Radial, RectCut } [System.Serializable] public class TrackEntry { public int trackIndex; public GameObject trackObject; public bool hideOriginalWhenCrashed = true; public float restoreDelay = 0f; } [Header("Track Crash")] [SerializeField] private List tracks = new List(5); [SerializeField] private bool useDestro2D = true; [SerializeField] private bool autoRestoreOnTrackCrash = false; [SerializeField] private float defaultRestoreDelay = 0f; [Tooltip("Editor-only debug hotkeys (1-5) to trigger track crash. Never active in builds.")] [SerializeField] private bool enableDebugKeyTrigger = false; [SerializeField] private bool debugLogs = false; [SerializeField] private bool useAutoFractureSize = true; [SerializeField] private float fractureCoreRadius = 0.05f; [SerializeField] private float fractureOuterRadius = 0.45f; [SerializeField] private float fractureOuterRadiusScale = 1.1f; [SerializeField] private float fractureNoiseScale = 10f; [SerializeField] private float fractureThickness = 0.12f; [SerializeField] private int fractureLines = 8; [SerializeField] private TrackCrashShape trackCrashShape = TrackCrashShape.Auto; [SerializeField] private float rectCutOversize = 1.15f; [SerializeField] private float chunkDetectDelay = 0.08f; [SerializeField] private int rectCutCount = 3; [SerializeField] private float rectCutBandScale = 0.05f; [SerializeField] private float rectCutPositionSpan = 0.65f; [SerializeField] private int rectCrossCutCount = 1; [SerializeField] private float rectCrossCutBandScale = 0.22f; [SerializeField] private float chunkExplodeForce = 2.4f; [SerializeField] private float chunkExplodeTorque = 30f; [SerializeField] private Vector2 chunkWindDirection = Vector2.zero; [SerializeField] private float chunkWindForce = 0f; [SerializeField] private Vector2 chunkGravityDirection = Vector2.down; [SerializeField] private bool keepDestro2DBaseTrackVisible = true; [SerializeField] private bool keepChunksInPlace = true; [SerializeField] private bool disableChunkGravity = true; [SerializeField] private bool autoTuneSplitHandler = true; [SerializeField] private int splitHandlerMaxChunkCount = 12; [SerializeField] private int splitHandlerMinCount = 3; [SerializeField] private bool manualChunkMotion = true; [SerializeField] private float chunkGravityStrength = 6f; [SerializeField] private float chunkLinearDamping = 4f; [SerializeField] private float chunkAngularDamping = 5f; [SerializeField] private float chunkMotionDuration = 0.45f; [SerializeField] private bool useRuntimeSpriteFallback = true; [SerializeField] private int runtimeFallbackColumns = 3; [SerializeField] private int runtimeFallbackRows = 12; [SerializeField] private int runtimeFallbackMinVisiblePieces = 6; [SerializeField] private float runtimeFallbackRandomOffset = 0.015f; private readonly Dictionary trackMap = new Dictionary(); private readonly HashSet crashedTracks = new HashSet(); private readonly HashSet processingTracks = new HashSet(); private readonly Dictionary restoreRoutines = new Dictionary(); private readonly Dictionary fractureProxies = new Dictionary(); private void Awake() { if (Instance == null) { Instance = this; } else if (Instance != this) { Destroy(gameObject); return; } RebuildCache(); } private void OnValidate() { if (!Application.isPlaying) return; RebuildCache(); } #if UNITY_EDITOR private void Update() { // Editor-only debug hotkeys; compiled out of player builds so a serialized scene // value can never leave number-key crash triggers or per-frame Input polling active. if (!enableDebugKeyTrigger) return; if (Input.GetKeyDown(KeyCode.Alpha1) || Input.GetKeyDown(KeyCode.Keypad1)) { if (debugLogs) Debug.Log("[TrackCrash] Debug key 1 pressed."); TriggerTrackCrash(0); } else if (Input.GetKeyDown(KeyCode.Alpha2) || Input.GetKeyDown(KeyCode.Keypad2)) { if (debugLogs) Debug.Log("[TrackCrash] Debug key 2 pressed."); TriggerTrackCrash(1); } else if (Input.GetKeyDown(KeyCode.Alpha3) || Input.GetKeyDown(KeyCode.Keypad3)) { if (debugLogs) Debug.Log("[TrackCrash] Debug key 3 pressed."); TriggerTrackCrash(2); } else if (Input.GetKeyDown(KeyCode.Alpha4) || Input.GetKeyDown(KeyCode.Keypad4)) { if (debugLogs) Debug.Log("[TrackCrash] Debug key 4 pressed."); TriggerTrackCrash(3); } else if (Input.GetKeyDown(KeyCode.Alpha5) || Input.GetKeyDown(KeyCode.Keypad5)) { if (debugLogs) Debug.Log("[TrackCrash] Debug key 5 pressed."); TriggerTrackCrash(4); } } #endif private void RebuildCache() { trackMap.Clear(); for (int i = 0; i < tracks.Count; i++) { TrackEntry entry = tracks[i]; if (entry == null || entry.trackObject == null) continue; trackMap[entry.trackIndex] = entry; } } public static void TrackCrash(int trackIndex) { var controller = Instance != null ? Instance : SceneObjectLookupCache.FindAny(); if (controller == null) return; controller.TriggerTrackCrash(trackIndex); } public static void RestoreTrack(int trackIndex) { var controller = Instance != null ? Instance : SceneObjectLookupCache.FindAny(); if (controller == null) return; controller.RestoreTrackInternal(trackIndex); } public void TriggerTrackCrash(int trackIndex) { if (!trackMap.TryGetValue(trackIndex, out TrackEntry entry) || entry == null || entry.trackObject == null) { if (debugLogs) Debug.LogWarning($"[TrackCrash] Missing track entry or track object for index {trackIndex}."); return; } if (crashedTracks.Contains(trackIndex)) { if (debugLogs) Debug.Log($"[TrackCrash] Track {trackIndex} is already crashed."); return; } if (processingTracks.Contains(trackIndex)) { if (debugLogs) Debug.Log($"[TrackCrash] Track {trackIndex} is already processing."); return; } StartCoroutine(TriggerTrackCrashRoutine(trackIndex, entry)); } private IEnumerator TriggerTrackCrashRoutine(int trackIndex, TrackEntry entry) { processingTracks.Add(trackIndex); try { DestroyFractureProxy(trackIndex); bool fractureSucceeded = !useDestro2D; SplitHandler splitHandler = null; int chunkCountBefore = -1; int chunkCountAfter = -1; float outerRadius = fractureOuterRadius; bool waitForChunkDetection = false; bool usedDestro2D = false; bool usedRuntimeSpriteFallback = false; Vector2 fractureCenter = entry.trackObject.transform.position; Destro2DMain resolvedDestro = null; SpriteRenderer resolvedSpriteRenderer = null; TrackCrashShape resolvedShape = TrackCrashShape.Radial; GameObject fractureTarget = entry.trackObject; if (useDestro2D) { fractureTarget = CreateFractureProxy(trackIndex, entry); var destro = fractureTarget != null ? fractureTarget.GetComponent() : null; if (destro == null) { destro = fractureTarget != null ? fractureTarget.GetComponentInChildren(true) : null; } if (destro != null) { usedDestro2D = true; resolvedDestro = destro; if (debugLogs) Debug.Log($"[TrackCrash] Fracturing track {trackIndex} on {fractureTarget.name}."); try { splitHandler = destro.GetComponent(); TuneSplitHandler(splitHandler); chunkCountBefore = splitHandler != null ? splitHandler.splitChunks.Count : -1; var spriteRenderer = ResolveDestroSpriteRenderer(destro, fractureTarget); resolvedSpriteRenderer = spriteRenderer; resolvedShape = ResolveCrashShape(spriteRenderer); if (spriteRenderer != null) { fractureCenter = spriteRenderer.bounds.center; if (useAutoFractureSize) { float extent = Mathf.Max(spriteRenderer.bounds.extents.x, spriteRenderer.bounds.extents.y); outerRadius = Mathf.Max(fractureOuterRadius, extent * fractureOuterRadiusScale); } } TriggerDestro2D(destro, spriteRenderer, fractureCenter, outerRadius); waitForChunkDetection = true; } catch (Exception ex) { fractureSucceeded = false; if (debugLogs) Debug.LogError($"[TrackCrash] Fracture failed for track {trackIndex}: {ex}"); } } else if (debugLogs) { Debug.LogWarning($"[TrackCrash] No Destro2DMain found on track {trackIndex} object {(fractureTarget != null ? fractureTarget.name : "NULL")} or its children."); } } if (waitForChunkDetection) { yield return new WaitForSecondsRealtime(Mathf.Max(0.01f, chunkDetectDelay)); chunkCountAfter = splitHandler != null ? splitHandler.splitChunks.Count : -1; fractureSucceeded = splitHandler == null || chunkCountAfter > chunkCountBefore; if (!fractureSucceeded && resolvedDestro != null && resolvedSpriteRenderer != null && resolvedShape == TrackCrashShape.RectCut) { if (debugLogs) { Debug.Log($"[TrackCrash] Track {trackIndex} primary cut did not split. Applying aggressive fallback cut."); } ApplyAggressiveRectCut(resolvedDestro, resolvedSpriteRenderer, fractureCenter); yield return new WaitForSecondsRealtime(Mathf.Max(0.01f, chunkDetectDelay)); chunkCountAfter = splitHandler != null ? splitHandler.splitChunks.Count : -1; fractureSucceeded = splitHandler == null || chunkCountAfter > chunkCountBefore; } if (fractureSucceeded && splitHandler != null) { int visibleChunkCount = CountLiveChunks(splitHandler); if (visibleChunkCount < 2) { fractureSucceeded = false; if (debugLogs) { Debug.Log($"[TrackCrash] Track {trackIndex} Destro2D only produced {visibleChunkCount} visible chunk(s); switching to runtime sprite fallback."); } } } if (fractureSucceeded && splitHandler != null) { PrepareChunksForFinalState(splitHandler, fractureCenter); } if (debugLogs) { Debug.Log($"[TrackCrash] Track {trackIndex} chunk count before={chunkCountBefore}, after={chunkCountAfter}, success={fractureSucceeded}, outerRadius={outerRadius}."); } } if (!fractureSucceeded && useRuntimeSpriteFallback) { DestroyFractureProxy(trackIndex); fractureTarget = CreateRuntimeSpriteFallbackProxy(trackIndex, entry, out int pieceCount); fractureSucceeded = fractureTarget != null && pieceCount >= runtimeFallbackMinVisiblePieces; usedRuntimeSpriteFallback = fractureSucceeded; if (debugLogs) { Debug.Log($"[TrackCrash] Track {trackIndex} runtime sprite fallback pieces={pieceCount}, success={fractureSucceeded}."); } } if (!fractureSucceeded) { DestroyFractureProxy(trackIndex); yield break; } crashedTracks.Add(trackIndex); bool shouldHideOriginalObject = entry.hideOriginalWhenCrashed; if (usedDestro2D) { shouldHideOriginalObject = true; } if (shouldHideOriginalObject) { entry.trackObject.SetActive(false); } if (usedDestro2D && !usedRuntimeSpriteFallback && fractureTarget != null) { if (keepDestro2DBaseTrackVisible) { fractureTarget.SetActive(true); } else { fractureTarget.SetActive(false); } } if (usedRuntimeSpriteFallback && fractureTarget != null) { fractureTarget.SetActive(true); } if (autoRestoreOnTrackCrash) { float delay = entry.restoreDelay > 0f ? entry.restoreDelay : defaultRestoreDelay; if (delay > 0f) { if (restoreRoutines.TryGetValue(trackIndex, out var routine) && routine != null) { StopCoroutine(routine); } restoreRoutines[trackIndex] = StartCoroutine(RestoreAfterDelay(trackIndex, delay)); } } } finally { processingTracks.Remove(trackIndex); } } private void TriggerDestro2D(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter, float outerRadius) { if (destro == null) return; TrackCrashShape shape = ResolveCrashShape(spriteRenderer); if (shape == TrackCrashShape.RectCut) { ApplyRectCut(destro, spriteRenderer, fractureCenter); return; } destro.DynamicFracture(fractureCenter, fractureCoreRadius, outerRadius, fractureNoiseScale, fractureThickness, fractureLines); } private TrackCrashShape ResolveCrashShape(SpriteRenderer spriteRenderer) { if (trackCrashShape != TrackCrashShape.Auto) return trackCrashShape; if (spriteRenderer == null) return TrackCrashShape.Radial; Vector3 size = spriteRenderer.bounds.size; return size.y > size.x * 2f || size.x > size.y * 2f ? TrackCrashShape.RectCut : TrackCrashShape.Radial; } private void ApplyRectCut(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter) { ApplyRectCutInternal(destro, spriteRenderer, fractureCenter, Mathf.Max(1, rectCutCount), Mathf.Clamp01(rectCutPositionSpan), Mathf.Max(0.005f, rectCutBandScale)); ApplyCrossRectCutInternal(destro, spriteRenderer, fractureCenter, Mathf.Max(0, rectCrossCutCount), Mathf.Max(0.01f, rectCrossCutBandScale)); } private void ApplyAggressiveRectCut(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter) { ApplyRectCutInternal( destro, spriteRenderer, fractureCenter, Mathf.Max(3, rectCutCount + 2), Mathf.Max(0.75f, Mathf.Clamp01(rectCutPositionSpan)), Mathf.Max(0.08f, rectCutBandScale)); ApplyCrossRectCutInternal( destro, spriteRenderer, fractureCenter, Mathf.Max(1, rectCrossCutCount + 1), Mathf.Max(0.12f, rectCrossCutBandScale)); } private void ApplyRectCutInternal(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter, int cutCount, float span, float bandScale) { if (destro == null || spriteRenderer == null) return; Vector3 extents = spriteRenderer.bounds.extents; bool verticalTrack = extents.y >= extents.x; for (int i = 0; i < cutCount; i++) { float t = cutCount == 1 ? 0.5f : (float)i / (cutCount - 1); float offsetNormalized = Mathf.Lerp(-span, span, t); Vector2 cutPosition = fractureCenter; RectDestruction rect = new RectDestruction(); if (verticalTrack) { cutPosition.y += extents.y * offsetNormalized; rect.l = Mathf.Max(fractureThickness, extents.x * rectCutOversize); rect.b = Mathf.Max(fractureThickness, extents.y * bandScale); } else { cutPosition.x += extents.x * offsetNormalized; rect.l = Mathf.Max(fractureThickness, extents.x * bandScale); rect.b = Mathf.Max(fractureThickness, extents.y * rectCutOversize); } rect.Setup(destro.gameObject); destro.DynamicDestroyWorld(cutPosition, new List { rect }); } } private void ApplyLongitudinalCut(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter) { if (destro == null || spriteRenderer == null) return; Vector3 extents = spriteRenderer.bounds.extents; bool verticalTrack = extents.y >= extents.x; RectDestruction rect = new RectDestruction(); if (verticalTrack) { rect.l = Mathf.Max(fractureThickness * 0.6f, extents.x * 0.1f); rect.b = Mathf.Max(fractureThickness, extents.y * rectCutOversize); } else { rect.l = Mathf.Max(fractureThickness, extents.x * rectCutOversize); rect.b = Mathf.Max(fractureThickness * 0.6f, extents.y * 0.1f); } rect.Setup(destro.gameObject); destro.DynamicDestroyWorld(fractureCenter, new List { rect }); } private void ApplyCrossRectCutInternal(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter, int cutCount, float bandScale) { if (destro == null || spriteRenderer == null || cutCount <= 0) return; Vector3 extents = spriteRenderer.bounds.extents; bool verticalTrack = extents.y >= extents.x; float usableSpan = 0.6f; for (int i = 0; i < cutCount; i++) { float t = cutCount == 1 ? 0.5f : (float)i / (cutCount - 1); float offsetNormalized = Mathf.Lerp(-usableSpan, usableSpan, t); Vector2 cutPosition = fractureCenter; RectDestruction rect = new RectDestruction(); if (verticalTrack) { cutPosition.x += extents.x * offsetNormalized; rect.l = Mathf.Max(fractureThickness, extents.x * bandScale); rect.b = Mathf.Max(fractureThickness, extents.y * rectCutOversize); } else { cutPosition.y += extents.y * offsetNormalized; rect.l = Mathf.Max(fractureThickness, extents.x * rectCutOversize); rect.b = Mathf.Max(fractureThickness, extents.y * bandScale); } rect.Setup(destro.gameObject); destro.DynamicDestroyWorld(cutPosition, new List { rect }); } } private void TuneSplitHandler(SplitHandler splitHandler) { if (!autoTuneSplitHandler || splitHandler == null) return; splitHandler.MaxChunkCount = Mathf.Max(splitHandler.MaxChunkCount, splitHandlerMaxChunkCount); splitHandler.minCount = Mathf.Max(1, Mathf.Min(splitHandler.minCount, splitHandlerMinCount)); } private SpriteRenderer ResolveDestroSpriteRenderer(Destro2DMain destro, GameObject fallbackObject) { if (destro != null) { SpriteRenderer directRenderer = destro.GetComponent(); if (directRenderer != null) return directRenderer; SpriteRenderer childRenderer = destro.GetComponentInChildren(true); if (childRenderer != null) return childRenderer; } if (fallbackObject != null) { SpriteRenderer entryRenderer = fallbackObject.GetComponent(); if (entryRenderer != null) return entryRenderer; return fallbackObject.GetComponentInChildren(true); } return null; } private void PushChunksAway(SplitHandler splitHandler, Vector2 fractureCenter) { if (splitHandler == null || splitHandler.splitChunks == null || splitHandler.splitChunks.Count == 0) return; Vector2 normalizedWindDirection = chunkWindDirection.sqrMagnitude > 0.0001f ? chunkWindDirection.normalized : Vector2.zero; foreach (GameObject chunk in splitHandler.splitChunks) { if (chunk == null) continue; Rigidbody2D rb = chunk.GetComponent(); SplitHandler chunkSplitHandler = chunk.GetComponent(); if (rb == null) continue; Vector2 worldChunkCenter = chunk.transform.position; if (chunkSplitHandler != null) { worldChunkCenter = chunk.transform.TransformPoint(chunkSplitHandler.chunkCentre); } Vector2 direction = worldChunkCenter - fractureCenter; if (direction.sqrMagnitude < 0.0001f) { direction = UnityEngine.Random.insideUnitCircle.normalized; } Vector2 forceVector = direction.normalized * chunkExplodeForce; if (normalizedWindDirection != Vector2.zero && chunkWindForce > 0f) { forceVector += normalizedWindDirection * chunkWindForce; } rb.linearVelocity = Vector2.zero; rb.angularVelocity = 0f; rb.AddForce(forceVector, ForceMode2D.Impulse); if (chunkExplodeTorque > 0f) { float torqueSign = UnityEngine.Random.value > 0.5f ? 1f : -1f; rb.AddTorque(chunkExplodeTorque * torqueSign, ForceMode2D.Impulse); } } } private void PrepareChunksForFinalState(SplitHandler splitHandler, Vector2 fractureCenter) { if (splitHandler == null || splitHandler.splitChunks == null || splitHandler.splitChunks.Count == 0) return; if (manualChunkMotion) { StartCoroutine(DriveChunksManually(splitHandler, fractureCenter)); return; } if (!keepChunksInPlace) { PushChunksAway(splitHandler, fractureCenter); return; } foreach (GameObject chunk in splitHandler.splitChunks) { if (chunk == null) continue; Rigidbody2D rb = chunk.GetComponent(); if (rb == null) continue; rb.linearVelocity = Vector2.zero; rb.angularVelocity = 0f; if (disableChunkGravity) { rb.gravityScale = 0f; } rb.constraints = RigidbodyConstraints2D.FreezePosition | RigidbodyConstraints2D.FreezeRotation; } } private System.Collections.IEnumerator RestoreAfterDelay(int trackIndex, float delay) { yield return new WaitForSecondsRealtime(delay); RestoreTrackInternal(trackIndex); restoreRoutines.Remove(trackIndex); processingTracks.Remove(trackIndex); } private void RestoreTrackInternal(int trackIndex) { if (!trackMap.TryGetValue(trackIndex, out TrackEntry entry) || entry == null || entry.trackObject == null) return; DestroyFractureProxy(trackIndex); if (entry.trackObject != null) { entry.trackObject.SetActive(true); } crashedTracks.Remove(trackIndex); } public void RebuildFromSceneLookup() { RebuildCache(); } private IEnumerator DriveChunksManually(SplitHandler splitHandler, Vector2 fractureCenter) { List rigidbodies = new List(); List velocities = new List(); List angularVelocities = new List(); Vector2 normalizedWind = chunkWindDirection.sqrMagnitude > 0.0001f ? chunkWindDirection.normalized : Vector2.zero; Vector2 normalizedGravity = disableChunkGravity || chunkGravityDirection.sqrMagnitude <= 0.0001f ? Vector2.zero : chunkGravityDirection.normalized; foreach (GameObject chunk in splitHandler.splitChunks) { if (chunk == null) continue; Rigidbody2D rb = chunk.GetComponent(); if (rb == null) continue; SplitHandler chunkSplitHandler = chunk.GetComponent(); Vector2 worldChunkCenter = chunk.transform.position; if (chunkSplitHandler != null) { worldChunkCenter = chunk.transform.TransformPoint(chunkSplitHandler.chunkCentre); } Vector2 radialDirection = worldChunkCenter - fractureCenter; if (radialDirection.sqrMagnitude < 0.0001f) { radialDirection = UnityEngine.Random.insideUnitCircle.normalized; } Vector2 velocity = Vector2.zero; if (!keepChunksInPlace) { velocity += radialDirection.normalized * chunkExplodeForce; velocity += normalizedWind * chunkWindForce; } rigidbodies.Add(rb); velocities.Add(velocity); angularVelocities.Add(UnityEngine.Random.Range(-chunkExplodeTorque, chunkExplodeTorque)); rb.gravityScale = 0f; rb.linearVelocity = Vector2.zero; rb.angularVelocity = 0f; rb.constraints = RigidbodyConstraints2D.FreezeRotation; } float elapsed = 0f; while (elapsed < chunkMotionDuration) { float dt = Time.deltaTime; for (int i = 0; i < rigidbodies.Count; i++) { Rigidbody2D rb = rigidbodies[i]; if (rb == null) continue; Vector2 velocity = velocities[i]; float angularVelocity = angularVelocities[i]; velocity += normalizedGravity * chunkGravityStrength * dt; velocity += normalizedWind * chunkWindForce * dt; velocity = Vector2.Lerp(velocity, Vector2.zero, Mathf.Clamp01(chunkLinearDamping * dt)); angularVelocity = Mathf.Lerp(angularVelocity, 0f, Mathf.Clamp01(chunkAngularDamping * dt)); rb.position += velocity * dt; rb.rotation += angularVelocity * dt; velocities[i] = velocity; angularVelocities[i] = angularVelocity; } elapsed += dt; yield return null; } for (int i = 0; i < rigidbodies.Count; i++) { if (rigidbodies[i] == null) continue; rigidbodies[i].linearVelocity = Vector2.zero; rigidbodies[i].angularVelocity = 0f; if (keepChunksInPlace) { rigidbodies[i].constraints = RigidbodyConstraints2D.FreezePosition | RigidbodyConstraints2D.FreezeRotation; } } } private GameObject CreateFractureProxy(int trackIndex, TrackEntry entry) { if (entry == null || entry.trackObject == null) return null; GameObject proxy = Instantiate(entry.trackObject, entry.trackObject.transform.parent); proxy.name = entry.trackObject.name + "_FractureProxy"; proxy.transform.position = entry.trackObject.transform.position; proxy.transform.rotation = entry.trackObject.transform.rotation; proxy.transform.localScale = entry.trackObject.transform.localScale; fractureProxies[trackIndex] = proxy; return proxy; } private void DestroyFractureProxy(int trackIndex) { if (!fractureProxies.TryGetValue(trackIndex, out GameObject proxy)) return; fractureProxies.Remove(trackIndex); if (proxy != null) { Destroy(proxy); } } private int CountLiveChunks(SplitHandler splitHandler) { if (splitHandler == null || splitHandler.splitChunks == null) return 0; int count = 0; foreach (GameObject chunk in splitHandler.splitChunks) { if (chunk == null || !chunk.activeInHierarchy) continue; SpriteRenderer spriteRenderer = chunk.GetComponent(); if (spriteRenderer == null || spriteRenderer.sprite == null) continue; count++; } return count; } private GameObject CreateRuntimeSpriteFallbackProxy(int trackIndex, TrackEntry entry, out int pieceCount) { pieceCount = 0; if (entry == null || entry.trackObject == null) return null; SpriteRenderer sourceRenderer = ResolveDestroSpriteRenderer(null, entry.trackObject); if (sourceRenderer == null || sourceRenderer.sprite == null || sourceRenderer.sprite.texture == null) return null; Sprite sourceSprite = sourceRenderer.sprite; Rect sourceRect = sourceSprite.rect; if (sourceRect.width < 2f || sourceRect.height < 2f) return null; int columns = Mathf.Max(1, runtimeFallbackColumns); int rows = Mathf.Max(1, runtimeFallbackRows); float aspect = sourceRect.height / Mathf.Max(1f, sourceRect.width); if (aspect > 2f) { rows = Mathf.Max(rows, Mathf.CeilToInt(aspect * columns * 1.75f)); } GameObject proxy = new GameObject(entry.trackObject.name + "_RuntimeFractureProxy"); proxy.transform.SetParent(entry.trackObject.transform.parent, false); proxy.transform.position = sourceRenderer.transform.position; proxy.transform.rotation = sourceRenderer.transform.rotation; proxy.transform.localScale = sourceRenderer.transform.lossyScale; fractureProxies[trackIndex] = proxy; TrackCrashRuntimeSpriteCleanup cleanup = proxy.AddComponent(); List pieceTransforms = new List(); List pieceVelocities = new List(); List pieceAngularVelocities = new List(); float pixelsPerUnit = Mathf.Max(1f, sourceSprite.pixelsPerUnit); float minLocalX = -sourceSprite.pivot.x / pixelsPerUnit; float minLocalY = -sourceSprite.pivot.y / pixelsPerUnit; for (int row = 0; row < rows; row++) { float normalizedY0 = (float)row / rows; float normalizedY1 = (float)(row + 1) / rows; float yMin = sourceRect.y + (sourceRect.height * normalizedY0); float yMax = sourceRect.y + (sourceRect.height * normalizedY1); yMin = Mathf.Clamp(yMin, sourceRect.y, sourceRect.yMax - 1f); yMax = Mathf.Clamp(yMax, yMin + 1f, sourceRect.yMax); for (int col = 0; col < columns; col++) { float normalizedX0 = (float)col / columns; float normalizedX1 = (float)(col + 1) / columns; float xMin = sourceRect.x + (sourceRect.width * normalizedX0); float xMax = sourceRect.x + (sourceRect.width * normalizedX1); xMin = Mathf.Clamp(xMin, sourceRect.x, sourceRect.xMax - 1f); xMax = Mathf.Clamp(xMax, xMin + 1f, sourceRect.xMax); Rect pieceRect = Rect.MinMaxRect(xMin, yMin, xMax, yMax); if (pieceRect.width < 1f || pieceRect.height < 1f) continue; GameObject piece = new GameObject($"Piece_{row}_{col}"); piece.transform.SetParent(proxy.transform, false); SpriteRenderer pieceRenderer = piece.AddComponent(); pieceRenderer.sprite = Sprite.Create( sourceSprite.texture, pieceRect, new Vector2(0.5f, 0.5f), pixelsPerUnit, 0, SpriteMeshType.FullRect); cleanup.Register(pieceRenderer.sprite); pieceRenderer.sharedMaterial = sourceRenderer.sharedMaterial; pieceRenderer.color = sourceRenderer.color; pieceRenderer.flipX = sourceRenderer.flipX; pieceRenderer.flipY = sourceRenderer.flipY; pieceRenderer.maskInteraction = sourceRenderer.maskInteraction; pieceRenderer.sortingLayerID = sourceRenderer.sortingLayerID; pieceRenderer.sortingOrder = sourceRenderer.sortingOrder; pieceRenderer.renderingLayerMask = sourceRenderer.renderingLayerMask; piece.layer = sourceRenderer.gameObject.layer; float localPieceX = (pieceRect.x - sourceRect.x); float localPieceY = (pieceRect.y - sourceRect.y); float localCenterX = minLocalX + (localPieceX + pieceRect.width * 0.5f) / pixelsPerUnit; float localCenterY = minLocalY + (localPieceY + pieceRect.height * 0.5f) / pixelsPerUnit; Vector2 randomOffset = runtimeFallbackRandomOffset > 0f ? UnityEngine.Random.insideUnitCircle * runtimeFallbackRandomOffset : Vector2.zero; piece.transform.localPosition = new Vector3(localCenterX + randomOffset.x, localCenterY + randomOffset.y, 0f); piece.transform.localRotation = Quaternion.identity; piece.transform.localScale = Vector3.one; pieceTransforms.Add(piece.transform); Vector2 radialDirection = ((Vector2)piece.transform.position - (Vector2)sourceRenderer.bounds.center); if (radialDirection.sqrMagnitude < 0.0001f) { radialDirection = UnityEngine.Random.insideUnitCircle.normalized; } Vector2 initialVelocity = Vector2.zero; if (!keepChunksInPlace) { initialVelocity += radialDirection.normalized * chunkExplodeForce; if (chunkWindDirection.sqrMagnitude > 0.0001f) { initialVelocity += chunkWindDirection.normalized * chunkWindForce; } } pieceVelocities.Add(initialVelocity); pieceAngularVelocities.Add(keepChunksInPlace ? 0f : UnityEngine.Random.Range(-chunkExplodeTorque, chunkExplodeTorque)); pieceCount++; } } if (pieceCount <= 0) { Destroy(proxy); fractureProxies.Remove(trackIndex); return null; } if (manualChunkMotion && !keepChunksInPlace) { StartCoroutine(DriveRuntimeSpritePieces(pieceTransforms, pieceVelocities, pieceAngularVelocities)); } return proxy; } private IEnumerator DriveRuntimeSpritePieces( List pieceTransforms, List pieceVelocities, List pieceAngularVelocities) { Vector2 normalizedWind = chunkWindDirection.sqrMagnitude > 0.0001f ? chunkWindDirection.normalized : Vector2.zero; Vector2 normalizedGravity = disableChunkGravity || chunkGravityDirection.sqrMagnitude <= 0.0001f ? Vector2.zero : chunkGravityDirection.normalized; float elapsed = 0f; while (elapsed < chunkMotionDuration) { float dt = Time.deltaTime; for (int i = 0; i < pieceTransforms.Count; i++) { Transform pieceTransform = pieceTransforms[i]; if (pieceTransform == null) continue; Vector2 velocity = pieceVelocities[i]; float angularVelocity = pieceAngularVelocities[i]; velocity += normalizedGravity * chunkGravityStrength * dt; velocity += normalizedWind * chunkWindForce * dt; velocity = Vector2.Lerp(velocity, Vector2.zero, Mathf.Clamp01(chunkLinearDamping * dt)); angularVelocity = Mathf.Lerp(angularVelocity, 0f, Mathf.Clamp01(chunkAngularDamping * dt)); pieceTransform.position += (Vector3)(velocity * dt); pieceTransform.Rotate(0f, 0f, angularVelocity * dt, Space.Self); pieceVelocities[i] = velocity; pieceAngularVelocities[i] = angularVelocity; } elapsed += dt; yield return null; } } } public sealed class TrackCrashRuntimeSpriteCleanup : MonoBehaviour { private readonly List runtimeSprites = new List(); public void Register(Sprite sprite) { if (sprite != null) { runtimeSprites.Add(sprite); } } private void OnDestroy() { for (int i = 0; i < runtimeSprites.Count; i++) { if (runtimeSprites[i] != null) { Destroy(runtimeSprites[i]); } } runtimeSprites.Clear(); } }