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

537 lines
21 KiB
C#

using UnityEngine;
using TMPro;
using System.Collections; // Documentation text normalized.
using System.Collections.Generic; // For List
public class AnimationController : MonoBehaviour
{
[HideInInspector] public bool isGlobalController = false;
public static AnimationController Global;
public GameObject redEffect; // Documentation text normalized.
public GameObject greenEffect; // Documentation text normalized.
public GameObject yellowEffect; // Documentation text normalized.
public GameObject purpleEffect; // Documentation text normalized.
public GameObject blueEffect; // Documentation text normalized.
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("Inspector")]
public string redColorHex = "#FF9390";
public string greenColorHex = "#38F6CA";
public string yellowColorHex = "#FFE1A2";
public string purpleColorHex = "#F083E4";
public string blueColorHex = "#7AF9FF";
[Header("Inspector")]
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("Inspector")]
public GameObject perfect_judge_prefab;
public GameObject great_judge_prefab;
public GameObject good_judge_prefab;
public GameObject miss_judge_prefab;
[Header("Inspector")]
// Documentation text normalized.
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;
}
// Documentation text normalized.
Color trackColor;
string hexCode = GetHexCodeForColor(color);
// Documentation text normalized.
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");
}
}
}
// Documentation text normalized.
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"; // Documentation text normalized.
}
}
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>
/// Documentation text normalized.
/// </summary>
private void SpawnJudgePrefabByTrackText(string color)
{
TextMeshProUGUI targetText = null;
Transform spawnPoint = null;
// Documentation text normalized.
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)
{
// Documentation text normalized.
if (JudgeManager.IsDebugEnabled) Debug.Log($"判定 {color} 当前读取到的文字为 [{targetText.text}]");
}
// Documentation text normalized.
if (targetText != null && spawnPoint != null && !string.IsNullOrEmpty(targetText.text))
{
DoExecutePrefabSpawn(targetText.text, spawnPoint);
}
}
/// <summary>
/// Documentation text normalized.
/// </summary>
private void DoExecutePrefabSpawn(string judgeResult, Transform spawnTransform)
{
GameObject prefabToUse = null;
// Documentation text normalized.
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)
{
// Documentation text normalized.
GameObject instance = Instantiate(prefabToUse, spawnTransform.position, Quaternion.identity);
// Documentation text normalized.
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));
}
}