using System.Collections; using UnityEngine; public class trackFractureController : MonoBehaviour { private const int TrackCount = 5; private static readonly int FadeId = Shader.PropertyToID("_Fade"); [System.Serializable] private class TrackSpriteFadeEntry { public SpriteRenderer spriteRendererA; public SpriteRenderer spriteRendererB; } [Header("Tracks")] [SerializeField] private GameObject[] trackObjects = new GameObject[TrackCount]; [SerializeField] private Material[] dissolveMaterials = new Material[TrackCount]; [SerializeField] private TrackSpriteFadeEntry[] trackSpriteFadeEntries = new TrackSpriteFadeEntry[TrackCount]; [SerializeField] private bool autoFetchFractureAndDrift = true; [SerializeField] private bool autoApplyDissolveMaterialOnStart = true; [Tooltip("If true, each track gets its own runtime CLONE of its dissolve material. The clone REPLACES the " + "material in the renderer slot, so you can no longer drive _Fade through the original material asset " + "reference. Only needed when several tracks share one material asset. If false (default), the assigned " + "dissolve material is used directly on the renderer, so controlling that material's _Fade (via this " + "controller or externally) drives the dissolve.")] [SerializeField] private bool cloneDissolveMaterialAtRuntime = false; [Header("Fracture Fade")] [Tooltip("float1: seconds to wait AFTER a track fractures before its dissolve _Fade starts changing.")] [SerializeField] private float fractureFadeDelay = 0.2f; [Tooltip("Seconds over which the linked SpriteRenderer pair fades from alpha 1 to 0 after fracture.")] [SerializeField] private float spriteRendererFadeDuration = 0.2f; [Tooltip("float2: seconds over which the dissolve _Fade is driven from 1 to 0 once the fade begins. " + "When this completes, the track's fragments are reclaimed asynchronously (collected one-by-one).")] [SerializeField] private float fractureFadeDuration = 1f; [Tooltip("The _Fade value written when the fracture dissolve begins.")] [SerializeField] private float fractureFadeStartValue = 0f; [Tooltip("The _Fade value written when the fracture dissolve completes.")] [SerializeField] private float fractureFadeEndValue = 1f; [Header("Start Z Move")] [SerializeField] private bool playStartZMove = true; [SerializeField] private float startLocalZ = 0f; [SerializeField] private float endLocalZ = 0.286f; [SerializeField] private float moveDuration = 0.35f; [Header("Debug")] [SerializeField] private bool enableDebugKeyTrigger = true; [SerializeField] private bool debugLogs = true; private readonly FractureAndDrift[] fractureDrivers = new FractureAndDrift[TrackCount]; private readonly Renderer[] trackRenderers = new Renderer[TrackCount]; private readonly Material[] runtimeDissolveMaterials = new Material[TrackCount]; // Original _Fade of each ASSIGNED material asset, captured before we touch it, so we can restore it // on teardown. Needed only when NOT cloning: we drive the asset directly, and Unity persists those // writes back to the .mat, so the value would otherwise stick after exiting play mode. NaN = not captured. private readonly float[] originalFadeValues = new float[TrackCount]; private Coroutine startMoveRoutine; private readonly Coroutine[] fadeRoutines = new Coroutine[TrackCount]; private void Awake() { CacheTrackReferences(); } private void Start() { CacheTrackReferences(); ApplyRuntimeDissolveMaterials(); SetAllTrackLocalZ(startLocalZ); if (playStartZMove) { startMoveRoutine = StartCoroutine(AnimateTracksToEndZ()); } } private void Update() { if (!enableDebugKeyTrigger) { return; } if (Input.GetKeyDown(KeyCode.Alpha1) || Input.GetKeyDown(KeyCode.Keypad1)) { if (debugLogs) Debug.Log("[trackFractureController] Debug key 1 pressed.", this); TriggerTrackFracture(0); } else if (Input.GetKeyDown(KeyCode.Alpha2) || Input.GetKeyDown(KeyCode.Keypad2)) { if (debugLogs) Debug.Log("[trackFractureController] Debug key 2 pressed.", this); TriggerTrackFracture(1); } else if (Input.GetKeyDown(KeyCode.Alpha3) || Input.GetKeyDown(KeyCode.Keypad3)) { if (debugLogs) Debug.Log("[trackFractureController] Debug key 3 pressed.", this); TriggerTrackFracture(2); } else if (Input.GetKeyDown(KeyCode.Alpha4) || Input.GetKeyDown(KeyCode.Keypad4)) { if (debugLogs) Debug.Log("[trackFractureController] Debug key 4 pressed.", this); TriggerTrackFracture(3); } else if (Input.GetKeyDown(KeyCode.Alpha5) || Input.GetKeyDown(KeyCode.Keypad5)) { if (debugLogs) Debug.Log("[trackFractureController] Debug key 5 pressed.", this); TriggerTrackFracture(4); } } private void OnValidate() { EnsureArraySizes(); if (!Application.isPlaying) { CacheTrackReferences(); } } private void OnDestroy() { // When not cloning, we drove the assigned .mat asset directly and Unity persists those writes. // Restore each asset's original _Fade so the value doesn't stick after play mode ends. RestoreOriginalFadeValues(); for (int i = 0; i < runtimeDissolveMaterials.Length; i++) { if (runtimeDissolveMaterials[i] != null) { Destroy(runtimeDissolveMaterials[i]); runtimeDissolveMaterials[i] = null; } } } private void RestoreOriginalFadeValues() { for (int i = 0; i < TrackCount; i++) { // Only restore assets we drove directly (clones are thrown away, no restore needed). if (runtimeDissolveMaterials[i] != null) { continue; } Material sourceMaterial = dissolveMaterials[i]; if (sourceMaterial == null || float.IsNaN(originalFadeValues[i]) || !sourceMaterial.HasProperty(FadeId)) { continue; } sourceMaterial.SetFloat(FadeId, originalFadeValues[i]); originalFadeValues[i] = float.NaN; } } public void TriggerTrackFractureByNumber(int trackNumber) { TriggerTrackFracture(trackNumber - 1); } public void TriggerTrackFracture(int trackIndex) { if (!IsValidTrackIndex(trackIndex)) { Debug.LogWarning($"[trackFractureController] Invalid track index: {trackIndex}", this); return; } FractureAndDrift fractureDriver = fractureDrivers[trackIndex]; if (fractureDriver == null) { Debug.LogWarning($"[trackFractureController] Missing FractureAndDrift on track {trackIndex + 1}.", this); return; } fractureDriver.Shatter(); StartFractureFade(trackIndex); } private void StartFractureFade(int trackIndex) { Material targetMaterial = GetFadeTargetMaterial(trackIndex); if (targetMaterial == null || !targetMaterial.HasProperty(FadeId)) { return; } if (fadeRoutines[trackIndex] != null) { StopCoroutine(fadeRoutines[trackIndex]); } fadeRoutines[trackIndex] = StartCoroutine(FadeTrackOut(trackIndex, targetMaterial)); } private IEnumerator FadeTrackOut(int trackIndex, Material targetMaterial) { TrackSpriteFadeEntry spriteFadeEntry = GetSpriteFadeEntry(trackIndex); SetSpriteFadeEntryAlpha(spriteFadeEntry, 1f); if (spriteRendererFadeDuration > 0f) { float spriteFadeElapsed = 0f; while (spriteFadeElapsed < spriteRendererFadeDuration) { spriteFadeElapsed += Time.deltaTime; float t = Mathf.Clamp01(spriteFadeElapsed / spriteRendererFadeDuration); SetSpriteFadeEntryAlpha(spriteFadeEntry, Mathf.Lerp(1f, 0f, t)); yield return null; } } else { SetSpriteFadeEntryAlpha(spriteFadeEntry, 0f); } SetSpriteFadeEntryAlpha(spriteFadeEntry, 0f); // float1: hold before the dissolve begins. Fragments have already started drifting during this time. if (fractureFadeDelay > 0f) { yield return GameplayClock.WaitForSeconds(fractureFadeDelay); } // float2: drive _Fade from the configured start value to the configured end value over this duration. float duration = Mathf.Max(0.0001f, fractureFadeDuration); float startFade = Mathf.Clamp01(fractureFadeStartValue); float endFade = Mathf.Clamp01(fractureFadeEndValue); float elapsed = 0f; targetMaterial.SetFloat(FadeId, startFade); while (elapsed < duration) { elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / duration); targetMaterial.SetFloat(FadeId, Mathf.Lerp(startFade, endFade, t)); yield return null; } targetMaterial.SetFloat(FadeId, endFade); fadeRoutines[trackIndex] = null; // Fade finished: asynchronously reclaim (collect) this track's fragments rather than destroying // them all at once. FractureAndDrift fractureDriver = fractureDrivers[trackIndex]; if (fractureDriver != null) { fractureDriver.ReclaimFragments(); } } public void SetTrackFadeByNumber(int trackNumber, float fadeValue) { SetTrackFade(trackNumber - 1, fadeValue); } public void SetTrackFade(int trackIndex, float fadeValue) { if (!IsValidTrackIndex(trackIndex)) { Debug.LogWarning($"[trackFractureController] Invalid track index: {trackIndex}", this); return; } Material targetMaterial = GetFadeTargetMaterial(trackIndex); if (targetMaterial == null) { Debug.LogWarning($"[trackFractureController] Missing dissolve material on track {trackIndex + 1}.", this); return; } if (!targetMaterial.HasProperty(FadeId)) { Debug.LogWarning($"[trackFractureController] Material on track {trackIndex + 1} has no _Fade property.", targetMaterial); return; } targetMaterial.SetFloat(FadeId, Mathf.Clamp01(fadeValue)); } public float GetTrackFade(int trackIndex) { if (!IsValidTrackIndex(trackIndex)) { return 0f; } Material targetMaterial = GetFadeTargetMaterial(trackIndex); if (targetMaterial == null || !targetMaterial.HasProperty(FadeId)) { return 0f; } return targetMaterial.GetFloat(FadeId); } public void SetAllTrackFade(float fadeValue) { for (int i = 0; i < TrackCount; i++) { SetTrackFade(i, fadeValue); } } public void RestartStartZMove() { SetAllTrackLocalZ(startLocalZ); if (startMoveRoutine != null) { StopCoroutine(startMoveRoutine); } startMoveRoutine = StartCoroutine(AnimateTracksToEndZ()); } public void TriggerTrack1Fracture() => TriggerTrackFracture(0); public void TriggerTrack2Fracture() => TriggerTrackFracture(1); public void TriggerTrack3Fracture() => TriggerTrackFracture(2); public void TriggerTrack4Fracture() => TriggerTrackFracture(3); public void TriggerTrack5Fracture() => TriggerTrackFracture(4); private void CacheTrackReferences() { EnsureArraySizes(); for (int i = 0; i < TrackCount; i++) { GameObject trackObject = trackObjects[i]; fractureDrivers[i] = null; trackRenderers[i] = null; if (trackObject == null) { continue; } if (autoFetchFractureAndDrift) { fractureDrivers[i] = trackObject.GetComponent(); if (fractureDrivers[i] == null) { fractureDrivers[i] = trackObject.GetComponentInChildren(true); } } trackRenderers[i] = trackObject.GetComponent(); if (trackRenderers[i] == null) { trackRenderers[i] = trackObject.GetComponentInChildren(true); } } } private void ApplyRuntimeDissolveMaterials() { for (int i = 0; i < TrackCount; i++) { if (runtimeDissolveMaterials[i] != null) { Destroy(runtimeDissolveMaterials[i]); runtimeDissolveMaterials[i] = null; } originalFadeValues[i] = float.NaN; Material sourceMaterial = dissolveMaterials[i]; Renderer trackRenderer = trackRenderers[i]; if (sourceMaterial == null) { continue; } // The material actually used on the renderer AND driven for fade. When cloning is off this is the // assigned asset itself, so external control of that asset's _Fade drives the dissolve directly. Material appliedMaterial = sourceMaterial; if (cloneDissolveMaterialAtRuntime) { appliedMaterial = new Material(sourceMaterial) { name = sourceMaterial.name + "_Runtime" }; runtimeDissolveMaterials[i] = appliedMaterial; } else if (sourceMaterial.HasProperty(FadeId)) { // Not cloning: we mutate the asset directly, so remember its original _Fade to restore later. originalFadeValues[i] = sourceMaterial.GetFloat(FadeId); } if (autoApplyDissolveMaterialOnStart && trackRenderer != null) { Material[] materials = trackRenderer.sharedMaterials; if (materials == null || materials.Length == 0) { trackRenderer.sharedMaterial = appliedMaterial; } else { bool replaced = false; for (int j = 0; j < materials.Length; j++) { if (materials[j] == sourceMaterial) { materials[j] = appliedMaterial; replaced = true; } } if (!replaced) { materials[0] = appliedMaterial; } trackRenderer.sharedMaterials = materials; } } } } private IEnumerator AnimateTracksToEndZ() { float duration = Mathf.Max(0.0001f, moveDuration); float elapsed = 0f; while (elapsed < duration) { elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / duration); float z = Mathf.Lerp(startLocalZ, endLocalZ, t); SetAllTrackLocalZ(z); yield return null; } SetAllTrackLocalZ(endLocalZ); startMoveRoutine = null; } private void SetAllTrackLocalZ(float zValue) { for (int i = 0; i < TrackCount; i++) { GameObject trackObject = trackObjects[i]; if (trackObject == null) { continue; } Transform targetTransform = trackObject.transform; Vector3 localPosition = targetTransform.localPosition; localPosition.z = zValue; targetTransform.localPosition = localPosition; } } private Material GetFadeTargetMaterial(int trackIndex) { if (!IsValidTrackIndex(trackIndex)) { return null; } // When cloning is on, drive the runtime clone. Otherwise drive the assigned asset directly - which is // also the material now used on the renderer, so controlling that asset's _Fade drives the dissolve. return runtimeDissolveMaterials[trackIndex] != null ? runtimeDissolveMaterials[trackIndex] : dissolveMaterials[trackIndex]; } private void EnsureArraySizes() { if (trackObjects == null || trackObjects.Length != TrackCount) { System.Array.Resize(ref trackObjects, TrackCount); } if (dissolveMaterials == null || dissolveMaterials.Length != TrackCount) { System.Array.Resize(ref dissolveMaterials, TrackCount); } if (trackSpriteFadeEntries == null || trackSpriteFadeEntries.Length != TrackCount) { System.Array.Resize(ref trackSpriteFadeEntries, TrackCount); } } private static bool IsValidTrackIndex(int trackIndex) { return trackIndex >= 0 && trackIndex < TrackCount; } private TrackSpriteFadeEntry GetSpriteFadeEntry(int trackIndex) { if (!IsValidTrackIndex(trackIndex) || trackSpriteFadeEntries == null || trackIndex >= trackSpriteFadeEntries.Length) { return null; } return trackSpriteFadeEntries[trackIndex]; } private static void SetSpriteFadeEntryAlpha(TrackSpriteFadeEntry entry, float alpha) { if (entry == null) { return; } SetSpriteRendererAlpha(entry.spriteRendererA, alpha); SetSpriteRendererAlpha(entry.spriteRendererB, alpha); } private static void SetSpriteRendererAlpha(SpriteRenderer spriteRenderer, float alpha) { if (spriteRenderer == null) { return; } Color color = spriteRenderer.color; color.a = Mathf.Clamp01(alpha); spriteRenderer.color = color; } }