using UnityEngine; using TMPro; using System.Collections; // 保持对 Coroutine 的支持 using System.Collections.Generic; // For List public class AnimationController : MonoBehaviour { [HideInInspector] public bool isGlobalController = false; public static AnimationController Global; public GameObject redEffect; // 红色击打动画对象 public GameObject greenEffect; // 绿色击打动画对象 public GameObject yellowEffect; // 黄色击打动画对象 public GameObject purpleEffect; // 紫色击打动画对象 public GameObject blueEffect; // 蓝色击打动画对象 private Animator redAnimator; private Animator greenAnimator; private Animator yellowAnimator; private Animator purpleAnimator; private Animator blueAnimator; // Scene object used as particle source (must be assigned in scene or found automatically) public GameObject hit_particular_object; public GameObject hit_ring_object; [Header("轨道颜色 (Hex Color Codes)")] // F86F64 (浅红色/珊瑚色) public string redColorHex = "#F86F64"; // 40EC90 (亮绿色) public string greenColorHex = "#40EC90"; // F7CB6C (金黄色) public string yellowColorHex = "#F7CB6C"; // F888E7 (亮粉色/洋红色) public string purpleColorHex = "#F888E7"; // 4FACFE (天蓝色) public string blueColorHex = "#4FACFE"; [Header("Hold 粒子配置")] public float holdParticleInterval = 0.2f; private Coroutine holdParticleCoroutine; private bool holdActive = false; private string currentHoldColor; private bool isHolding = false; // Track active particle instances for immediate cleanup private List activeParticles = new List(); [Header("判定飘字prefabs")] public GameObject perfect_judge_prefab; public GameObject great_judge_prefab; public GameObject good_judge_prefab; public GameObject miss_judge_prefab; [Header("轨道关联的判定文字 (Legacy Text)")] // 请在 Inspector 中将对应的 UI Text 拖入 public TextMeshProUGUI red_track_judgementText; public TextMeshProUGUI green_track_judgementText; public TextMeshProUGUI yellow_track_judgementText; public TextMeshProUGUI purple_track_judgementText; public TextMeshProUGUI blue_track_judgementText; public void StartHoldParticles(string color) { // Prefer the Global controller if available so Start/Stop always affect the same instance if (AnimationController.Global != null && AnimationController.Global != this) { AnimationController.Global.InternalStartHoldParticles(color); return; } InternalStartHoldParticles(color); } public void StopHoldParticles() { if (AnimationController.Global != null && AnimationController.Global != this) { AnimationController.Global.InternalStopHoldParticles(); return; } InternalStopHoldParticles(); } // Internal implementations operate on this instance private void InternalStartHoldParticles(string color) { if (holdActive) return; holdActive = true; currentHoldColor = color; if (holdParticleCoroutine != null) { StopCoroutine(holdParticleCoroutine); } holdParticleCoroutine = StartCoroutine(HoldParticleRoutine(color)); } private void InternalStopHoldParticles() { holdActive = false; if (holdParticleCoroutine != null) { try { StopCoroutine(holdParticleCoroutine); } catch { } holdParticleCoroutine = null; } // Immediately destroy all active particle instances foreach (var p in activeParticles) { if (p != null) { Destroy(p); } } activeParticles.Clear(); } private IEnumerator HoldParticleRoutine(string color) { while (holdActive) { yield return new WaitForSeconds(Mathf.Max(0.01f, holdParticleInterval)); if (!holdActive) break; PlayDestroyAnimation(color); } } private void Awake() { if (isGlobalController) { if (Global != null && Global != this) { Debug.LogWarning("[AnimationController] Duplicate global detected, destroying self"); Destroy(this); return; } Global = this; DontDestroyOnLoad(gameObject); Debug.Log("[AnimationController] Global AnimationController registered"); } // Animator cache redAnimator = redEffect != null ? redEffect.GetComponent() : null; greenAnimator = greenEffect != null ? greenEffect.GetComponent() : null; yellowAnimator = yellowEffect != null ? yellowEffect.GetComponent() : null; purpleAnimator = purpleEffect != null ? purpleEffect.GetComponent() : null; blueAnimator = blueEffect != null ? blueEffect.GetComponent() : null; // If the scene object reference was not assigned on this instance (common when this script is on a prefab), // try to locate a scene object automatically so runtime instances can still use a shared particle source. if (hit_particular_object == null) { // First try a tag-based lookup. Designer should assign the scene particle source a tag "HitParticleSource". try { var byTag = GameObject.FindWithTag("HitParticleSource"); if (byTag != null) { hit_particular_object = byTag; Debug.Log("AnimationController: assigned hit_particular_object via tag 'HitParticleSource'."); } } catch { } // Next try common names if tag not used if (hit_particular_object == null) { var byName = GameObject.Find("HitParticleSource") ?? GameObject.Find("hit_particular_object") ?? GameObject.Find("Hit_Particle_Source"); if (byName != null) { hit_particular_object = byName; Debug.Log("AnimationController: assigned hit_particular_object via GameObject.Find by name."); } } if (hit_particular_object == null) { Debug.LogWarning("AnimationController: hit_particular_object not assigned in Inspector and automatic lookup failed.\n" + "You can either assign a scene object to 'hit_particular_object' on the prefab instance in the scene,\n" + "or tag the scene particle source with 'HitParticleSource', or place a GameObject named 'HitParticleSource' in the scene."); } } } public void PlayDestroyAnimation(string color) { // Only use the scene object as the template. No prefab fallback. if (hit_particular_object == null) { Debug.LogError("hit_particular_object is null. Assign a scene particle source to AnimationController.hit_particular_object or tag a GameObject 'HitParticleSource'."); // As a fallback, trigger the Animator-based effects TriggerAnimatorEffect(color); return; } GameObject source = hit_particular_object; // choose spawn position: prefer per-color effect transform if available Vector3 spawnPos = transform.position; Transform parentTransform = null; switch (color) { case "red": if (redEffect != null) { spawnPos = redEffect.transform.position; parentTransform = redEffect.transform; } break; case "green": if (greenEffect != null) { spawnPos = greenEffect.transform.position; parentTransform = greenEffect.transform; } break; case "yellow": if (yellowEffect != null) { spawnPos = yellowEffect.transform.position; parentTransform = yellowEffect.transform; } break; case "purple": if (purpleEffect != null) { spawnPos = purpleEffect.transform.position; parentTransform = purpleEffect.transform; } break; case "blue": if (blueEffect != null) { spawnPos = blueEffect.transform.position; parentTransform = blueEffect.transform; } break; } // 获取并解析颜色 Color trackColor; string hexCode = GetHexCodeForColor(color); // 尝试解析 16 进制颜色代码。如果失败,使用白色作为默认值。 if (!ColorUtility.TryParseHtmlString(hexCode, out trackColor)) { trackColor = Color.white; Debug.LogWarning($"Failed to parse hex color for '{color}' ({hexCode}). Using white."); } SpawnJudgePrefabByTrackText(color); // Instantiate a copy of the scene object GameObject particleInstance = Instantiate(source, spawnPos, Quaternion.identity); if (particleInstance == null) { Debug.LogError("Failed to instantiate hit_particular_object"); TriggerAnimatorEffect(color); return; } particleInstance.SetActive(true); // Find all ParticleSystems on the instance (root + children) and set their startColor var systems = particleInstance.GetComponentsInChildren(true); if (systems != null && systems.Length > 0) { float maxLifetime = 0f; foreach (var ps in systems) { var main = ps.main; // Set start color (works for most setups) main.startColor = trackColor; // Play the system ps.Play(); // determine lifetime (use constantMax for safety) var lifetime = main.startLifetime; float life = lifetime.constantMax; if (life <= 0f) life = lifetime.constant; // fallback if (life > maxLifetime) maxLifetime = life; } // Optionally parent the instance under the effect object so it moves with UI/slot if (parentTransform != null) { particleInstance.transform.SetParent(parentTransform, true); } // Destroy after max lifetime + small buffer Destroy(particleInstance, Mathf.Max(0.5f, maxLifetime + 0.1f)); // Track for immediate cleanup activeParticles.Add(particleInstance); } else { Debug.LogError("No ParticleSystem components found on hit_particular_object instance"); Destroy(particleInstance); TriggerAnimatorEffect(color); } // NEW: instantiate and play ring effect (overlay) if assigned if (hit_ring_object != null) { GameObject ringInstance = Instantiate(hit_ring_object, spawnPos, Quaternion.identity); if (ringInstance != null) { Debug.Log($"AnimationController: instantiated hit_ring_object '{ringInstance.name}' at {spawnPos}. parentTransform={(parentTransform != null ? parentTransform.name : "null")} "); // Parent before activation to avoid transform surprises when Simulation Space = Local if (parentTransform != null) { ringInstance.transform.SetParent(parentTransform, false); // keep world position at spawnPos ringInstance.transform.position = spawnPos; Debug.Log("AnimationController: ringInstance parent set to " + parentTransform.name); } // Activate after parenting ringInstance.SetActive(true); var ringSystems = ringInstance.GetComponentsInChildren(true); Debug.Log("AnimationController: ringSystems count=" + (ringSystems != null ? ringSystems.Length : 0)); if (ringSystems != null && ringSystems.Length > 0) { float ringMaxLife = 0f; foreach (var rps in ringSystems) { var rmain = rps.main; // log important runtime properties for debugging Debug.Log($"ring PS: {rps.gameObject.name} simulationSpace={rmain.simulationSpace} startLifetime={rmain.startLifetime.constant} startColor={rmain.startColor.color}"); // reuse same track color rmain.startColor = trackColor; rps.Play(); var rlifetime = rmain.startLifetime; float rlife = rlifetime.constantMax; if (rlife <= 0f) rlife = rlifetime.constant; if (rlife > ringMaxLife) ringMaxLife = rlife; // Also log emission rate if available try { var emission = rps.emission; var rate = emission.rateOverTime; Debug.Log($"ring PS emission rateOverTime.constant (approx) = {rate.constant}"); } catch { } } Destroy(ringInstance, Mathf.Max(0.5f, ringMaxLife + 0.1f)); // Track for immediate cleanup activeParticles.Add(ringInstance); } else { Debug.LogWarning("hit_ring_object has no ParticleSystem components"); Destroy(ringInstance); } } else { Debug.LogError("Failed to instantiate hit_ring_object"); } } } // 用于根据颜色名称获取对应的 16 进制代码 private string GetHexCodeForColor(string color) { switch (color) { case "red": return redColorHex; case "green": return greenColorHex; case "yellow": return yellowColorHex; case "purple": return purpleColorHex; case "blue": return blueColorHex; default: return "#FFFFFF"; // 默认返回白色 } } private void TriggerAnimatorEffect(string color) { // Fallback to original Animator triggers if particle source missing switch (color) { case "red": if (redAnimator != null) { redAnimator.ResetTrigger("PlayRedDestroy"); redAnimator.SetTrigger("PlayRedDestroy"); } break; case "green": if (greenAnimator != null) { greenAnimator.ResetTrigger("PlayGreenDestroy"); greenAnimator.SetTrigger("PlayGreenDestroy"); } break; case "yellow": if (yellowAnimator != null) { yellowAnimator.ResetTrigger("PlayYellowDestroy"); yellowAnimator.SetTrigger("PlayYellowDestroy"); } break; case "purple": if (purpleAnimator != null) { purpleAnimator.ResetTrigger("PlayPurpleDestroy"); purpleAnimator.SetTrigger("PlayPurpleDestroy"); } break; case "blue": if (blueAnimator != null) { blueAnimator.ResetTrigger("PlayBlueDestroy"); blueAnimator.SetTrigger("PlayBlueDestroy"); } break; } } /// /// 根据传入的颜色频道,定位到对应的 UI Text 并读取其内容 /// private void SpawnJudgePrefabByTrackText(string color) { TextMeshProUGUI targetText = null; Transform spawnPoint = null; // 1. 匹配对应的文本组件和位置 switch (color) { case "red": targetText = red_track_judgementText; spawnPoint = redEffect != null ? redEffect.transform : null; break; case "green": targetText = green_track_judgementText; spawnPoint = greenEffect != null ? greenEffect.transform : null; break; case "yellow": targetText = yellow_track_judgementText; spawnPoint = yellowEffect != null ? yellowEffect.transform : null; break; case "purple": targetText = purple_track_judgementText; spawnPoint = purpleEffect != null ? purpleEffect.transform : null; break; case "blue": targetText = blue_track_judgementText; spawnPoint = blueEffect != null ? blueEffect.transform : null; break; } if (targetText != null && spawnPoint != null) { // 打印调试信息,看程序到底读到了什么文字 Debug.LogError($"轨道 {color} 当前读到的文字是: [{targetText.text}]"); } // 2. 如果找到了文本且文本不为空,则执行喷出逻辑 if (targetText != null && spawnPoint != null && !string.IsNullOrEmpty(targetText.text)) { DoExecutePrefabSpawn(targetText.text, spawnPoint); } } /// /// 最终执行实例化的函数 /// private void DoExecutePrefabSpawn(string judgeResult, Transform spawnTransform) { GameObject prefabToUse = null; // 字符串匹配(需确保与 InputManager 中传入的字符串一致) if (judgeResult.Contains("Perfect")) prefabToUse = perfect_judge_prefab; else if (judgeResult.Contains("Great")) prefabToUse = great_judge_prefab; else if (judgeResult.Contains("Good")) prefabToUse = good_judge_prefab; else if (judgeResult.Contains("Miss")) prefabToUse = miss_judge_prefab; if (prefabToUse != null) { // 在对应特效点生成判定 Prefab GameObject instance = Instantiate(prefabToUse, spawnTransform.position, Quaternion.identity); // 自动销毁,防止堆积 Destroy(instance, 1.0f); } } /// /// Public API to prewarm particle instances. Call during loading/pause time to avoid hitch on first use. /// This will instantiate the configured scene particle sources (hit_particular_object / hit_ring_object) /// and briefly play their ParticleSystems to force any internal setup. /// public void PrewarmParticles(int perTemplate = 2) { // start coroutine to avoid blocking main thread with many instantiations StartCoroutine(PrewarmCoroutine(perTemplate)); } private IEnumerator PrewarmCoroutine(int perTemplate) { // List of templates to warm var templates = new List(); if (hit_particular_object != null) templates.Add(hit_particular_object); if (hit_ring_object != null) templates.Add(hit_ring_object); for (int i = 0; i < templates.Count; i++) { var prefab = templates[i]; for (int j = 0; j < perTemplate; j++) { GameObject inst = null; try { inst = Instantiate(prefab, this.transform); inst.SetActive(true); var systems = inst.GetComponentsInChildren(true); if (systems != null) { foreach (var ps in systems) { try { ps.Play(); } catch { } } } } catch { } // wait a frame to allow any internal initialization to run yield return null; // stop and destroy the instance to free memory - the warmup work is done if (inst != null) { var systems = inst.GetComponentsInChildren(true); if (systems != null) { foreach (var ps in systems) { try { ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); } catch { } } } Destroy(inst); } // small yield to spread work across frames yield return null; } } yield break; } /// /// Public routine variant that callers can yield on to wait until prewarm completes. /// Use this when you need to ensure particle templates are fully instantiated and torn down /// before proceeding (to avoid hiccups at first real use). /// public IEnumerator PrewarmParticlesRoutine(int perTemplate = 2) { yield return StartCoroutine(PrewarmCoroutine(perTemplate)); } }