Unity6 Game Development · Lesson 18

Pooling and reset-before-activation

Reuse objects while treating every enable as a new activation lifetime.

Unity 6 Editor walkthrough

The Unity script needs the Editor and the setup listed below. The separate C# companion practice runs in SharpRune without UnityEngine.

A pool reuses identity, not gameplay state

Pooling keeps an instance available for later reuse instead of constructing and destroying an equivalent object for every short gameplay use. That saves some repeated work only when the surrounding design is appropriate; it does not automatically erase old health, targets, timers, event handlers or animation state. Treat each rent as a new activation lifetime even when the object identity is unchanged. Define who may return the object, which state must be prepared before callbacks observe it, and what must be detached when its lease ends.

PrefabPoolLab creates instances from a prefab whose root is inactive. Its acquisition callback assigns localPosition before SetActive(true), so OnEnable observers do not see an unprepared position. Its release callback deactivates the object, and the destroy callback removes an excess returned object. The sample only resets the position; a real projectile or enemy would need an explicit broader reset contract. In particular, long-lived publishers must not keep subscriptions from a finished lease, and a delayed timer from an earlier lease must not later return an object that has already been borrowed again.

Portable C# companion: reset a returned token

In the SharpRune companion, LeaseCache creates one plain Token with Health ten. The first borrower changes Health to three, and Return restores it to ten before storing it in the free stack. The second Rent retrieves that same reference, so the printed values are True, 10 and 1. The last value is this companion's creation counter. It is not a measured Unity allocation count, and the small cache does not provide UnityEngine.Pool's lifecycle callbacks, double-return checking or thread-safety guarantees. Follow identity and reset state as separate facts.

var cache = new LeaseCache(); var first = cache.Rent(); first.Health = 3; cache.Return(first); var second = cache.Rent();
Console.WriteLine(ReferenceEquals(first, second)); Console.WriteLine(second.Health); Console.WriteLine(cache.Created);
sealed class Token { public int Health = 10; }
sealed class LeaseCache { private readonly Stack<Token> free = new(); public int Created { get; private set; } public Token Rent() { if (free.Count > 0) return free.Pop(); Created++; return new Token(); } public void Return(Token token) { token.Health = 10; free.Push(token); } }

Unity Editor: prepare the object before activation

In the Unity Editor, assign an inactive-root prefab to PrefabPoolLab.cs and invoke Rent And Return in Play mode. The pool's callbacks below show preparation, activation, deactivation and final destruction policy. Repeating this immediate rent/release normally reuses one instance and leaves one inactive item after each call; actual instance IDs vary. The object may be active too briefly to render visibly, so use the logs and optional lifecycle probe rather than expecting a lasting cube on screen. This experiment verifies ownership behavior, not a measured performance improvement.

private void Awake()
    {
        if (inactivePrefab == null || inactivePrefab.activeSelf)
        { Debug.LogError("Assign a prefab whose root is inactive.", this); enabled = false; return; }
        pool = new ObjectPool<GameObject>(
            () => Instantiate(inactivePrefab, transform),
            item => { item.transform.localPosition = Vector3.zero; item.SetActive(true); },
            item => item.SetActive(false),
            item => { if (item != null) Destroy(item); },
            true, 4, 16);
    }

Capacity, cleanup and release ownership

The constructor's defaultCapacity of four reserves internal storage; it does not instantiate four prefab objects. Its maximum of sixteen limits inactive items retained on return, rather than forbidding a seventeenth simultaneous borrower. The Unity pool is not a background-thread-safe service. Most importantly, do not add an OnDisable callback that calls Release when Release already disables the object: that creates recursive or duplicate return behavior. Dispose handles the pool's retained objects, so production teardown must also account for outstanding borrowers rather than assuming every active lease has already returned itself.

Two independent pooling experiments

First, independently extend the console cache experiment with a second field such as a target identifier and deliberately omit its reset. Rent, mutate, return and rent again, predicting the stale value that the new borrower sees. Then add the reset and repeat. Hint: identity reuse is expected; leakage of lease-specific state is the defect. Keep this experiment about state ownership rather than adding a Unity stub. Explain why resetting health alone would not remove an event subscription or invalidate a delayed completion callback owned by an earlier lease.

Second, in a separate Editor experiment, borrow two objects before returning either one and compare their instance IDs and pool counts. Then return both and borrow again. Hint: a nonempty inactive pool can satisfy a request, but an empty pool must create another instance. Observe activation callbacks and ensure preparation happens before SetActive(true). Do not make the prefab return itself from OnDisable. Finally describe how you would invalidate a timer when a lease ends, using a lease identifier or cancellation policy so an old callback cannot act on a newly borrowed incarnation.

Review each lease as a fresh activation

Review the full lease path: create inactive, prepare, activate, use, detach, deactivate and retain or destroy. Separate identity reuse from state reset and both from measured allocation savings. For the supplied immediate experiment, one retained instance is sufficient, but that does not establish a safe concurrent-borrowing limit or complete teardown policy. Keep one clear release authority, do not infer prewarming from initial capacity, and inventory every timer and subscription that can outlive a lease. Profile a representative Player before claiming that a pool has improved the game's frame behavior.

  • Require an inactive prefab root for the demonstrated activation order.
  • Reset all lease-specific state before consumers observe activation.
  • Do not call Release recursively from release-driven OnDisable.
  • Distinguish storage capacity, inactive retention and outstanding leases.
Runs in Unity 6 Editor

Unity Editor example

Set up in Unity 6

  1. Create a Cube prefab with an inactive root; optionally add LifecycleProbe from lesson3 before saving it.
  2. Attach PrefabPoolLab.cs to an active root object and assign that inactive prefab asset.
  3. Enter Play mode and invoke Rent And Return repeatedly; compare logged instance IDs and pool counts.
  4. Observe OnEnable/OnDisable on each rent/release if the probe is attached; this immediate experiment may not render the briefly active object.
  5. Do not add an automatic pool.Release call to the prefab’s OnDisable; the pool already disables it during release.
using UnityEngine;
using UnityEngine.Pool;
public sealed class PrefabPoolLab : MonoBehaviour
{
    [SerializeField] private GameObject inactivePrefab;
    private ObjectPool<GameObject> pool;
    private void Awake()
    {
        if (inactivePrefab == null || inactivePrefab.activeSelf)
        { Debug.LogError("Assign a prefab whose root is inactive.", this); enabled = false; return; }
        pool = new ObjectPool<GameObject>(
            () => Instantiate(inactivePrefab, transform),
            item => { item.transform.localPosition = Vector3.zero; item.SetActive(true); },
            item => item.SetActive(false),
            item => { if (item != null) Destroy(item); },
            true, 4, 16);
    }
    [ContextMenu("Rent And Return")]
    public void RentAndReturn()
    {
        if (!Application.isPlaying || pool == null) return;
        GameObject item = pool.Get();
        Debug.Log("Borrowed instance: " + item.GetInstanceID(), item);
        pool.Release(item);
        Debug.Log("Created=" + pool.CountAll + ", Inactive=" + pool.CountInactive, this);
    }
    private void OnDestroy() { if (pool != null) pool.Dispose(); }
}

What to observe in Unity

Repeated immediate rent/release calls reuse an instance and report one created, one inactive object after each call. Activation callbacks can repeat; this is not an Editor profiling measurement.

The console practice below isolates the C# logic. It does not run a Unity scene or emulate Unity engine callbacks.

C# companion practice

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var cache = new LeaseCache(); var first = cache.Rent(); first.Health = 3; cache.Return(first); var second = cache.Rent();
Console.WriteLine(ReferenceEquals(first, second)); Console.WriteLine(second.Health); Console.WriteLine(cache.Created);
sealed class Token { public int Health = 10; }
sealed class LeaseCache { private readonly Stack<Token> free = new(); public int Created { get; private set; } public Token Rent() { if (free.Count > 0) return free.Pop(); Created++; return new Token(); } public void Return(Token token) { token.Health = 10; free.Push(token); } }
Open C# practice

C# practice output

True
10
1
Try this
Reset before activation, and give release authority to one explicit owner.

Further reading