The round trip up to 99 balls take and the counters at each stage, which are what the test actually asserts. Drawn from the pinned source and the September 8 PlayMode receipt.
The round trip up to 99 balls take and the counters at each stage, which are what the test actually asserts. Drawn from the pinned source and the September 8 PlayMode receipt.

A design that reuses balls instead of remaking 99 a volley

An Ice Breaker volley starts at 10 balls, grows through +1 pickups, and the balance clamps it to a hard ceiling of 99. Each ball carries a Rigidbody2D, a CircleCollider2D and a TrailRenderer. Rather than creating and destroying that set every volley, the design returns balls to a Stack<Ball> pool to reduce creation and allocation work. The linked Unity API page covers the SetActive switch this relies on. How much that reduction matters on a real device was not measured.

It is state that leaks, not objects

The real pooling failure is not a missing object but live state leaking into the next use. The list PrepareForPool clears on return is exactly the leak candidate list.

This list is a source observation; the test below does not check each cleared field. On the way out, Launch reinitializes the same physics and volley fields and SpawnBall re-wires the delegates. That a reused ball is identical to a fresh one is also source observation, not a test guarantee.

  1. Physics: linear velocity, angular velocity and rotation back to zero, simulated off, collider off.
  2. Delegates: OnFloorHit, OnFullyRecalled, OnSideWallHit, GetDirectHitDamage and OnDirectHitResolved all become null. A breakthrough-only callback surviving into a normal volley would fire a rule that does not exist.
  3. Volley-owned values: VolleyOrdinal and the five breakthrough flags reset. A charged ball must not come out charged next volley.
  4. Visuals and roster: the trail clears and the static AllLive list drops the entry. A pooled ball counted as live would make both counters lie.

What the test locks

NinetyNineBallsAreReusedWithoutLeakingLiveState invokes the private SpawnBall through reflection 99 times to force the round trip, and it asserts only counters.

  1. Right after firing: GameSession.ActiveBallCount and Ball.AllLive both read 99.
  2. Full recall: InstantDestroyRecalled on every ball brings both to zero while PooledBallCount reaches at least 99. A destroyed ball cannot reach the pool, so this value proves the return path.
  3. Reuse round trip: one more spawn makes both counters read 1, and recalling it returns them to 0.

What the source adds beyond the test

  1. InstantDestroyRecalled falls back to Destroy when OnFullyRecalled is unset; the test only travels the wired return path.
  2. RegisterLive scrubs dead entries on registration and applies Physics2D.IgnoreCollision between live balls.
  3. PrewarmFirstVolley builds one dormant ball per title-screen frame up to the starting count (10 by default, clamped at 99) and prewarms the launch effects PixelOneShotFx and PixelVolleyStreak to the same count. It reads as an intent to keep the first volley from allocating; actual allocation was not measured.

A sequence to copy in another project

  1. Check that two independent records, a session counter and a live roster, still agree after spawn and after recall.
  2. Write down the state list your return path clears, and confirm no delegate is missing.
  3. Pin the round trip in a test: spawn N, recall all, pool at least N, respawn one.
  4. Separate the no-callback destroy path from the wired return path and verify both.
  5. Prewarm one object per frame on a menu screen instead of all at once.

What this record does not prove

The September 8 PlayMode 30/30 receipt and the pinned source were compared on September 21. The test locks only the counter round trip. Per-field clearing, the no-callback destroy path and reused-ball equivalence are source observations, not test guarantees. The 99 ceiling, the pool's fit for real device frame times and memory, the actual volley coroutine skipped by reflection, and leaks that surface after the checked frame were not verified.

Verification scope

Retained runtime evidence and source review

Verified

Test environmentUnity 6000.5.3f1 · Windows Editor · September 8 retained PlayMode receipts; Ball and GameSession source review on September 21

Compared the merged Ice Breaker PR #40 source with the September 8 Unity 6000.5.3f1 PlayMode 30/30 test XML, which contains NinetyNineBallsAreReusedWithoutLeakingLiveState. The author did not rerun the game. The test locks a pooling contract; it does not prove the 99-ball ceiling or this pool shape fits real device frame times or memory.