Perf: pool one-shot SFX + authored VFX, cut per-frame presentation allocation
Track B. All 21 one-shot cues funnelled through FeedbackFx.PlayClip -> AudioSource.PlayClipAtPoint, which allocates a GameObject + AudioSource per call and schedules a delayed Destroy — ~20-33 times a second in light combat. New OneShotAudioPool is a 32-voice 3D ring behind an UNCHANGED PlayClip signature, so all 20 consuming call sites are untouched. Parity is the whole game here: PlayClipAtPoint sets spatialBlend = 1 explicitly (a fresh AudioSource is 2D) and leaves the rest at stock defaults. Two deliberate divergences, both forced by the voices being long-lived: playOnAwake = false, and dopplerLevel = 0 because a pooled voice TELEPORTS between events and would otherwise pitch-bend. Root is DontDestroyOnLoad (WorldLauncher does LoadScene(Single) while the client world is alive) with a SubsystemRegistration reset, or session two rents destroyed voices. Authored impact VFX are pooled per prefab instead of Instantiate/Destroy per hit: components cached per INSTANCE (refs are instance-scoped), main.stopAction forced to None (a prefab set to Destroy silently drains the pool), instances filled under an inactive root so Awake/Start never run — which is what makes the DestroyImmediate in StripCosmetic safe — ps.Clear before Play, TrailRenderer.Clear after the reposition, and a Rented flag as the at-most-once guard against a double Return aliasing one instance to two callers. Per-frame allocation: the slash-arc and enemy-wedge mesh builders each allocated four arrays on every call (up to twice a frame, and once per winding enemy); HUD and ability-bar labels rebuilt their strings every frame; damage-number fades rewrote TextMesh vertex colours every frame; health bars pushed uGUI writes unconditionally; two systems played back an empty EntityCommandBuffer (a structural-change sync point) every frame. Also closes an AudioClip leak across all seven clip-owning systems: an AudioClip.Create'd clip is a standalone UnityEngine.Object, so destroying a system's FX root left it alive (MusicSystem ~6.8 MB, AmbientAudioSystem ~2 MB per client-world teardown). CombatFeedbackSystem's TryHold call sites go with this commit because they share the file; the camera-side removal lands in the next one. Verified live: PlayClipAtPoint's "One shot audio" GameObject never appears again across 270 frames of combat with kills; the VFX pool fills to its retain cap and stabilises; real cues route through the ring. 304/304 EditMode green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,10 @@ namespace ProjectM.Client
|
||||
public GameObject CanvasGo; public UnityEngine.UI.Image Fill; public UnityEngine.UI.Image Bg;
|
||||
public float ShowTimer; public bool Visible;
|
||||
public float LastHp; public float MaxHp; public float3 Pos;
|
||||
// Track B: last values actually pushed to uGUI, so a bar that is merely showing pushes nothing.
|
||||
// CreateHealthBar seeds these to -1 (a struct would otherwise default them to 0, which is a
|
||||
// legitimate frac and would skip the first render of a fully-drained bar).
|
||||
public float LastFrac; public float LastAlpha;
|
||||
}
|
||||
|
||||
const int HealthBarPoolLimit = 24;
|
||||
@@ -151,7 +155,8 @@ namespace ProjectM.Client
|
||||
var entry = new HealthBarEntry
|
||||
{
|
||||
CanvasGo = go, Fill = fillImg, Bg = bgImg, ShowTimer = 0f, Visible = false,
|
||||
LastHp = prev.LastHp, MaxHp = prev.MaxHp, Pos = prev.Pos
|
||||
LastHp = prev.LastHp, MaxHp = prev.MaxHp, Pos = prev.Pos,
|
||||
LastFrac = -1f, LastAlpha = -1f // force the first visual push (a real frac/alpha is never negative)
|
||||
};
|
||||
_healthBars[entity] = entry;
|
||||
return entry;
|
||||
@@ -209,8 +214,17 @@ namespace ProjectM.Client
|
||||
}
|
||||
float alpha = (!alwaysOn && entry.ShowTimer < 0f)
|
||||
? 1f - math.saturate(-entry.ShowTimer / HealthBarFadeDuration) : 1f;
|
||||
if (entry.Fill != null) { var c = entry.Fill.color; c.a = alpha; entry.Fill.color = c; entry.Fill.rectTransform.anchorMax = new Vector2(frac, 1f); }
|
||||
if (entry.Bg != null) { var c = entry.Bg.color; c.a = 0.82f * alpha; entry.Bg.color = c; }
|
||||
// Track B: these ran unconditionally for every visible bar every frame. Writing
|
||||
// anchorMax triggers OnRectTransformDimensionsChange and an Image.color write dirties the
|
||||
// canvas — so a bar that is merely SHOWING (not changing) used to keep re-laying-out uGUI.
|
||||
// Epsilon-gated: a bar only pushes when its fill or fade actually moved.
|
||||
if (entry.Fill != null && (math.abs(frac - entry.LastFrac) > 0.002f || math.abs(alpha - entry.LastAlpha) > 0.002f))
|
||||
{
|
||||
var c = entry.Fill.color; c.a = alpha; entry.Fill.color = c;
|
||||
entry.Fill.rectTransform.anchorMax = new Vector2(frac, 1f);
|
||||
if (entry.Bg != null) { var bgc = entry.Bg.color; bgc.a = 0.82f * alpha; entry.Bg.color = bgc; }
|
||||
entry.LastFrac = frac; entry.LastAlpha = alpha;
|
||||
}
|
||||
}
|
||||
else if (entry.Visible) { entry.CanvasGo.SetActive(false); entry.Visible = false; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user