ui基本完毕,修了一大把的bug

This commit is contained in:
FloatGaming
2026-07-13 02:28:39 +08:00
parent 1e20d73e90
commit fd22501f71
958 changed files with 378289 additions and 41038 deletions
@@ -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;
}
}