548 lines
19 KiB
C#
548 lines
19 KiB
C#
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")]
|
|
[Tooltip("Editor-only debug hotkeys (1-5) to trigger track fracture. Never active in builds.")]
|
|
[SerializeField] private bool enableDebugKeyTrigger = false;
|
|
[SerializeField] private bool debugLogs = false;
|
|
|
|
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();
|
|
|
|
// 游戏准备开始时强制把所有轨道的 _Fade 复位为 1(完整显示,未溶解)。
|
|
// 碎裂溶解是 _Fade 1→0 的过程(见 fractureFadeStartValue/EndValue),1 表示轨道完整。
|
|
// 不克隆(cloneDissolveMaterialAtRuntime=0)时直接驱动 .mat 资产,若上次运行因异常
|
|
// (编辑器崩溃/强杀)未走 OnDestroy 的恢复,_Fade 可能残留为 0 导致本局轨道不显示。
|
|
// 这里在开局主动复位,作为兜底,避免此类未恢复的显示 bug。
|
|
SetAllTrackFade(1f);
|
|
|
|
SetAllTrackLocalZ(startLocalZ);
|
|
|
|
if (playStartZMove)
|
|
{
|
|
startMoveRoutine = StartCoroutine(AnimateTracksToEndZ());
|
|
}
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
private void Update()
|
|
{
|
|
// Editor-only debug hotkeys. Compiled out of player builds entirely so a serialized
|
|
// scene value can never leave number-key fracture triggers or per-frame Input polling
|
|
// active in a shipped game.
|
|
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);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
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<FractureAndDrift>();
|
|
if (fractureDrivers[i] == null)
|
|
{
|
|
fractureDrivers[i] = trackObject.GetComponentInChildren<FractureAndDrift>(true);
|
|
}
|
|
}
|
|
|
|
trackRenderers[i] = trackObject.GetComponent<Renderer>();
|
|
if (trackRenderers[i] == null)
|
|
{
|
|
trackRenderers[i] = trackObject.GetComponentInChildren<Renderer>(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;
|
|
}
|
|
}
|