t1k:cocos:playable:object-pool
| Field | Value |
|---|---|
| Module | playable |
| Version | 0.15.0 |
| Effort | high |
| Tools | — |
Keywords: ads, cocos, object pool, playable, pooling, prefab, spawn
How to invoke
Section titled “How to invoke”/t1k:cocos:playable:object-poolObjectPoolManager
Section titled “ObjectPoolManager”Static singleton for reusing Node instances. Avoids runtime instantiate()/destroy() overhead critical for playable ad performance. See t1k-cocos-playable-asset-management for AssetsManager used by LoadAsync.
Architecture
Section titled “Architecture”ObjectPools (scene root Node)└── Pool_Enemies (category Node, key="Enemies/Goblin") └── Goblin_Pool ├── [inactive nodes] ← pooledObjects[] └── [active nodes] ← spawnedObjects Set- Category extracted from key prefix:
"Enemies/Goblin"→ category"Enemies" - Keys without
/go into"Default"category resetNode()resets position/rotation/scale and callsReset()on all components that implement it
Key APIs
Section titled “Key APIs”const pool = ObjectPoolManager.instance;
// --- Initialization ---// From prefab reference (sync)pool.Load("Effects/Coin", coinPrefab, 20);pool.Load(prefabRef); // key = prefab.name, count = 10
// From resources/ folder (async, tries multiple path variants)await pool.LoadAsync("Effects/Coin", 20);
// --- Spawn ---const node = pool.Spawn("Effects/Coin");const node = pool.Spawn("Effects/Coin", position, rotation, parentNode);
// --- Recycle ---pool.Recycle(node); // auto-finds pool by node membershippool.Recycle("Effects/Coin", node); // explicit key (faster)
// --- Batch recycle ---pool.RecycleAll("Effects/Coin");
// --- Cleanup (trim idle objects, keep retainCount) ---pool.Cleanup("Effects/Coin", 5); // destroy all but 5 idle instances
// --- Teardown ---pool.Unload("Effects/Coin"); // destroy pool + container nodepool.UnloadAll(); // destroy all pools
// --- Monitoring ---const info = pool.GetPoolInfo("Effects/Coin");// { available: 8, active: 2, total: 10 }pool.HasPool("Effects/Coin"); // booleanpool.GetAllPoolKeys(); // string[]Reset() Callback
Section titled “Reset() Callback”Components on pooled nodes can implement Reset() to clean up state on recycle:
export class CoinFX extends Component { private _velocity: Vec3 = new Vec3();
public Reset(): void { // Called automatically by ObjectPoolManager.Recycle() this._velocity.set(0, 0, 0); this.node.setOpacity(255); }}Workflow: Common Tasks
Section titled “Workflow: Common Tasks”One-time pool setup at game start:
// In a loading state or GameView.onLoad():pool.Load("Coin", coinPrefab, 30);pool.Load("HitFX", hitFxPrefab, 10);Async load from resources folder:
// Prefab must be at: assets/resources/Effects/Coin.prefab// or assets/resources/prefab/Coin.prefab (LoadAsync tries variants)await pool.LoadAsync("Effects/Coin", 20);Recycle from animation complete callback:
const node = pool.Spawn("Effects/Coin", startPos, null, canvas);// ... animate ...tween(node).then(() => pool.Recycle(node)).start();FlyingAnimationController pattern (Spawn → auto-Recycle on complete):
// FlyingAnimationController manages its own Spawn/Recycle internally.// Preload via controller:flyingController.preloadPool("Effects/Star", starPrefab, 10);await flyingController.playAnimation({ poolKey: "Effects/Star", ... });AudioService pattern (AudioEmitter pooling):
AudioService internally uses ObjectPoolManager with AudioEmitter prefab from resources/prefab/AudioEmitter.prefab. Do not manually pool audio nodes.
Common Mistakes
Section titled “Common Mistakes”- Calling
Spawn()beforeLoad()/LoadAsync()— returnsnullwith a console warning - Destroying a pooled node manually instead of calling
Recycle()— leaves dangling entry inspawnedObjects - Using the same key string for different prefabs — pools are keyed by string, collisions corrupt the pool
- Not implementing
Reset()— node retains state (position, opacity, tween callbacks) from previous spawn - Calling
LoadAsync()twice for the same key concurrently — safe (second call skips if pool exists), but wasteful - Self-recycling pooled objects leak on owning-view teardown. If a pooled node recycles itself from its OWN
update()(e.g. “despawn when off-screen” / lifetime cap), deactivating the owning view stops thatupdate()mid-flight — the node never reachesRecycle(), so it stays inspawnedObjects(hidden, frozen) and resumes from a stale position on the next replay. The owning controller MUST drain live objects in itsonDisable():
onDisable() { // ... unsubscribe / unschedule ... ObjectPoolManager.instance.RecycleAll(POOL_KEY); // drains spawned (incl. inactive) nodes}Gotchas
Section titled “Gotchas”Recycle()on a node the pool no longer tracks DESTROYS it.Recycle(node)finds the owning pool by scanning forpool.spawnedObjects.has(node). A secondRecycle()of the same node misses (the first call already deleted it from that set), so the manager treats it as foreign: it logsNode does not belong to any pool, destroying itand callsnode.destroy()— while that same node is already sitting inpooledObjects. The pool now hands out destroyed nodes on the nextSpawn(). AnyrecycleSelf()-style method on a pooled view must be idempotent:
private _recycled = false; // reset in EVERY configure*/spawn entry point
public recycleSelf(): void { if (this._recycled) return; this._recycled = true; ObjectPoolManager.instance.Recycle(this.node);}This bites whenever gameplay recycles a view early (collect, despawn) while a placement service still holds it in a _spawned[] list that a later teardown iterates. Symptom: level 1 is fine, level 2+ spawns fewer objects than the data declares, with no error.
- Nodes spawned into a SHARED layer are not reclaimed by a service’s
recycleAll(). A service that tracks only the objects it spawned will not free their side-effect nodes (e.g. a checkmark parented to a shared overlay, not to the object). Either recycle the side-effect node from the owner’srecycleSelf(), or drain its pool withRecycleAll(SIDE_EFFECT_KEY)in teardown. Auditing this is cheap; the leak is invisible until a shorter next level leaves stale nodes on screen. - Pool prewarm must happen before first acquire — first acquire on cold pool stutters; prewarm in
onLoad. - Returned objects need state reset — pooling a node with active tweens or active children leaks residual state into the next user.
- Pool size cap is mandatory — uncapped pools grow until OOM on long playable sessions.
Spawn()activates then reparents — breaks lifecycle-armed controllers —Spawn(key, pos, rot, parent)setsnode.active = trueand THEN reparents the node to the passedparent. Reparenting an already-active node firesonDisable → onEnableon it. Any component that arms itself inonEnable(binds node-local events, subscribes signals, registers) and whose setup must be followed by a separateconfigure()/init call will have its arming churned by that reparent cycle — so the follow-upconfigure()runs against a half-armed controller and the object silently fails to drive/animate. Real case:AttackingAnimalController._arm()bindsTargetHealthEvent.DEATHinonEnableand_disarm()unbinds inonDisable; spawning a raider via the pool left it un-driven. Fix: for spawns that needconfigure()after activation, use the direct-instantiate pattern (mirrorsAmbientFoxSpawner): instantiate once,node.active=false, parent once while inactive, then on spawn dosetWorldPosition(anchor) → node.active = true → configure(...)on the already-parented node — never activate-then-reparent. See alsot1k-cocos-playable-animation-corefor animation controller lifecycle patterns.