688 lines
27 KiB
C#
688 lines
27 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 static bool loggedMissingHitParticleSource;
|
|
|
|
// Track active particle instances for immediate cleanup
|
|
private List<GameObject> activeParticles = new List<GameObject>();
|
|
|
|
// Per-hit FX pooling. PlayDestroyAnimation used to Instantiate + Destroy three
|
|
// objects on every judge (hit particle, hit ring, judge text prefab). Under dense
|
|
// note bursts that is the dominant GC + instantiation spike. We now reuse instances
|
|
// from a pool keyed by template, and cache each instance's ParticleSystem[] +
|
|
// computed lifetime so the hot path never calls GetComponentsInChildren per hit.
|
|
// Business behavior is unchanged: same templates, same tint, same lifetime, same
|
|
// parenting; instances are recycled instead of created and destroyed.
|
|
private sealed class FxCacheEntry
|
|
{
|
|
public ParticleSystem[] Systems;
|
|
public float Lifetime;
|
|
}
|
|
|
|
private readonly Dictionary<GameObject, Queue<GameObject>> fxPools =
|
|
new Dictionary<GameObject, Queue<GameObject>>();
|
|
private readonly Dictionary<GameObject, FxCacheEntry> fxCache =
|
|
new Dictionary<GameObject, FxCacheEntry>();
|
|
private readonly Dictionary<GameObject, GameObject> fxInstanceTemplate =
|
|
new Dictionary<GameObject, GameObject>();
|
|
// Instances currently rented out. Return is guarded by this set so the timed
|
|
// RecycleFxRoutine and an explicit StopHoldParticles cleanup can never return the
|
|
// same instance twice (which would double-enqueue it and hand it out concurrently).
|
|
private readonly HashSet<GameObject> rentedFx = new HashSet<GameObject>();
|
|
// Per-rent token. Each rent bumps a counter and records it for the instance. The
|
|
// timed RecycleFxRoutine captures its token and only returns the instance if the
|
|
// token still matches, so a stale timer from a previous rent cannot recycle an
|
|
// instance that was already returned early (StopHoldParticles) and re-rented.
|
|
private int fxRentCounter;
|
|
private readonly Dictionary<GameObject, int> fxRentId = new Dictionary<GameObject, int>();
|
|
private Transform fxPoolRoot;
|
|
|
|
private void EnsureFxPoolRoot()
|
|
{
|
|
if (fxPoolRoot == null)
|
|
{
|
|
var root = new GameObject("AnimationControllerFxPool");
|
|
root.transform.SetParent(transform, false);
|
|
root.SetActive(false);
|
|
fxPoolRoot = root.transform;
|
|
}
|
|
}
|
|
|
|
// Creates one instance of the template, caches its particle systems + lifetime,
|
|
// warms the play path once, and leaves it stopped/cleared ready for reuse.
|
|
private GameObject CreateFxInstance(GameObject template)
|
|
{
|
|
if (template == null)
|
|
return null;
|
|
|
|
GameObject instance = Instantiate(template);
|
|
var systems = instance.GetComponentsInChildren<ParticleSystem>(true);
|
|
if (systems == null)
|
|
systems = new ParticleSystem[0];
|
|
|
|
var entry = new FxCacheEntry
|
|
{
|
|
Systems = systems,
|
|
Lifetime = ComputeFxLifetime(systems)
|
|
};
|
|
|
|
foreach (var ps in systems)
|
|
{
|
|
if (ps == null) continue;
|
|
try { ps.Play(true); } catch { }
|
|
try { ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); } catch { }
|
|
try { ps.Clear(true); } catch { }
|
|
}
|
|
|
|
fxCache[instance] = entry;
|
|
fxInstanceTemplate[instance] = template;
|
|
return instance;
|
|
}
|
|
|
|
private GameObject RentFxInstance(GameObject template, out FxCacheEntry entry, out int rentToken)
|
|
{
|
|
entry = null;
|
|
rentToken = -1;
|
|
if (template == null)
|
|
return null;
|
|
|
|
if (fxPools.TryGetValue(template, out var pool))
|
|
{
|
|
while (pool.Count > 0)
|
|
{
|
|
GameObject pooled = pool.Dequeue();
|
|
if (pooled != null && fxCache.TryGetValue(pooled, out entry))
|
|
{
|
|
rentToken = MarkRented(pooled);
|
|
return pooled;
|
|
}
|
|
}
|
|
}
|
|
|
|
GameObject created = CreateFxInstance(template);
|
|
if (created != null)
|
|
{
|
|
fxCache.TryGetValue(created, out entry);
|
|
rentToken = MarkRented(created);
|
|
}
|
|
return created;
|
|
}
|
|
|
|
private int CurrentRentToken(GameObject instance)
|
|
{
|
|
return fxRentId.TryGetValue(instance, out int token) ? token : -1;
|
|
}
|
|
|
|
private int MarkRented(GameObject instance)
|
|
{
|
|
rentedFx.Add(instance);
|
|
int token = ++fxRentCounter;
|
|
fxRentId[instance] = token;
|
|
return token;
|
|
}
|
|
|
|
private void ReturnFxInstance(GameObject instance)
|
|
{
|
|
if (instance == null)
|
|
return;
|
|
|
|
// Idempotent: if this instance was already returned, ignore the second call.
|
|
if (!rentedFx.Remove(instance))
|
|
return;
|
|
|
|
fxRentId.Remove(instance);
|
|
activeParticles.Remove(instance);
|
|
|
|
if (fxCache.TryGetValue(instance, out var entry) && entry.Systems != null)
|
|
{
|
|
foreach (var ps in entry.Systems)
|
|
{
|
|
if (ps == null) continue;
|
|
try { ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); } catch { }
|
|
try { ps.Clear(true); } catch { }
|
|
}
|
|
}
|
|
|
|
instance.SetActive(false);
|
|
EnsureFxPoolRoot();
|
|
instance.transform.SetParent(fxPoolRoot, false);
|
|
|
|
if (!fxInstanceTemplate.TryGetValue(instance, out var template) || template == null)
|
|
{
|
|
// Unknown origin (should not happen for pooled instances): drop it.
|
|
Destroy(instance);
|
|
return;
|
|
}
|
|
|
|
if (!fxPools.TryGetValue(template, out var pool))
|
|
{
|
|
pool = new Queue<GameObject>();
|
|
fxPools[template] = pool;
|
|
}
|
|
pool.Enqueue(instance);
|
|
}
|
|
|
|
private IEnumerator RecycleFxRoutine(GameObject instance, int rentToken, float delay)
|
|
{
|
|
yield return GameplayClock.WaitForSeconds(delay);
|
|
if (instance == null)
|
|
yield break;
|
|
|
|
// Only recycle if this instance is still on the same rent. If it was
|
|
// returned early (StopHoldParticles) and re-rented in the meantime, its
|
|
// token changed and this stale timer must not touch it.
|
|
if (CurrentRentToken(instance) != rentToken)
|
|
yield break;
|
|
|
|
ReturnFxInstance(instance);
|
|
}
|
|
|
|
private static float ComputeFxLifetime(ParticleSystem[] systems)
|
|
{
|
|
float maxLifetime = 0f;
|
|
if (systems == null)
|
|
return maxLifetime;
|
|
|
|
foreach (var ps in systems)
|
|
{
|
|
if (ps == null) continue;
|
|
var main = ps.main;
|
|
var lifetime = main.startLifetime;
|
|
float life = lifetime.constantMax;
|
|
if (life <= 0f) life = lifetime.constant;
|
|
if (life > maxLifetime) maxLifetime = life;
|
|
}
|
|
return maxLifetime;
|
|
}
|
|
|
|
[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 return all active pooled instances (was Destroy). Returning is
|
|
// idempotent and token-guarded, so a later timed RecycleFxRoutine for the same
|
|
// instance becomes a no-op. Iterate a snapshot because ReturnFxInstance removes
|
|
// the instance from activeParticles (mutating the list mid-enumeration would throw).
|
|
var snapshot = activeParticles.ToArray();
|
|
foreach (var p in snapshot)
|
|
{
|
|
if (p != null)
|
|
{
|
|
ReturnFxInstance(p);
|
|
}
|
|
}
|
|
activeParticles.Clear();
|
|
}
|
|
|
|
private IEnumerator HoldParticleRoutine(string color)
|
|
{
|
|
while (holdActive)
|
|
{
|
|
float waitSeconds = Mathf.Max(0.01f, holdParticleInterval);
|
|
yield return GameplayClock.WaitForSeconds(waitSeconds);
|
|
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 = SceneObjectLookupCache.Find("HitParticleSource") ?? SceneObjectLookupCache.Find("hit_particular_object") ?? SceneObjectLookupCache.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);
|
|
// Rent a pooled copy of the scene particle source (was Instantiate + Destroy per hit).
|
|
GameObject particleInstance = RentFxInstance(source, out var particleEntry, out int particleToken);
|
|
if (particleInstance == null)
|
|
{
|
|
Debug.LogError("Failed to instantiate hit_particular_object");
|
|
TriggerAnimatorEffect(color);
|
|
return;
|
|
}
|
|
|
|
// Place at spawn position, then optionally parent under the effect object so it
|
|
// moves with UI/slot (worldPositionStays=true keeps world pos at spawnPos).
|
|
particleInstance.transform.SetParent(parentTransform, false);
|
|
particleInstance.transform.position = spawnPos;
|
|
particleInstance.transform.rotation = Quaternion.identity;
|
|
particleInstance.SetActive(true);
|
|
|
|
var systems = particleEntry != null ? particleEntry.Systems : null;
|
|
if (systems != null && systems.Length > 0)
|
|
{
|
|
foreach (var ps in systems)
|
|
{
|
|
if (ps == null) continue;
|
|
var main = ps.main;
|
|
main.startColor = trackColor;
|
|
ps.Play();
|
|
}
|
|
|
|
// Recycle after cached lifetime + small buffer (token-guarded).
|
|
StartCoroutine(RecycleFxRoutine(particleInstance, particleToken, Mathf.Max(0.5f, particleEntry.Lifetime + 0.1f)));
|
|
|
|
// Track for immediate cleanup on hold stop.
|
|
activeParticles.Add(particleInstance);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("No ParticleSystem components found on hit_particular_object instance");
|
|
ReturnFxInstance(particleInstance);
|
|
TriggerAnimatorEffect(color);
|
|
}
|
|
|
|
// Ring effect (overlay) if assigned.
|
|
if (hit_ring_object != null)
|
|
{
|
|
GameObject ringInstance = RentFxInstance(hit_ring_object, out var ringEntry, out int ringToken);
|
|
if (ringInstance != null)
|
|
{
|
|
// Parent before activation to avoid transform surprises when Simulation Space = Local.
|
|
ringInstance.transform.SetParent(parentTransform, false);
|
|
ringInstance.transform.position = spawnPos;
|
|
ringInstance.transform.rotation = Quaternion.identity;
|
|
ringInstance.SetActive(true);
|
|
|
|
var ringSystems = ringEntry != null ? ringEntry.Systems : null;
|
|
if (ringSystems != null && ringSystems.Length > 0)
|
|
{
|
|
foreach (var rps in ringSystems)
|
|
{
|
|
if (rps == null) continue;
|
|
var rmain = rps.main;
|
|
rmain.startColor = trackColor;
|
|
rps.Play();
|
|
}
|
|
|
|
StartCoroutine(RecycleFxRoutine(ringInstance, ringToken, Mathf.Max(0.5f, ringEntry.Lifetime + 0.1f)));
|
|
activeParticles.Add(ringInstance);
|
|
}
|
|
else
|
|
{
|
|
if (JudgeManager.IsDebugEnabled) Debug.LogWarning("hit_ring_object has no ParticleSystem components");
|
|
ReturnFxInstance(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)
|
|
{
|
|
// Rent a pooled copy of the judge-text prefab (was Instantiate + Destroy per hit).
|
|
GameObject instance = RentFxInstance(prefabToUse, out _, out int judgeToken);
|
|
if (instance == null)
|
|
return;
|
|
|
|
instance.transform.SetParent(null, false);
|
|
instance.transform.position = spawnTransform.position;
|
|
instance.transform.rotation = Quaternion.identity;
|
|
instance.SetActive(true);
|
|
|
|
// Same 1.0s visible lifetime as before, token-guarded recycle instead of Destroy.
|
|
StartCoroutine(RecycleFxRoutine(instance, judgeToken, 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)
|
|
{
|
|
// Templates that flow through PlayDestroyAnimation on every judge. Seeding the
|
|
// FX pool here (instead of instantiate-then-destroy) means the first real hits
|
|
// rent a ready instance rather than paying Instantiate + first-Play on the hot
|
|
// path. CreateFxInstance already warms each instance's play path once.
|
|
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);
|
|
if (perfect_judge_prefab != null) templates.Add(perfect_judge_prefab);
|
|
if (great_judge_prefab != null) templates.Add(great_judge_prefab);
|
|
if (good_judge_prefab != null) templates.Add(good_judge_prefab);
|
|
if (miss_judge_prefab != null) templates.Add(miss_judge_prefab);
|
|
|
|
EnsureFxPoolRoot();
|
|
|
|
for (int i = 0; i < templates.Count; i++)
|
|
{
|
|
var template = templates[i];
|
|
for (int j = 0; j < perTemplate; j++)
|
|
{
|
|
GameObject inst = null;
|
|
try
|
|
{
|
|
inst = CreateFxInstance(template);
|
|
if (inst != null)
|
|
{
|
|
inst.SetActive(false);
|
|
inst.transform.SetParent(fxPoolRoot, false);
|
|
|
|
if (!fxPools.TryGetValue(template, out var pool))
|
|
{
|
|
pool = new Queue<GameObject>();
|
|
fxPools[template] = pool;
|
|
}
|
|
pool.Enqueue(inst);
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
// spread work across frames to avoid a long load frame
|
|
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));
|
|
}
|
|
}
|