This commit is contained in:
FloatGaming
2026-07-26 03:44:27 +08:00
parent 86351dbd2a
commit add675e45d
223 changed files with 1960 additions and 10735 deletions
@@ -67,6 +67,13 @@ public class AnimationController : MonoBehaviour
public bool[] ChildInitialActive;
}
private struct PendingFxReturn
{
public GameObject Instance;
public int RentToken;
public float ReturnTime;
}
private readonly Dictionary<GameObject, Queue<GameObject>> fxPools =
new Dictionary<GameObject, Queue<GameObject>>();
private readonly Dictionary<GameObject, FxCacheEntry> fxCache =
@@ -83,6 +90,7 @@ public class AnimationController : MonoBehaviour
// instance that was already returned early (StopHoldParticles) and re-rented.
private int fxRentCounter;
private readonly Dictionary<GameObject, int> fxRentId = new Dictionary<GameObject, int>();
private readonly List<PendingFxReturn> pendingFxReturns = new List<PendingFxReturn>(64);
private Transform fxPoolRoot;
private string hitRingPathUnderParticleSource;
@@ -349,6 +357,41 @@ public class AnimationController : MonoBehaviour
ReturnFxInstance(instance);
}
private void ScheduleFxReturn(GameObject instance, int rentToken, float delay)
{
if (instance == null) return;
pendingFxReturns.Add(new PendingFxReturn
{
Instance = instance,
RentToken = rentToken,
ReturnTime = Time.unscaledTime + Mathf.Max(0f, delay)
});
}
private void Update()
{
if (pendingFxReturns.Count == 0) return;
float now = Time.unscaledTime;
for (int i = pendingFxReturns.Count - 1; i >= 0; i--)
{
PendingFxReturn pending = pendingFxReturns[i];
if (pending.Instance == null)
{
pendingFxReturns.RemoveAt(i);
continue;
}
if (now < pending.ReturnTime) continue;
pendingFxReturns.RemoveAt(i);
if (CurrentRentToken(pending.Instance) != pending.RentToken) continue;
ReturnFxInstance(pending.Instance);
}
}
private void ReturnAllRentedFx()
{
var snapshot = new List<GameObject>(rentedFx);
@@ -364,6 +407,7 @@ public class AnimationController : MonoBehaviour
{
holdActive = false;
holdParticleCoroutine = null;
pendingFxReturns.Clear();
ReturnAllRentedFx();
}
@@ -409,6 +453,10 @@ public class AnimationController : MonoBehaviour
main.startColor = tint;
try { ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); } catch { }
try { ps.Clear(true); } catch { }
// Force pooled FX back to time 0 before replaying, matching a fresh Instantiate
// without paying allocation/destruction cost on every hit.
try { ps.Simulate(0f, false, true); } catch { }
try { ps.Clear(true); } catch { }
}
}
@@ -736,85 +784,48 @@ public class AnimationController : MonoBehaviour
// 判定字(PERFECT/GREAT 等)统一且仅由 Animation_GenerateJudgementSituationPrefab 负责生成,
// 本方法只负责打击粒子 + ring,不碰判定字。
// 【彻底去池化】前几轮所有"粒子变多/变少、ring 不激活、复用状态错乱"都源于对象池复用时
// 子节点激活态/粒子播放态无法可靠复位。改回最朴素可靠的 Instantiate + 定时 Destroy
// 每次判定 new 一个全新干净实例(与 prefab 设计态完全一致),播放完销毁。无复用=无脏状态。
// 这正是加池化之前本就正常工作的行为。
// 粒子本体:以场景源为模板实例化一份全新拷贝。
SpawnOneShotFx(source, spawnPos, parentTransform, trackColor, true);
// 粒子本体:走对象池,避免高频判定时 Instantiate/Destroy 造成移动端掉帧。
SpawnOneShotFx(source, spawnPos, parentTransform, trackColor, true, trackAsHoldParticle);
// Ring(叠加环)tap / hold 头尾 / hold 持续 tick 都按同一频率生成一次性实例。
// 实例会按自身生命周期 Destroy,不会修改或启动场景里的模板对象。
// 实例会按自身生命周期回池,不会修改或启动场景里的模板对象。
if (hit_ring_object != null)
{
SpawnOneShotFx(hit_ring_object, spawnPos, parentTransform, trackColor, true);
SpawnOneShotFx(hit_ring_object, spawnPos, parentTransform, trackColor, true, trackAsHoldParticle);
}
}
// 实例化一个一次性特效(粒子/ring),播放后按自身生命周期 Destroy。无对象池、无复用
// applyTint=true 时把粒子 startColor 染成轨道色(粒子本体)ring 保留自身配色(false)
private void SpawnOneShotFx(GameObject template, Vector3 spawnPos, Transform parentTransform, Color tint, bool applyTint)
// 播放一个一次性特效(粒子/ring),播放后按自身生命周期回池
// applyTint=true 时把粒子 startColor 染成轨道色;当前调用保持旧行为,粒子和 ring 都染色
private void SpawnOneShotFx(GameObject template, Vector3 spawnPos, Transform parentTransform, Color tint, bool applyTint, bool trackAsHoldParticle)
{
if (template == null) return;
GameObject instance = Instantiate(template);
GameObject instance = RentFxInstance(template, out FxCacheEntry entry, out int rentToken);
if (instance == null) return;
// 去除粒子本体克隆里"自带的嵌套 ring"hit_ring_object 本身是 hit_particular_object 的子物体,
// Instantiate(粒子本体) 会连它一起克隆(原色,未染色) → 与我另外单独实例化的染色 ring 叠成双层。
// 先 SetActive(false) 立即让它停播,并从下面的系统列表中排除,再销毁;只保留单独实例化的染色 ring。
Transform nestedRing = null;
if (template == hit_particular_object && !string.IsNullOrEmpty(hitRingPathUnderParticleSource))
{
nestedRing = instance.transform.Find(hitRingPathUnderParticleSource);
if (nestedRing != null)
{
nestedRing.gameObject.SetActive(false);
}
}
// 定位:跟随分轨效果对象(worldPositionStays=false 保留模板设计的本地朝向,如 ring 的 -90°X)。
instance.transform.SetParent(parentTransform, false);
instance.transform.position = spawnPos;
instance.transform.localRotation = template.transform.localRotation;
instance.transform.localRotation = entry != null ? entry.LocalRotation : template.transform.localRotation;
instance.SetActive(true);
// 排除嵌套 ring 的粒子系统,避免把那层原色 ring 播出来。
var systems = FilterParticleSystems(instance.GetComponentsInChildren<ParticleSystem>(true), nestedRing);
if (nestedRing != null) Destroy(nestedRing.gameObject);
float lifetime = 0.5f;
if (systems != null && systems.Length > 0)
if (applyTint)
{
lifetime = ComputeFxLifetime(systems, null);
// 关键修复(开局粒子多、之后变少的衰减):模板 hit_particular_object 是活动场景对象,
// Instantiate 会连同模板粒子系统"当前缓冲里的活粒子"一起克隆——歌曲早期模板残留/预热粒子多
// → 克隆多;随模板粒子消亡 → 克隆少。故必须把克隆继承的模拟状态彻底重置为"时间0、零粒子"
// 再从头播放,保证每次一致、与模板当时状态无关。
foreach (var ps in systems)
{
if (ps == null) continue;
var main = ps.main;
if (applyTint) main.startColor = tint;
try { ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); } catch { }
try { ps.Clear(true); } catch { }
// Simulate(0, withChildren:false, restart:true) 强制该系统回到时间 0 的干净态,
// 抹掉 Instantiate 继承的任何已模拟粒子(单靠 Clear 有时不彻底)。
try { ps.Simulate(0f, false, true); } catch { }
try { ps.Clear(true); } catch { }
}
// 全部重置后,只对根系统 Play(true) 级联播放,从零开始正常发射。
var rootPs = instance.GetComponent<ParticleSystem>();
if (rootPs == null) rootPs = systems[0];
try { rootPs.Play(true); } catch { }
RestartFxPlayback(entry, tint);
}
else
{
var animators = instance.GetComponentsInChildren<Animator>(true);
lifetime = ComputeFxLifetime(null, animators);
RestartFxPlayback(entry, Color.white);
}
Destroy(instance, Mathf.Max(0.5f, lifetime + 0.1f));
if (trackAsHoldParticle)
{
activeHoldParticles.Add(instance);
}
float lifetime = entry != null ? entry.Lifetime : 0.5f;
ScheduleFxReturn(instance, rentToken, Mathf.Max(0.5f, lifetime + 0.1f));
}
// Documentation text normalized.