Skip to content

t1k:cocos:playable:object-pool

FieldValue
Moduleplayable
Version0.5.6
Efforthigh
Tools

Keywords: ads, cocos, object pool, playable, pooling, prefab, spawn

/t1k:cocos:playable:object-pool

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.

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 calls Reset() on all components that implement it
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 membership
pool.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 node
pool.UnloadAll(); // destroy all pools
// --- Monitoring ---
const info = pool.GetPoolInfo("Effects/Coin");
// { available: 8, active: 2, total: 10 }
pool.HasPool("Effects/Coin"); // boolean
pool.GetAllPoolKeys(); // string[]

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);
}
}

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.

  • Calling Spawn() before Load()/LoadAsync() — returns null with a console warning
  • Destroying a pooled node manually instead of calling Recycle() — leaves dangling entry in spawnedObjects
  • 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
  • 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 controllersSpawn(key, pos, rot, parent) sets node.active = true and THEN reparents the node to the passed parent. Reparenting an already-active node fires onDisable → onEnable on it. Any component that arms itself in onEnable (binds node-local events, subscribes signals, registers) and whose setup must be followed by a separate configure()/init call will have its arming churned by that reparent cycle — so the follow-up configure() runs against a half-armed controller and the object silently fails to drive/animate. Real case: AttackingAnimalController._arm() binds TargetHealthEvent.DEATH in onEnable and _disarm() unbinds in onDisable; spawning a raider via the pool left it un-driven. Fix: for spawns that need configure() after activation, use the direct-instantiate pattern (mirrors AmbientFoxSpawner): instantiate once, node.active=false, parent once while inactive, then on spawn do setWorldPosition(anchor) → node.active = true → configure(...) on the already-parented node — never activate-then-reparent. See also t1k-cocos-playable-animation-core for animation controller lifecycle patterns.