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:
@@ -85,6 +85,10 @@ namespace ProjectM.Client
|
||||
if (_kindMats != null)
|
||||
for (int i = 0; i < _kindMats.Length; i++)
|
||||
if (_kindMats[i] != null) Object.Destroy(_kindMats[i]);
|
||||
// Procedural clips are not owned by _fxRoot — see FeedbackFx.DestroyClip.
|
||||
FeedbackFx.DestroyClip(ref _strikeBeepClip);
|
||||
if (_kindGrowls != null)
|
||||
for (int i = 0; i < _kindGrowls.Length; i++) FeedbackFx.DestroyClip(ref _kindGrowls[i]);
|
||||
foreach (var kv in _dangerZones)
|
||||
if (kv.Value != null) { var mf = kv.Value.GetComponent<MeshFilter>(); if (mf != null && mf.sharedMesh != null) Object.Destroy(mf.sharedMesh); }
|
||||
}
|
||||
@@ -245,28 +249,52 @@ namespace ProjectM.Client
|
||||
}
|
||||
|
||||
// Filled forward wedge (pizza-slice) from the enemy out to `range`, vertex-alpha ramped by `intensity`.
|
||||
// Track B: the four arrays used to be allocated on EVERY call — once per winding enemy per frame
|
||||
// (~840 B a time). seg is a compile-time constant, so they are hoisted to scratch and filled in place;
|
||||
// UVs/triangles are argument-independent and upload to a given mesh only on its first fill.
|
||||
static void BuildDangerMesh(Mesh mesh, float range, float halfAngle, float intensity)
|
||||
{
|
||||
const int seg = 14;
|
||||
var verts = new Vector3[seg + 2];
|
||||
var cols = new Color[seg + 2];
|
||||
var uvs = new Vector2[seg + 2];
|
||||
var tris = new int[seg * 3];
|
||||
const int seg = WedgeSeg;
|
||||
if (!s_wedgeStaticsBuilt)
|
||||
{
|
||||
s_wedgeUvs[0] = new Vector2(0.5f, 0.5f);
|
||||
for (int i = 0; i <= seg; i++) s_wedgeUvs[i + 1] = new Vector2(0.5f, 0.5f);
|
||||
for (int i = 0; i < seg; i++) { s_wedgeTris[i * 3] = 0; s_wedgeTris[i * 3 + 1] = i + 1; s_wedgeTris[i * 3 + 2] = i + 2; }
|
||||
s_wedgeStaticsBuilt = true;
|
||||
}
|
||||
|
||||
float aCenter = 0.18f + 0.62f * intensity;
|
||||
verts[0] = Vector3.zero; cols[0] = new Color(1f, 1f, 1f, aCenter); uvs[0] = new Vector2(0.5f, 0.5f);
|
||||
s_wedgeVerts[0] = Vector3.zero;
|
||||
s_wedgeCols[0] = new Color(1f, 1f, 1f, aCenter);
|
||||
for (int i = 0; i <= seg; i++)
|
||||
{
|
||||
float a = Mathf.Lerp(-halfAngle, halfAngle, i / (float)seg);
|
||||
verts[i + 1] = new Vector3(Mathf.Sin(a) * range, 0f, Mathf.Cos(a) * range);
|
||||
cols[i + 1] = new Color(1f, 1f, 1f, aCenter * 0.22f);
|
||||
uvs[i + 1] = new Vector2(0.5f, 0.5f);
|
||||
s_wedgeVerts[i + 1] = new Vector3(Mathf.Sin(a) * range, 0f, Mathf.Cos(a) * range);
|
||||
s_wedgeCols[i + 1] = new Color(1f, 1f, 1f, aCenter * 0.22f);
|
||||
}
|
||||
|
||||
if (mesh.vertexCount != s_wedgeVerts.Length)
|
||||
{
|
||||
mesh.Clear();
|
||||
mesh.vertices = s_wedgeVerts; mesh.colors = s_wedgeCols;
|
||||
mesh.uv = s_wedgeUvs; mesh.triangles = s_wedgeTris;
|
||||
}
|
||||
else
|
||||
{
|
||||
mesh.vertices = s_wedgeVerts; mesh.colors = s_wedgeCols;
|
||||
}
|
||||
for (int i = 0; i < seg; i++) { tris[i * 3] = 0; tris[i * 3 + 1] = i + 1; tris[i * 3 + 2] = i + 2; }
|
||||
mesh.Clear();
|
||||
mesh.vertices = verts; mesh.colors = cols; mesh.uv = uvs; mesh.triangles = tris;
|
||||
mesh.RecalculateBounds();
|
||||
}
|
||||
|
||||
// Wedge scratch (see BuildDangerMesh). Static is safe: presentation systems are main-thread only, and
|
||||
// the contents are deterministic geometry, so surviving a domain reload leaves them valid.
|
||||
const int WedgeSeg = 14;
|
||||
static readonly Vector3[] s_wedgeVerts = new Vector3[WedgeSeg + 2];
|
||||
static readonly Color[] s_wedgeCols = new Color[WedgeSeg + 2];
|
||||
static readonly Vector2[] s_wedgeUvs = new Vector2[WedgeSeg + 2];
|
||||
static readonly int[] s_wedgeTris = new int[WedgeSeg * 3];
|
||||
static bool s_wedgeStaticsBuilt;
|
||||
|
||||
// MC-3: a thin forward LANE (filled quad in local +Z) for a Spitter's ranged aim telegraph, vertex-alpha
|
||||
// ramped by `intensity` (brightening toward the shot). Built into the same pooled danger mesh; the GO is
|
||||
// already rotated to the enemy facing, so +Z is "toward the locked target".
|
||||
|
||||
Reference in New Issue
Block a user