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:
@@ -49,7 +49,10 @@ namespace ProjectM.Client
|
||||
|
||||
// Pass 1: discover enemies, ensure each is tracked + its render children carry the override component.
|
||||
_seen.Clear();
|
||||
var ecb = new EntityCommandBuffer(Unity.Collections.Allocator.Temp);
|
||||
// Track B: created lazily — it only ever records on a newly-seen enemy, but it used to be built
|
||||
// and played back (a sync point) every single frame.
|
||||
EntityCommandBuffer ecb = default;
|
||||
bool hasEcb = false;
|
||||
foreach (var (health, entity) in
|
||||
SystemAPI.Query<RefRO<Health>>().WithAny<EnemyTag, PlayerTag>().WithAll<LinkedEntityGroup>().WithEntityAccess())
|
||||
{
|
||||
@@ -64,13 +67,17 @@ namespace ProjectM.Client
|
||||
if (!EntityManager.Exists(c) || !EntityManager.HasComponent<MaterialMeshInfo>(c)) continue;
|
||||
entry.RenderKids.Add(c);
|
||||
if (!EntityManager.HasComponent<URPMaterialPropertyBaseColor>(c))
|
||||
{
|
||||
if (!hasEcb) { ecb = new EntityCommandBuffer(Unity.Collections.Allocator.Temp); hasEcb = true; }
|
||||
ecb.AddComponent(c, new URPMaterialPropertyBaseColor { Value = White });
|
||||
}
|
||||
}
|
||||
// Render children can lag ghost instantiation a frame; only finalize once we actually found them (else retry next frame).
|
||||
if (entry.RenderKids.Count > 0) _tracked[entity] = entry;
|
||||
}
|
||||
ecb.Playback(EntityManager);
|
||||
ecb.Dispose();
|
||||
// Only pay the structural-change sync point on the frames that actually recorded something
|
||||
// (i.e. a newly-seen enemy) instead of every frame.
|
||||
if (hasEcb) { ecb.Playback(EntityManager); ecb.Dispose(); }
|
||||
|
||||
// Pass 2: edge-detect Health, drive + decay the flash, write _BaseColor to the render children.
|
||||
var bc = FeelConfig.BodyFlashColor;
|
||||
|
||||
Reference in New Issue
Block a user