using UnityEngine; using UnityEngine.UI; using System.Collections; using System.Collections.Generic; public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour { private sealed class PooledJudgeTag : MonoBehaviour { public GameObject prefabSource; } public static Animation_GenerateJudgementSituationPrefab Instance; [Header("Inspector")] public GameObject redEffect; public GameObject greenEffect; public GameObject yellowEffect; public GameObject purpleEffect; public GameObject blueEffect; [Header("Inspector")] public GameObject perfect_judge_prefab; public GameObject great_judge_prefab; public GameObject good_judge_prefab; public GameObject miss_judge_prefab; [Header("Inspector")] public GameObject trackSkill_prefab; [Header("Inspector")] public float baseScale = 1.0f; public bool useRandomScale = true; public float minScaleMultiplier = 0.8f; public float maxScaleMultiplier = 1.2f; [System.Serializable] public class PopupMotionSettings { public float jumpForce = 5.0f; public float gravity = -16f; [Tooltip("Horizontal drift distance range for popup text. Adds a real arc instead of a straight pop.")] public Vector2 horizontalDriftRange = new Vector2(0.25f, 0.65f); [Tooltip("Random Z rotation range applied over the popup lifetime.")] public float rotationRange = 8f; [Tooltip("Temporary scale punch at the beginning of the popup.")] public float scalePunch = 0.18f; [Tooltip("How long the initial scale punch takes before settling back.")] public float scalePunchDuration = 0.18f; public float fadeInTime = 0.1f; public float fadeOutStartTime = 0.6f; public float fadeOutDuration = 0.35f; } [Header("Judge Popup")] public PopupMotionSettings judgePopup = new PopupMotionSettings() { jumpForce = 7.2f, gravity = -16f, horizontalDriftRange = new Vector2(0.25f, 0.65f), rotationRange = 8f, scalePunch = 0.18f, scalePunchDuration = 0.18f, fadeInTime = 0.1f, fadeOutStartTime = 0.3f, fadeOutDuration = 0.1f }; [Header("Track Skill Popup")] public PopupMotionSettings trackSkillPopup = new PopupMotionSettings() { jumpForce = 6.8f, gravity = -12.5f, horizontalDriftRange = new Vector2(0.18f, 0.48f), rotationRange = 6f, scalePunch = 0.14f, scalePunchDuration = 0.16f, fadeInTime = 0.08f, fadeOutStartTime = 0.36f, fadeOutDuration = 0.1f }; [Header("Track Skill UI")] [Tooltip("UI prefab spawned under world-space Effect nodes needs a small world scale. Judge SpriteRenderer prefabs still use baseScale directly.")] public float trackSkillUiScaleMultiplier = 0.01f; public int trackSkillSortingOrder = 9000; [Header("Track Skill Burst Stagger")] [Tooltip("If multiple track-skill popups spawn within this time window, increase later popups' Y launch speed to reduce overlap.")] public float trackSkillBurstWindow = 0.08f; [Tooltip("Extra jump force added per near-simultaneous popup.")] public float trackSkillBurstJumpForceStep = 0.65f; [Tooltip("Maximum burst step applied to later popups.")] public int trackSkillBurstMaxStep = 3; private Transform redSpawn; private Transform greenSpawn; private Transform yellowSpawn; private Transform purpleSpawn; private Transform blueSpawn; private int lastSceneIndex = -1; private Coroutine prewarmJudgeCoroutine; private readonly Dictionary> judgePools = new Dictionary>(); private readonly HashSet rentedJudges = new HashSet(); private float lastTrackSkillSpawnTime = -999f; private int trackSkillBurstCount = 0; private void Awake() { if (Instance == null) { Instance = this; } else if (Instance != this) { Destroy(gameObject); return; } CacheSpawnPoints(); lastSceneIndex = UnityEngine.SceneManagement.SceneManager.GetActiveScene().buildIndex; } private void Start() { CacheSpawnPointsIfNeeded(); } private void OnEnable() { int currentSceneIndex = UnityEngine.SceneManagement.SceneManager.GetActiveScene().buildIndex; if (currentSceneIndex != lastSceneIndex) { if (JudgeManager.IsDebugEnabled) Debug.Log($"[Animation_Generate] Scene changed detected (from {lastSceneIndex} to {currentSceneIndex}), refreshing spawn point cache"); lastSceneIndex = currentSceneIndex; CacheSpawnPoints(); } else { CacheSpawnPointsIfNeeded(); } } private void CacheSpawnPointsIfNeeded() { if (redSpawn == null || greenSpawn == null || yellowSpawn == null || purpleSpawn == null || blueSpawn == null) { bool anyInvalid = (redEffect == null || redEffect.transform == null) || (greenEffect == null || greenEffect.transform == null) || (yellowEffect == null || yellowEffect.transform == null) || (purpleEffect == null || purpleEffect.transform == null) || (blueEffect == null || blueEffect.transform == null); if (anyInvalid || redSpawn == null) { CacheSpawnPoints(); } } } private void CacheSpawnPoints() { redSpawn = redEffect != null ? redEffect.transform : null; greenSpawn = greenEffect != null ? greenEffect.transform : null; yellowSpawn = yellowEffect != null ? yellowEffect.transform : null; purpleSpawn = purpleEffect != null ? purpleEffect.transform : null; blueSpawn = blueEffect != null ? blueEffect.transform : null; if (JudgeManager.IsDebugEnabled) { Debug.Log($"[Animation_Generate] Cached spawn points: red={redSpawn != null}, green={greenSpawn != null}, yellow={yellowSpawn != null}, purple={purpleSpawn != null}, blue={blueSpawn != null}"); } } public void SpawnJudgePrefab(string color, string judgeResult) { if (string.IsNullOrEmpty(color)) { Debug.LogError("[Animation_Generate] SpawnJudgePrefab called with empty color!"); return; } if (string.IsNullOrEmpty(judgeResult)) { Debug.LogError("[Animation_Generate] SpawnJudgePrefab called with empty judgeResult!"); return; } Transform spawnPoint = GetSpawnPoint(color); if (spawnPoint == null) { Debug.LogError($"[Animation_Generate] Could not find spawn point for color: {color}"); return; } GameObject prefabToUse = GetJudgePrefab(judgeResult); if (prefabToUse == null) { Debug.LogError($"[Animation_Generate] Could not find judge prefab for result: {judgeResult}. Available: perfect={perfect_judge_prefab != null}, great={great_judge_prefab != null}, good={good_judge_prefab != null}, miss={miss_judge_prefab != null}"); return; } if (JudgeManager.IsDebugEnabled) Debug.Log($"[Animation_Generate] Spawning judge prefab: color={color}, result={judgeResult}, prefab={prefabToUse.name}"); GameObject instance = GetPooledJudge(prefabToUse, spawnPoint); if (instance == null) return; instance.transform.localPosition = Vector3.zero; instance.transform.localRotation = Quaternion.identity; float finalScale = baseScale; if (useRandomScale) finalScale *= Random.Range(minScaleMultiplier, maxScaleMultiplier); instance.transform.localScale = new Vector3(finalScale, finalScale, 1f); StartCoroutine(AnimatePopup(instance, judgePopup, false)); } public void SpawnTrackSkillPrefab(string color, Sprite skillIcon, string skillName) { if (string.IsNullOrEmpty(color) || trackSkill_prefab == null) return; Transform spawnPoint = GetSpawnPoint(color); if (spawnPoint == null) return; GameObject instance = Instantiate(trackSkill_prefab, spawnPoint, false); if (instance == null) return; EnsureRenderableTrackSkillInstance(instance); BindTrackSkillContent(instance, skillIcon, skillName); instance.transform.localPosition = Vector3.zero; instance.transform.localRotation = Quaternion.identity; float finalScale = baseScale; if (useRandomScale) finalScale *= Random.Range(minScaleMultiplier, maxScaleMultiplier); ApplyTrackSkillScale(instance, finalScale); instance.SetActive(true); PopupMotionSettings popupSettings = CreateTrackSkillPopupSettings(); StartCoroutine(AnimatePopup(instance, popupSettings, true)); } private PopupMotionSettings CreateTrackSkillPopupSettings() { PopupMotionSettings popupSettings = new PopupMotionSettings { jumpForce = trackSkillPopup.jumpForce, gravity = trackSkillPopup.gravity, horizontalDriftRange = trackSkillPopup.horizontalDriftRange, rotationRange = trackSkillPopup.rotationRange, scalePunch = trackSkillPopup.scalePunch, scalePunchDuration = trackSkillPopup.scalePunchDuration, fadeInTime = trackSkillPopup.fadeInTime, fadeOutStartTime = trackSkillPopup.fadeOutStartTime, fadeOutDuration = trackSkillPopup.fadeOutDuration }; float now = Time.unscaledTime; float window = Mathf.Max(0f, trackSkillBurstWindow); trackSkillBurstCount = now - lastTrackSkillSpawnTime <= window ? trackSkillBurstCount + 1 : 0; lastTrackSkillSpawnTime = now; int appliedStep = Mathf.Clamp(trackSkillBurstCount, 0, Mathf.Max(0, trackSkillBurstMaxStep)); popupSettings.jumpForce += Mathf.Max(0f, trackSkillBurstJumpForceStep) * appliedStep; return popupSettings; } public void PrewarmJudgePrefabs(int perPrefab = 1) { if (!Application.isPlaying) return; if (prewarmJudgeCoroutine != null) { StopCoroutine(prewarmJudgeCoroutine); } prewarmJudgeCoroutine = StartCoroutine(PrewarmJudgePrefabsRoutine(Mathf.Max(1, perPrefab))); } public IEnumerator PrewarmJudgePrefabsRoutine(int perPrefab = 1) { GameObject[] prefabs = { perfect_judge_prefab, great_judge_prefab, good_judge_prefab, miss_judge_prefab }; int count = Mathf.Max(1, perPrefab); foreach (GameObject prefab in prefabs) { if (prefab == null) continue; for (int i = 0; i < count; i++) { GameObject instance = null; try { instance = CreatePooledJudge(prefab); if (instance != null) { rentedJudges.Add(instance); } ReturnJudgeToPool(instance); } catch { } yield return null; } } prewarmJudgeCoroutine = null; } private IEnumerator AnimatePopup(GameObject obj, PopupMotionSettings settings, bool destroyAtEnd = false) { if (obj == null) yield break; if (settings == null) settings = judgePopup; SpriteRenderer[] renderers = obj.GetComponentsInChildren(true); Graphic[] graphics = obj.GetComponentsInChildren(true); CanvasGroup canvasGroup = obj.GetComponent(); float elapsed = 0f; float vVelocity = settings.jumpForce; float fadeDuration = Mathf.Max(0.001f, settings.fadeOutDuration); float estimatedApexTime = settings.gravity < -0.001f ? Mathf.Max(0f, settings.jumpForce / -settings.gravity) : settings.fadeOutStartTime; float totalLifeTime = Mathf.Max(settings.fadeInTime + fadeDuration, estimatedApexTime + fadeDuration); float driftSign = Random.value < 0.5f ? -1f : 1f; float driftDistance = Random.Range(settings.horizontalDriftRange.x, settings.horizontalDriftRange.y) * driftSign; float horizontalVelocity = totalLifeTime > 0.001f ? driftDistance / totalLifeTime : 0f; float targetRotation = Random.Range(-settings.rotationRange, settings.rotationRange); Vector3 currentLocalPos = Vector3.zero; Vector3 baseScaleValue = obj.transform.localScale; bool fadeOutStarted = false; float fadeOutElapsed = 0f; float fadeOutStartAlpha = 1f; float currentAlpha = 0f; SetRenderersAlpha(renderers, 0f); SetGraphicsAlpha(graphics, canvasGroup, 0f); while (!fadeOutStarted || fadeOutElapsed < fadeDuration) { if (obj == null) yield break; float dt = Time.deltaTime; elapsed += dt; float previousVelocity = vVelocity; vVelocity += settings.gravity * dt; currentLocalPos.x += horizontalVelocity * dt; currentLocalPos.y += vVelocity * dt; obj.transform.localPosition = currentLocalPos; obj.transform.localRotation = Quaternion.Euler(0f, 0f, Mathf.Lerp(0f, targetRotation, Mathf.Clamp01(elapsed / totalLifeTime))); float scaleT = settings.scalePunchDuration > 0.001f ? Mathf.Clamp01(elapsed / settings.scalePunchDuration) : 1f; float punch = Mathf.Sin(scaleT * Mathf.PI) * settings.scalePunch; obj.transform.localScale = baseScaleValue * (1f + punch); bool reachedApex = previousVelocity > 0f && vVelocity <= 0f; if (!fadeOutStarted && (reachedApex || elapsed >= totalLifeTime - fadeDuration)) { fadeOutStarted = true; fadeOutElapsed = 0f; fadeOutStartAlpha = currentAlpha; } float alpha; if (fadeOutStarted) { fadeOutElapsed += dt; alpha = Mathf.Lerp(fadeOutStartAlpha, 0f, Mathf.Clamp01(fadeOutElapsed / fadeDuration)); } else if (elapsed < settings.fadeInTime) { alpha = Mathf.InverseLerp(0f, settings.fadeInTime, elapsed); } else { alpha = 1f; } alpha = Mathf.Clamp01(alpha); currentAlpha = alpha; SetRenderersAlpha(renderers, alpha); SetGraphicsAlpha(graphics, canvasGroup, alpha); yield return null; } if (obj != null) { obj.transform.localScale = baseScaleValue; obj.transform.localRotation = Quaternion.identity; SetRenderersAlpha(renderers, 0f); SetGraphicsAlpha(graphics, canvasGroup, 0f); } if (obj != null) { if (destroyAtEnd) Destroy(obj); else ReturnJudgeToPool(obj); } } private static void SetRenderersAlpha(SpriteRenderer[] renderers, float alpha) { if (renderers == null) return; for (int i = 0; i < renderers.Length; i++) { SpriteRenderer sr = renderers[i]; if (sr == null) continue; Color c = sr.color; c.a = alpha; sr.color = c; } } private static void SetGraphicsAlpha(Graphic[] graphics, CanvasGroup canvasGroup, float alpha) { if (canvasGroup != null) { canvasGroup.alpha = alpha; return; } if (graphics == null) return; for (int i = 0; i < graphics.Length; i++) { Graphic graphic = graphics[i]; if (graphic == null) continue; Color c = graphic.color; c.a = alpha; graphic.color = c; } } private void EnsureRenderableTrackSkillInstance(GameObject instance) { if (instance == null) return; if (instance.GetComponentInChildren(true) == null) return; Canvas canvas = instance.GetComponent(); if (canvas == null) canvas = instance.AddComponent(); canvas.renderMode = RenderMode.WorldSpace; canvas.overrideSorting = true; canvas.sortingOrder = trackSkillSortingOrder; canvas.pixelPerfect = false; CanvasGroup canvasGroup = instance.GetComponent(); if (canvasGroup == null) canvasGroup = instance.AddComponent(); canvasGroup.alpha = 1f; } private void ApplyTrackSkillScale(GameObject instance, float finalScale) { if (instance == null) return; bool isUiPrefab = instance.GetComponentInChildren(true) != null; float appliedScale = isUiPrefab ? finalScale * trackSkillUiScaleMultiplier : finalScale; instance.transform.localScale = new Vector3(appliedScale, appliedScale, 1f); } private void BindTrackSkillContent(GameObject instance, Sprite skillIcon, string skillName) { if (instance == null) return; Image targetImage = null; Text targetText = null; TrackSkillPrefab modern = instance.GetComponent(); if (modern != null) { modern.Bind(skillIcon, skillName); targetImage = modern.skillImage; targetText = modern.skillText; } else { trackSkillPrefab legacy = instance.GetComponent(); if (legacy != null) { targetImage = legacy.track_skillIcon; targetText = legacy.track_skillName; } } if (targetImage == null) targetImage = instance.GetComponentInChildren(true); if (targetText == null) targetText = instance.GetComponentInChildren(true); if (targetImage != null) { targetImage.sprite = skillIcon; targetImage.enabled = skillIcon != null; } if (targetText != null) { targetText.text = string.IsNullOrWhiteSpace(skillName) ? string.Empty : skillName; } } private GameObject GetPooledJudge(GameObject prefab, Transform parent) { if (prefab == null || parent == null) return null; if (!judgePools.TryGetValue(prefab, out var pool)) { pool = new Stack(); judgePools[prefab] = pool; } GameObject instance = null; while (pool.Count > 0 && instance == null) { instance = pool.Pop(); } if (instance == null) { instance = CreatePooledJudge(prefab); } if (instance == null) return null; instance.transform.SetParent(parent, false); instance.SetActive(true); rentedJudges.Add(instance); return instance; } private GameObject CreatePooledJudge(GameObject prefab) { if (prefab == null) return null; GameObject instance = Instantiate(prefab, transform); var tag = instance.GetComponent(); if (tag == null) tag = instance.AddComponent(); tag.prefabSource = prefab; instance.SetActive(false); return instance; } private void ReturnJudgeToPool(GameObject instance) { if (instance == null) return; if (!rentedJudges.Remove(instance)) return; var tag = instance.GetComponent(); if (tag == null || tag.prefabSource == null) { Destroy(instance); return; } if (!judgePools.TryGetValue(tag.prefabSource, out var pool)) { pool = new Stack(); judgePools[tag.prefabSource] = pool; } instance.transform.SetParent(transform, false); instance.transform.localPosition = new Vector3(99999f, 99999f, 0f); instance.transform.localRotation = Quaternion.identity; instance.SetActive(false); pool.Push(instance); } private Transform GetSpawnPoint(string color) { if (string.IsNullOrEmpty(color)) return null; Transform point = null; switch (color.ToLower()) { case "red": point = redSpawn; break; case "green": point = greenSpawn; break; case "yellow": point = yellowSpawn; break; case "purple": point = purpleSpawn; break; case "blue": point = blueSpawn; break; default: Debug.LogWarning($"[Animation_Generate] Unknown color: {color}"); return null; } if (point == null) { Debug.LogError($"[Animation_Generate] Spawn point for color {color} is null. Check if effects are assigned in inspector. Make sure the effect object is active in the scene."); } return point; } private GameObject GetJudgePrefab(string result) { if (string.IsNullOrEmpty(result)) return null; GameObject prefab = null; switch (result.ToLower()) { case "perfect": prefab = perfect_judge_prefab; break; case "great": prefab = great_judge_prefab; break; case "good": prefab = good_judge_prefab; break; case "miss": prefab = miss_judge_prefab; break; default: Debug.LogWarning($"[Animation_Generate] Unknown judge result: {result}"); return null; } if (prefab == null) { Debug.LogError($"[Animation_Generate] Judge prefab for '{result}' is not assigned in inspector! Check Animation_GenerateJudgementSituationPrefab component."); } return prefab; } }