ui基本完毕,修了一大把的bug
This commit is contained in:
@@ -45,6 +45,195 @@ public class AnimationController : MonoBehaviour
|
||||
// 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 new 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;
|
||||
@@ -107,12 +296,16 @@ public class AnimationController : MonoBehaviour
|
||||
holdParticleCoroutine = null;
|
||||
}
|
||||
|
||||
// Immediately destroy all active particle instances
|
||||
foreach (var p in activeParticles)
|
||||
// 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)
|
||||
{
|
||||
Destroy(p);
|
||||
ReturnFxInstance(p);
|
||||
}
|
||||
}
|
||||
activeParticles.Clear();
|
||||
@@ -235,8 +428,8 @@ public class AnimationController : MonoBehaviour
|
||||
}
|
||||
|
||||
SpawnJudgePrefabByTrackText(color);
|
||||
// Instantiate a copy of the scene object
|
||||
GameObject particleInstance = Instantiate(source, spawnPos, Quaternion.identity);
|
||||
// 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");
|
||||
@@ -244,104 +437,67 @@ public class AnimationController : MonoBehaviour
|
||||
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);
|
||||
|
||||
// Find all ParticleSystems on the instance (root + children) and set their startColor
|
||||
var systems = particleInstance.GetComponentsInChildren<ParticleSystem>(true);
|
||||
var systems = particleEntry != null ? particleEntry.Systems : null;
|
||||
if (systems != null && systems.Length > 0)
|
||||
{
|
||||
float maxLifetime = 0f;
|
||||
foreach (var ps in systems)
|
||||
{
|
||||
if (ps == null) continue;
|
||||
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);
|
||||
}
|
||||
// Recycle after cached lifetime + small buffer (token-guarded).
|
||||
StartCoroutine(RecycleFxRoutine(particleInstance, particleToken, Mathf.Max(0.5f, particleEntry.Lifetime + 0.1f)));
|
||||
|
||||
// Destroy after max lifetime + small buffer
|
||||
Destroy(particleInstance, Mathf.Max(0.5f, maxLifetime + 0.1f));
|
||||
|
||||
// Track for immediate cleanup
|
||||
// Track for immediate cleanup on hold stop.
|
||||
activeParticles.Add(particleInstance);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("No ParticleSystem components found on hit_particular_object instance");
|
||||
Destroy(particleInstance);
|
||||
ReturnFxInstance(particleInstance);
|
||||
TriggerAnimatorEffect(color);
|
||||
}
|
||||
|
||||
// NEW: instantiate and play ring effect (overlay) if assigned
|
||||
// Ring effect (overlay) if assigned.
|
||||
if (hit_ring_object != null)
|
||||
{
|
||||
GameObject ringInstance = Instantiate(hit_ring_object, spawnPos, Quaternion.identity);
|
||||
GameObject ringInstance = RentFxInstance(hit_ring_object, out var ringEntry, out int ringToken);
|
||||
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
|
||||
// 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 = ringInstance.GetComponentsInChildren<ParticleSystem>(true);
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log("AnimationController: ringSystems count=" + (ringSystems != null ? ringSystems.Length : 0));
|
||||
var ringSystems = ringEntry != null ? ringEntry.Systems : null;
|
||||
if (ringSystems != null && ringSystems.Length > 0)
|
||||
{
|
||||
float ringMaxLife = 0f;
|
||||
foreach (var rps in ringSystems)
|
||||
{
|
||||
if (rps == null) continue;
|
||||
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
|
||||
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");
|
||||
Destroy(ringInstance);
|
||||
ReturnFxInstance(ringInstance);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -452,11 +608,18 @@ public class AnimationController : MonoBehaviour
|
||||
|
||||
if (prefabToUse != null)
|
||||
{
|
||||
// Documentation text normalized.
|
||||
GameObject instance = Instantiate(prefabToUse, spawnTransform.position, Quaternion.identity);
|
||||
// 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;
|
||||
|
||||
// Documentation text normalized.
|
||||
Destroy(instance, 1.0f);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,50 +636,45 @@ public class AnimationController : MonoBehaviour
|
||||
|
||||
private IEnumerator PrewarmCoroutine(int perTemplate)
|
||||
{
|
||||
// List of templates to warm
|
||||
// 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 prefab = templates[i];
|
||||
var template = 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)
|
||||
inst = CreateFxInstance(template);
|
||||
if (inst != null)
|
||||
{
|
||||
foreach (var ps in systems)
|
||||
inst.SetActive(false);
|
||||
inst.transform.SetParent(fxPoolRoot, false);
|
||||
|
||||
if (!fxPools.TryGetValue(template, out var pool))
|
||||
{
|
||||
try { ps.Play(); } catch { }
|
||||
pool = new Queue<GameObject>();
|
||||
fxPools[template] = pool;
|
||||
}
|
||||
pool.Enqueue(inst);
|
||||
}
|
||||
}
|
||||
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
|
||||
// spread work across frames to avoid a long load frame
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user