using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; /// /// Gameplay start-time track entrance animation controller. /// Triggered by readyLetsGo when 3-2-1-Go starts. /// Uses unscaled time so it can run while Time.timeScale == 0. /// public class GameplayTrackStartIntroAnimator : MonoBehaviour { [Header("Scope")] [SerializeField] private string requiredSceneName = "gamePlay_gamePlay"; [SerializeField] private bool playOnlyOncePerScene = true; [Header("Timing")] [SerializeField] private float stepStagger = 0.06f; [SerializeField] private float moveDuration = 0.30f; [SerializeField] private float flashDuration = 0.26f; [SerializeField] private int flashTimes = 2; [SerializeField] private float phaseGap = 0.06f; [SerializeField] private float laneRectFlashDuration = 0.18f; [SerializeField] private float allyFlashDuration = 0.16f; [SerializeField] private float hudFlashDuration = 0.14f; [SerializeField] private bool keepHiddenBeforeStart = true; [Header("Preset Values")] [SerializeField] private float keyDownStartY = -30f; [SerializeField] private float keyDownTargetY = 5.3f; [SerializeField] private float keyDownStartRotY = -30f; [SerializeField] private float keyDownTargetRotY = 0f; [SerializeField] private float gameObjectStartY = -10f; [SerializeField] private float gameObjectTargetY = -4.523f; [SerializeField] private float allyStartRotY = 130.8f; [SerializeField] private float allyTargetRotY = 0f; [Header("Ally Turn")] [Tooltip("Use pseudo Y-turn on UI cards to avoid color/mask artifacts caused by true Y rotation on complex UI roots.")] [SerializeField] private bool allyUsePseudoYTurnForUI = true; [SerializeField] private float allyPseudoStartScaleX = 0.68f; private bool playedOnce; private bool refsCached; private bool preStartPrepared; private Coroutine playRoutine; private Transform keyDown; private Transform colorLineStatic; private Transform enemyRoot; private Transform progressLine; private Transform score; private Transform pause; private Transform bgReplace; private Transform totalEnemyTop; private Transform totalEnemyFade; private Transform enemyImg; private Transform enemySpineSystem; private Transform enemyListBall; private Transform enemyStatics; private readonly List keyDownRects = new List(); private readonly List gameObjectRects = new List(); private readonly List colorRects = new List(); private readonly List allies = new List(); private readonly List explicitKeyDownRects = new List(5); private readonly Dictionary baseGraphicAlpha = new Dictionary(); private readonly Dictionary baseSpriteAlpha = new Dictionary(); private readonly Dictionary baseCanvasGroupAlpha = new Dictionary(); private readonly Dictionary baseLocalScale = new Dictionary(); private readonly Dictionary rootCanvasGroups = new Dictionary(); private void Awake() { if (!IsInRequiredScene()) return; CacheRefsIfNeeded(); PreparePreStartState(); } private void Start() { if (!IsInRequiredScene()) return; StartCoroutine(PreparePreStartStateNextFrame()); } private void LateUpdate() { if (!keepHiddenBeforeStart) return; if (!IsInRequiredScene()) return; if (playedOnce || playRoutine != null) return; CacheRefsIfNeeded(); // Keep hidden state stable before player presses start. PreparePreStartState(force: true); } public void PlayIfNeeded() { if (!IsInRequiredScene()) return; if (playRoutine != null) return; if (playOnlyOncePerScene && playedOnce) return; CacheRefsIfNeeded(); playRoutine = StartCoroutine(PlayRoutine()); } public void SetExplicitKeyDownRects(IEnumerable rects) { explicitKeyDownRects.Clear(); if (rects != null) { foreach (Transform rect in rects) { if (rect != null) { explicitKeyDownRects.Add(rect); } } } refsCached = false; preStartPrepared = false; } private IEnumerator PreparePreStartStateNextFrame() { // Re-apply after one frame so late UI initializers don't bring these elements visible before start. yield return null; if (playRoutine == null && !playedOnce) { PreparePreStartState(force: true); } } public void ResetPlayState() { playedOnce = false; if (playRoutine != null) { StopCoroutine(playRoutine); playRoutine = null; } CacheRefsIfNeeded(); PreparePreStartState(force: true); } private bool IsInRequiredScene() { if (string.IsNullOrWhiteSpace(requiredSceneName)) return true; Scene active = SceneManager.GetActiveScene(); return string.Equals(active.name, requiredSceneName, System.StringComparison.OrdinalIgnoreCase); } private void CacheRefsIfNeeded() { if (refsCached) return; keyDown = FindSceneTransformByName("KEY_down"); colorLineStatic = FindSceneTransformByName("colorLineStatic"); enemyRoot = FindSceneTransformByName("enemyObject_toWiggle"); progressLine = FindSceneTransformByName("progressLine"); score = FindSceneTransformByName("score"); pause = FindPauseInScene(); bgReplace = FindSceneTransformByName("bg_replace"); totalEnemyTop = FindSceneTransformByName("TOTALenemy_healthbar_top"); totalEnemyFade = FindSceneTransformByName("TOTALenemy_healthbar_fade"); enemyImg = FindSceneTransformByNameUnder("img", enemyRoot); enemySpineSystem = FindSceneTransformByNameUnder("spineSystem", enemyImg != null ? enemyImg : enemyRoot); enemyListBall = FindSceneTransformByNameUnder("enemyListBall", enemyRoot); enemyStatics = FindSceneTransformByNameUnder("statics", enemyRoot); keyDownRects.Clear(); if (explicitKeyDownRects.Count > 0) { keyDownRects.AddRange(explicitKeyDownRects); } else { if (keyDown != null) keyDownRects.AddRange(CollectDescendantsByNameContains(keyDown, "sdw 3")); keyDownRects.Sort((a, b) => a.localPosition.x.CompareTo(b.localPosition.x)); FilterLaneRects(keyDownRects, keyDownTargetY, 5); } gameObjectRects.Clear(); List byNamedRoots = CollectSdw3UnderNamedRoots("GameObject", keyDown); if (byNamedRoots.Count > 0) gameObjectRects.AddRange(byNamedRoots); else gameObjectRects.AddRange(CollectGameObjectSdw3Excluding(keyDown)); gameObjectRects.Sort((a, b) => a.localPosition.x.CompareTo(b.localPosition.x)); FilterLaneRects(gameObjectRects, gameObjectTargetY, 5); colorRects.Clear(); colorRects.AddRange(CollectColorRects(colorLineStatic)); colorRects.Sort((a, b) => a.GetSiblingIndex().CompareTo(b.GetSiblingIndex())); allies.Clear(); for (int i = 1; i <= 5; i++) { Transform ally = FindSceneTransformByName($"ally_0{i}"); if (ally != null) allies.Add(ally); } allies.Sort((a, b) => a.name.CompareTo(b.name)); refsCached = true; } private void PreparePreStartState(bool force = false) { if (preStartPrepared && !force) return; if (bgReplace != null) SetAlphaNormalized(bgReplace, 0f, true); if (colorLineStatic != null) SetAlphaNormalized(colorLineStatic, 0f, true); if (enemyRoot != null) SetAlphaNormalized(enemyRoot, 0f, true); if (enemyListBall != null) SetAlphaNormalized(enemyListBall, 0f, true); if (enemyStatics != null) SetAlphaNormalized(enemyStatics, 0f, true); if (enemySpineSystem != null) enemySpineSystem.gameObject.SetActive(false); if (progressLine != null) SetAlphaNormalized(progressLine, 0f, true); if (score != null) SetAlphaNormalized(score, 0f, true); if (pause != null) SetAlphaNormalized(pause, 0f, true); for (int i = 0; i < colorRects.Count; i++) { if (colorRects[i] == null) continue; SetAlphaNormalized(colorRects[i], 0f, true); } for (int i = 0; i < keyDownRects.Count; i++) { Transform t = keyDownRects[i]; if (t == null) continue; SetAlphaNormalized(t, 0f, true); SetLocalY(t, keyDownStartY); SetLocalRotY(t, keyDownStartRotY); } for (int i = 0; i < gameObjectRects.Count; i++) { Transform t = gameObjectRects[i]; if (t == null) continue; SetAlphaNormalized(t, 0f, true); SetLocalY(t, gameObjectStartY); } for (int i = 0; i < allies.Count; i++) { Transform ally = allies[i]; if (ally == null) continue; SetRootCanvasAlpha(ally, 0f); if (allyUsePseudoYTurnForUI) { SetPseudoTurnScaleX(ally, allyPseudoStartScaleX); SetLocalRotY(ally, allyTargetRotY); } else { SetLocalRotY(ally, allyStartRotY); } } if (totalEnemyTop != null) SetScaleY(totalEnemyTop, 0f); if (totalEnemyFade != null) SetScaleY(totalEnemyFade, 0f); preStartPrepared = true; } private IEnumerator PlayRoutine() { PreparePreStartState(); // 1) bg_replace flash in, then KEY_down sdw3 move/rotate in sequence if (bgReplace != null) { yield return StartCoroutine(FlashIn(bgReplace, flashTimes, flashDuration, true)); } yield return StartCoroutine(AnimateRectsYAndRotY( keyDownRects, keyDownStartY, keyDownTargetY, keyDownStartRotY, keyDownTargetRotY, moveDuration, stepStagger)); yield return WaitRealtime(phaseGap); // 2) GameObject sdw3 move in sequence yield return StartCoroutine(AnimateRectsYOnlyWithFlash( gameObjectRects, gameObjectStartY, gameObjectTargetY, moveDuration, stepStagger, laneRectFlashDuration)); // 3) colorLineStatic enters // 4) enemy enters (parallel) // 5) progressLine enters (parallel) // 6) allies then score/pause (parallel) Coroutine colorStep = StartCoroutine(FlashInSequence(colorRects, flashTimes, flashDuration, stepStagger)); Coroutine enemyStep = StartCoroutine(PlayEnemyStep(enemyRoot, enemyListBall, enemyStatics, enemySpineSystem, totalEnemyTop, totalEnemyFade)); Coroutine progressStep = StartCoroutine(FlashIn(progressLine, flashTimes, flashDuration, true)); Coroutine alliesStep = StartCoroutine(PlayAlliesAndHudStep(allies, score, pause)); if (colorStep != null) yield return colorStep; if (enemyStep != null) yield return enemyStep; if (progressStep != null) yield return progressStep; if (alliesStep != null) yield return alliesStep; playedOnce = true; playRoutine = null; } private IEnumerator PlayEnemyStep( Transform enemyRootTr, Transform enemyListBallTr, Transform enemyStaticsTr, Transform enemySpineSystemTr, Transform totalEnemyTopTr, Transform totalEnemyFadeTr) { if (enemyRootTr != null) yield return StartCoroutine(FlashIn(enemyRootTr, flashTimes, flashDuration, true)); if (enemyListBallTr != null) yield return StartCoroutine(FlashIn(enemyListBallTr, flashTimes, flashDuration, true)); if (enemyStaticsTr != null) yield return StartCoroutine(FlashIn(enemyStaticsTr, flashTimes, flashDuration, true)); if (enemySpineSystemTr != null) yield return StartCoroutine(FlashInByActiveToggle(enemySpineSystemTr, flashTimes, flashDuration)); if (totalEnemyTopTr != null) SetScaleY(totalEnemyTopTr, 0f); if (totalEnemyFadeTr != null) SetScaleY(totalEnemyFadeTr, 0f); float elapsed = 0f; float dur = Mathf.Max(0.01f, moveDuration); while (elapsed < dur) { elapsed += Time.unscaledDeltaTime; float t = EaseOutCubic(Mathf.Clamp01(elapsed / dur)); if (totalEnemyTopTr != null) SetScaleY(totalEnemyTopTr, Mathf.LerpUnclamped(0f, 1f, t)); if (totalEnemyFadeTr != null) SetScaleY(totalEnemyFadeTr, Mathf.LerpUnclamped(0f, 1f, t)); yield return null; } if (totalEnemyTopTr != null) SetScaleY(totalEnemyTopTr, 1f); if (totalEnemyFadeTr != null) SetScaleY(totalEnemyFadeTr, 1f); } private IEnumerator PlayAlliesAndHudStep(List allyList, Transform scoreTr, Transform pauseTr) { for (int i = 0; i < allyList.Count; i++) { Transform ally = allyList[i]; if (ally == null) continue; yield return StartCoroutine(FlashInCanvasGroup(ally, flashTimes, allyFlashDuration)); if (allyUsePseudoYTurnForUI) yield return StartCoroutine(AnimatePseudoTurnScaleX(ally, allyPseudoStartScaleX, 1f, moveDuration)); else yield return StartCoroutine(AnimateRotY(ally, allyStartRotY, allyTargetRotY, moveDuration)); if (i < allyList.Count - 1 && stepStagger > 0f) yield return WaitRealtime(stepStagger); } if (scoreTr != null) yield return StartCoroutine(FlashIn(scoreTr, flashTimes, hudFlashDuration, true)); if (pauseTr != null) yield return StartCoroutine(FlashIn(pauseTr, flashTimes, hudFlashDuration, true)); } private IEnumerator FlashInSequence(List list, int flashes, float duration, float stagger) { if (list == null || list.Count == 0) yield break; for (int i = 0; i < list.Count; i++) { Transform t = list[i]; if (t == null) continue; yield return StartCoroutine(FlashIn(t, flashes, duration, true)); if (i < list.Count - 1 && stagger > 0f) yield return WaitRealtime(stagger); } } private IEnumerator AnimateRectsYAndRotY(List list, float fromY, float toY, float fromRotY, float toRotY, float duration, float stagger) { if (list == null || list.Count == 0) yield break; for (int i = 0; i < list.Count; i++) { Transform t = list[i]; if (t == null) continue; SetAlphaNormalized(t, 1f, true); SetLocalY(t, fromY); SetLocalRotY(t, fromRotY); StartCoroutine(AnimateYAndRotY(t, fromY, toY, fromRotY, toRotY, duration)); if (i < list.Count - 1 && stagger > 0f) yield return WaitRealtime(stagger); } yield return WaitRealtime(duration); } private IEnumerator AnimateRectsYOnly(List list, float fromY, float toY, float duration, float stagger) { if (list == null || list.Count == 0) yield break; for (int i = 0; i < list.Count; i++) { Transform t = list[i]; if (t == null) continue; SetAlphaNormalized(t, 1f, true); SetLocalY(t, fromY); StartCoroutine(AnimateYOnly(t, fromY, toY, duration)); if (i < list.Count - 1 && stagger > 0f) yield return WaitRealtime(stagger); } yield return WaitRealtime(duration); } private IEnumerator AnimateRectsYOnlyWithFlash(List list, float fromY, float toY, float duration, float stagger, float flashDur) { if (list == null || list.Count == 0) yield break; for (int i = 0; i < list.Count; i++) { Transform t = list[i]; if (t == null) continue; SetAlphaNormalized(t, 0f, true); SetLocalY(t, fromY); StartCoroutine(FlashIn(t, flashTimes, flashDur, true)); StartCoroutine(AnimateYOnly(t, fromY, toY, duration)); if (i < list.Count - 1 && stagger > 0f) yield return WaitRealtime(stagger); } yield return WaitRealtime(Mathf.Max(duration, flashDur)); } private IEnumerator AnimateYAndRotY(Transform t, float fromY, float toY, float fromRotY, float toRotY, float duration) { if (t == null) yield break; float elapsed = 0f; float dur = Mathf.Max(0.01f, duration); float fromRot = NormalizeEulerY(fromRotY); float toRot = NormalizeEulerY(toRotY); while (elapsed < dur) { elapsed += Time.unscaledDeltaTime; float k = EaseOutCubic(Mathf.Clamp01(elapsed / dur)); SetLocalY(t, Mathf.LerpUnclamped(fromY, toY, k)); SetLocalRotY(t, Mathf.LerpAngle(fromRot, toRot, k)); yield return null; } SetLocalY(t, toY); SetLocalRotY(t, toRotY); } private IEnumerator AnimateYOnly(Transform t, float fromY, float toY, float duration) { if (t == null) yield break; float elapsed = 0f; float dur = Mathf.Max(0.01f, duration); while (elapsed < dur) { elapsed += Time.unscaledDeltaTime; float k = EaseOutCubic(Mathf.Clamp01(elapsed / dur)); SetLocalY(t, Mathf.LerpUnclamped(fromY, toY, k)); yield return null; } SetLocalY(t, toY); } private IEnumerator AnimateRotY(Transform t, float fromY, float toY, float duration) { if (t == null) yield break; float elapsed = 0f; float dur = Mathf.Max(0.01f, duration); float from = NormalizeEulerY(fromY); float to = NormalizeEulerY(toY); SetLocalRotY(t, from); while (elapsed < dur) { elapsed += Time.unscaledDeltaTime; float k = EaseOutCubic(Mathf.Clamp01(elapsed / dur)); SetLocalRotY(t, Mathf.LerpAngle(from, to, k)); yield return null; } SetLocalRotY(t, to); } private IEnumerator AnimatePseudoTurnScaleX(Transform t, float fromScaleX, float toScaleX, float duration) { if (t == null) yield break; float elapsed = 0f; float dur = Mathf.Max(0.01f, duration); SetPseudoTurnScaleX(t, fromScaleX); while (elapsed < dur) { elapsed += Time.unscaledDeltaTime; float k = EaseOutCubic(Mathf.Clamp01(elapsed / dur)); float sx = Mathf.LerpUnclamped(fromScaleX, toScaleX, k); SetPseudoTurnScaleX(t, sx); yield return null; } SetPseudoTurnScaleX(t, toScaleX); } private IEnumerator FlashIn(Transform target, int flashes, float duration, bool includeChildren) { if (target == null) yield break; int count = Mathf.Max(1, flashes); float total = Mathf.Max(0.04f, duration); float unit = total / (count * 2f); SetAlphaNormalized(target, 0f, includeChildren); for (int i = 0; i < count; i++) { SetAlphaNormalized(target, 1f, includeChildren); yield return WaitRealtime(unit); if (i < count - 1) { SetAlphaNormalized(target, 0f, includeChildren); yield return WaitRealtime(unit); } } SetAlphaNormalized(target, 1f, includeChildren); } private IEnumerator FlashInByActiveToggle(Transform target, int flashes, float duration) { if (target == null) yield break; int count = Mathf.Max(1, flashes); float total = Mathf.Max(0.04f, duration); float unit = total / (count * 2f); if (target.gameObject.activeSelf) target.gameObject.SetActive(false); for (int i = 0; i < count; i++) { target.gameObject.SetActive(true); yield return WaitRealtime(unit); if (i < count - 1) { target.gameObject.SetActive(false); yield return WaitRealtime(unit); } } target.gameObject.SetActive(true); } private static IEnumerator WaitRealtime(float duration) { float elapsed = 0f; while (elapsed < duration) { elapsed += Time.unscaledDeltaTime; yield return null; } } private static float EaseOutCubic(float t) { float inv = 1f - t; return 1f - inv * inv * inv; } private static float NormalizeEulerY(float y) { float v = y % 360f; if (v < 0f) v += 360f; return v; } private void SetPseudoTurnScaleX(Transform t, float scaleX) { if (t == null) return; Vector3 baseScale = GetBaseLocalScale(t); Vector3 s = t.localScale; s.x = Mathf.Max(0.01f, baseScale.x * scaleX); s.y = baseScale.y; s.z = baseScale.z; t.localScale = s; } private Vector3 GetBaseLocalScale(Transform t) { if (t == null) return Vector3.one; if (!baseLocalScale.TryGetValue(t, out Vector3 s)) { s = t.localScale; baseLocalScale[t] = s; } return s; } private static void SetScaleY(Transform t, float y) { if (t == null) return; Vector3 s = t.localScale; s.y = y; t.localScale = s; } private static void SetLocalY(Transform t, float y) { if (t == null) return; Vector3 p = t.localPosition; p.y = y; t.localPosition = p; } private static void SetLocalRotY(Transform t, float y) { if (t == null) return; Vector3 e = t.localEulerAngles; e.y = NormalizeEulerY(y); t.localEulerAngles = e; } private void SetAlphaNormalized(Transform root, float normalized, bool includeChildren) { if (root == null) return; normalized = Mathf.Clamp01(normalized); if (includeChildren) { CanvasGroup[] cgs = root.GetComponentsInChildren(true); for (int i = 0; i < cgs.Length; i++) { CanvasGroup cg = cgs[i]; if (cg == null) continue; float baseA = GetBaseAlpha(cg); cg.alpha = baseA * normalized; } Graphic[] graphics = root.GetComponentsInChildren(true); for (int i = 0; i < graphics.Length; i++) { Graphic g = graphics[i]; if (g == null) continue; float baseA = GetBaseAlpha(g); Color c = g.color; c.a = baseA * normalized; g.color = c; } SpriteRenderer[] sprites = root.GetComponentsInChildren(true); for (int i = 0; i < sprites.Length; i++) { SpriteRenderer sr = sprites[i]; if (sr == null) continue; float baseA = GetBaseAlpha(sr); Color c = sr.color; c.a = baseA * normalized; sr.color = c; } } else { CanvasGroup cg = root.GetComponent(); if (cg != null) { float baseA = GetBaseAlpha(cg); cg.alpha = baseA * normalized; } Graphic g = root.GetComponent(); if (g != null) { float baseA = GetBaseAlpha(g); Color c = g.color; c.a = baseA * normalized; g.color = c; } SpriteRenderer sr = root.GetComponent(); if (sr != null) { float baseA = GetBaseAlpha(sr); Color c = sr.color; c.a = baseA * normalized; sr.color = c; } } } private float GetBaseAlpha(CanvasGroup cg) { if (!baseCanvasGroupAlpha.TryGetValue(cg, out float a)) { a = cg.alpha; if (a <= 0.001f) a = 1f; baseCanvasGroupAlpha[cg] = a; } return a; } private CanvasGroup GetOrAddRootCanvasGroup(Transform root) { if (root == null) return null; if (!rootCanvasGroups.TryGetValue(root, out CanvasGroup cg) || cg == null) { cg = root.GetComponent(); if (cg == null) cg = root.gameObject.AddComponent(); rootCanvasGroups[root] = cg; } return cg; } private void SetRootCanvasAlpha(Transform root, float normalized) { if (root == null) return; CanvasGroup cg = GetOrAddRootCanvasGroup(root); if (cg == null) return; float n = Mathf.Clamp01(normalized); // Use direct alpha for root group to avoid touching child graphic colors. cg.alpha = n; } private IEnumerator FlashInCanvasGroup(Transform root, int flashes, float duration) { if (root == null) yield break; int count = Mathf.Max(1, flashes); float total = Mathf.Max(0.04f, duration); float unit = total / (count * 2f); SetRootCanvasAlpha(root, 0f); for (int i = 0; i < count; i++) { SetRootCanvasAlpha(root, 1f); yield return WaitRealtime(unit); if (i < count - 1) { SetRootCanvasAlpha(root, 0f); yield return WaitRealtime(unit); } } SetRootCanvasAlpha(root, 1f); } private float GetBaseAlpha(Graphic g) { if (!baseGraphicAlpha.TryGetValue(g, out float a)) { a = g.color.a; if (a <= 0.001f) a = 1f; baseGraphicAlpha[g] = a; } return a; } private float GetBaseAlpha(SpriteRenderer sr) { if (!baseSpriteAlpha.TryGetValue(sr, out float a)) { a = sr.color.a; if (a <= 0.001f) a = 1f; baseSpriteAlpha[sr] = a; } return a; } private List CollectColorRects(Transform colorLineStaticRoot) { List list = new List(); if (colorLineStaticRoot == null) return list; List byEnglish = CollectDescendantsByNameContains(colorLineStaticRoot, "Rectangle 6"); if (byEnglish.Count > 0) return byEnglish; for (int i = 0; i < colorLineStaticRoot.childCount; i++) { Transform child = colorLineStaticRoot.GetChild(i); if (child == null) continue; if (child.GetComponentInChildren(true) != null || child.GetComponentInChildren(true) != null) { list.Add(child); } } return list; } private List CollectGameObjectSdw3Excluding(Transform keyDownRoot) { List list = new List(); HashSet seen = new HashSet(); Transform[] all = GetAllSceneTransforms(); for (int i = 0; i < all.Length; i++) { Transform t = all[i]; if (t == null) continue; if (t.name == null || t.name.IndexOf("sdw 3", System.StringComparison.OrdinalIgnoreCase) < 0) continue; if (keyDownRoot != null && t.IsChildOf(keyDownRoot)) continue; if (!seen.Add(t.GetInstanceID())) continue; list.Add(t); } return list; } private List CollectSdw3UnderNamedRoots(string rootName, Transform excludeRoot) { List list = new List(); if (string.IsNullOrEmpty(rootName)) return list; HashSet seen = new HashSet(); Transform[] all = GetAllSceneTransforms(); for (int i = 0; i < all.Length; i++) { Transform root = all[i]; if (root == null) continue; if (!string.Equals(root.name, rootName, System.StringComparison.OrdinalIgnoreCase)) continue; if (excludeRoot != null && root.IsChildOf(excludeRoot)) continue; List descendants = CollectDescendantsByNameContains(root, "sdw 3"); for (int j = 0; j < descendants.Count; j++) { Transform t = descendants[j]; if (t == null) continue; if (!seen.Add(t.GetInstanceID())) continue; list.Add(t); } } return list; } private static void FilterLaneRects(List list, float targetY, int expectedCount) { if (list == null) return; list.RemoveAll(t => t == null); if (list.Count == 0) return; const float yTolerance = 2.5f; List nearTarget = new List(); for (int i = 0; i < list.Count; i++) { Transform t = list[i]; if (Mathf.Abs(t.localPosition.y - targetY) <= yTolerance) nearTarget.Add(t); } List selected = (nearTarget.Count >= expectedCount) ? nearTarget : new List(list); selected.Sort((a, b) => { float da = Mathf.Abs(a.localPosition.y - targetY); float db = Mathf.Abs(b.localPosition.y - targetY); int yCmp = da.CompareTo(db); if (yCmp != 0) return yCmp; return a.localPosition.x.CompareTo(b.localPosition.x); }); if (expectedCount > 0 && selected.Count > expectedCount) selected = selected.GetRange(0, expectedCount); selected.Sort((a, b) => a.localPosition.x.CompareTo(b.localPosition.x)); list.Clear(); list.AddRange(selected); } private static List CollectDescendantsByNameContains(Transform root, string keyword) { List list = new List(); if (root == null || string.IsNullOrEmpty(keyword)) return list; Transform[] all = root.GetComponentsInChildren(true); for (int i = 0; i < all.Length; i++) { Transform child = all[i]; if (child == null || child == root) continue; if (child.name != null && child.name.IndexOf(keyword, System.StringComparison.OrdinalIgnoreCase) >= 0) { list.Add(child); } } return list; } private Transform FindPauseInScene() { Transform[] all = GetAllSceneTransforms(); Transform fallback = null; for (int i = 0; i < all.Length; i++) { Transform t = all[i]; if (t == null || !string.Equals(t.name, "Pause", System.StringComparison.OrdinalIgnoreCase)) continue; if (fallback == null) fallback = t; if (t.GetComponent