Files
bansonic_beta_main/Assets/scripts/gamePlay_gameplay/AnimationController.cs
T

537 lines
22 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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));
}
}