537 lines
22 KiB
C#
537 lines
22 KiB
C#
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)")]
|
||
public string redColorHex = "#FF9390";
|
||
public string greenColorHex = "#38F6CA";
|
||
public string yellowColorHex = "#FFE1A2";
|
||
public string purpleColorHex = "#F083E4";
|
||
public string blueColorHex = "#7AF9FF";
|
||
[Header("Hold ��������")]
|
||
public float holdParticleInterval = 0.2f;
|
||
|
||
private Coroutine holdParticleCoroutine;
|
||
private bool holdActive = false;
|
||
private string currentHoldColor;
|
||
private bool isHolding = false;
|
||
private WaitForSeconds cachedHoldWait;
|
||
private float cachedHoldWaitSeconds = -1f;
|
||
private static bool loggedMissingHitParticleSource;
|
||
|
||
// Track active particle instances for immediate cleanup
|
||
private List<GameObject> activeParticles = new List<GameObject>();
|
||
|
||
[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)
|
||
{
|
||
float waitSeconds = Mathf.Max(0.01f, holdParticleInterval);
|
||
if (cachedHoldWait == null || !Mathf.Approximately(cachedHoldWaitSeconds, waitSeconds))
|
||
{
|
||
cachedHoldWaitSeconds = waitSeconds;
|
||
cachedHoldWait = new WaitForSeconds(waitSeconds);
|
||
}
|
||
yield return cachedHoldWait;
|
||
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);
|
||
if (JudgeManager.IsDebugEnabled) Debug.Log("[AnimationController] Global AnimationController registered");
|
||
}
|
||
|
||
// Animator cache
|
||
redAnimator = redEffect != null ? redEffect.GetComponent<Animator>() : null;
|
||
greenAnimator = greenEffect != null ? greenEffect.GetComponent<Animator>() : null;
|
||
yellowAnimator = yellowEffect != null ? yellowEffect.GetComponent<Animator>() : null;
|
||
purpleAnimator = purpleEffect != null ? purpleEffect.GetComponent<Animator>() : null;
|
||
blueAnimator = blueEffect != null ? blueEffect.GetComponent<Animator>() : 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;
|
||
if (JudgeManager.IsDebugEnabled) 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;
|
||
if (JudgeManager.IsDebugEnabled) Debug.Log("AnimationController: assigned hit_particular_object via GameObject.Find by name.");
|
||
}
|
||
}
|
||
|
||
if (hit_particular_object == null)
|
||
{
|
||
if (JudgeManager.IsDebugEnabled)
|
||
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)
|
||
{
|
||
if (!loggedMissingHitParticleSource)
|
||
{
|
||
loggedMissingHitParticleSource = true;
|
||
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;
|
||
if (JudgeManager.IsDebugEnabled) 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<ParticleSystem>(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)
|
||
{
|
||
if (JudgeManager.IsDebugEnabled) 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;
|
||
if (JudgeManager.IsDebugEnabled) Debug.Log("AnimationController: ringInstance parent set to " + parentTransform.name);
|
||
}
|
||
|
||
// Activate after parenting
|
||
ringInstance.SetActive(true);
|
||
|
||
var ringSystems = ringInstance.GetComponentsInChildren<ParticleSystem>(true);
|
||
if (JudgeManager.IsDebugEnabled) 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
|
||
if (JudgeManager.IsDebugEnabled) 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;
|
||
if (JudgeManager.IsDebugEnabled) 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
|
||
{
|
||
if (JudgeManager.IsDebugEnabled) 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;
|
||
}
|
||
}
|
||
/// <summary>
|
||
/// ���ݴ������ɫƵ������λ����Ӧ�� UI Text ����ȡ������
|
||
/// </summary>
|
||
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)
|
||
{
|
||
// 打印调试信息,确认判定到底读取到的是什么文字
|
||
if (JudgeManager.IsDebugEnabled) Debug.Log($"判定 {color} 当前读取到的文字为: [{targetText.text}]");
|
||
}
|
||
|
||
// 2. ����ҵ����ı����ı���Ϊ�գ���ִ�������
|
||
if (targetText != null && spawnPoint != null && !string.IsNullOrEmpty(targetText.text))
|
||
{
|
||
DoExecutePrefabSpawn(targetText.text, spawnPoint);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// ����ִ��ʵ�����ĺ���
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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<GameObject>();
|
||
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<ParticleSystem>(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<ParticleSystem>(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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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).
|
||
/// </summary>
|
||
public IEnumerator PrewarmParticlesRoutine(int perTemplate = 2)
|
||
{
|
||
yield return StartCoroutine(PrewarmCoroutine(perTemplate));
|
||
}
|
||
}
|